Биллинг подписок mono (зонтик): контракт billing + ledger2 + coopback + UI — НЕ мерджить, стек на ревью #129
@@ -23,3 +23,5 @@ components/contracts/cpp/ledger2/scripts/out/
|
||||
|
||||
.env.testnet
|
||||
.pnpm-store
|
||||
schema.gql
|
||||
.claude
|
||||
|
||||
@@ -72,7 +72,7 @@ Workflow:
|
||||
pnpm jest tests/unit/marketplace/marketplace-onboarding-service.test.ts --runInBand
|
||||
```
|
||||
|
||||
`pnpm generate-schema` / `pnpm generate-client` — **не запускать локально**; та же memory/CPU полка вешает контейнер controller'а. Либо CI, либо пользователь сам когда контейнер остановлен.
|
||||
`pnpm generate-schema` / `pnpm generate-client` — **запускать локально можно** (ограничение снято 2026-05-26). Раньше боялись, что memory/CPU-полка повесит контейнер controller'а, но на практике генерация отрабатывает; запускать при поднятом controller'е.
|
||||
|
||||
Перед коммитом достаточно `tsc --noEmit` (быстрый, не блокирует).
|
||||
|
||||
|
||||
@@ -128,4 +128,9 @@ export default [
|
||||
path: path.join(userBase, 'apps'),
|
||||
target: 'apps',
|
||||
},
|
||||
{
|
||||
name: 'billing',
|
||||
path: path.join(userBase, 'billing'),
|
||||
target: 'billing',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -154,6 +154,10 @@ export default {
|
||||
name: 'apps',
|
||||
code_permissions_to: ['apps'],
|
||||
},
|
||||
{
|
||||
name: 'billing',
|
||||
code_permissions_to: ['billing'],
|
||||
},
|
||||
// {
|
||||
// name: provider_chairman,
|
||||
// },
|
||||
|
||||
@@ -29,12 +29,22 @@ export async function bootExtra() {
|
||||
// провайдере (PENDING→RENT). boot:extra используется НЕ только для аренды
|
||||
// сервера (например, просто пересев совета/чейна для других задач), поэтому
|
||||
// провижининг partner1 включается ТОЛЬКО под флагом EXTRA_RENT=1.
|
||||
//
|
||||
// EXTRA_SEED=1 (взаимоисключающее с EXTRA_RENT) — сидит partner1 как
|
||||
// пайщика-organization Восхода (newaccount + reguser + wallet-signagree +
|
||||
// засев coopback) БЕЗ regcoop+preInit. Pre-state для ручного прохода
|
||||
// ConnectionAgreement: signin под WIF из фикстуры → UI выбора тарифа /
|
||||
// подписания / активации руками. EXTRA_RENT приоритетен.
|
||||
if (process.env.EXTRA_RENT === '1') {
|
||||
console.log('EXTRA_RENT=1 → installExtraData: провижининг partner1 (триггер аренды)')
|
||||
await installExtraData(blockchain) // Регистрируем partner1 как coop (active)
|
||||
await installExtraData(blockchain, 'full')
|
||||
}
|
||||
else if (process.env.EXTRA_SEED === '1') {
|
||||
console.log('EXTRA_SEED=1 → installExtraData (candidate): partner1 — пайщик-organization, готов к UI Connect')
|
||||
await installExtraData(blockchain, 'candidate')
|
||||
}
|
||||
else {
|
||||
console.log('EXTRA_RENT не задан → пропускаем провижининг partner1 (аренда не запускается)')
|
||||
console.log('EXTRA_RENT/EXTRA_SEED не заданы → пропускаем провижининг partner1')
|
||||
}
|
||||
|
||||
console.log('Инициализируем статус системы в PostgreSQL')
|
||||
|
||||
@@ -580,7 +580,10 @@ export async function installInitialData(blockchain: Blockchain, isExtended = fa
|
||||
console.log('Начальные данные установлены')
|
||||
}
|
||||
|
||||
export async function installExtraData(blockchain: Blockchain) {
|
||||
export async function installExtraData(
|
||||
blockchain: Blockchain,
|
||||
mode: 'full' | 'candidate' = 'full',
|
||||
) {
|
||||
// Регистрируем partner1 как coop с авто-approve от провайдера (Восхода).
|
||||
// Без этого реальный UI flow требует ручной активации председателем —
|
||||
// провайдер не подтянет partner1 в свою DB пока в registrator.coops его
|
||||
@@ -592,33 +595,58 @@ export async function installExtraData(blockchain: Blockchain) {
|
||||
// program_id > 0 с проверкой «подписывается через wallet::signagree»).
|
||||
// Минимум для попадания в registrator.coops со status=active:
|
||||
// newaccount → reguser(type=organization) → regcoop → stcoopstatus(active)
|
||||
console.log('\n=== installExtraData: регистрируем partner1 как coop (active) ===')
|
||||
//
|
||||
// mode='candidate' — пропускает regcoop+preInit. partner1 остаётся
|
||||
// пайщиком-organization Восхода: можно signin под WIF из фикстуры и пройти
|
||||
// UI «Connect / выбор тарифа / подписание / активация» руками. Используется
|
||||
// для отладки UX подключения без авто-провижининга.
|
||||
console.log(`\n=== installExtraData (${mode}): partner1 ===`)
|
||||
|
||||
const username = 'partner1'
|
||||
|
||||
// Берём WIF из docs-harness/state/cooperatives/partner1.json, если он там есть.
|
||||
// Без этого boot:extra на каждом запуске генерирует случайный privateKey,
|
||||
// а harness 08 (chairman install wizard /partner1/install) подаёт WIF из
|
||||
// фикстуры на шаге 1 — они расходятся, startInstall mutation не принимает
|
||||
// ключ, wizard зависает на шаге 1 без видимой ошибки. На репозиторий
|
||||
// фикстура коммит'ится разработчиком вручную; здесь только читаем.
|
||||
// Источники WIF (по приоритету): env PARTNER1_WIF → фикстура
|
||||
// docs-harness/state/cooperatives/partner1.json → DEFAULT_PARTNER1_WIF.
|
||||
//
|
||||
// DEFAULT_PARTNER1_WIF — общеизвестный test-ключ EOSIO (тот же, что у root
|
||||
// системного eosio в boot/configs/config.ini). На dev-стенде использовать
|
||||
// безопасно: контур закрытый, на проде partner1 не существует. Стабильный
|
||||
// дефолт удобнее env-override'ов и фикстур — каждый reboot:extra даёт тот
|
||||
// же signin, без записи в .env.
|
||||
const DEFAULT_PARTNER1_WIF = '5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3'
|
||||
let seededKeys: { privateKey: string; publicKey: string } | undefined
|
||||
try {
|
||||
const fixturePath = resolve(
|
||||
__dirname,
|
||||
'../../../docs-harness/state/cooperatives/partner1.json',
|
||||
)
|
||||
if (existsSync(fixturePath)) {
|
||||
const fx = JSON.parse(readFileSync(fixturePath, 'utf-8'))
|
||||
if (fx?.wif) {
|
||||
// publicKey не хранится в фикстуре — деривим из приватного.
|
||||
const pub = fx.publicKey || await ecc.privateToPublic(fx.wif)
|
||||
seededKeys = { privateKey: fx.wif, publicKey: pub }
|
||||
console.log(`✓ partner1: используем seeded WIF из фикстуры (pub=${pub.slice(0, 12)}…)`)
|
||||
}
|
||||
const envWif = process.env.PARTNER1_WIF?.trim()
|
||||
if (envWif) {
|
||||
try {
|
||||
const pub = await ecc.privateToPublic(envWif)
|
||||
seededKeys = { privateKey: envWif, publicKey: pub }
|
||||
console.log(`✓ partner1: используем PARTNER1_WIF из env (pub=${pub.slice(0, 12)}…)`)
|
||||
} catch (e: any) {
|
||||
console.warn(`⚠ PARTNER1_WIF недопустим (${e.message}) — пробуем фикстуру`)
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.warn(`⚠ partner1 fixture read failed (${e.message}) — генерируем новый ключ`)
|
||||
}
|
||||
if (!seededKeys) {
|
||||
try {
|
||||
const fixturePath = resolve(
|
||||
__dirname,
|
||||
'../../../docs-harness/state/cooperatives/partner1.json',
|
||||
)
|
||||
if (existsSync(fixturePath)) {
|
||||
const fx = JSON.parse(readFileSync(fixturePath, 'utf-8'))
|
||||
if (fx?.wif) {
|
||||
// publicKey не хранится в фикстуре — деривим из приватного.
|
||||
const pub = fx.publicKey || await ecc.privateToPublic(fx.wif)
|
||||
seededKeys = { privateKey: fx.wif, publicKey: pub }
|
||||
console.log(`✓ partner1: используем seeded WIF из фикстуры (pub=${pub.slice(0, 12)}…)`)
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.warn(`⚠ partner1 fixture read failed (${e.message}) — пробуем дефолт`)
|
||||
}
|
||||
}
|
||||
if (!seededKeys) {
|
||||
const pub = await ecc.privateToPublic(DEFAULT_PARTNER1_WIF)
|
||||
seededKeys = { privateKey: DEFAULT_PARTNER1_WIF, publicKey: pub }
|
||||
console.log(`✓ partner1: используем DEFAULT_PARTNER1_WIF (pub=${pub.slice(0, 12)}…)`)
|
||||
}
|
||||
|
||||
const account = await blockchain.generateKeypair(username, seededKeys, 'Аккаунт partner1')
|
||||
@@ -648,34 +676,39 @@ export async function installExtraData(blockchain: Blockchain) {
|
||||
registration_hash,
|
||||
})
|
||||
|
||||
await blockchain.registerCooperative({
|
||||
username: account.username,
|
||||
coopname: account.username,
|
||||
params: {
|
||||
is_cooperative: true,
|
||||
coop_type: 'conscoop',
|
||||
announce: 'partner-dev.coopenomics.world',
|
||||
description: 'Партнёрский кооператив (тестовый dev-стенд)',
|
||||
initial: `100.0000 ${config.token.govern_symbol}`,
|
||||
minimum: `300.0000 ${config.token.govern_symbol}`,
|
||||
org_initial: `1000.0000 ${config.token.govern_symbol}`,
|
||||
org_minimum: `3000.0000 ${config.token.govern_symbol}`,
|
||||
},
|
||||
document: {
|
||||
hash: registration_hash,
|
||||
signatures: [],
|
||||
meta: '{}',
|
||||
version: '1.0.0',
|
||||
doc_hash: registration_hash,
|
||||
meta_hash: registration_hash,
|
||||
} as any,
|
||||
})
|
||||
if (mode === 'full') {
|
||||
await blockchain.registerCooperative({
|
||||
username: account.username,
|
||||
coopname: account.username,
|
||||
params: {
|
||||
is_cooperative: true,
|
||||
coop_type: 'conscoop',
|
||||
announce: 'partner-dev.coopenomics.world',
|
||||
description: 'Партнёрский кооператив (тестовый dev-стенд)',
|
||||
initial: `100.0000 ${config.token.govern_symbol}`,
|
||||
minimum: `300.0000 ${config.token.govern_symbol}`,
|
||||
org_initial: `1000.0000 ${config.token.govern_symbol}`,
|
||||
org_minimum: `3000.0000 ${config.token.govern_symbol}`,
|
||||
},
|
||||
document: {
|
||||
hash: registration_hash,
|
||||
signatures: [],
|
||||
meta: '{}',
|
||||
version: '1.0.0',
|
||||
doc_hash: registration_hash,
|
||||
meta_hash: registration_hash,
|
||||
} as any,
|
||||
})
|
||||
|
||||
await blockchain.preInit({
|
||||
coopname: account.username,
|
||||
username: config.provider,
|
||||
status: 'active',
|
||||
})
|
||||
await blockchain.preInit({
|
||||
coopname: account.username,
|
||||
username: config.provider,
|
||||
status: 'active',
|
||||
})
|
||||
}
|
||||
else {
|
||||
console.log('candidate-mode: пропускаем regcoop+preInit (UI Connect делает руками)')
|
||||
}
|
||||
|
||||
// Записываем partner1 в ЦПП Кошелька оператора (voskhod, program_id=1).
|
||||
// БЕЗ этого провайдерский performInitialTransfers (150 AXON на partner1 ДО
|
||||
|
||||
@@ -58,6 +58,7 @@ add_contract_build(registrator)
|
||||
add_contract_build(gateway)
|
||||
add_contract_build(marketplace)
|
||||
add_contract_build(apps)
|
||||
add_contract_build(billing)
|
||||
add_contract_build(system)
|
||||
|
||||
# Опции для тестов
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
cmake_minimum_required(VERSION 3.5)
|
||||
|
||||
# Проект
|
||||
project(billing)
|
||||
|
||||
find_package(cdt)
|
||||
|
||||
# Добавление контракта
|
||||
add_contract(billing billing billing.cpp)
|
||||
if(IS_TESTNET)
|
||||
target_compile_definitions(billing PUBLIC IS_TESTNET=1)
|
||||
else()
|
||||
# Не определяем IS_TESTNET для продакшн режима
|
||||
endif()
|
||||
@@ -0,0 +1,18 @@
|
||||
#include "billing.hpp"
|
||||
#include <ctime>
|
||||
#include <eosio/transaction.hpp>
|
||||
|
||||
#include "src/convert.cpp"
|
||||
#include "src/pay.cpp"
|
||||
|
||||
using namespace eosio;
|
||||
|
||||
/**
|
||||
* @brief Миграция данных контракта.
|
||||
* @ingroup public_billing_actions
|
||||
* @note Авторизация требуется от аккаунта: @p billing
|
||||
*/
|
||||
[[eosio::action]]
|
||||
void billing::migrate() {
|
||||
require_auth(_billing);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
#pragma once
|
||||
|
||||
#include <eosio/asset.hpp>
|
||||
#include <eosio/eosio.hpp>
|
||||
#include "../lib/index.hpp"
|
||||
|
||||
/**
|
||||
\defgroup public_billing Контракт BILLING
|
||||
|
||||
* Минимальный смарт-контракт оплаты инфраструктурных подписок членскими взносами
|
||||
* пайщика (Epic 12). Ровно два действия:
|
||||
* - `convert` — трансляция паевого взноса пайщика в членский на персональный
|
||||
* биллинг-кошелёк (`w.wal.bill`) по подписанному заявлению (document2);
|
||||
* - `pay` — списание с биллинг-кошелька суммарной стоимости подписок в
|
||||
* инфраструктурный кошелёк кооператива (`w.sov.infra`) по идентификатору
|
||||
* платежа (`payment_hash`), идемпотентно.
|
||||
*
|
||||
* Состав, цены и даты подписок on-chain НЕ хранятся — это зона оператора
|
||||
* (provider backend); контракт несёт только сумму + `payment_hash` + memo.
|
||||
*/
|
||||
|
||||
/**
|
||||
\defgroup public_billing_actions Действия
|
||||
\ingroup public_billing
|
||||
*/
|
||||
|
||||
/**
|
||||
\defgroup public_billing_tables Таблицы
|
||||
\ingroup public_billing
|
||||
*/
|
||||
|
||||
namespace Billing {
|
||||
/**
|
||||
* @brief Проверка, что аккаунт-плательщик является кооперативом (presence-only).
|
||||
*
|
||||
* По требованию @ant (2026-05-25): проверяем только НАЛИЧИЕ записи в реестре
|
||||
* кооперативов регистратора и флаг `is_cooperative`, БЕЗ проверки статуса —
|
||||
* заблокированные/на рассмотрении кооперативы тоже проходят этот guard
|
||||
* (статус контролируется выше по потоку, на стороне оператора/ledger2).
|
||||
*/
|
||||
inline void assert_payer_is_cooperative(eosio::name coopname) {
|
||||
cooperatives2_index coops(_registrator, _registrator.value);
|
||||
auto org = coops.find(coopname.value);
|
||||
eosio::check(org != coops.end(), "Плательщик не найден в реестре кооперативов");
|
||||
eosio::check(org->is_coop(), "Плательщик не является кооперативом");
|
||||
}
|
||||
} // namespace Billing
|
||||
|
||||
/**
|
||||
* \ingroup public_contracts
|
||||
* @brief Контракт Billing (оплата подписок членскими взносами).
|
||||
*/
|
||||
class [[eosio::contract]] billing : public contract {
|
||||
public:
|
||||
using contract::contract;
|
||||
|
||||
[[eosio::action]] void migrate();
|
||||
|
||||
// Конвертация паевого взноса в членский на биллинг-кошелёк пайщика.
|
||||
// convert_hash — детерминированный sha256 от (coopname, username, amount,
|
||||
// anchor) — служит process-якорем: используется как ключ операции ledger2
|
||||
// и как package_hash для Soviet::make_complete_document (документ в реестре
|
||||
// ищется именно по convert_hash). Повтор с тем же convert_hash —
|
||||
// идемпотентный no-op на уровне реестра soviet (newresolved уже зафиксирован).
|
||||
[[eosio::action]] void convert(eosio::name coopname, eosio::name username,
|
||||
eosio::asset amount, eosio::checksum256 convert_hash,
|
||||
document2 document);
|
||||
|
||||
// Списание с биллинг-кошелька стоимости подписок (оплата провайдеру).
|
||||
[[eosio::action]] void pay(eosio::name coopname, eosio::name username,
|
||||
eosio::asset amount, eosio::checksum256 payment_hash,
|
||||
std::string memo);
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
using namespace eosio;
|
||||
|
||||
/**
|
||||
* @brief Конвертация паевого взноса пайщика в членский на биллинг-кошелёк.
|
||||
*
|
||||
* Пайщик подаёт подписанное заявление (document2) «прошу транслировать мой
|
||||
* паевой взнос в членский за использование инфраструктуры». Контракт одним
|
||||
* движением ledger2 (`o.bil.fund`) переводит средства с паевого кошелька
|
||||
* `w.wal.share` на персональный биллинг-кошелёк `w.wal.bill` (Dr 80 / Cr 86):
|
||||
* паевой взнос (возвратный) превращается в членский (целевой, невозвратный),
|
||||
* зарезервированный под оплату подписок. Сам факт конвертации трактуется как
|
||||
* согласие пайщика на последующие рекуррентные списания (см. `pay`).
|
||||
*
|
||||
* @param coopname Кооператив-плательщик (его подписью релеит бэкенд после JWT).
|
||||
* @param username Пайщик, чей паевой конвертируется.
|
||||
* @param amount Сумма конвертации (RUB, > 0).
|
||||
* @param convert_hash Детерминированный sha256-якорь процесса конвертации
|
||||
* (coopname, username, amount, anchor). Используется как
|
||||
* process_hash ledger2-операции и как package_hash в
|
||||
* Soviet::make_complete_document — по нему документ
|
||||
* находится в реестре кооператива. Обеспечивает связность
|
||||
* процесса и идемпотентность на уровне soviet-реестра.
|
||||
* @param document Подписанное пайщиком заявление (обязательно, непустой hash).
|
||||
*
|
||||
* @ingroup public_billing_actions
|
||||
* @note Авторизация требуется от аккаунта: @p coopname
|
||||
*/
|
||||
void billing::convert(name coopname, name username, asset amount,
|
||||
checksum256 convert_hash, document2 document) {
|
||||
require_auth(coopname);
|
||||
|
||||
// Плательщик должен быть кооперативом (presence-only, без статуса).
|
||||
Billing::assert_payer_is_cooperative(coopname);
|
||||
|
||||
check(amount.is_valid() && amount.amount > 0, "Сумма конвертации должна быть положительной");
|
||||
check(amount.symbol == _root_govern_symbol, "Неверный символ валюты для конвертации");
|
||||
check(convert_hash != checksum256{}, "convert_hash обязателен (детерминированный якорь процесса)");
|
||||
|
||||
// Заявление обязательно и должно быть подписано пайщиком. Ловушка: пустой
|
||||
// checksum256 ломает ABI-кодирование/верификацию — документ должен быть реальным.
|
||||
check(!is_empty_document(document), "Заявление на конвертацию обязательно (пустой документ недопустим)");
|
||||
verify_document_or_fail(document, std::vector<name>{username});
|
||||
|
||||
const std::string memo =
|
||||
"Трансляция паевого взноса в членский на биллинг-кошелёк, пайщик=" + username.to_string();
|
||||
|
||||
Ledger2::apply(
|
||||
_billing,
|
||||
coopname,
|
||||
operations::billing::CONVERT,
|
||||
amount,
|
||||
username,
|
||||
convert_hash,
|
||||
memo
|
||||
);
|
||||
|
||||
// Фиксируем заявление в общем реестре документов кооператива (newsubmitted +
|
||||
// newresolved в soviet — как в registrator::regcoop / capital::createpinv /
|
||||
// soviet::converttoaxn). action_name = operations::billing::CONVERT
|
||||
// (= "o.bil.fund"_n) — уникальное имя ledger2-операции, не пересекается с
|
||||
// generic "convert"_n из других контрактов. package_hash = convert_hash —
|
||||
// документ привязан к конкретному процессу; повтор с тем же convert_hash на
|
||||
// уровне soviet-реестра идемпотентен.
|
||||
Soviet::make_complete_document(
|
||||
_billing,
|
||||
coopname,
|
||||
username,
|
||||
operations::billing::CONVERT,
|
||||
convert_hash,
|
||||
document
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using namespace eosio;
|
||||
|
||||
/**
|
||||
* @brief Списание стоимости подписок с биллинг-кошелька пайщика (оплата).
|
||||
*
|
||||
* Оператор (Восход) одним действием списывает с биллинг-кошелька `w.wal.bill`
|
||||
* пайщика суммарную стоимость его активных подписок и зачисляет её в
|
||||
* инфраструктурный кошелёк кооператива `w.sov.infra` (ledger2 `o.bil.pay`).
|
||||
* Отдельная подпись пайщика не требуется — авторизация обеспечена нахождением
|
||||
* контракта в `contracts_whitelist` + фактом конвертации (`convert` = согласие).
|
||||
*
|
||||
* Состав подписок on-chain не раскрывается: контракт несёт только сумму,
|
||||
* `payment_hash` (ссылка на запись в БД провайдера) и memo. Дедуп по
|
||||
* `payment_hash` — у provider'а (Single-Hub v5: парсер Восхода ловит pay-event,
|
||||
* coopback Восхода дёргает provider `POST /billing/payment-confirmed`, provider
|
||||
* сам отвечает на повторы). Контракт своих таблиц не ведёт.
|
||||
*
|
||||
* @param coopname Кооператив-плательщик.
|
||||
* @param username Пайщик, с чьего биллинг-кошелька списываем.
|
||||
* @param amount Сумма к оплате (RUB, > 0).
|
||||
* @param payment_hash Идентификатор платежа из БД провайдера (обязателен).
|
||||
* @param memo Произвольный комментарий (< 256 символов).
|
||||
*
|
||||
* @ingroup public_billing_actions
|
||||
* @note Авторизация требуется от аккаунта: @p coopname
|
||||
*/
|
||||
void billing::pay(name coopname, name username, asset amount,
|
||||
checksum256 payment_hash, std::string memo) {
|
||||
require_auth(coopname);
|
||||
|
||||
// Плательщик должен быть кооперативом (presence-only, без статуса).
|
||||
Billing::assert_payer_is_cooperative(coopname);
|
||||
|
||||
check(amount.is_valid() && amount.amount > 0, "Сумма оплаты должна быть положительной");
|
||||
check(amount.symbol == _root_govern_symbol, "Неверный символ валюты для оплаты");
|
||||
check(payment_hash != checksum256{}, "payment_hash обязателен");
|
||||
check(memo.size() < 256, "memo не должен превышать 255 символов");
|
||||
|
||||
const std::string ledger_memo =
|
||||
memo.empty() ? std::string("Оплата подписки за инфраструктуру") : memo;
|
||||
|
||||
// Списание членских взносов пайщика → инфраструктурный кошелёк кооператива.
|
||||
// Если на w.wal.bill недостаточно средств — walletop упадёт доменной ошибкой;
|
||||
// оператор фиксирует подписку как просроченную (Epic 4: past_due → grace).
|
||||
Ledger2::apply(
|
||||
_billing,
|
||||
coopname,
|
||||
operations::billing::PAY,
|
||||
amount,
|
||||
username,
|
||||
payment_hash,
|
||||
ledger_memo
|
||||
);
|
||||
}
|
||||
@@ -140,6 +140,7 @@ static constexpr uint64_t _capital_program_id = 4;
|
||||
#define LEDGER "ledger"
|
||||
#define LEDGER2 "ledger2"
|
||||
#define APPS "apps"
|
||||
#define BILLING "billing"
|
||||
|
||||
|
||||
/**
|
||||
@@ -165,6 +166,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 _billing = "billing"_n;
|
||||
static constexpr eosio::name _power_account = "eosio.power"_n;
|
||||
static constexpr eosio::name _saving_account = "eosio.saving"_n;
|
||||
|
||||
@@ -193,7 +195,8 @@ static constexpr uint64_t _capital_program_id = 4;
|
||||
"ledger"_n,
|
||||
"ledger2"_n,
|
||||
"capital"_n,
|
||||
"apps"_n
|
||||
"apps"_n,
|
||||
"billing"_n
|
||||
// Добавьте остальные стандартные или пользовательские контракты по необходимости
|
||||
};
|
||||
|
||||
|
||||
@@ -93,6 +93,14 @@ namespace operations {
|
||||
inline constexpr eosio::name CONVERT_AXN = "o.sov.axncnv"_n; ///< Трансляция паевого взноса в членский (Dr 80 / Cr 86, TRANSFER SHARE_FUND_PAY → DELEGATE_FEES).
|
||||
}
|
||||
|
||||
// billing (Epic 12 — контракт billing)
|
||||
// ВНИМАНИЕ: имя константы НЕ `FUND` — `FUND` занят макросом `#define FUND "fund"`
|
||||
// в consts.hpp (имя контракта fund). Используем `CONVERT` (бэкенд-имя действия).
|
||||
namespace billing {
|
||||
inline constexpr eosio::name CONVERT = "o.bil.fund"_n; ///< Трансляция паевого взноса в членский на биллинг-кошелёк пайщика (Dr 80 / Cr 86, TRANSFER SHARE_FUND_PAY → BILLING_FUND_PAY).
|
||||
inline constexpr eosio::name PAY = "o.bil.pay"_n; ///< Оплата подписки членскими взносами пайщика (TRANSFER BILLING_FUND_PAY → INFRA_FEES, без Dr/Cr — оба разреза счёта 86).
|
||||
}
|
||||
|
||||
// migration (только из migrate.cpp)
|
||||
//
|
||||
// В OPERATION_REGISTRY включены **только** те транзиты, которые проводятся
|
||||
@@ -320,6 +328,27 @@ static constexpr OperationRegistryEntry OPERATION_REGISTRY[] = {
|
||||
0, 0,
|
||||
"Конвертация сегмента: РИД → ЦПП «Благорост»" },
|
||||
|
||||
// 20. Биллинг — пополнение биллинг-кошелька: Dr 80 / Cr 86, TRANSFER SHARE_FUND_PAY → BILLING_FUND_PAY.
|
||||
// Зеркало o.sov.axncnv (трансляция паевого в членский), но назначение —
|
||||
// персональный USER_SHARED биллинг-кошелёк пайщика, а не кооперативный пул
|
||||
// делегатских взносов. После конвертации средства невозвратны (членские),
|
||||
// лежат на личном разрезе пайщика до списания за подписки (o.bil.pay). (Epic 12)
|
||||
{ operations::billing::CONVERT, processes::billing::CONVERT, WalletOp::TRANSFER,
|
||||
ledger2_wallets::SHARE_FUND_PAY, ledger2_wallets::BILLING_FUND_PAY,
|
||||
ledger2_accounts::SHARE_FUND, ledger2_accounts::TARGET_RECEIPTS,
|
||||
"Трансляция паевого взноса в членский на биллинг-кошелёк пайщика" },
|
||||
|
||||
// 21. Биллинг — оплата подписки: TRANSFER BILLING_FUND_PAY → INFRA_FEES, без Dr/Cr.
|
||||
// Оба кошелька — аналитические разрезы счёта 86 (целевое финансирование):
|
||||
// перенос «членский взнос пайщика, зарезервированный под подписки» →
|
||||
// «членские взносы за инфраструктуру платформы». Бухпроводка не требуется
|
||||
// (реклассификация внутри 86), по аналогии с o.cap.invest/o.wal.wthreq. (Epic 12)
|
||||
{ operations::billing::PAY, processes::billing::PAY, WalletOp::TRANSFER,
|
||||
ledger2_wallets::BILLING_FUND_PAY, ledger2_wallets::INFRA_FEES,
|
||||
0, 0,
|
||||
"Оплата подписки за инфраструктуру членскими взносами пайщика" },
|
||||
|
||||
|
||||
// ----- Миграционные (o.mig.*) — вызываются только из migrate.cpp -----
|
||||
|
||||
// 15. Миграция: минимальный паевой: Dr 51 / Cr 80, ISSUE MIN_SHARE_FUND
|
||||
|
||||
@@ -65,6 +65,14 @@ namespace processes {
|
||||
inline constexpr eosio::name AXN_CONVERT = "p.sov.axncnv"_n; ///< Конвертация паевого RUB → делегатский ЧВ (одноактовый).
|
||||
}
|
||||
|
||||
// billing (Epic 12 — контракт billing)
|
||||
// ВНИМАНИЕ: имя константы НЕ `FUND` — `FUND` занят макросом `#define FUND "fund"`
|
||||
// в consts.hpp (имя контракта fund). Используем `CONVERT` (бэкенд-имя действия).
|
||||
namespace billing {
|
||||
inline constexpr eosio::name CONVERT = "p.bil.fund"_n; ///< Пополнение биллинг-кошелька пайщика (трансляция паевого в членский, одноактовый; anchor = hash заявления document2).
|
||||
inline constexpr eosio::name PAY = "p.bil.pay"_n; ///< Оплата подписки с биллинг-кошелька (одноактовый; anchor = payment_hash из БД провайдера).
|
||||
}
|
||||
|
||||
// migration
|
||||
namespace migration {
|
||||
inline constexpr eosio::name TRANSIT = "p.mig.trans"_n; ///< Транзитный перенос остатков legacy (серия apply на кооп).
|
||||
|
||||
@@ -46,6 +46,7 @@ struct ledger2_wallets {
|
||||
// wallet — паевой фонд + возвраты + ЦК
|
||||
static constexpr eosio::name SHARE_FUND_PAY = "w.wal.share"_n; ///< Паевой взнос пайщика (USER_SHARED)
|
||||
static constexpr eosio::name CK_MEMBER = "w.wal.member"_n; ///< ЦК — членская часть пайщика (USER_SHARED)
|
||||
static constexpr eosio::name BILLING_FUND_PAY = "w.wal.bill"_n; ///< ЦК — биллинг (USER_SHARED). Источник для контракта `billing`: o.bil.fund зачисляет (паевой→членский), o.bil.pay списывает в w.sov.infra.
|
||||
static constexpr eosio::name WITHDRAWALS_SINK = "w.wal.wthdrw"_n; ///< DEPRECATED 2026-05-21: исторический sink возвратов. Оставлен в реестре для исторических L2-балансов (накопленные возвраты до перехода). Не использовать в новых операциях.
|
||||
static constexpr eosio::name WITHDRAW_PENDING = "w.wal.wpend"_n; ///< Резерв паевого под заявку на возврат (COOPERATIVE-пул). o.wal.wthreq переводит сюда с w.wal.share, o.wal.wthdec возвращает обратно, o.wal.wthcpl сжигает отсюда. Заменил механику blocked/BLOCK/UNBLOCK 2026-05-24.
|
||||
|
||||
@@ -95,11 +96,12 @@ struct Ledger2WalletMeta {
|
||||
WalletKind kind;
|
||||
};
|
||||
|
||||
inline constexpr std::array<Ledger2WalletMeta, 15> LEDGER2_WALLET_REGISTRY = {{
|
||||
// USER_SHARED (5) — L3-разрез по пайщику
|
||||
inline constexpr std::array<Ledger2WalletMeta, 16> LEDGER2_WALLET_REGISTRY = {{
|
||||
// USER_SHARED (6) — 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::SHARE_FUND_PAY, "ЦК — паевой", WalletKind::USER_SHARED },
|
||||
{ ledger2_wallets::CK_MEMBER, "ЦК — членский", WalletKind::USER_SHARED },
|
||||
{ ledger2_wallets::BILLING_FUND_PAY, "ЦК — биллинг", WalletKind::USER_SHARED },
|
||||
{ ledger2_wallets::BLAGOROST_FUND, "ЦПП «Благорост» — единый кошелёк программы у пайщика", WalletKind::USER_SHARED },
|
||||
{ ledger2_wallets::PREIMP_FUND, "Первичный учёт РИД-взносов до перехода на электронный учёт", WalletKind::USER_SHARED },
|
||||
|
||||
@@ -230,10 +232,11 @@ struct Ledger2WalletProgramMapping {
|
||||
uint64_t required_program_id; // 0 = исключение (без проверки)
|
||||
};
|
||||
|
||||
inline constexpr std::array<Ledger2WalletProgramMapping, 6> LEDGER2_USER_SHARED_PROGRAM_MAPPING = {{
|
||||
inline constexpr std::array<Ledger2WalletProgramMapping, 7> 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::SHARE_FUND_PAY, 1 /* ЦК — паевой */ },
|
||||
{ ledger2_wallets::CK_MEMBER, 1 /* ЦК — членский */ },
|
||||
{ ledger2_wallets::BILLING_FUND_PAY, 1 /* ЦК — биллинг */ },
|
||||
{ ledger2_wallets::BLAGOROST_FUND, 4 /* Благорост */ },
|
||||
{ ledger2_wallets::GENERATOR_FUND, 3 /* Генератор */ },
|
||||
{ ledger2_wallets::PREIMP_FUND, 0 /* w.cap.preimp — РИД-учёт до перехода на электронный учёт, без проверки */ },
|
||||
|
||||
+363
-917
File diff suppressed because it is too large
Load Diff
@@ -69,6 +69,7 @@ import { WalletModule } from './application/wallet/wallet.module';
|
||||
import { NotificationModule } from './application/notification/notification.module';
|
||||
import { LedgerModule } from './application/ledger/ledger.module';
|
||||
import { Ledger2Module } from './application/ledger2/ledger2.module';
|
||||
import { BillingModule } from './application/billing/billing.module';
|
||||
import { ProcessRegistryModule } from './application/process-registry/process-registry.module';
|
||||
import { BlockchainExplorerModule } from './application/blockchain-explorer/blockchain-explorer.module';
|
||||
import { ProviderModule } from './application/provider/provider.module';
|
||||
@@ -161,6 +162,10 @@ import { MutationLoggingInterceptor } from './application/common/interceptors/mu
|
||||
NotificationModule,
|
||||
LedgerModule,
|
||||
Ledger2Module,
|
||||
// Single-Hub v5: BillingModule подключается ТОЛЬКО на Восходе-хабе
|
||||
// (BILLING_HUB_MODE=true). На спицах модуль не регистрируется —
|
||||
// ни GraphQL Billing.*, ни крон-сервис, ни provider-client.
|
||||
...(process.env.BILLING_HUB_MODE === 'true' ? [BillingModule] : []),
|
||||
ProcessRegistryModule,
|
||||
BlockchainExplorerModule,
|
||||
ProviderModule,
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { BillingService } from './services/billing.service';
|
||||
import { BillingResolver } from './resolvers/billing.resolver';
|
||||
import { BillingProviderClient } from '~/infrastructure/billing/billing-provider.client';
|
||||
import { BillingCronService } from '~/domain/billing/services/billing-cron.service';
|
||||
import { ProviderModule } from '~/application/provider/provider.module';
|
||||
import { DocumentDomainModule } from '~/domain/document/document.module';
|
||||
|
||||
/**
|
||||
* Модуль billing (Epic 12 / Single-Hub v5):
|
||||
* - GraphQL-мутации оплаты подписок (convert/pay) — BillingResolver/BillingService;
|
||||
* - периодическое списание — BillingCronService + HTTP-клиент провайдера;
|
||||
* - список коопов для тика берётся из on-chain `registrator.coops` через
|
||||
* ProviderService (никаких env-CSV).
|
||||
*
|
||||
* Blockchain-порт (`BILLING_BLOCKCHAIN_PORT`) предоставляется глобальным
|
||||
* BlockchainModule. Подключается в корневой AppModule только при
|
||||
* BILLING_HUB_MODE=true.
|
||||
*/
|
||||
@Module({
|
||||
imports: [ProviderModule, DocumentDomainModule],
|
||||
providers: [BillingService, BillingResolver, BillingProviderClient, BillingCronService],
|
||||
exports: [BillingService],
|
||||
})
|
||||
export class BillingModule {}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
import { IsString, Matches, ValidateNested } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { BillingConversionStatementSignedDocumentInputDTO } from '~/application/document/documents-dto/billing-conversion-statement-document.dto';
|
||||
|
||||
/**
|
||||
* Input мутации `billingConvert` (действие `billing::convert`, operation `o.bil.fund`).
|
||||
*
|
||||
* Конвертирует паевой взнос пайщика в членский на персональный биллинг-кошелёк
|
||||
* (`w.wal.bill`). `amount` — строка с символом, как в apply: `"1500.0000 RUB"`.
|
||||
* `document` — подписанное пайщиком заявление (document2): согласие на конвертацию.
|
||||
*/
|
||||
@InputType('BillingConvertInput')
|
||||
export class BillingConvertInputDTO {
|
||||
@Field(() => String, { description: 'Имя аккаунта кооператива' })
|
||||
@IsString()
|
||||
coopname!: string;
|
||||
|
||||
@Field(() => String, { description: 'Имя аккаунта пайщика — владельца биллинг-кошелька' })
|
||||
@IsString()
|
||||
username!: string;
|
||||
|
||||
@Field(() => String, { description: 'Сумма с символом, например "1500.0000 RUB"' })
|
||||
@IsString()
|
||||
@Matches(/^\d+(\.\d+)?\s+[A-Z]{1,7}$/, { message: 'Формат "<amount> <SYMBOL>", например "1500.0000 RUB"' })
|
||||
amount!: string;
|
||||
|
||||
@Field(() => BillingConversionStatementSignedDocumentInputDTO, {
|
||||
description:
|
||||
'Подписанное пайщиком заявление 1095.BillingConversionStatement ' +
|
||||
'(document2 с типизированной meta: convert_amount).',
|
||||
})
|
||||
@ValidateNested()
|
||||
@Type(() => BillingConversionStatementSignedDocumentInputDTO)
|
||||
document!: BillingConversionStatementSignedDocumentInputDTO;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
import { IsString, Matches, MaxLength, Length } from 'class-validator';
|
||||
|
||||
/**
|
||||
* Input мутации `billingPay` (действие `billing::pay`, operation `o.bil.pay`).
|
||||
*
|
||||
* Списывает с биллинг-кошелька пайщика суммарную стоимость подписок в
|
||||
* инфраструктурный кошелёк кооператива. Идемпотентно по `paymentHash`.
|
||||
* Состав и цены подписок on-chain не передаются — только сумма + payment_hash.
|
||||
*/
|
||||
@InputType('BillingPayInput')
|
||||
export class BillingPayInputDTO {
|
||||
@Field(() => String, { description: 'Имя аккаунта кооператива' })
|
||||
@IsString()
|
||||
coopname!: string;
|
||||
|
||||
@Field(() => String, { description: 'Имя аккаунта пайщика — владельца биллинг-кошелька' })
|
||||
@IsString()
|
||||
username!: string;
|
||||
|
||||
@Field(() => String, { description: 'Сумма с символом, например "1500.0000 RUB"' })
|
||||
@IsString()
|
||||
@Matches(/^\d+(\.\d+)?\s+[A-Z]{1,7}$/, { message: 'Формат "<amount> <SYMBOL>", например "1500.0000 RUB"' })
|
||||
amount!: string;
|
||||
|
||||
@Field(() => String, {
|
||||
description: 'Детерминированный идентификатор платежа (payment_hash из getBillingSummary провайдера)',
|
||||
})
|
||||
@IsString()
|
||||
@Length(1, 64)
|
||||
paymentHash!: string;
|
||||
|
||||
@Field(() => String, { description: 'Назначение платежа' })
|
||||
@IsString()
|
||||
@MaxLength(255)
|
||||
memo!: string;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
/**
|
||||
* Результат мутаций `billingConvert` / `billingPay`.
|
||||
*
|
||||
* `transactionId` — id транзакции в чейне для трассировки.
|
||||
* `paymentHash` — для `billingPay` дублирует идентификатор платежа (для UI/сверки);
|
||||
* для `billingConvert` пуст.
|
||||
*/
|
||||
@ObjectType('BillingResult')
|
||||
export class BillingResultDTO {
|
||||
@Field(() => String)
|
||||
transactionId!: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
paymentHash?: string;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Field, ObjectType, Int, Float } from '@nestjs/graphql';
|
||||
|
||||
/**
|
||||
* Позиция разбивки «суммы к оплате» по подписке (из provider getBillingSummary).
|
||||
*/
|
||||
@ObjectType('BillingSummaryItem')
|
||||
export class BillingSummaryItemDTO {
|
||||
@Field(() => Int)
|
||||
subscriptionId!: number;
|
||||
|
||||
@Field(() => Int)
|
||||
subscriptionTypeId!: number;
|
||||
|
||||
@Field(() => String)
|
||||
subscriptionTypeName!: string;
|
||||
|
||||
@Field(() => String)
|
||||
status!: string;
|
||||
|
||||
@Field(() => Float)
|
||||
amount!: number;
|
||||
|
||||
@Field(() => Boolean)
|
||||
isFree!: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Сумма к оплате кооператива за период (проекция provider getBillingSummary в
|
||||
* GraphQL для реестра кооперативов Восхода, Epic 12 / Story 12.5).
|
||||
*/
|
||||
@ObjectType('BillingSummary')
|
||||
export class BillingSummaryDTO {
|
||||
@Field(() => String)
|
||||
coopname!: string;
|
||||
|
||||
@Field(() => Int)
|
||||
periodDays!: number;
|
||||
|
||||
@Field(() => Float)
|
||||
totalAmount!: number;
|
||||
|
||||
@Field(() => String)
|
||||
currency!: string;
|
||||
|
||||
@Field(() => [BillingSummaryItemDTO])
|
||||
items!: BillingSummaryItemDTO[];
|
||||
|
||||
@Field(() => String)
|
||||
paymentHash!: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
nextPaymentDue?: string | null;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||
import { 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 { BillingService } from '../services/billing.service';
|
||||
import { BillingConvertInputDTO } from '../dto/billing-convert-input.dto';
|
||||
import { BillingPayInputDTO } from '../dto/billing-pay-input.dto';
|
||||
import { BillingResultDTO } from '../dto/billing-result.dto';
|
||||
import { BillingSummaryDTO } from '../dto/billing-summary.dto';
|
||||
import { BillingConversionStatementGenerateDocumentInputDTO } from '~/application/document/documents-dto/billing-conversion-statement-document.dto';
|
||||
import { GenerateDocumentOptionsInputDTO } from '~/application/document/dto/generate-document-options-input.dto';
|
||||
import { GeneratedDocumentDTO } from '~/application/document/dto/generated-document.dto';
|
||||
|
||||
/**
|
||||
* GraphQL фасад billing (Epic 12 — оплата инфраструктурных подписок).
|
||||
* - `billingConvert` — пайщик конвертирует паевой взнос в членский на личный
|
||||
* биллинг-кошелёк (`w.wal.bill`), приложив подписанное заявление (document2);
|
||||
* - `billingPay` — списание стоимости подписок с биллинг-кошелька в
|
||||
* инфраструктурный кошелёк кооператива (оператор/председатель), идемпотентно.
|
||||
*
|
||||
* Подпись `coopname@active` (backend ретранслирует после JWT). Состав и цены
|
||||
* подписок on-chain не хранятся — они живут на стороне оператора (provider).
|
||||
*/
|
||||
@Resolver()
|
||||
export class BillingResolver {
|
||||
constructor(private readonly service: BillingService) {}
|
||||
|
||||
@Query(() => BillingSummaryDTO, {
|
||||
name: 'getBillingSummary',
|
||||
description:
|
||||
'Сумма к оплате кооператива за период (стоимость платных подписок, разбивка, ' +
|
||||
'дата следующего платежа, payment_hash). Источник — provider backend оператора. ' +
|
||||
'Для реестра кооперативов Восхода.',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['chairman', 'member'])
|
||||
getBillingSummary(
|
||||
@Args('coopname', { type: () => String }) coopname: string,
|
||||
@Args('period', { type: () => Number, nullable: true }) period?: number,
|
||||
): Promise<BillingSummaryDTO> {
|
||||
return this.service.getBillingSummary(coopname, period ?? 30);
|
||||
}
|
||||
|
||||
@Mutation(() => GeneratedDocumentDTO, {
|
||||
name: 'generateBillingConversionStatement',
|
||||
description:
|
||||
'Генерирует заявление 1095.BillingConversionStatement (перед подписью пайщиком) — ' +
|
||||
'аналог generateConvertToAxonStatement, канон documents-dto.',
|
||||
})
|
||||
@Throttle({ default: { limit: 3, ttl: 60000 } })
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['member', 'chairman'])
|
||||
generateBillingConversionStatement(
|
||||
@Args('data', { type: () => BillingConversionStatementGenerateDocumentInputDTO })
|
||||
data: BillingConversionStatementGenerateDocumentInputDTO,
|
||||
@Args('options', { type: () => GenerateDocumentOptionsInputDTO, nullable: true })
|
||||
options: GenerateDocumentOptionsInputDTO,
|
||||
): Promise<GeneratedDocumentDTO> {
|
||||
return this.service.generateConversionStatement(data, options);
|
||||
}
|
||||
|
||||
@Mutation(() => BillingResultDTO, {
|
||||
name: 'billingConvert',
|
||||
description:
|
||||
'Конвертация паевого взноса пайщика в членский на биллинг-кошелёк (operation o.bil.fund). ' +
|
||||
'Принимает подписанное пайщиком заявление 1095.BillingConversionStatement.',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['user', 'member', 'chairman'])
|
||||
billingConvert(
|
||||
@Args('input', { type: () => BillingConvertInputDTO }) input: BillingConvertInputDTO,
|
||||
): Promise<BillingResultDTO> {
|
||||
return this.service.convert(input);
|
||||
}
|
||||
|
||||
@Mutation(() => BillingResultDTO, {
|
||||
name: 'billingPay',
|
||||
description:
|
||||
'Списание стоимости подписок с биллинг-кошелька пайщика в инфраструктурный ' +
|
||||
'кошелёк кооператива (operation o.bil.pay). Идемпотентно по payment_hash.',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['chairman'])
|
||||
billingPay(
|
||||
@Args('input', { type: () => BillingPayInputDTO }) input: BillingPayInputDTO,
|
||||
): Promise<BillingResultDTO> {
|
||||
return this.service.pay(input);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import {
|
||||
BILLING_BLOCKCHAIN_PORT,
|
||||
type BillingBlockchainPort,
|
||||
} from '~/domain/billing/ports/billing-blockchain.port';
|
||||
import { BillingProviderClient } from '~/infrastructure/billing/billing-provider.client';
|
||||
import { DocumentDomainService } from '~/domain/document/services/document-domain.service';
|
||||
import { AmountFormatterUtils } from '~/shared/utils/amount-formatter.utils';
|
||||
import { DomainToBlockchainUtils } from '~/shared/utils/domain-to-blockchain.utils';
|
||||
import { TransactionUtils } from '~/shared/utils/transaction.utils';
|
||||
import { Cooperative } from 'cooptypes';
|
||||
import { BillingConversionStatementGenerateDocumentInputDTO } from '~/application/document/documents-dto/billing-conversion-statement-document.dto';
|
||||
import { GenerateDocumentOptionsInputDTO } from '~/application/document/dto/generate-document-options-input.dto';
|
||||
import { GeneratedDocumentDTO } from '~/application/document/dto/generated-document.dto';
|
||||
import type { BillingConvertInputDTO } from '../dto/billing-convert-input.dto';
|
||||
import type { BillingPayInputDTO } from '../dto/billing-pay-input.dto';
|
||||
import type { BillingResultDTO } from '../dto/billing-result.dto';
|
||||
import type { BillingSummaryDTO } from '../dto/billing-summary.dto';
|
||||
|
||||
/**
|
||||
* Application-сервис billing (Epic 12). Тонкий фасад: валидацию входа делают
|
||||
* class-validator-декораторы DTO, согласие пайщика (для convert) и presence-check
|
||||
* кооператива проверяет on-chain сам контракт `billing`. Сервис делегирует подпись
|
||||
* и отправку в blockchain-порт (`coopname@active`, WIF из vault).
|
||||
*/
|
||||
@Injectable()
|
||||
export class BillingService {
|
||||
constructor(
|
||||
@Inject(BILLING_BLOCKCHAIN_PORT) private readonly blockchainPort: BillingBlockchainPort,
|
||||
private readonly providerClient: BillingProviderClient,
|
||||
private readonly documentDomainService: DocumentDomainService,
|
||||
private readonly domainToBlockchainUtils: DomainToBlockchainUtils,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Генерирует заявление 1095.BillingConversionStatement — подписывает пайщик
|
||||
* на стороне desktop перед `billingConvert`. Канон: provider.service →
|
||||
* generateConvertToAxonStatement.
|
||||
*/
|
||||
async generateConversionStatement(
|
||||
data: BillingConversionStatementGenerateDocumentInputDTO,
|
||||
options: GenerateDocumentOptionsInputDTO,
|
||||
): Promise<GeneratedDocumentDTO> {
|
||||
data.registry_id = Cooperative.Registry.BillingConversionStatement.registry_id;
|
||||
data.convert_amount = AmountFormatterUtils.formatAmount(data.convert_amount);
|
||||
const document = await this.documentDomainService.generateDocument({ data, options });
|
||||
return document as unknown as GeneratedDocumentDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* Сумма к оплате кооператива за период — проекция provider getBillingSummary
|
||||
* для реестра кооперативов (сумма/дата след. платежа/free-метки/payment_hash).
|
||||
*/
|
||||
async getBillingSummary(coopname: string, periodDays = 30): Promise<BillingSummaryDTO> {
|
||||
const s = await this.providerClient.getBillingSummary(coopname, periodDays);
|
||||
return {
|
||||
coopname: s.coopname,
|
||||
periodDays: s.period_days,
|
||||
totalAmount: s.total_amount,
|
||||
currency: s.currency,
|
||||
paymentHash: s.payment_hash,
|
||||
nextPaymentDue: s.next_payment_due,
|
||||
items: (s.items ?? []).map((i) => ({
|
||||
subscriptionId: i.subscription_id,
|
||||
subscriptionTypeId: i.subscription_type_id,
|
||||
subscriptionTypeName: i.subscription_type_name,
|
||||
status: i.status,
|
||||
amount: i.amount,
|
||||
isFree: i.is_free,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
async convert(input: BillingConvertInputDTO): Promise<BillingResultDTO> {
|
||||
const result = await this.blockchainPort.convert({
|
||||
coopname: input.coopname,
|
||||
username: input.username,
|
||||
quantity: input.amount,
|
||||
document: this.domainToBlockchainUtils.convertSignedDocumentToBlockchainFormat(input.document),
|
||||
});
|
||||
return { transactionId: TransactionUtils.extractTransactionId(result) };
|
||||
}
|
||||
|
||||
async pay(input: BillingPayInputDTO): Promise<BillingResultDTO> {
|
||||
const result = await this.blockchainPort.pay({
|
||||
coopname: input.coopname,
|
||||
username: input.username,
|
||||
quantity: input.amount,
|
||||
paymentHash: input.paymentHash,
|
||||
memo: input.memo,
|
||||
});
|
||||
return {
|
||||
transactionId: TransactionUtils.extractTransactionId(result),
|
||||
paymentHash: input.paymentHash,
|
||||
};
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import { InputType, Field, IntersectionType, OmitType } from '@nestjs/graphql';
|
||||
import { IsString } from 'class-validator';
|
||||
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';
|
||||
import type { ExcludeCommonProps } from '~/application/document/types';
|
||||
|
||||
// интерфейс параметров для генерации (registry_id = 1095)
|
||||
type action = Cooperative.Registry.BillingConversionStatement.Action;
|
||||
|
||||
@InputType(`BaseBillingConversionStatementMetaDocumentInput`)
|
||||
class BaseBillingConversionStatementMetaDocumentInputDTO implements ExcludeCommonProps<action> {
|
||||
@Field({ description: 'Сумма к конвертации паевого в членский на биллинг-кошелёк' })
|
||||
@IsString()
|
||||
convert_amount!: string;
|
||||
}
|
||||
|
||||
@InputType(`BillingConversionStatementGenerateDocumentInput`)
|
||||
export class BillingConversionStatementGenerateDocumentInputDTO
|
||||
extends IntersectionType(
|
||||
BaseBillingConversionStatementMetaDocumentInputDTO,
|
||||
OmitType(GenerateMetaDocumentInputDTO, ['registry_id'] as const)
|
||||
)
|
||||
implements action
|
||||
{
|
||||
registry_id!: number;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
@InputType(`BillingConversionStatementSignedMetaDocumentInput`)
|
||||
export class BillingConversionStatementSignedMetaDocumentInputDTO
|
||||
extends IntersectionType(BaseBillingConversionStatementMetaDocumentInputDTO, MetaDocumentInputDTO)
|
||||
implements action {}
|
||||
|
||||
@InputType(`BillingConversionStatementSignedDocumentInput`)
|
||||
export class BillingConversionStatementSignedDocumentInputDTO extends SignedDigitalDocumentInputDTO {
|
||||
@Field(() => BillingConversionStatementSignedMetaDocumentInputDTO, {
|
||||
description: 'Метаинформация заявления на конвертацию паевого в биллинг-кошелёк',
|
||||
})
|
||||
public readonly meta!: BillingConversionStatementSignedMetaDocumentInputDTO;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||
import { createHash, randomBytes } from 'node:crypto';
|
||||
import { Ledger2 } from 'cooptypes';
|
||||
import { TransactionUtils } from '~/shared/utils/transaction.utils';
|
||||
import {
|
||||
LEDGER2_STATE_PORT,
|
||||
type Ledger2StatePort,
|
||||
@@ -136,7 +137,7 @@ export class Ledger2Service {
|
||||
|
||||
return {
|
||||
processHash,
|
||||
transactionId: this.extractTransactionId(result),
|
||||
transactionId: TransactionUtils.extractTransactionId(result),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -185,16 +186,4 @@ export class Ledger2Service {
|
||||
private generateProcessHash(): string {
|
||||
return createHash('sha256').update(randomBytes(32)).digest('hex');
|
||||
}
|
||||
|
||||
private extractTransactionId(result: unknown): string {
|
||||
if (result && typeof result === 'object' && 'transaction_id' in result) {
|
||||
const tx = (result as { transaction_id?: unknown }).transaction_id;
|
||||
if (typeof tx === 'string') return tx;
|
||||
}
|
||||
if (result && typeof result === 'object' && 'response' in result) {
|
||||
const resp = (result as { response?: { transaction_id?: unknown } }).response;
|
||||
if (resp && typeof resp.transaction_id === 'string') return resp.transaction_id;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
import { ProviderSubscriptionDTO } from './provider-subscription.dto';
|
||||
|
||||
/**
|
||||
* Элемент реестра кооперативов для оператора (Восход).
|
||||
*
|
||||
* Сводит в одну запись:
|
||||
* - on-chain данные кооператива из registrator.coops (статус pending|active|blocked, домен, дата);
|
||||
* - данные провайдера (подписки/инстанс/биллинг) из provider-backend, привязанные по coopname.
|
||||
*
|
||||
* Источник on-chain: BlockchainPort.getAllRows(registrator, coops).
|
||||
* Источник provider: ProviderService.getUserSubscriptions(coopname) (server-secret канал).
|
||||
*/
|
||||
@ObjectType('CooperativeRegistryItem')
|
||||
export class CooperativeRegistryItemDTO {
|
||||
@Field(() => String, { description: 'Имя аккаунта кооператива (coopname)' })
|
||||
coopname!: string;
|
||||
|
||||
@Field(() => String, { nullable: true, description: 'Наименование организации кооператива' })
|
||||
name?: string;
|
||||
|
||||
@Field(() => String, { nullable: true, description: 'Анонсированный домен/сайт кооператива' })
|
||||
announce?: string;
|
||||
|
||||
@Field(() => String, { description: 'Статус кооператива в блокчейне: pending | active | blocked' })
|
||||
status!: string;
|
||||
|
||||
@Field(() => String, { nullable: true, description: 'Дата регистрации заявки кооператива (on-chain)' })
|
||||
created_at?: string;
|
||||
|
||||
@Field(() => Boolean, { description: 'Есть ли у кооператива данные провайдера (хотя бы одна подписка)' })
|
||||
has_provider_data!: boolean;
|
||||
|
||||
@Field(() => [ProviderSubscriptionDTO], { description: 'Подписки кооператива у провайдера (с инстансом и биллингом)' })
|
||||
subscriptions!: ProviderSubscriptionDTO[];
|
||||
}
|
||||
@@ -3,11 +3,12 @@ import { ProviderService } from './services/provider.service';
|
||||
import { ProviderResolver } from './resolvers/provider.resolver';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { DocumentDomainModule } from '~/domain/document/document.module';
|
||||
import { BlockchainModule } from '~/infrastructure/blockchain/blockchain.module';
|
||||
// Импортируем для регистрации GraphQL enum
|
||||
import '~/domain/instance-status.enum';
|
||||
|
||||
@Module({
|
||||
imports: [ConfigModule, DocumentDomainModule],
|
||||
imports: [ConfigModule, DocumentDomainModule, BlockchainModule],
|
||||
providers: [ProviderService, ProviderResolver],
|
||||
exports: [ProviderService],
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@ import { UseGuards } from '@nestjs/common';
|
||||
import { ProviderService } from '../services/provider.service';
|
||||
import { ProviderSubscriptionDTO } from '../dto/provider-subscription.dto';
|
||||
import { CurrentInstanceDTO } from '../dto/current-instance.dto';
|
||||
import { CooperativeRegistryItemDTO } from '../dto/cooperative-registry-item.dto';
|
||||
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';
|
||||
@@ -18,6 +19,16 @@ import { Throttle } from '@nestjs/throttler';
|
||||
export class ProviderResolver {
|
||||
constructor(private readonly providerService: ProviderService) {}
|
||||
|
||||
@Query(() => [CooperativeRegistryItemDTO], {
|
||||
name: 'getCooperativesRegistry',
|
||||
description: 'Реестр кооперативов оператора: список кооперативов из блокчейна с данными провайдера (подписки, инстанс, биллинг)',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['member', 'chairman'])
|
||||
async getCooperativesRegistry(): Promise<CooperativeRegistryItemDTO[]> {
|
||||
return this.providerService.getCooperativesRegistry();
|
||||
}
|
||||
|
||||
@Query(() => [ProviderSubscriptionDTO], {
|
||||
name: 'getProviderSubscriptions',
|
||||
description: 'Получить подписки пользователя у провайдера',
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { Injectable, Logger, BadRequestException, Inject } from '@nestjs/common';
|
||||
import { RegistratorContract } from 'cooptypes';
|
||||
import { ProviderSubscriptionDTO } from '../dto/provider-subscription.dto';
|
||||
import { CurrentInstanceDTO } from '../dto/current-instance.dto';
|
||||
import { CooperativeRegistryItemDTO } from '../dto/cooperative-registry-item.dto';
|
||||
import { InstanceStatus } from '~/domain/instance-status.enum';
|
||||
import { Client, configureClient } from '@coopenomics/provider-client';
|
||||
import { config } from '~/config';
|
||||
import { BLOCKCHAIN_PORT, type BlockchainPort } from '~/domain/common/ports/blockchain.port';
|
||||
import { ORGANIZATION_REPOSITORY, type OrganizationRepository } from '~/domain/common/repositories/organization.repository';
|
||||
import { DocumentDomainService } from '~/domain/document/services/document-domain.service';
|
||||
import { ConvertToAxonStatementGenerateDocumentInputDTO } from '~/application/document/documents-dto/convert-to-axon-statement-document.dto';
|
||||
import { GenerateDocumentOptionsInputDTO } from '~/application/document/dto/generate-document-options-input.dto';
|
||||
@@ -19,7 +23,9 @@ export class ProviderService {
|
||||
|
||||
constructor(
|
||||
private readonly documentDomainService: DocumentDomainService,
|
||||
@Inject(SYSTEM_BLOCKCHAIN_PORT) private readonly systemBlockchainPort: SystemBlockchainPort
|
||||
@Inject(SYSTEM_BLOCKCHAIN_PORT) private readonly systemBlockchainPort: SystemBlockchainPort,
|
||||
@Inject(BLOCKCHAIN_PORT) private readonly blockchainPort: BlockchainPort,
|
||||
@Inject(ORGANIZATION_REPOSITORY) private readonly organizationRepository: OrganizationRepository
|
||||
) {
|
||||
// Проверяем наличие PROVIDER_BASE_URL
|
||||
const providerBaseUrl = config.provider_base_url;
|
||||
@@ -43,6 +49,64 @@ export class ProviderService {
|
||||
return config.provider_base_url !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Реестр кооперативов для оператора: сводит on-chain список кооперативов
|
||||
* (registrator.coops) с данными провайдера (подписки/инстанс/биллинг по coopname).
|
||||
*
|
||||
* On-chain — источник истины по статусу кооператива (pending|active|blocked).
|
||||
* Provider — источник данных подписок; если провайдер не настроен или у кооператива
|
||||
* нет подписки, поле subscriptions остаётся пустым (кооператив всё равно в списке).
|
||||
*/
|
||||
async getCooperativesRegistry(): Promise<CooperativeRegistryItemDTO[]> {
|
||||
const coops = (await this.blockchainPort.getAllRows(
|
||||
RegistratorContract.contractName.production,
|
||||
RegistratorContract.contractName.production,
|
||||
RegistratorContract.Tables.Cooperatives.tableName
|
||||
)) as RegistratorContract.Tables.Cooperatives.ICooperative[];
|
||||
|
||||
const providerAvailable = this.isProviderAvailable();
|
||||
|
||||
return Promise.all(
|
||||
coops.map(async (coop) => {
|
||||
const item = new CooperativeRegistryItemDTO();
|
||||
item.coopname = coop.username;
|
||||
item.name = await this.resolveOrganizationName(coop.username, coop.description);
|
||||
item.announce = coop.announce;
|
||||
item.status = coop.status;
|
||||
item.created_at = coop.created_at;
|
||||
item.subscriptions = [];
|
||||
|
||||
if (providerAvailable) {
|
||||
try {
|
||||
item.subscriptions = await this.getUserSubscriptions(coop.username);
|
||||
} catch (error: any) {
|
||||
// Провайдер недоступен или у кооператива нет подписок — не роняем весь реестр.
|
||||
this.logger.warn(`Не удалось получить подписки провайдера для ${coop.username}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
item.has_provider_data = item.subscriptions.length > 0;
|
||||
return item;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Извлечь человеко-читаемое наименование кооператива. Сначала пробуем
|
||||
* organization-запись в Mongo (short_name → full_name), потом on-chain
|
||||
* description; если и его нет — null, фронт сам отрисует coopname.
|
||||
*/
|
||||
private async resolveOrganizationName(username: string, onChainDescription: string): Promise<string | undefined> {
|
||||
try {
|
||||
const organization = await this.organizationRepository.findByUsername(username);
|
||||
const name = organization?.short_name || organization?.full_name;
|
||||
if (name) return name;
|
||||
} catch {
|
||||
// organization ещё не зарегистрирована — fallback на on-chain
|
||||
}
|
||||
return onChainDescription || undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить подписки пользователя по username
|
||||
*/
|
||||
|
||||
@@ -87,6 +87,24 @@ const envVarsSchema = z.object({
|
||||
.describe('адрес сервиса GRAPHQL'),
|
||||
PROVIDER_BASE_URL: z.string().default('').describe('базовый URL сервиса провайдера'),
|
||||
|
||||
// Billing Single-Hub v5: BillingModule подключается только на Восходе-хабе.
|
||||
// На спицах флаг false → модуль вообще не регистрируется, endpoints/cron
|
||||
// не появляются. Antelope не поддерживает deferred_trx — тик крона
|
||||
// инициирует backend.
|
||||
BILLING_HUB_MODE: z
|
||||
.string()
|
||||
.default('false')
|
||||
.transform((v) => v === 'true' || v === '1')
|
||||
.describe('узел работает как биллинг-хаб (только Восход): включает BillingModule, крон и GraphQL Billing.*'),
|
||||
BILLING_CRON_EXPRESSION: z
|
||||
.string()
|
||||
.default('0 * * * *')
|
||||
.describe('cron-выражение тика биллинга (по умолчанию ежечасно)'),
|
||||
BILLING_CRON_PAYER: z
|
||||
.string()
|
||||
.default('')
|
||||
.describe('пайщик-плательщик, чей w.wal.bill дебетуется (пусто — списание пропускается)'),
|
||||
|
||||
// Параметры союза кооперативов
|
||||
UNION_LINK: z
|
||||
.string()
|
||||
@@ -262,6 +280,13 @@ export default {
|
||||
coopname: envVars.data.COOPNAME,
|
||||
graphql_service: envVars.data.GRAPHQL_SERVICE,
|
||||
provider_base_url: envVars.data.PROVIDER_BASE_URL,
|
||||
billing: {
|
||||
hub_mode: envVars.data.BILLING_HUB_MODE,
|
||||
cron_expression: envVars.data.BILLING_CRON_EXPRESSION,
|
||||
// Список коопов — из on-chain `registrator.coops` через ProviderService
|
||||
// (см. BillingCronService.activeCoopnames). Env-override отсутствует.
|
||||
payer: envVars.data.BILLING_CRON_PAYER,
|
||||
},
|
||||
union: {
|
||||
link: envVars.data.UNION_LINK,
|
||||
is_unioned: envVars.data.IS_UNIONED,
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { TransactionResult } from '~/domain/blockchain/types/transaction-result.type';
|
||||
import type { Cooperative } from 'cooptypes';
|
||||
|
||||
/**
|
||||
* Параметры конвертации паевого взноса пайщика в членский на биллинг-кошелёк
|
||||
* (`w.wal.bill`) — действие `billing::convert` (operation `o.bil.fund`).
|
||||
* Несёт подписанное пайщиком заявление (`document`).
|
||||
*/
|
||||
export interface BillingConvertBlockchainDomainInterface {
|
||||
coopname: string;
|
||||
/** Пайщик-владелец биллинг-кошелька. */
|
||||
username: string;
|
||||
/** Сумма с символом, например `"1500.0000 RUB"`. */
|
||||
quantity: string;
|
||||
/** Подписанное пайщиком заявление (document2). */
|
||||
document: Cooperative.Document.IChainDocument2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Параметры списания стоимости подписок с биллинг-кошелька пайщика в
|
||||
* инфраструктурный кошелёк кооператива — действие `billing::pay`
|
||||
* (operation `o.bil.pay`). Идемпотентно по `paymentHash`.
|
||||
*/
|
||||
export interface BillingPayBlockchainDomainInterface {
|
||||
coopname: string;
|
||||
username: string;
|
||||
/** Сумма с символом, например `"1500.0000 RUB"`. */
|
||||
quantity: string;
|
||||
/** Детерминированный идентификатор платежа (из provider getBillingSummary). */
|
||||
paymentHash: string;
|
||||
memo: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Порт billing для записи в блокчейн. Подпись `coopname@active` (WIF из vault);
|
||||
* согласие пайщика для convert несёт `document`.
|
||||
*/
|
||||
export interface BillingBlockchainPort {
|
||||
convert(data: BillingConvertBlockchainDomainInterface): Promise<TransactionResult>;
|
||||
pay(data: BillingPayBlockchainDomainInterface): Promise<TransactionResult>;
|
||||
}
|
||||
|
||||
export const BILLING_BLOCKCHAIN_PORT = Symbol('BILLING_BLOCKCHAIN_PORT');
|
||||
@@ -0,0 +1,153 @@
|
||||
import { Injectable, OnModuleInit, OnModuleDestroy, Logger, Inject } from '@nestjs/common';
|
||||
import cron from 'node-cron';
|
||||
import config from '~/config/config';
|
||||
import {
|
||||
BILLING_BLOCKCHAIN_PORT,
|
||||
type BillingBlockchainPort,
|
||||
} from '~/domain/billing/ports/billing-blockchain.port';
|
||||
import { BillingProviderClient } from '~/infrastructure/billing/billing-provider.client';
|
||||
import { ProviderService } from '~/application/provider/services/provider.service';
|
||||
|
||||
/**
|
||||
* Периодическое списание подписок (Epic 12, Story 12.6) — oracle-паттерн.
|
||||
*
|
||||
* Antelope не поддерживает deferred_trx, поэтому рекуррентность инициирует
|
||||
* backend: на каждый тик узел запрашивает у провайдера «сумму к оплате»
|
||||
* (источник истины по составу/ценам — provider, on-chain их нет), и если есть
|
||||
* что списывать и срок подошёл — проводит on-chain `billing::pay`, затем
|
||||
* подтверждает платёж провайдеру (тот продлевает подписки по `payment_hash`).
|
||||
*
|
||||
* Инварианты:
|
||||
* - сумма = 0 (все подписки free) → on-chain списание НЕ выполняется;
|
||||
* - идемпотентность по `payment_hash` (контракт no-op + провайдер идемпотентен);
|
||||
* - падение `pay` не зацикливает узел: ошибка логируется, тик продолжает
|
||||
* следующий кооператив (grace/уведомления — на стороне провайдера/Epic 9).
|
||||
*
|
||||
* Плательщик (`config.billing.payer`) — пайщик, чей USER_SHARED-кошелёк
|
||||
* `w.wal.bill` дебетуется. Списание per-пайщик, поэтому без указанного
|
||||
* плательщика тик пропускается (см. конфиг `BILLING_CRON_PAYER`).
|
||||
*/
|
||||
@Injectable()
|
||||
export class BillingCronService implements OnModuleInit, OnModuleDestroy {
|
||||
private readonly logger = new Logger(BillingCronService.name);
|
||||
private cronJob: cron.ScheduledTask | null = null;
|
||||
private running = false;
|
||||
|
||||
constructor(
|
||||
@Inject(BILLING_BLOCKCHAIN_PORT) private readonly blockchainPort: BillingBlockchainPort,
|
||||
private readonly providerClient: BillingProviderClient,
|
||||
private readonly providerService: ProviderService,
|
||||
) {}
|
||||
|
||||
onModuleInit() {
|
||||
if (!this.providerClient.isConfigured()) {
|
||||
this.logger.warn('BillingCronService: PROVIDER_BASE_URL не задан — тик не будет запущен');
|
||||
return;
|
||||
}
|
||||
if (!cron.validate(config.billing.cron_expression)) {
|
||||
this.logger.error(`BillingCronService: некорректное cron-выражение "${config.billing.cron_expression}"`);
|
||||
return;
|
||||
}
|
||||
this.cronJob = cron.schedule(config.billing.cron_expression, () => {
|
||||
void this.tick();
|
||||
});
|
||||
this.logger.log(`BillingCronService запущен (cron="${config.billing.cron_expression}")`);
|
||||
}
|
||||
|
||||
onModuleDestroy() {
|
||||
if (this.cronJob) {
|
||||
this.cronJob.stop();
|
||||
this.cronJob = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Список коопов для тика берётся ИЗ on-chain `registrator.coops`
|
||||
* (через ProviderService), отфильтрованный по `status === 'active'`.
|
||||
* Никаких env-CSV — кооперативы всегда есть в блокчейне.
|
||||
*/
|
||||
private async activeCoopnames(): Promise<string[]> {
|
||||
const registry = await this.providerService.getCooperativesRegistry();
|
||||
return registry.filter((c) => c.status === 'active').map((c) => c.coopname);
|
||||
}
|
||||
|
||||
/**
|
||||
* Один тик: обходит активные коопы из on-chain реестра, списывает подошедшие
|
||||
* к оплате. Защита от наложения тиков (`running`) — если предыдущий ещё идёт,
|
||||
* пропускаем.
|
||||
*/
|
||||
async tick(): Promise<void> {
|
||||
if (this.running) {
|
||||
this.logger.warn('BillingCronService: предыдущий тик ещё выполняется — пропуск');
|
||||
return;
|
||||
}
|
||||
const payer = config.billing.payer;
|
||||
if (!payer) {
|
||||
this.logger.warn('BillingCronService: BILLING_CRON_PAYER не задан — нечего дебетовать, пропуск тика');
|
||||
return;
|
||||
}
|
||||
this.running = true;
|
||||
try {
|
||||
const coopnames = await this.activeCoopnames();
|
||||
for (const coopname of coopnames) {
|
||||
await this.processCoop(coopname, payer);
|
||||
}
|
||||
} finally {
|
||||
this.running = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async processCoop(coopname: string, payer: string): Promise<void> {
|
||||
try {
|
||||
const summary = await this.providerClient.getBillingSummary(coopname);
|
||||
|
||||
if (!summary || summary.total_amount <= 0) {
|
||||
return; // всё free или нет подписок — пустые транзакции не гоняем
|
||||
}
|
||||
if (!this.isDue(summary.next_payment_due)) {
|
||||
return; // срок ещё не подошёл
|
||||
}
|
||||
|
||||
const quantity = `${summary.total_amount.toFixed(config.blockchain.root_govern_precision)} ${summary.currency || config.blockchain.root_govern_symbol}`;
|
||||
|
||||
const result = await this.blockchainPort.pay({
|
||||
coopname,
|
||||
username: payer,
|
||||
quantity,
|
||||
paymentHash: summary.payment_hash,
|
||||
memo: `Оплата подписок за ${summary.period_days} дн.`,
|
||||
});
|
||||
|
||||
const transactionId =
|
||||
result && typeof result === 'object' && 'transaction_id' in result
|
||||
? String((result as { transaction_id?: unknown }).transaction_id ?? '')
|
||||
: '';
|
||||
|
||||
await this.providerClient.confirmPayment({
|
||||
coopname,
|
||||
paymentHash: summary.payment_hash,
|
||||
amount: summary.total_amount,
|
||||
blockchainTransactionId: transactionId,
|
||||
periodDays: summary.period_days,
|
||||
});
|
||||
|
||||
this.logger.log(`Списано ${quantity} за подписки ${coopname} (payment_hash=${summary.payment_hash})`);
|
||||
} catch (error: any) {
|
||||
// Падение списания (например, недостаток средств на w.wal.bill) не должно
|
||||
// зацикливать узел: фиксируем и продолжаем. Перевод подписки в past_due/grace
|
||||
// и уведомления — на стороне провайдера (Epic 4/9).
|
||||
this.logger.error(`BillingCronService: списание для ${coopname} не выполнено: ${error?.message ?? error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Срок оплаты подошёл, если дата следующего платежа не задана (первое списание)
|
||||
* либо она в прошлом/сегодня.
|
||||
*/
|
||||
private isDue(nextPaymentDue: string | null): boolean {
|
||||
if (!nextPaymentDue) return true;
|
||||
const due = new Date(nextPaymentDue).getTime();
|
||||
if (Number.isNaN(due)) return true;
|
||||
return due <= Date.now();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import axios from 'axios';
|
||||
import config from '~/config/config';
|
||||
|
||||
/**
|
||||
* Позиция разбивки суммы к оплате (из provider getBillingSummary, Story 12.5).
|
||||
*/
|
||||
export interface ProviderBillingSummaryItem {
|
||||
subscription_id: number;
|
||||
subscription_type_id: number;
|
||||
subscription_type_name: string;
|
||||
status: string;
|
||||
amount: number;
|
||||
is_free: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ответ агрегатора «сумма к оплате» провайдера (Story 12.5).
|
||||
*/
|
||||
export interface ProviderBillingSummary {
|
||||
coopname: string;
|
||||
period_days: number;
|
||||
total_amount: number;
|
||||
currency: string;
|
||||
items: ProviderBillingSummaryItem[];
|
||||
payment_hash: string;
|
||||
next_payment_due: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP-клиент к provider backend (Восход) для биллинга подписок (Epic 12).
|
||||
*
|
||||
* Провайдер — источник истины по составу/ценам подписок (on-chain их нет).
|
||||
* Доступ защищён `server-secret` (ServerSecretGuard на стороне провайдера).
|
||||
* `provider_base_url` и `server_secret` берутся из конфига узла.
|
||||
*/
|
||||
@Injectable()
|
||||
export class BillingProviderClient {
|
||||
private readonly logger = new Logger(BillingProviderClient.name);
|
||||
|
||||
private get baseUrl(): string {
|
||||
return config.provider_base_url.replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
private headers() {
|
||||
return { 'server-secret': config.server_secret };
|
||||
}
|
||||
|
||||
isConfigured(): boolean {
|
||||
return this.baseUrl.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Сумма к оплате кооператива за период (по умолчанию 30 дней).
|
||||
*/
|
||||
async getBillingSummary(coopname: string, periodDays = 30): Promise<ProviderBillingSummary> {
|
||||
const url = `${this.baseUrl}/subscriptions/billing-summary/${coopname}`;
|
||||
const { data } = await axios.get<ProviderBillingSummary>(url, {
|
||||
params: { period: periodDays },
|
||||
headers: this.headers(),
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Подтверждение проведённого on-chain платежа: провайдер фиксирует факт и
|
||||
* продлевает (`extend`) все подписки, входившие в `payment_hash`. Идемпотентно
|
||||
* по `transaction_id = payment_hash` (Epic 3 / Story 12.5).
|
||||
*/
|
||||
async confirmPayment(input: {
|
||||
coopname: string;
|
||||
paymentHash: string;
|
||||
amount: number;
|
||||
blockchainTransactionId: string;
|
||||
periodDays?: number;
|
||||
}): Promise<void> {
|
||||
const url = `${this.baseUrl}/subscriptions/confirm-batch-payment`;
|
||||
await axios.post(
|
||||
url,
|
||||
{
|
||||
coopname: input.coopname,
|
||||
transaction_id: input.paymentHash,
|
||||
payment_hash: input.paymentHash,
|
||||
amount: input.amount,
|
||||
blockchain_transaction_id: input.blockchainTransactionId,
|
||||
period_days: input.periodDays ?? 30,
|
||||
},
|
||||
{ headers: this.headers() },
|
||||
);
|
||||
this.logger.log(`confirmPayment ${input.coopname} payment_hash=${input.paymentHash}`);
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import httpStatus from 'http-status';
|
||||
import { BillingContract } from 'cooptypes';
|
||||
import { TransactResult } from '@wharfkit/session';
|
||||
import { BlockchainService } from '../blockchain.service';
|
||||
import { VAULT_DOMAIN_SERVICE, VaultDomainService } from '~/domain/vault/services/vault-domain.service';
|
||||
import { HttpApiError } from '~/utils/httpApiError';
|
||||
import type { TransactionResult } from '~/domain/blockchain/types/transaction-result.type';
|
||||
import type {
|
||||
BillingBlockchainPort,
|
||||
BillingConvertBlockchainDomainInterface,
|
||||
BillingPayBlockchainDomainInterface,
|
||||
} from '~/domain/billing/ports/billing-blockchain.port';
|
||||
import { DomainToBlockchainUtils } from '~/shared/utils/domain-to-blockchain.utils';
|
||||
|
||||
/**
|
||||
* Блокчейн-адаптер billing (Epic 12) — оплата подписок членскими взносами.
|
||||
*
|
||||
* Подпись `coopname@active` через `BlockchainService.transact` с WIF из vault.
|
||||
* Имена действий и payload — из cooptypes (`BillingContract.Actions.{Convert,Pay}`),
|
||||
* без сырых строк. Состав/цены подписок on-chain не передаются — только сумма,
|
||||
* payment_hash и memo.
|
||||
*/
|
||||
@Injectable()
|
||||
export class BillingBlockchainAdapter implements BillingBlockchainPort {
|
||||
private readonly logger = new Logger(BillingBlockchainAdapter.name);
|
||||
|
||||
constructor(
|
||||
private readonly blockchainService: BlockchainService,
|
||||
private readonly domainToBlockchainUtils: DomainToBlockchainUtils,
|
||||
@Inject(VAULT_DOMAIN_SERVICE) private readonly vaultDomainService: VaultDomainService,
|
||||
) {}
|
||||
|
||||
private async initForCoop(coopname: string): Promise<void> {
|
||||
const wif = await this.vaultDomainService.getWif(coopname);
|
||||
if (!wif) {
|
||||
throw new HttpApiError(httpStatus.BAD_GATEWAY, 'Не найден приватный ключ кооператива для подписания биллинг-операции');
|
||||
}
|
||||
this.blockchainService.initialize(coopname, wif);
|
||||
}
|
||||
|
||||
async convert(data: BillingConvertBlockchainDomainInterface): Promise<TransactionResult> {
|
||||
await this.initForCoop(data.coopname);
|
||||
const formattedQuantity = this.domainToBlockchainUtils.formatQuantityWithPrecision(data.quantity);
|
||||
|
||||
const payload: BillingContract.Actions.Convert.IConvert = {
|
||||
coopname: data.coopname,
|
||||
username: data.username,
|
||||
amount: formattedQuantity,
|
||||
document: data.document as BillingContract.Actions.Convert.IConvert['document'],
|
||||
};
|
||||
|
||||
const result = (await this.blockchainService.transact({
|
||||
account: BillingContract.contractName.production,
|
||||
name: BillingContract.Actions.Convert.actionName,
|
||||
authorization: [{ actor: data.coopname, permission: 'active' }],
|
||||
data: payload,
|
||||
})) as TransactResult;
|
||||
|
||||
this.logger.log(`billing::convert ${data.coopname}/${data.username} ${formattedQuantity}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
async pay(data: BillingPayBlockchainDomainInterface): Promise<TransactionResult> {
|
||||
await this.initForCoop(data.coopname);
|
||||
const formattedQuantity = this.domainToBlockchainUtils.formatQuantityWithPrecision(data.quantity);
|
||||
|
||||
const payload: BillingContract.Actions.Pay.IPay = {
|
||||
coopname: data.coopname,
|
||||
username: data.username,
|
||||
amount: formattedQuantity,
|
||||
payment_hash: data.paymentHash,
|
||||
memo: data.memo,
|
||||
};
|
||||
|
||||
const result = (await this.blockchainService.transact({
|
||||
account: BillingContract.contractName.production,
|
||||
name: BillingContract.Actions.Pay.actionName,
|
||||
authorization: [{ actor: data.coopname, permission: 'active' }],
|
||||
data: payload,
|
||||
})) as TransactResult;
|
||||
|
||||
this.logger.log(`billing::pay ${data.coopname}/${data.username} ${formattedQuantity}, payment_hash=${data.paymentHash}`);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,8 @@ import { LedgerBlockchainAdapter } from './adapters/ledger-blockchain.adapter';
|
||||
import { LEDGER_BLOCKCHAIN_PORT } from '~/domain/ledger/ports/ledger.port';
|
||||
import { Ledger2BlockchainAdapter } from './adapters/ledger2-blockchain.adapter';
|
||||
import { LEDGER2_BLOCKCHAIN_PORT } from '~/domain/ledger2/ports/ledger2-blockchain.port';
|
||||
import { BillingBlockchainAdapter } from './adapters/billing-blockchain.adapter';
|
||||
import { BILLING_BLOCKCHAIN_PORT } from '~/domain/billing/ports/billing-blockchain.port';
|
||||
import { SovietContractInfoService } from './services/soviet-contract-info.service';
|
||||
import { WalletContractInfoService } from './services/wallet-contract-info.service';
|
||||
import { Ledger2ContractInfoService } from './services/ledger2-contract-info.service';
|
||||
@@ -88,6 +90,11 @@ import { Ledger2ContractInfoService } from './services/ledger2-contract-info.ser
|
||||
provide: LEDGER2_BLOCKCHAIN_PORT,
|
||||
useClass: Ledger2BlockchainAdapter,
|
||||
},
|
||||
// Провайдеры для billing (оплата подписок — convert/pay, Epic 12)
|
||||
{
|
||||
provide: BILLING_BLOCKCHAIN_PORT,
|
||||
useClass: BillingBlockchainAdapter,
|
||||
},
|
||||
DomainToBlockchainUtils,
|
||||
SovietContractInfoService,
|
||||
WalletContractInfoService,
|
||||
@@ -108,6 +115,7 @@ import { Ledger2ContractInfoService } from './services/ledger2-contract-info.ser
|
||||
WALLET_BLOCKCHAIN_PORT,
|
||||
LEDGER_BLOCKCHAIN_PORT,
|
||||
LEDGER2_BLOCKCHAIN_PORT,
|
||||
BILLING_BLOCKCHAIN_PORT,
|
||||
DomainToBlockchainUtils,
|
||||
SovietContractInfoService,
|
||||
WalletContractInfoService,
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Утилиты для работы с результатом on-chain транзакции, который возвращают
|
||||
* blockchain-порты (`BillingBlockchainPort`, `Ledger2BlockchainPort` и т.п.).
|
||||
* Форма ответа Wharfkit/Antelope разная: иногда `{ transaction_id }` прямо
|
||||
* в корне, иногда `{ response: { transaction_id } }`. Выносим разбор в одно
|
||||
* место, чтобы сервисы не дублировали.
|
||||
*/
|
||||
export class TransactionUtils {
|
||||
/**
|
||||
* Извлекает on-chain `transaction_id` из произвольного объекта-результата
|
||||
* подписи/отправки. Возвращает пустую строку, если поле не найдено.
|
||||
*/
|
||||
static extractTransactionId(result: unknown): string {
|
||||
if (result && typeof result === 'object' && 'transaction_id' in result) {
|
||||
const tx = (result as { transaction_id?: unknown }).transaction_id;
|
||||
if (typeof tx === 'string') return tx;
|
||||
}
|
||||
if (result && typeof result === 'object' && 'response' in result) {
|
||||
const resp = (result as { response?: { transaction_id?: unknown } }).response;
|
||||
if (resp && typeof resp.transaction_id === 'string') return resp.transaction_id;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -127,6 +127,22 @@ export const AllTypesProps: Record<string,any> = {
|
||||
},
|
||||
BankAccountInput:{
|
||||
details:"BankAccountDetailsInput"
|
||||
},
|
||||
BillingConversionStatementGenerateDocumentInput:{
|
||||
|
||||
},
|
||||
BillingConversionStatementSignedDocumentInput:{
|
||||
meta:"BillingConversionStatementSignedMetaDocumentInput",
|
||||
signatures:"SignatureInfoInput"
|
||||
},
|
||||
BillingConversionStatementSignedMetaDocumentInput:{
|
||||
|
||||
},
|
||||
BillingConvertInput:{
|
||||
document:"BillingConversionStatementSignedDocumentInput"
|
||||
},
|
||||
BillingPayInput:{
|
||||
|
||||
},
|
||||
BuhotchSignerType: "enum" as const,
|
||||
CalculateVotesInput:{
|
||||
@@ -644,6 +660,12 @@ export const AllTypesProps: Record<string,any> = {
|
||||
addTrustedAccount:{
|
||||
data:"AddTrustedAccountInput"
|
||||
},
|
||||
billingConvert:{
|
||||
input:"BillingConvertInput"
|
||||
},
|
||||
billingPay:{
|
||||
input:"BillingPayInput"
|
||||
},
|
||||
cancelRequest:{
|
||||
data:"CancelRequestInput"
|
||||
},
|
||||
@@ -1027,6 +1049,10 @@ export const AllTypesProps: Record<string,any> = {
|
||||
data:"AnnualGeneralMeetingVotingBallotGenerateDocumentInput",
|
||||
options:"GenerateDocumentOptionsInput"
|
||||
},
|
||||
generateBillingConversionStatement:{
|
||||
data:"BillingConversionStatementGenerateDocumentInput",
|
||||
options:"GenerateDocumentOptionsInput"
|
||||
},
|
||||
generateConvertToAxonStatement:{
|
||||
data:"ConvertToAxonStatementGenerateDocumentInput",
|
||||
options:"GenerateDocumentOptionsInput"
|
||||
@@ -1501,6 +1527,9 @@ export const AllTypesProps: Record<string,any> = {
|
||||
getActions:{
|
||||
filters:"ActionFiltersInput",
|
||||
pagination:"PaginationInput"
|
||||
},
|
||||
getBillingSummary:{
|
||||
|
||||
},
|
||||
getBranches:{
|
||||
data:"GetBranchesInput"
|
||||
@@ -2112,6 +2141,27 @@ export const ReturnTypes: Record<string,any> = {
|
||||
title:"String",
|
||||
voting:"CapitalProjectVotingData"
|
||||
},
|
||||
BillingResult:{
|
||||
paymentHash:"String",
|
||||
transactionId:"String"
|
||||
},
|
||||
BillingSummary:{
|
||||
coopname:"String",
|
||||
currency:"String",
|
||||
items:"BillingSummaryItem",
|
||||
nextPaymentDue:"String",
|
||||
paymentHash:"String",
|
||||
periodDays:"Int",
|
||||
totalAmount:"Float"
|
||||
},
|
||||
BillingSummaryItem:{
|
||||
amount:"Float",
|
||||
isFree:"Boolean",
|
||||
status:"String",
|
||||
subscriptionId:"Int",
|
||||
subscriptionTypeId:"Int",
|
||||
subscriptionTypeName:"String"
|
||||
},
|
||||
BlockchainAccount:{
|
||||
account_name:"String",
|
||||
core_liquid_balance:"String",
|
||||
@@ -3015,6 +3065,15 @@ export const ReturnTypes: Record<string,any> = {
|
||||
is_active:"Boolean",
|
||||
program_type:"String"
|
||||
},
|
||||
CooperativeRegistryItem:{
|
||||
announce:"String",
|
||||
coopname:"String",
|
||||
created_at:"String",
|
||||
has_provider_data:"Boolean",
|
||||
name:"String",
|
||||
status:"String",
|
||||
subscriptions:"ProviderSubscription"
|
||||
},
|
||||
CreateSubscriptionResponse:{
|
||||
message:"String",
|
||||
subscription:"WebPushSubscriptionDto",
|
||||
@@ -3505,6 +3564,8 @@ export const ReturnTypes: Record<string,any> = {
|
||||
addParticipant:"Account",
|
||||
addPaymentMethod:"PaymentMethod",
|
||||
addTrustedAccount:"Branch",
|
||||
billingConvert:"BillingResult",
|
||||
billingPay:"BillingResult",
|
||||
cancelRequest:"Transaction",
|
||||
capitalAddAuthor:"CapitalProject",
|
||||
capitalApproveCommit:"CapitalCommit",
|
||||
@@ -3624,6 +3685,7 @@ export const ReturnTypes: Record<string,any> = {
|
||||
generateAssetContributionDecision:"GeneratedDocument",
|
||||
generateAssetContributionStatement:"GeneratedDocument",
|
||||
generateBallotForAnnualGeneralMeetDocument:"GeneratedDocument",
|
||||
generateBillingConversionStatement:"GeneratedDocument",
|
||||
generateConvertToAxonStatement:"GeneratedDocument",
|
||||
generateDocument:"GeneratedDocument",
|
||||
generateFreeDecision:"GeneratedDocument",
|
||||
@@ -4168,11 +4230,13 @@ export const ReturnTypes: Record<string,any> = {
|
||||
getActions:"PaginatedActionsPaginationResult",
|
||||
getAgenda:"AgendaWithDocuments",
|
||||
getAvailableReports:"AvailableReport",
|
||||
getBillingSummary:"BillingSummary",
|
||||
getBranches:"Branch",
|
||||
getCapitalIssueLogs:"PaginatedCapitalLogsPaginationResult",
|
||||
getCapitalOnboardingState:"CapitalOnboardingState",
|
||||
getCapitalProjectLogs:"PaginatedCapitalLogsPaginationResult",
|
||||
getChairmanOnboardingState:"ChairmanOnboardingState",
|
||||
getCooperativesRegistry:"CooperativeRegistryItem",
|
||||
getCurrentInstance:"CurrentInstanceDTO",
|
||||
getCurrentTableStates:"PaginatedCurrentTableStatesPaginationResult",
|
||||
getDeltas:"PaginatedDeltasPaginationResult",
|
||||
|
||||
@@ -2078,6 +2078,119 @@ export type ValueTypes = {
|
||||
voting?:ValueTypes["CapitalProjectVotingData"],
|
||||
__typename?: boolean | `@${string}`,
|
||||
['...on BaseCapitalProject']?: Omit<ValueTypes["BaseCapitalProject"], "...on BaseCapitalProject">
|
||||
}>;
|
||||
["BillingConversionStatementGenerateDocumentInput"]: {
|
||||
/** Номер блока, на котором был создан документ */
|
||||
block_num?: number | undefined | null | Variable<any, string>,
|
||||
/** Сумма к конвертации паевого в членский на биллинг-кошелёк */
|
||||
convert_amount: string | Variable<any, string>,
|
||||
/** Название кооператива, связанное с документом */
|
||||
coopname: string | Variable<any, string>,
|
||||
/** Дата и время создания документа */
|
||||
created_at?: string | undefined | null | Variable<any, string>,
|
||||
/** Имя генератора, использованного для создания документа */
|
||||
generator?: string | undefined | null | Variable<any, string>,
|
||||
/** Язык документа */
|
||||
lang?: string | undefined | null | Variable<any, string>,
|
||||
/** Ссылки, связанные с документом */
|
||||
links?: Array<string> | undefined | null | Variable<any, string>,
|
||||
/** Часовой пояс, в котором был создан документ */
|
||||
timezone?: string | undefined | null | Variable<any, string>,
|
||||
/** Название документа */
|
||||
title?: string | undefined | null | Variable<any, string>,
|
||||
/** Имя пользователя, создавшего документ */
|
||||
username: string | Variable<any, string>,
|
||||
/** Версия генератора, использованного для создания документа */
|
||||
version?: string | undefined | null | Variable<any, string>
|
||||
};
|
||||
["BillingConversionStatementSignedDocumentInput"]: {
|
||||
/** Хэш содержимого документа */
|
||||
doc_hash: string | Variable<any, string>,
|
||||
/** Общий хэш (doc_hash + meta_hash) */
|
||||
hash: string | Variable<any, string>,
|
||||
/** Метаинформация заявления на конвертацию паевого в биллинг-кошелёк */
|
||||
meta: ValueTypes["BillingConversionStatementSignedMetaDocumentInput"] | Variable<any, string>,
|
||||
/** Хэш мета-данных */
|
||||
meta_hash: string | Variable<any, string>,
|
||||
/** Вектор подписей */
|
||||
signatures: Array<ValueTypes["SignatureInfoInput"]> | Variable<any, string>,
|
||||
/** Версия стандарта документа */
|
||||
version: string | Variable<any, string>
|
||||
};
|
||||
["BillingConversionStatementSignedMetaDocumentInput"]: {
|
||||
/** Номер блока, на котором был создан документ */
|
||||
block_num: number | Variable<any, string>,
|
||||
/** Сумма к конвертации паевого в членский на биллинг-кошелёк */
|
||||
convert_amount: string | Variable<any, string>,
|
||||
/** Название кооператива, связанное с документом */
|
||||
coopname: string | Variable<any, string>,
|
||||
/** Дата и время создания документа */
|
||||
created_at: string | Variable<any, string>,
|
||||
/** Имя генератора, использованного для создания документа */
|
||||
generator: string | Variable<any, string>,
|
||||
/** Язык документа */
|
||||
lang: string | Variable<any, string>,
|
||||
/** Ссылки, связанные с документом */
|
||||
links: Array<string> | Variable<any, string>,
|
||||
/** ID документа в реестре */
|
||||
registry_id: number | Variable<any, string>,
|
||||
/** Часовой пояс, в котором был создан документ */
|
||||
timezone: string | Variable<any, string>,
|
||||
/** Название документа */
|
||||
title: string | Variable<any, string>,
|
||||
/** Имя пользователя, создавшего документ */
|
||||
username: string | Variable<any, string>,
|
||||
/** Версия генератора, использованного для создания документа */
|
||||
version: string | Variable<any, string>
|
||||
};
|
||||
["BillingConvertInput"]: {
|
||||
/** Сумма с символом, например "1500.0000 RUB" */
|
||||
amount: string | Variable<any, string>,
|
||||
/** Имя аккаунта кооператива */
|
||||
coopname: string | Variable<any, string>,
|
||||
/** Подписанное пайщиком заявление 1095.BillingConversionStatement (document2 с типизированной meta: convert_amount). */
|
||||
document: ValueTypes["BillingConversionStatementSignedDocumentInput"] | Variable<any, string>,
|
||||
/** Имя аккаунта пайщика — владельца биллинг-кошелька */
|
||||
username: string | Variable<any, string>
|
||||
};
|
||||
["BillingPayInput"]: {
|
||||
/** Сумма с символом, например "1500.0000 RUB" */
|
||||
amount: string | Variable<any, string>,
|
||||
/** Имя аккаунта кооператива */
|
||||
coopname: string | Variable<any, string>,
|
||||
/** Назначение платежа */
|
||||
memo: string | Variable<any, string>,
|
||||
/** Детерминированный идентификатор платежа (payment_hash из getBillingSummary провайдера) */
|
||||
paymentHash: string | Variable<any, string>,
|
||||
/** Имя аккаунта пайщика — владельца биллинг-кошелька */
|
||||
username: string | Variable<any, string>
|
||||
};
|
||||
["BillingResult"]: AliasType<{
|
||||
paymentHash?:boolean | `@${string}`,
|
||||
transactionId?:boolean | `@${string}`,
|
||||
__typename?: boolean | `@${string}`,
|
||||
['...on BillingResult']?: Omit<ValueTypes["BillingResult"], "...on BillingResult">
|
||||
}>;
|
||||
["BillingSummary"]: AliasType<{
|
||||
coopname?:boolean | `@${string}`,
|
||||
currency?:boolean | `@${string}`,
|
||||
items?:ValueTypes["BillingSummaryItem"],
|
||||
nextPaymentDue?:boolean | `@${string}`,
|
||||
paymentHash?:boolean | `@${string}`,
|
||||
periodDays?:boolean | `@${string}`,
|
||||
totalAmount?:boolean | `@${string}`,
|
||||
__typename?: boolean | `@${string}`,
|
||||
['...on BillingSummary']?: Omit<ValueTypes["BillingSummary"], "...on BillingSummary">
|
||||
}>;
|
||||
["BillingSummaryItem"]: AliasType<{
|
||||
amount?:boolean | `@${string}`,
|
||||
isFree?:boolean | `@${string}`,
|
||||
status?:boolean | `@${string}`,
|
||||
subscriptionId?:boolean | `@${string}`,
|
||||
subscriptionTypeId?:boolean | `@${string}`,
|
||||
subscriptionTypeName?:boolean | `@${string}`,
|
||||
__typename?: boolean | `@${string}`,
|
||||
['...on BillingSummaryItem']?: Omit<ValueTypes["BillingSummaryItem"], "...on BillingSummaryItem">
|
||||
}>;
|
||||
["BlockchainAccount"]: AliasType<{
|
||||
/** Имя аккаунта */
|
||||
@@ -4268,6 +4381,24 @@ export type ValueTypes = {
|
||||
program_type?:boolean | `@${string}`,
|
||||
__typename?: boolean | `@${string}`,
|
||||
['...on CooperativeProgram']?: Omit<ValueTypes["CooperativeProgram"], "...on CooperativeProgram">
|
||||
}>;
|
||||
["CooperativeRegistryItem"]: AliasType<{
|
||||
/** Анонсированный домен/сайт кооператива */
|
||||
announce?:boolean | `@${string}`,
|
||||
/** Имя аккаунта кооператива (coopname) */
|
||||
coopname?:boolean | `@${string}`,
|
||||
/** Дата регистрации заявки кооператива (on-chain) */
|
||||
created_at?:boolean | `@${string}`,
|
||||
/** Есть ли у кооператива данные провайдера (хотя бы одна подписка) */
|
||||
has_provider_data?:boolean | `@${string}`,
|
||||
/** Наименование организации кооператива */
|
||||
name?:boolean | `@${string}`,
|
||||
/** Статус кооператива в блокчейне: pending | active | blocked */
|
||||
status?:boolean | `@${string}`,
|
||||
/** Подписки кооператива у провайдера (с инстансом и биллингом) */
|
||||
subscriptions?:ValueTypes["ProviderSubscription"],
|
||||
__typename?: boolean | `@${string}`,
|
||||
['...on CooperativeRegistryItem']?: Omit<ValueTypes["CooperativeRegistryItem"], "...on CooperativeRegistryItem">
|
||||
}>;
|
||||
/** Страна регистрации пользователя */
|
||||
["Country"]:Country;
|
||||
@@ -6417,6 +6548,8 @@ acceptChildOrder?: [{ data: ValueTypes["AcceptChildOrderInput"] | Variable<any,
|
||||
addParticipant?: [{ data: ValueTypes["AddParticipantInput"] | Variable<any, string>},ValueTypes["Account"]],
|
||||
addPaymentMethod?: [{ data: ValueTypes["AddPaymentMethodInput"] | Variable<any, string>},ValueTypes["PaymentMethod"]],
|
||||
addTrustedAccount?: [{ data: ValueTypes["AddTrustedAccountInput"] | Variable<any, string>},ValueTypes["Branch"]],
|
||||
billingConvert?: [{ input: ValueTypes["BillingConvertInput"] | Variable<any, string>},ValueTypes["BillingResult"]],
|
||||
billingPay?: [{ input: ValueTypes["BillingPayInput"] | Variable<any, string>},ValueTypes["BillingResult"]],
|
||||
cancelRequest?: [{ data: ValueTypes["CancelRequestInput"] | Variable<any, string>},ValueTypes["Transaction"]],
|
||||
capitalAddAuthor?: [{ data: ValueTypes["AddAuthorInput"] | Variable<any, string>},ValueTypes["CapitalProject"]],
|
||||
capitalApproveCommit?: [{ data: ValueTypes["CommitApproveInput"] | Variable<any, string>},ValueTypes["CapitalCommit"]],
|
||||
@@ -6539,6 +6672,7 @@ generateAssetContributionAct?: [{ data: ValueTypes["AssetContributionActGenerate
|
||||
generateAssetContributionDecision?: [{ data: ValueTypes["AssetContributionDecisionGenerateDocumentInput"] | Variable<any, string>, options?: ValueTypes["GenerateDocumentOptionsInput"] | undefined | null | Variable<any, string>},ValueTypes["GeneratedDocument"]],
|
||||
generateAssetContributionStatement?: [{ data: ValueTypes["AssetContributionStatementGenerateDocumentInput"] | Variable<any, string>, options?: ValueTypes["GenerateDocumentOptionsInput"] | undefined | null | Variable<any, string>},ValueTypes["GeneratedDocument"]],
|
||||
generateBallotForAnnualGeneralMeetDocument?: [{ data: ValueTypes["AnnualGeneralMeetingVotingBallotGenerateDocumentInput"] | Variable<any, string>, options?: ValueTypes["GenerateDocumentOptionsInput"] | undefined | null | Variable<any, string>},ValueTypes["GeneratedDocument"]],
|
||||
generateBillingConversionStatement?: [{ data: ValueTypes["BillingConversionStatementGenerateDocumentInput"] | Variable<any, string>, options?: ValueTypes["GenerateDocumentOptionsInput"] | undefined | null | Variable<any, string>},ValueTypes["GeneratedDocument"]],
|
||||
generateConvertToAxonStatement?: [{ data: ValueTypes["ConvertToAxonStatementGenerateDocumentInput"] | Variable<any, string>, options?: ValueTypes["GenerateDocumentOptionsInput"] | undefined | null | Variable<any, string>},ValueTypes["GeneratedDocument"]],
|
||||
generateDocument?: [{ input: ValueTypes["GenerateAnyDocumentInput"] | Variable<any, string>},ValueTypes["GeneratedDocument"]],
|
||||
generateFreeDecision?: [{ data: ValueTypes["FreeDecisionGenerateDocumentInput"] | Variable<any, string>, options?: ValueTypes["GenerateDocumentOptionsInput"] | undefined | null | Variable<any, string>},ValueTypes["GeneratedDocument"]],
|
||||
@@ -7836,6 +7970,7 @@ getActions?: [{ filters?: ValueTypes["ActionFiltersInput"] | undefined | null |
|
||||
|
||||
Требуемые роли: chairman. */
|
||||
getAvailableReports?:ValueTypes["AvailableReport"],
|
||||
getBillingSummary?: [{ coopname: string | Variable<any, string>, period?: number | undefined | null | Variable<any, string>},ValueTypes["BillingSummary"]],
|
||||
getBranches?: [{ data: ValueTypes["GetBranchesInput"] | Variable<any, string>},ValueTypes["Branch"]],
|
||||
getCapitalIssueLogs?: [{ data: ValueTypes["GetCapitalIssueLogsInput"] | Variable<any, string>, options?: ValueTypes["PaginationInput"] | undefined | null | Variable<any, string>},ValueTypes["PaginatedCapitalLogsPaginationResult"]],
|
||||
/** Получить состояние онбординга capital
|
||||
@@ -7847,6 +7982,10 @@ getCapitalProjectLogs?: [{ data: ValueTypes["GetCapitalLogsInput"] | Variable<an
|
||||
|
||||
Требуемые роли: chairman. */
|
||||
getChairmanOnboardingState?:ValueTypes["ChairmanOnboardingState"],
|
||||
/** Реестр кооперативов оператора: список кооперативов из блокчейна с данными провайдера (подписки, инстанс, биллинг)
|
||||
|
||||
Требуемые роли: member, chairman. */
|
||||
getCooperativesRegistry?:ValueTypes["CooperativeRegistryItem"],
|
||||
/** Получить текущий инстанс пользователя
|
||||
|
||||
Требуемые роли: member, chairman, user. */
|
||||
@@ -10524,6 +10663,116 @@ export type ResolverInputTypes = {
|
||||
/** Данные голосования по методу Водянова */
|
||||
voting?:ResolverInputTypes["CapitalProjectVotingData"],
|
||||
__typename?: boolean | `@${string}`
|
||||
}>;
|
||||
["BillingConversionStatementGenerateDocumentInput"]: {
|
||||
/** Номер блока, на котором был создан документ */
|
||||
block_num?: number | undefined | null,
|
||||
/** Сумма к конвертации паевого в членский на биллинг-кошелёк */
|
||||
convert_amount: string,
|
||||
/** Название кооператива, связанное с документом */
|
||||
coopname: string,
|
||||
/** Дата и время создания документа */
|
||||
created_at?: string | undefined | null,
|
||||
/** Имя генератора, использованного для создания документа */
|
||||
generator?: string | undefined | null,
|
||||
/** Язык документа */
|
||||
lang?: string | undefined | null,
|
||||
/** Ссылки, связанные с документом */
|
||||
links?: Array<string> | undefined | null,
|
||||
/** Часовой пояс, в котором был создан документ */
|
||||
timezone?: string | undefined | null,
|
||||
/** Название документа */
|
||||
title?: string | undefined | null,
|
||||
/** Имя пользователя, создавшего документ */
|
||||
username: string,
|
||||
/** Версия генератора, использованного для создания документа */
|
||||
version?: string | undefined | null
|
||||
};
|
||||
["BillingConversionStatementSignedDocumentInput"]: {
|
||||
/** Хэш содержимого документа */
|
||||
doc_hash: string,
|
||||
/** Общий хэш (doc_hash + meta_hash) */
|
||||
hash: string,
|
||||
/** Метаинформация заявления на конвертацию паевого в биллинг-кошелёк */
|
||||
meta: ResolverInputTypes["BillingConversionStatementSignedMetaDocumentInput"],
|
||||
/** Хэш мета-данных */
|
||||
meta_hash: string,
|
||||
/** Вектор подписей */
|
||||
signatures: Array<ResolverInputTypes["SignatureInfoInput"]>,
|
||||
/** Версия стандарта документа */
|
||||
version: string
|
||||
};
|
||||
["BillingConversionStatementSignedMetaDocumentInput"]: {
|
||||
/** Номер блока, на котором был создан документ */
|
||||
block_num: number,
|
||||
/** Сумма к конвертации паевого в членский на биллинг-кошелёк */
|
||||
convert_amount: string,
|
||||
/** Название кооператива, связанное с документом */
|
||||
coopname: string,
|
||||
/** Дата и время создания документа */
|
||||
created_at: string,
|
||||
/** Имя генератора, использованного для создания документа */
|
||||
generator: string,
|
||||
/** Язык документа */
|
||||
lang: string,
|
||||
/** Ссылки, связанные с документом */
|
||||
links: Array<string>,
|
||||
/** ID документа в реестре */
|
||||
registry_id: number,
|
||||
/** Часовой пояс, в котором был создан документ */
|
||||
timezone: string,
|
||||
/** Название документа */
|
||||
title: string,
|
||||
/** Имя пользователя, создавшего документ */
|
||||
username: string,
|
||||
/** Версия генератора, использованного для создания документа */
|
||||
version: string
|
||||
};
|
||||
["BillingConvertInput"]: {
|
||||
/** Сумма с символом, например "1500.0000 RUB" */
|
||||
amount: string,
|
||||
/** Имя аккаунта кооператива */
|
||||
coopname: string,
|
||||
/** Подписанное пайщиком заявление 1095.BillingConversionStatement (document2 с типизированной meta: convert_amount). */
|
||||
document: ResolverInputTypes["BillingConversionStatementSignedDocumentInput"],
|
||||
/** Имя аккаунта пайщика — владельца биллинг-кошелька */
|
||||
username: string
|
||||
};
|
||||
["BillingPayInput"]: {
|
||||
/** Сумма с символом, например "1500.0000 RUB" */
|
||||
amount: string,
|
||||
/** Имя аккаунта кооператива */
|
||||
coopname: string,
|
||||
/** Назначение платежа */
|
||||
memo: string,
|
||||
/** Детерминированный идентификатор платежа (payment_hash из getBillingSummary провайдера) */
|
||||
paymentHash: string,
|
||||
/** Имя аккаунта пайщика — владельца биллинг-кошелька */
|
||||
username: string
|
||||
};
|
||||
["BillingResult"]: AliasType<{
|
||||
paymentHash?:boolean | `@${string}`,
|
||||
transactionId?:boolean | `@${string}`,
|
||||
__typename?: boolean | `@${string}`
|
||||
}>;
|
||||
["BillingSummary"]: AliasType<{
|
||||
coopname?:boolean | `@${string}`,
|
||||
currency?:boolean | `@${string}`,
|
||||
items?:ResolverInputTypes["BillingSummaryItem"],
|
||||
nextPaymentDue?:boolean | `@${string}`,
|
||||
paymentHash?:boolean | `@${string}`,
|
||||
periodDays?:boolean | `@${string}`,
|
||||
totalAmount?:boolean | `@${string}`,
|
||||
__typename?: boolean | `@${string}`
|
||||
}>;
|
||||
["BillingSummaryItem"]: AliasType<{
|
||||
amount?:boolean | `@${string}`,
|
||||
isFree?:boolean | `@${string}`,
|
||||
status?:boolean | `@${string}`,
|
||||
subscriptionId?:boolean | `@${string}`,
|
||||
subscriptionTypeId?:boolean | `@${string}`,
|
||||
subscriptionTypeName?:boolean | `@${string}`,
|
||||
__typename?: boolean | `@${string}`
|
||||
}>;
|
||||
["BlockchainAccount"]: AliasType<{
|
||||
/** Имя аккаунта */
|
||||
@@ -12652,6 +12901,23 @@ export type ResolverInputTypes = {
|
||||
/** Тип программы: wallet/generator/blagorost/marketplace и т.п. */
|
||||
program_type?:boolean | `@${string}`,
|
||||
__typename?: boolean | `@${string}`
|
||||
}>;
|
||||
["CooperativeRegistryItem"]: AliasType<{
|
||||
/** Анонсированный домен/сайт кооператива */
|
||||
announce?:boolean | `@${string}`,
|
||||
/** Имя аккаунта кооператива (coopname) */
|
||||
coopname?:boolean | `@${string}`,
|
||||
/** Дата регистрации заявки кооператива (on-chain) */
|
||||
created_at?:boolean | `@${string}`,
|
||||
/** Есть ли у кооператива данные провайдера (хотя бы одна подписка) */
|
||||
has_provider_data?:boolean | `@${string}`,
|
||||
/** Наименование организации кооператива */
|
||||
name?:boolean | `@${string}`,
|
||||
/** Статус кооператива в блокчейне: pending | active | blocked */
|
||||
status?:boolean | `@${string}`,
|
||||
/** Подписки кооператива у провайдера (с инстансом и биллингом) */
|
||||
subscriptions?:ResolverInputTypes["ProviderSubscription"],
|
||||
__typename?: boolean | `@${string}`
|
||||
}>;
|
||||
/** Страна регистрации пользователя */
|
||||
["Country"]:Country;
|
||||
@@ -14748,6 +15014,8 @@ acceptChildOrder?: [{ data: ResolverInputTypes["AcceptChildOrderInput"]},Resolve
|
||||
addParticipant?: [{ data: ResolverInputTypes["AddParticipantInput"]},ResolverInputTypes["Account"]],
|
||||
addPaymentMethod?: [{ data: ResolverInputTypes["AddPaymentMethodInput"]},ResolverInputTypes["PaymentMethod"]],
|
||||
addTrustedAccount?: [{ data: ResolverInputTypes["AddTrustedAccountInput"]},ResolverInputTypes["Branch"]],
|
||||
billingConvert?: [{ input: ResolverInputTypes["BillingConvertInput"]},ResolverInputTypes["BillingResult"]],
|
||||
billingPay?: [{ input: ResolverInputTypes["BillingPayInput"]},ResolverInputTypes["BillingResult"]],
|
||||
cancelRequest?: [{ data: ResolverInputTypes["CancelRequestInput"]},ResolverInputTypes["Transaction"]],
|
||||
capitalAddAuthor?: [{ data: ResolverInputTypes["AddAuthorInput"]},ResolverInputTypes["CapitalProject"]],
|
||||
capitalApproveCommit?: [{ data: ResolverInputTypes["CommitApproveInput"]},ResolverInputTypes["CapitalCommit"]],
|
||||
@@ -14870,6 +15138,7 @@ generateAssetContributionAct?: [{ data: ResolverInputTypes["AssetContributionAct
|
||||
generateAssetContributionDecision?: [{ data: ResolverInputTypes["AssetContributionDecisionGenerateDocumentInput"], options?: ResolverInputTypes["GenerateDocumentOptionsInput"] | undefined | null},ResolverInputTypes["GeneratedDocument"]],
|
||||
generateAssetContributionStatement?: [{ data: ResolverInputTypes["AssetContributionStatementGenerateDocumentInput"], options?: ResolverInputTypes["GenerateDocumentOptionsInput"] | undefined | null},ResolverInputTypes["GeneratedDocument"]],
|
||||
generateBallotForAnnualGeneralMeetDocument?: [{ data: ResolverInputTypes["AnnualGeneralMeetingVotingBallotGenerateDocumentInput"], options?: ResolverInputTypes["GenerateDocumentOptionsInput"] | undefined | null},ResolverInputTypes["GeneratedDocument"]],
|
||||
generateBillingConversionStatement?: [{ data: ResolverInputTypes["BillingConversionStatementGenerateDocumentInput"], options?: ResolverInputTypes["GenerateDocumentOptionsInput"] | undefined | null},ResolverInputTypes["GeneratedDocument"]],
|
||||
generateConvertToAxonStatement?: [{ data: ResolverInputTypes["ConvertToAxonStatementGenerateDocumentInput"], options?: ResolverInputTypes["GenerateDocumentOptionsInput"] | undefined | null},ResolverInputTypes["GeneratedDocument"]],
|
||||
generateDocument?: [{ input: ResolverInputTypes["GenerateAnyDocumentInput"]},ResolverInputTypes["GeneratedDocument"]],
|
||||
generateFreeDecision?: [{ data: ResolverInputTypes["FreeDecisionGenerateDocumentInput"], options?: ResolverInputTypes["GenerateDocumentOptionsInput"] | undefined | null},ResolverInputTypes["GeneratedDocument"]],
|
||||
@@ -16112,6 +16381,7 @@ getActions?: [{ filters?: ResolverInputTypes["ActionFiltersInput"] | undefined |
|
||||
|
||||
Требуемые роли: chairman. */
|
||||
getAvailableReports?:ResolverInputTypes["AvailableReport"],
|
||||
getBillingSummary?: [{ coopname: string, period?: number | undefined | null},ResolverInputTypes["BillingSummary"]],
|
||||
getBranches?: [{ data: ResolverInputTypes["GetBranchesInput"]},ResolverInputTypes["Branch"]],
|
||||
getCapitalIssueLogs?: [{ data: ResolverInputTypes["GetCapitalIssueLogsInput"], options?: ResolverInputTypes["PaginationInput"] | undefined | null},ResolverInputTypes["PaginatedCapitalLogsPaginationResult"]],
|
||||
/** Получить состояние онбординга capital
|
||||
@@ -16123,6 +16393,10 @@ getCapitalProjectLogs?: [{ data: ResolverInputTypes["GetCapitalLogsInput"]},Reso
|
||||
|
||||
Требуемые роли: chairman. */
|
||||
getChairmanOnboardingState?:ResolverInputTypes["ChairmanOnboardingState"],
|
||||
/** Реестр кооперативов оператора: список кооперативов из блокчейна с данными провайдера (подписки, инстанс, биллинг)
|
||||
|
||||
Требуемые роли: member, chairman. */
|
||||
getCooperativesRegistry?:ResolverInputTypes["CooperativeRegistryItem"],
|
||||
/** Получить текущий инстанс пользователя
|
||||
|
||||
Требуемые роли: member, chairman, user. */
|
||||
@@ -18737,6 +19011,113 @@ export type ModelTypes = {
|
||||
title: string,
|
||||
/** Данные голосования по методу Водянова */
|
||||
voting: ModelTypes["CapitalProjectVotingData"]
|
||||
};
|
||||
["BillingConversionStatementGenerateDocumentInput"]: {
|
||||
/** Номер блока, на котором был создан документ */
|
||||
block_num?: number | undefined | null,
|
||||
/** Сумма к конвертации паевого в членский на биллинг-кошелёк */
|
||||
convert_amount: string,
|
||||
/** Название кооператива, связанное с документом */
|
||||
coopname: string,
|
||||
/** Дата и время создания документа */
|
||||
created_at?: string | undefined | null,
|
||||
/** Имя генератора, использованного для создания документа */
|
||||
generator?: string | undefined | null,
|
||||
/** Язык документа */
|
||||
lang?: string | undefined | null,
|
||||
/** Ссылки, связанные с документом */
|
||||
links?: Array<string> | undefined | null,
|
||||
/** Часовой пояс, в котором был создан документ */
|
||||
timezone?: string | undefined | null,
|
||||
/** Название документа */
|
||||
title?: string | undefined | null,
|
||||
/** Имя пользователя, создавшего документ */
|
||||
username: string,
|
||||
/** Версия генератора, использованного для создания документа */
|
||||
version?: string | undefined | null
|
||||
};
|
||||
["BillingConversionStatementSignedDocumentInput"]: {
|
||||
/** Хэш содержимого документа */
|
||||
doc_hash: string,
|
||||
/** Общий хэш (doc_hash + meta_hash) */
|
||||
hash: string,
|
||||
/** Метаинформация заявления на конвертацию паевого в биллинг-кошелёк */
|
||||
meta: ModelTypes["BillingConversionStatementSignedMetaDocumentInput"],
|
||||
/** Хэш мета-данных */
|
||||
meta_hash: string,
|
||||
/** Вектор подписей */
|
||||
signatures: Array<ModelTypes["SignatureInfoInput"]>,
|
||||
/** Версия стандарта документа */
|
||||
version: string
|
||||
};
|
||||
["BillingConversionStatementSignedMetaDocumentInput"]: {
|
||||
/** Номер блока, на котором был создан документ */
|
||||
block_num: number,
|
||||
/** Сумма к конвертации паевого в членский на биллинг-кошелёк */
|
||||
convert_amount: string,
|
||||
/** Название кооператива, связанное с документом */
|
||||
coopname: string,
|
||||
/** Дата и время создания документа */
|
||||
created_at: string,
|
||||
/** Имя генератора, использованного для создания документа */
|
||||
generator: string,
|
||||
/** Язык документа */
|
||||
lang: string,
|
||||
/** Ссылки, связанные с документом */
|
||||
links: Array<string>,
|
||||
/** ID документа в реестре */
|
||||
registry_id: number,
|
||||
/** Часовой пояс, в котором был создан документ */
|
||||
timezone: string,
|
||||
/** Название документа */
|
||||
title: string,
|
||||
/** Имя пользователя, создавшего документ */
|
||||
username: string,
|
||||
/** Версия генератора, использованного для создания документа */
|
||||
version: string
|
||||
};
|
||||
["BillingConvertInput"]: {
|
||||
/** Сумма с символом, например "1500.0000 RUB" */
|
||||
amount: string,
|
||||
/** Имя аккаунта кооператива */
|
||||
coopname: string,
|
||||
/** Подписанное пайщиком заявление 1095.BillingConversionStatement (document2 с типизированной meta: convert_amount). */
|
||||
document: ModelTypes["BillingConversionStatementSignedDocumentInput"],
|
||||
/** Имя аккаунта пайщика — владельца биллинг-кошелька */
|
||||
username: string
|
||||
};
|
||||
["BillingPayInput"]: {
|
||||
/** Сумма с символом, например "1500.0000 RUB" */
|
||||
amount: string,
|
||||
/** Имя аккаунта кооператива */
|
||||
coopname: string,
|
||||
/** Назначение платежа */
|
||||
memo: string,
|
||||
/** Детерминированный идентификатор платежа (payment_hash из getBillingSummary провайдера) */
|
||||
paymentHash: string,
|
||||
/** Имя аккаунта пайщика — владельца биллинг-кошелька */
|
||||
username: string
|
||||
};
|
||||
["BillingResult"]: {
|
||||
paymentHash?: string | undefined | null,
|
||||
transactionId: string
|
||||
};
|
||||
["BillingSummary"]: {
|
||||
coopname: string,
|
||||
currency: string,
|
||||
items: Array<ModelTypes["BillingSummaryItem"]>,
|
||||
nextPaymentDue?: string | undefined | null,
|
||||
paymentHash: string,
|
||||
periodDays: number,
|
||||
totalAmount: number
|
||||
};
|
||||
["BillingSummaryItem"]: {
|
||||
amount: number,
|
||||
isFree: boolean,
|
||||
status: string,
|
||||
subscriptionId: number,
|
||||
subscriptionTypeId: number,
|
||||
subscriptionTypeName: string
|
||||
};
|
||||
["BlockchainAccount"]: {
|
||||
/** Имя аккаунта */
|
||||
@@ -20798,6 +21179,22 @@ export type ModelTypes = {
|
||||
is_active: boolean,
|
||||
/** Тип программы: wallet/generator/blagorost/marketplace и т.п. */
|
||||
program_type: string
|
||||
};
|
||||
["CooperativeRegistryItem"]: {
|
||||
/** Анонсированный домен/сайт кооператива */
|
||||
announce?: string | undefined | null,
|
||||
/** Имя аккаунта кооператива (coopname) */
|
||||
coopname: string,
|
||||
/** Дата регистрации заявки кооператива (on-chain) */
|
||||
created_at?: string | undefined | null,
|
||||
/** Есть ли у кооператива данные провайдера (хотя бы одна подписка) */
|
||||
has_provider_data: boolean,
|
||||
/** Наименование организации кооператива */
|
||||
name?: string | undefined | null,
|
||||
/** Статус кооператива в блокчейне: pending | active | blocked */
|
||||
status: string,
|
||||
/** Подписки кооператива у провайдера (с инстансом и биллингом) */
|
||||
subscriptions: Array<ModelTypes["ProviderSubscription"]>
|
||||
};
|
||||
["Country"]:Country;
|
||||
["CreateAnnualGeneralMeetInput"]: {
|
||||
@@ -22836,6 +23233,14 @@ export type ModelTypes = {
|
||||
|
||||
Требуемые роли: chairman. */
|
||||
addTrustedAccount: ModelTypes["Branch"],
|
||||
/** Конвертация паевого взноса пайщика в членский на биллинг-кошелёк (operation o.bil.fund). Принимает подписанное пайщиком заявление 1095.BillingConversionStatement.
|
||||
|
||||
Требуемые роли: user, member, chairman. */
|
||||
billingConvert: ModelTypes["BillingResult"],
|
||||
/** Списание стоимости подписок с биллинг-кошелька пайщика в инфраструктурный кошелёк кооператива (operation o.bil.pay). Идемпотентно по payment_hash.
|
||||
|
||||
Требуемые роли: chairman. */
|
||||
billingPay: ModelTypes["BillingResult"],
|
||||
/** Отменить заявку */
|
||||
cancelRequest: ModelTypes["Transaction"],
|
||||
/** Добавление автора проекта в CAPITAL контракте
|
||||
@@ -23288,6 +23693,10 @@ export type ModelTypes = {
|
||||
|
||||
Требуемые роли: member. */
|
||||
generateBallotForAnnualGeneralMeetDocument: ModelTypes["GeneratedDocument"],
|
||||
/** Генерирует заявление 1095.BillingConversionStatement (перед подписью пайщиком) — аналог generateConvertToAxonStatement, канон documents-dto.
|
||||
|
||||
Требуемые роли: member, chairman. */
|
||||
generateBillingConversionStatement: ModelTypes["GeneratedDocument"],
|
||||
/** Генерирует заявление на конвертацию паевого взноса в членский взнос
|
||||
|
||||
Требуемые роли: member, chairman. */
|
||||
@@ -24694,6 +25103,10 @@ export type ModelTypes = {
|
||||
|
||||
Требуемые роли: chairman. */
|
||||
getAvailableReports: Array<ModelTypes["AvailableReport"]>,
|
||||
/** Сумма к оплате кооператива за период (стоимость платных подписок, разбивка, дата следующего платежа, payment_hash). Источник — provider backend оператора. Для реестра кооперативов Восхода.
|
||||
|
||||
Требуемые роли: chairman, member. */
|
||||
getBillingSummary: ModelTypes["BillingSummary"],
|
||||
/** Получить список кооперативных участков */
|
||||
getBranches: Array<ModelTypes["Branch"]>,
|
||||
/** Получить логи событий по задаче */
|
||||
@@ -24708,6 +25121,10 @@ export type ModelTypes = {
|
||||
|
||||
Требуемые роли: chairman. */
|
||||
getChairmanOnboardingState: ModelTypes["ChairmanOnboardingState"],
|
||||
/** Реестр кооперативов оператора: список кооперативов из блокчейна с данными провайдера (подписки, инстанс, биллинг)
|
||||
|
||||
Требуемые роли: member, chairman. */
|
||||
getCooperativesRegistry: Array<ModelTypes["CooperativeRegistryItem"]>,
|
||||
/** Получить текущий инстанс пользователя
|
||||
|
||||
Требуемые роли: member, chairman, user. */
|
||||
@@ -27405,6 +27822,119 @@ export type GraphQLTypes = {
|
||||
/** Данные голосования по методу Водянова */
|
||||
voting: GraphQLTypes["CapitalProjectVotingData"],
|
||||
['...on BaseCapitalProject']: Omit<GraphQLTypes["BaseCapitalProject"], "...on BaseCapitalProject">
|
||||
};
|
||||
["BillingConversionStatementGenerateDocumentInput"]: {
|
||||
/** Номер блока, на котором был создан документ */
|
||||
block_num?: number | undefined | null,
|
||||
/** Сумма к конвертации паевого в членский на биллинг-кошелёк */
|
||||
convert_amount: string,
|
||||
/** Название кооператива, связанное с документом */
|
||||
coopname: string,
|
||||
/** Дата и время создания документа */
|
||||
created_at?: string | undefined | null,
|
||||
/** Имя генератора, использованного для создания документа */
|
||||
generator?: string | undefined | null,
|
||||
/** Язык документа */
|
||||
lang?: string | undefined | null,
|
||||
/** Ссылки, связанные с документом */
|
||||
links?: Array<string> | undefined | null,
|
||||
/** Часовой пояс, в котором был создан документ */
|
||||
timezone?: string | undefined | null,
|
||||
/** Название документа */
|
||||
title?: string | undefined | null,
|
||||
/** Имя пользователя, создавшего документ */
|
||||
username: string,
|
||||
/** Версия генератора, использованного для создания документа */
|
||||
version?: string | undefined | null
|
||||
};
|
||||
["BillingConversionStatementSignedDocumentInput"]: {
|
||||
/** Хэш содержимого документа */
|
||||
doc_hash: string,
|
||||
/** Общий хэш (doc_hash + meta_hash) */
|
||||
hash: string,
|
||||
/** Метаинформация заявления на конвертацию паевого в биллинг-кошелёк */
|
||||
meta: GraphQLTypes["BillingConversionStatementSignedMetaDocumentInput"],
|
||||
/** Хэш мета-данных */
|
||||
meta_hash: string,
|
||||
/** Вектор подписей */
|
||||
signatures: Array<GraphQLTypes["SignatureInfoInput"]>,
|
||||
/** Версия стандарта документа */
|
||||
version: string
|
||||
};
|
||||
["BillingConversionStatementSignedMetaDocumentInput"]: {
|
||||
/** Номер блока, на котором был создан документ */
|
||||
block_num: number,
|
||||
/** Сумма к конвертации паевого в членский на биллинг-кошелёк */
|
||||
convert_amount: string,
|
||||
/** Название кооператива, связанное с документом */
|
||||
coopname: string,
|
||||
/** Дата и время создания документа */
|
||||
created_at: string,
|
||||
/** Имя генератора, использованного для создания документа */
|
||||
generator: string,
|
||||
/** Язык документа */
|
||||
lang: string,
|
||||
/** Ссылки, связанные с документом */
|
||||
links: Array<string>,
|
||||
/** ID документа в реестре */
|
||||
registry_id: number,
|
||||
/** Часовой пояс, в котором был создан документ */
|
||||
timezone: string,
|
||||
/** Название документа */
|
||||
title: string,
|
||||
/** Имя пользователя, создавшего документ */
|
||||
username: string,
|
||||
/** Версия генератора, использованного для создания документа */
|
||||
version: string
|
||||
};
|
||||
["BillingConvertInput"]: {
|
||||
/** Сумма с символом, например "1500.0000 RUB" */
|
||||
amount: string,
|
||||
/** Имя аккаунта кооператива */
|
||||
coopname: string,
|
||||
/** Подписанное пайщиком заявление 1095.BillingConversionStatement (document2 с типизированной meta: convert_amount). */
|
||||
document: GraphQLTypes["BillingConversionStatementSignedDocumentInput"],
|
||||
/** Имя аккаунта пайщика — владельца биллинг-кошелька */
|
||||
username: string
|
||||
};
|
||||
["BillingPayInput"]: {
|
||||
/** Сумма с символом, например "1500.0000 RUB" */
|
||||
amount: string,
|
||||
/** Имя аккаунта кооператива */
|
||||
coopname: string,
|
||||
/** Назначение платежа */
|
||||
memo: string,
|
||||
/** Детерминированный идентификатор платежа (payment_hash из getBillingSummary провайдера) */
|
||||
paymentHash: string,
|
||||
/** Имя аккаунта пайщика — владельца биллинг-кошелька */
|
||||
username: string
|
||||
};
|
||||
["BillingResult"]: {
|
||||
__typename: "BillingResult",
|
||||
paymentHash?: string | undefined | null,
|
||||
transactionId: string,
|
||||
['...on BillingResult']: Omit<GraphQLTypes["BillingResult"], "...on BillingResult">
|
||||
};
|
||||
["BillingSummary"]: {
|
||||
__typename: "BillingSummary",
|
||||
coopname: string,
|
||||
currency: string,
|
||||
items: Array<GraphQLTypes["BillingSummaryItem"]>,
|
||||
nextPaymentDue?: string | undefined | null,
|
||||
paymentHash: string,
|
||||
periodDays: number,
|
||||
totalAmount: number,
|
||||
['...on BillingSummary']: Omit<GraphQLTypes["BillingSummary"], "...on BillingSummary">
|
||||
};
|
||||
["BillingSummaryItem"]: {
|
||||
__typename: "BillingSummaryItem",
|
||||
amount: number,
|
||||
isFree: boolean,
|
||||
status: string,
|
||||
subscriptionId: number,
|
||||
subscriptionTypeId: number,
|
||||
subscriptionTypeName: string,
|
||||
['...on BillingSummaryItem']: Omit<GraphQLTypes["BillingSummaryItem"], "...on BillingSummaryItem">
|
||||
};
|
||||
["BlockchainAccount"]: {
|
||||
__typename: "BlockchainAccount",
|
||||
@@ -29595,6 +30125,24 @@ export type GraphQLTypes = {
|
||||
/** Тип программы: wallet/generator/blagorost/marketplace и т.п. */
|
||||
program_type: string,
|
||||
['...on CooperativeProgram']: Omit<GraphQLTypes["CooperativeProgram"], "...on CooperativeProgram">
|
||||
};
|
||||
["CooperativeRegistryItem"]: {
|
||||
__typename: "CooperativeRegistryItem",
|
||||
/** Анонсированный домен/сайт кооператива */
|
||||
announce?: string | undefined | null,
|
||||
/** Имя аккаунта кооператива (coopname) */
|
||||
coopname: string,
|
||||
/** Дата регистрации заявки кооператива (on-chain) */
|
||||
created_at?: string | undefined | null,
|
||||
/** Есть ли у кооператива данные провайдера (хотя бы одна подписка) */
|
||||
has_provider_data: boolean,
|
||||
/** Наименование организации кооператива */
|
||||
name?: string | undefined | null,
|
||||
/** Статус кооператива в блокчейне: pending | active | blocked */
|
||||
status: string,
|
||||
/** Подписки кооператива у провайдера (с инстансом и биллингом) */
|
||||
subscriptions: Array<GraphQLTypes["ProviderSubscription"]>,
|
||||
['...on CooperativeRegistryItem']: Omit<GraphQLTypes["CooperativeRegistryItem"], "...on CooperativeRegistryItem">
|
||||
};
|
||||
/** Страна регистрации пользователя */
|
||||
["Country"]: Country;
|
||||
@@ -31753,6 +32301,14 @@ export type GraphQLTypes = {
|
||||
|
||||
Требуемые роли: chairman. */
|
||||
addTrustedAccount: GraphQLTypes["Branch"],
|
||||
/** Конвертация паевого взноса пайщика в членский на биллинг-кошелёк (operation o.bil.fund). Принимает подписанное пайщиком заявление 1095.BillingConversionStatement.
|
||||
|
||||
Требуемые роли: user, member, chairman. */
|
||||
billingConvert: GraphQLTypes["BillingResult"],
|
||||
/** Списание стоимости подписок с биллинг-кошелька пайщика в инфраструктурный кошелёк кооператива (operation o.bil.pay). Идемпотентно по payment_hash.
|
||||
|
||||
Требуемые роли: chairman. */
|
||||
billingPay: GraphQLTypes["BillingResult"],
|
||||
/** Отменить заявку */
|
||||
cancelRequest: GraphQLTypes["Transaction"],
|
||||
/** Добавление автора проекта в CAPITAL контракте
|
||||
@@ -32205,6 +32761,10 @@ export type GraphQLTypes = {
|
||||
|
||||
Требуемые роли: member. */
|
||||
generateBallotForAnnualGeneralMeetDocument: GraphQLTypes["GeneratedDocument"],
|
||||
/** Генерирует заявление 1095.BillingConversionStatement (перед подписью пайщиком) — аналог generateConvertToAxonStatement, канон documents-dto.
|
||||
|
||||
Требуемые роли: member, chairman. */
|
||||
generateBillingConversionStatement: GraphQLTypes["GeneratedDocument"],
|
||||
/** Генерирует заявление на конвертацию паевого взноса в членский взнос
|
||||
|
||||
Требуемые роли: member, chairman. */
|
||||
@@ -33742,6 +34302,10 @@ export type GraphQLTypes = {
|
||||
|
||||
Требуемые роли: chairman. */
|
||||
getAvailableReports: Array<GraphQLTypes["AvailableReport"]>,
|
||||
/** Сумма к оплате кооператива за период (стоимость платных подписок, разбивка, дата следующего платежа, payment_hash). Источник — provider backend оператора. Для реестра кооперативов Восхода.
|
||||
|
||||
Требуемые роли: chairman, member. */
|
||||
getBillingSummary: GraphQLTypes["BillingSummary"],
|
||||
/** Получить список кооперативных участков */
|
||||
getBranches: Array<GraphQLTypes["Branch"]>,
|
||||
/** Получить логи событий по задаче */
|
||||
@@ -33756,6 +34320,10 @@ export type GraphQLTypes = {
|
||||
|
||||
Требуемые роли: chairman. */
|
||||
getChairmanOnboardingState: GraphQLTypes["ChairmanOnboardingState"],
|
||||
/** Реестр кооперативов оператора: список кооперативов из блокчейна с данными провайдера (подписки, инстанс, биллинг)
|
||||
|
||||
Требуемые роли: member, chairman. */
|
||||
getCooperativesRegistry: Array<GraphQLTypes["CooperativeRegistryItem"]>,
|
||||
/** Получить текущий инстанс пользователя
|
||||
|
||||
Требуемые роли: member, chairman, user. */
|
||||
@@ -36003,6 +36571,11 @@ type ZEUS_VARIABLES = {
|
||||
["AssetContributionStatementSignedMetaDocumentInput"]: ValueTypes["AssetContributionStatementSignedMetaDocumentInput"];
|
||||
["BankAccountDetailsInput"]: ValueTypes["BankAccountDetailsInput"];
|
||||
["BankAccountInput"]: ValueTypes["BankAccountInput"];
|
||||
["BillingConversionStatementGenerateDocumentInput"]: ValueTypes["BillingConversionStatementGenerateDocumentInput"];
|
||||
["BillingConversionStatementSignedDocumentInput"]: ValueTypes["BillingConversionStatementSignedDocumentInput"];
|
||||
["BillingConversionStatementSignedMetaDocumentInput"]: ValueTypes["BillingConversionStatementSignedMetaDocumentInput"];
|
||||
["BillingConvertInput"]: ValueTypes["BillingConvertInput"];
|
||||
["BillingPayInput"]: ValueTypes["BillingPayInput"];
|
||||
["BuhotchSignerType"]: ValueTypes["BuhotchSignerType"];
|
||||
["CalculateVotesInput"]: ValueTypes["CalculateVotesInput"];
|
||||
["CalendarEntryStatus"]: ValueTypes["CalendarEntryStatus"];
|
||||
|
||||
@@ -82,3 +82,8 @@ export const _apps = {
|
||||
production: 'apps',
|
||||
testnet: 'apps',
|
||||
} as const
|
||||
|
||||
export const _billing = {
|
||||
production: 'billing',
|
||||
testnet: 'billing',
|
||||
} as const
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import * as Permissions from '../../../common/permissions'
|
||||
import type * as Billing from '../../../interfaces/billing'
|
||||
import { Actors } from '../../../common'
|
||||
|
||||
/**
|
||||
* Требуется авторизация {@link Actors._coopname | аккаунта кооператива}
|
||||
* (бэкенд ретранслирует после JWT; согласие пайщика несёт `document`).
|
||||
*/
|
||||
export const authorizations = [{ permissions: [Permissions.active], actor: Actors._coopname }] as const
|
||||
|
||||
/**
|
||||
* Имя действия
|
||||
*/
|
||||
export const actionName = 'convert'
|
||||
|
||||
/**
|
||||
* @interface
|
||||
*/
|
||||
export type IConvert = Billing.IConvert
|
||||
@@ -0,0 +1,5 @@
|
||||
export * as Convert from './convert'
|
||||
|
||||
export * as Pay from './pay'
|
||||
|
||||
export * as Migrate from './migrate'
|
||||
@@ -0,0 +1,18 @@
|
||||
import * as Permissions from '../../../common/permissions'
|
||||
import type * as Billing from '../../../interfaces/billing'
|
||||
import { Actors } from '../../../common'
|
||||
|
||||
/**
|
||||
* Требуется авторизация {@link Actors._contract | аккаунта контракта billing}.
|
||||
*/
|
||||
export const authorizations = [{ permissions: [Permissions.active], actor: Actors._contract }] as const
|
||||
|
||||
/**
|
||||
* Имя действия
|
||||
*/
|
||||
export const actionName = 'migrate'
|
||||
|
||||
/**
|
||||
* @interface
|
||||
*/
|
||||
export type IMigrate = Billing.IMigrate
|
||||
@@ -0,0 +1,19 @@
|
||||
import * as Permissions from '../../../common/permissions'
|
||||
import type * as Billing from '../../../interfaces/billing'
|
||||
import { Actors } from '../../../common'
|
||||
|
||||
/**
|
||||
* Требуется авторизация {@link Actors._coopname | аккаунта кооператива}
|
||||
* (списание инициирует оператор от имени кооператива).
|
||||
*/
|
||||
export const authorizations = [{ permissions: [Permissions.active], actor: Actors._coopname }] as const
|
||||
|
||||
/**
|
||||
* Имя действия
|
||||
*/
|
||||
export const actionName = 'pay'
|
||||
|
||||
/**
|
||||
* @interface
|
||||
*/
|
||||
export type IPay = Billing.IPay
|
||||
@@ -0,0 +1,11 @@
|
||||
import * as contractNames from '../../common/names'
|
||||
|
||||
export * as Actions from './actions'
|
||||
export * as Tables from './tables'
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export * as Interfaces from '../../interfaces/billing'
|
||||
|
||||
export const contractName = contractNames._billing
|
||||
@@ -0,0 +1 @@
|
||||
export * as Payments from './payments'
|
||||
@@ -0,0 +1,18 @@
|
||||
import type * as Billing from '../../../interfaces/billing'
|
||||
import { Actors } from '../../../common'
|
||||
|
||||
/**
|
||||
* Имя таблицы
|
||||
*/
|
||||
export const tableName = 'payments'
|
||||
|
||||
/**
|
||||
* Таблица хранится в {@link Actors._coopname | области памяти кооператива}.
|
||||
*/
|
||||
export const scope = Actors._coopname
|
||||
|
||||
/**
|
||||
* @interface
|
||||
* Факты оплаты подписок: сумма, идентификатор платежа (`payment_hash`) и время.
|
||||
*/
|
||||
export type IPayment = Billing.IPayment
|
||||
@@ -94,3 +94,11 @@ export * as MeetContract from './meet'
|
||||
* Деплоится на корневой KE-цепи и обслуживает несколько подсетей одновременно.
|
||||
*/
|
||||
export * as AppsContract from './apps'
|
||||
|
||||
/**
|
||||
* Смарт-контракт оплаты инфраструктурных подписок членскими взносами пайщика
|
||||
* (Epic 12). Ровно два действия: `convert` (паевой → членский на `w.wal.bill`
|
||||
* по подписанному заявлению) и `pay` (списание стоимости подписок, идемпотентно
|
||||
* по `payment_hash`). Состав и цены подписок on-chain не хранятся.
|
||||
*/
|
||||
export * as BillingContract from './billing'
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import type { IGenerate, IMetaDocument } from '../../document'
|
||||
import type { ICommonUser, ICooperativeData, IVars } from '../../model'
|
||||
|
||||
export const registry_id = 1095
|
||||
|
||||
/**
|
||||
* Интерфейс генерации заявления на конвертацию паевого взноса в членский на
|
||||
* биллинг-кошелёк пайщика (Epic 12, действие billing::convert). Пайщик просит
|
||||
* транслировать паевой взнос в членский, зарезервированный под оплату
|
||||
* инфраструктурных подписок; сам факт конвертации трактуется как согласие на
|
||||
* последующие рекуррентные списания.
|
||||
*/
|
||||
export interface Action extends IGenerate {
|
||||
registry_id: number
|
||||
convert_amount: string
|
||||
}
|
||||
|
||||
export type Meta = IMetaDocument & Action
|
||||
|
||||
// Модель данных
|
||||
export interface Model {
|
||||
meta: IMetaDocument
|
||||
vars: IVars
|
||||
coop: ICooperativeData
|
||||
commonUser: ICommonUser
|
||||
}
|
||||
|
||||
export const title = 'Заявление о конвертации паевого взноса в членский на биллинг-кошелёк'
|
||||
export const description = 'Форма заявления о конвертации паевого взноса в членский взнос для оплаты инфраструктурных подписок (биллинг-кошелёк пайщика)'
|
||||
|
||||
export const context = `<style>.digital-document h1 {margin: 0px;text-align:center;}.digital-document {padding: 20px;}.subheader {padding-bottom: 20px;}</style><div class="digital-document"><div style="text-align: right; margin:"><p>{% trans 'to_council_of' %} {{vars.full_abbr_genitive}} «{{vars.name}}»</p><p>{% trans 'from_participant' %}</p><p>{{ commonUser.full_name_or_short_name }}</p></div><h1 class="header">{% trans 'convert_statement' %}</h1><p style="text-align: center">{{vars.full_abbr_genitive}} «{{vars.name}}»</p><p style="text-align: right">{{ meta.created_at }}, {{coop.city}}</p><p>{% trans 'convert_request_text', meta.convert_amount %}</p><div class="signature"><p>{% trans 'signature' %} </p><p>{{ commonUser.full_name_or_short_name }}</p></div></div>`
|
||||
|
||||
export const translations = {
|
||||
ru: {
|
||||
to_council_of: 'В Совет',
|
||||
convert_request_text: 'Прошу транслировать мой паевой взнос в сумме {0} в членский взнос на персональный биллинг-кошелёк для оплаты инфраструктурных подписок. Подтверждаю своё согласие на последующие списания членских взносов с биллинг-кошелька в счёт оплаты подписок.',
|
||||
signature: 'Документ подписан электронной подписью',
|
||||
convert_statement: 'Заявление о конвертации паевого взноса на биллинг-кошелёк',
|
||||
from_participant: 'от пайщика',
|
||||
},
|
||||
}
|
||||
|
||||
export const exampleData = {
|
||||
meta: {
|
||||
created_at: '04.03.2024 10:54',
|
||||
convert_amount: '1500.00 RUB',
|
||||
},
|
||||
coop: {
|
||||
city: 'Москва',
|
||||
},
|
||||
commonUser: {
|
||||
full_name_or_short_name: 'ПК "РОМАШКА"',
|
||||
},
|
||||
vars: {
|
||||
full_abbr_genitive: 'потребительского кооператива',
|
||||
name: 'ВОСХОД',
|
||||
},
|
||||
}
|
||||
@@ -65,6 +65,8 @@ export * as GenerationConvertStatement from './1080.GenerationConvertStatement'
|
||||
|
||||
export * as CapitalizationToMainWalletConvertStatement from './1090.CapitalizationToMainWalletConvertStatement'
|
||||
|
||||
export * as BillingConversionStatement from './1095.BillingConversionStatement'
|
||||
|
||||
export * as SosediAgreement from './699.SosediAgreement'
|
||||
|
||||
// общие собрания
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
// Epic 12: интерфейсы контракта billing (оплата подписок членскими взносами).
|
||||
// Формат повторяет авто-генерируемые ABI-интерфейсы (eosio-abi2ts), но billing
|
||||
// мал и стабилен (ровно convert/pay/migrate + таблица payments), поэтому
|
||||
// поддерживается вручную до подключения контракта в общий abi2ts-пайплайн.
|
||||
|
||||
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 IUint32 = number
|
||||
export type IUint64 = number | string
|
||||
|
||||
export interface ISignatureInfo {
|
||||
id: IUint32
|
||||
signed_hash: IChecksum256
|
||||
signer: IName
|
||||
public_key: IPublicKey
|
||||
signature: ISignature
|
||||
signed_at: ITimePointSec
|
||||
meta: string
|
||||
}
|
||||
|
||||
export interface IDocument2 {
|
||||
version: string
|
||||
hash: IChecksum256
|
||||
doc_hash: IChecksum256
|
||||
meta_hash: IChecksum256
|
||||
meta: string
|
||||
signatures: ISignatureInfo[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Конвертация паевого взноса пайщика в членский на персональный биллинг-кошелёк
|
||||
* (`w.wal.bill`). Несёт подписанное заявление пайщика (`document`).
|
||||
*/
|
||||
export interface IConvert {
|
||||
coopname: IName
|
||||
username: IName
|
||||
amount: IAsset
|
||||
document: IDocument2
|
||||
}
|
||||
|
||||
/**
|
||||
* Списание с биллинг-кошелька пайщика суммарной стоимости подписок в
|
||||
* инфраструктурный кошелёк кооператива. Идемпотентно по `payment_hash`.
|
||||
* Состав и цены подписок on-chain не хранятся (зона оператора).
|
||||
*/
|
||||
export interface IPay {
|
||||
coopname: IName
|
||||
username: IName
|
||||
amount: IAsset
|
||||
payment_hash: IChecksum256
|
||||
memo: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Сервисное действие миграции таблиц контракта.
|
||||
*/
|
||||
export interface IMigrate {
|
||||
}
|
||||
|
||||
/**
|
||||
* Строка таблицы платежей (`payments`, scope = коопнейм). Хранит факт оплаты
|
||||
* подписок: сумму, идентификатор платежа и время.
|
||||
*/
|
||||
export interface IPayment {
|
||||
id: IUint64
|
||||
coopname: IName
|
||||
username: IName
|
||||
amount: IAsset
|
||||
payment_hash: IChecksum256
|
||||
paid_at: ITimePointSec
|
||||
}
|
||||
@@ -20,8 +20,9 @@ export interface WalletMeta {
|
||||
*/
|
||||
export const LEDGER2_WALLET_REGISTRY: readonly WalletMeta[] = [
|
||||
{ name: "w.reg.minshr", human_name: "Минимальный паевой взнос", kind: "USER_SHARED" },
|
||||
{ name: "w.wal.share", human_name: "Паевой взнос пайщика", kind: "USER_SHARED" },
|
||||
{ name: "w.wal.member", human_name: "ЦК — членская часть пайщика", kind: "USER_SHARED" },
|
||||
{ name: "w.wal.share", human_name: "ЦК — паевой", kind: "USER_SHARED" },
|
||||
{ name: "w.wal.member", human_name: "ЦК — членский", kind: "USER_SHARED" },
|
||||
{ name: "w.wal.bill", 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.cap.gen", human_name: "ЦПП «Генератор» — единый кошелёк программы", kind: "COOPERATIVE" },
|
||||
@@ -51,8 +52,9 @@ export interface ProgramWalletMapping {
|
||||
*/
|
||||
export const LEDGER2_USER_SHARED_PROGRAM_MAPPING: readonly ProgramWalletMapping[] = [
|
||||
{ wallet_name: "w.reg.minshr", required_program_id: 0, program_label: null },
|
||||
{ wallet_name: "w.wal.share", required_program_id: 1, program_label: "ЦК" },
|
||||
{ wallet_name: "w.wal.member", required_program_id: 1, program_label: "ЦК" },
|
||||
{ wallet_name: "w.wal.share", required_program_id: 1, program_label: "ЦК — паевой" },
|
||||
{ wallet_name: "w.wal.member", required_program_id: 1, program_label: "ЦК — членский" },
|
||||
{ wallet_name: "w.wal.bill", required_program_id: 1, program_label: "ЦК — биллинг" },
|
||||
{ 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 },
|
||||
|
||||
@@ -6,6 +6,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 { UnionPageCooperativeDetail } from 'src/pages/Union/CooperativeDetail';
|
||||
import type { IWorkspaceConfig } from 'src/shared/lib/types/workspace';
|
||||
|
||||
export default async function (): Promise<IWorkspaceConfig[]> {
|
||||
@@ -94,6 +95,18 @@ export default async function (): Promise<IWorkspaceConfig[]> {
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'union/cooperatives/:detailCoopname',
|
||||
name: 'union-cooperative-detail',
|
||||
component: markRaw(UnionPageCooperativeDetail),
|
||||
meta: {
|
||||
title: 'Кооператив',
|
||||
icon: 'fa-solid fa-handshake',
|
||||
roles: ['chairman', 'member'],
|
||||
conditions: 'coopname === "voskhod"',
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { client } from 'src/shared/api/client'
|
||||
import { Queries } from '@coopenomics/sdk'
|
||||
import type { IBillingSummary } from '../model'
|
||||
|
||||
async function loadBillingSummary(coopname: string, period?: number): Promise<IBillingSummary> {
|
||||
const { [Queries.Billing.GetBillingSummary.name]: result } = await client.Query(
|
||||
Queries.Billing.GetBillingSummary.query,
|
||||
{ variables: { coopname, period } },
|
||||
)
|
||||
return result as IBillingSummary
|
||||
}
|
||||
|
||||
export const api = {
|
||||
loadBillingSummary,
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { api as billingApi } from './api'
|
||||
export type { IBillingSummary } from './model'
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { Queries } from '@coopenomics/sdk'
|
||||
|
||||
// Тип строго из SDK
|
||||
export type IBillingSummary =
|
||||
Queries.Billing.GetBillingSummary.IOutput[typeof Queries.Billing.GetBillingSummary.name]
|
||||
@@ -1,13 +1,20 @@
|
||||
import { RegistratorContract } from 'cooptypes'
|
||||
import { Queries } from '@coopenomics/sdk'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
const namespace = 'union'
|
||||
|
||||
/**
|
||||
* Элемент реестра кооперативов: on-chain данные кооператива + данные провайдера
|
||||
* (подписки/инстанс/биллинг), сведённые бэкендом (coopback) по coopname.
|
||||
*/
|
||||
export type ICooperativeRegistryItem =
|
||||
Queries.System.GetCooperativesRegistry.IOutput['getCooperativesRegistry'][number]
|
||||
|
||||
export const useUnionStore = defineStore(
|
||||
namespace,
|
||||
() => {
|
||||
|
||||
const coops = [] as RegistratorContract.Tables.Cooperatives.ICooperative[]
|
||||
const coops = [] as ICooperativeRegistryItem[]
|
||||
|
||||
return {
|
||||
coops
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './types'
|
||||
export * from './stores'
|
||||
export * from './useCooperativeMainWallet'
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import { ref, watchEffect, type Ref } from 'vue'
|
||||
import { Queries, Zeus } from '@coopenomics/sdk'
|
||||
import { client } from 'src/shared/api/client'
|
||||
|
||||
/**
|
||||
* Балансы MAIN-программы (split-кошелёк): `available` — паевой взнос
|
||||
* (w.wal.share), `membership_contribution` — кошелёк членских/инфраструктурных
|
||||
* взносов (w.wal.member/w.wal.bill). Backend отдаёт оба поля одной записью
|
||||
* MAIN-программы, отдельных запросов для каждого waltype не требуется.
|
||||
*
|
||||
* coopname/username реактивны: смена → перезапрос.
|
||||
*/
|
||||
export function useCooperativeMainWallet(
|
||||
coopname: Ref<string> | (() => string),
|
||||
username: Ref<string> | (() => string),
|
||||
) {
|
||||
const loading = ref(false)
|
||||
// initialLoading — true ТОЛЬКО до первого успешного ответа. UI должен
|
||||
// ориентироваться на него (а не на loading), иначе при тихих рефрешах
|
||||
// (закрытие диалога конвертации, реактивная смена coopname в две фазы)
|
||||
// плашка моргает «загрузка → значение → загрузка → значение».
|
||||
const initialLoading = ref(true)
|
||||
const error = ref('')
|
||||
const available = ref('0')
|
||||
const membership = ref('0')
|
||||
const symbol = ref('')
|
||||
|
||||
const getCoopname = () =>
|
||||
typeof coopname === 'function' ? coopname() : coopname.value
|
||||
const getUsername = () =>
|
||||
typeof username === 'function' ? username() : username.value
|
||||
|
||||
const splitAsset = (asset?: string | null) => {
|
||||
if (!asset) return { amount: '0', symbol: '' }
|
||||
const [amount, sym = ''] = String(asset).split(' ')
|
||||
return { amount: amount || '0', symbol: sym }
|
||||
}
|
||||
|
||||
let inFlight = 0
|
||||
let lastKey = ''
|
||||
|
||||
const refresh = async () => {
|
||||
const c = getCoopname()
|
||||
const u = getUsername()
|
||||
if (!c || !u) return
|
||||
// Дедуп параллельных вызовов: если уже летит запрос на тот же ключ — пропустить.
|
||||
const key = `${c}/${u}`
|
||||
if (inFlight > 0 && lastKey === key) return
|
||||
lastKey = key
|
||||
inFlight++
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const { [Queries.Wallet.GetProgramWallet.name]: wallet } = await client.Query(
|
||||
Queries.Wallet.GetProgramWallet.query,
|
||||
{
|
||||
variables: {
|
||||
filter: {
|
||||
coopname: c,
|
||||
username: u,
|
||||
program_type: Zeus.ProgramType.MAIN,
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
const av = splitAsset(wallet?.available)
|
||||
const mb = splitAsset(wallet?.membership_contribution)
|
||||
available.value = av.amount
|
||||
membership.value = mb.amount
|
||||
symbol.value = av.symbol || mb.symbol
|
||||
} catch (e: any) {
|
||||
// У пайщика может ещё не быть MAIN-кошелька (новый coop) —
|
||||
// показываем нули, а не ошибку.
|
||||
const msg = e?.message ?? String(e)
|
||||
if (/not found|null/i.test(msg)) {
|
||||
available.value = '0'
|
||||
membership.value = '0'
|
||||
symbol.value = ''
|
||||
} else {
|
||||
error.value = msg
|
||||
}
|
||||
} finally {
|
||||
inFlight = Math.max(0, inFlight - 1)
|
||||
loading.value = false
|
||||
initialLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
void getCoopname()
|
||||
void getUsername()
|
||||
void refresh()
|
||||
})
|
||||
|
||||
return { available, membership, symbol, loading, initialLoading, error, refresh }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { client } from 'src/shared/api/client'
|
||||
import { Mutations } from '@coopenomics/sdk'
|
||||
import type { IConvertToBillingInput } from '../model'
|
||||
|
||||
async function convertToBilling(input: IConvertToBillingInput) {
|
||||
const { [Mutations.Billing.Convert.name]: result } = await client.Mutation(
|
||||
Mutations.Billing.Convert.mutation,
|
||||
{ variables: { input } },
|
||||
)
|
||||
return result
|
||||
}
|
||||
|
||||
export const api = {
|
||||
convertToBilling,
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export { useConvertToBilling, useConvertToBillingVisibility } from './model'
|
||||
export type { IConvertToBillingInput, IConvertToBillingOutput } from './model'
|
||||
export { default as ConvertToBillingButton } from './ui/ConvertToBillingButton.vue'
|
||||
export { default as ConvertToBillingDialog } from './ui/ConvertToBillingDialog.vue'
|
||||
@@ -0,0 +1,80 @@
|
||||
import { ref } from 'vue'
|
||||
import type { Mutations } from '@coopenomics/sdk'
|
||||
import { Cooperative } from 'cooptypes'
|
||||
import { DigitalDocument } from 'src/shared/lib/document'
|
||||
import { useSystemStore } from 'src/entities/System/model'
|
||||
import { useSessionStore } from 'src/entities/Session'
|
||||
import { api } from '../api'
|
||||
|
||||
// Типы строго из SDK
|
||||
export type IConvertToBillingInput = Mutations.Billing.Convert.IInput['input']
|
||||
export type IConvertToBillingOutput =
|
||||
Mutations.Billing.Convert.IOutput[typeof Mutations.Billing.Convert.name]
|
||||
|
||||
// Видимость диалога — синглтон на уровне модуля (кнопка-триггер и сам диалог
|
||||
// должны делить один ref, как useSelectBranch().isVisible).
|
||||
const isVisible = ref(false)
|
||||
|
||||
export function useConvertToBillingVisibility() {
|
||||
return { isVisible }
|
||||
}
|
||||
|
||||
/**
|
||||
* Конвертация паевого взноса пайщика в членский на биллинг-кошелёк (Epic 12).
|
||||
*
|
||||
* Двухшаговый flow (как select-branch): generate (заявление document2 по
|
||||
* registry 1095) → sign (WIF пайщика) → мутация billingConvert.
|
||||
* `amountRub` — сумма в рублях (строка/число), к мутации уходит как "<N> RUB".
|
||||
*/
|
||||
export function useConvertToBilling() {
|
||||
const isLoading = ref(false)
|
||||
const isSubmitting = ref(false)
|
||||
const step = ref(1)
|
||||
const amountRub = ref<string>('')
|
||||
const generated = ref<Awaited<ReturnType<DigitalDocument['generate']>>>()
|
||||
|
||||
const system = useSystemStore()
|
||||
const session = useSessionStore()
|
||||
const digitalDocument = new DigitalDocument()
|
||||
|
||||
const assetAmount = () => `${Number(amountRub.value)} ${system.info.symbols?.root_govern_symbol ?? 'RUB'}`
|
||||
|
||||
const generate = async () => {
|
||||
isLoading.value = true
|
||||
try {
|
||||
generated.value = await digitalDocument.generate<Cooperative.Registry.BillingConversionStatement.Action>({
|
||||
registry_id: Cooperative.Registry.BillingConversionStatement.registry_id,
|
||||
coopname: system.info.coopname,
|
||||
username: session.username,
|
||||
convert_amount: assetAmount(),
|
||||
})
|
||||
step.value = 2
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const sign = async (): Promise<IConvertToBillingOutput> => {
|
||||
isSubmitting.value = true
|
||||
try {
|
||||
const document = await digitalDocument.sign<Cooperative.Registry.BillingConversionStatement.Meta>(
|
||||
session.username,
|
||||
)
|
||||
const result = await api.convertToBilling({
|
||||
coopname: system.info.coopname,
|
||||
username: session.username,
|
||||
amount: assetAmount(),
|
||||
document,
|
||||
})
|
||||
isVisible.value = false
|
||||
step.value = 1
|
||||
amountRub.value = ''
|
||||
return result
|
||||
} finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return { isVisible, isLoading, isSubmitting, step, amountRub, generated, generate, sign }
|
||||
}
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
<template lang="pug">
|
||||
q-btn(
|
||||
@click='open',
|
||||
:color='micro ? "accent" : "primary"',
|
||||
:flat='micro',
|
||||
:dense='micro',
|
||||
:size='micro ? "sm" : undefined',
|
||||
:unelevated='!micro'
|
||||
)
|
||||
q-icon(:name='micro ? "fa-solid fa-right-left" : "sync_alt"')
|
||||
span(v-if='!micro').q-ml-sm Конвертировать в членский
|
||||
q-tooltip(v-if='micro') Конвертировать в членский
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useConvertToBillingVisibility } from '../model'
|
||||
|
||||
interface Props {
|
||||
micro?: boolean;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
micro: false,
|
||||
});
|
||||
|
||||
const { isVisible } = useConvertToBillingVisibility()
|
||||
|
||||
const open = () => {
|
||||
isVisible.value = true
|
||||
}
|
||||
</script>
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
<template lang="pug">
|
||||
BaseDialog(
|
||||
v-model="isVisible"
|
||||
title="Конвертация паевого взноса в членский"
|
||||
:close-on-backdrop="false"
|
||||
:close-on-escape="false"
|
||||
)
|
||||
div.q-pa-md
|
||||
div(v-if="step === 1")
|
||||
p
|
||||
| Паевой взнос (возвратный) транслируется в членский (целевой) на ваш
|
||||
| персональный биллинг-кошелёк для оплаты инфраструктурных подписок.
|
||||
| Конвертация подтверждается подписанным заявлением.
|
||||
Form(
|
||||
:handler-submit="onGenerate"
|
||||
:is-submitting="isLoading"
|
||||
:showSubmit="!isLoading"
|
||||
:showCancel="true"
|
||||
:button-submit-txt="'Сформировать заявление'"
|
||||
@cancel="onCancel"
|
||||
)
|
||||
q-input(
|
||||
v-model="amountRub"
|
||||
type="number"
|
||||
min="1"
|
||||
step="0.01"
|
||||
label="Сумма конвертации, ₽"
|
||||
:rules="[(v) => Number(v) > 0 || 'Введите сумму больше нуля']"
|
||||
outlined
|
||||
).q-mb-md
|
||||
|
||||
div(v-else-if="step === 2")
|
||||
Loader(v-if="isLoading" :text="'Формируем документ...'")
|
||||
div(v-else-if="generated")
|
||||
DocumentHtmlReader(:html="generated.html")
|
||||
div.q-mt-md
|
||||
q-btn(@click="step = 1" flat) назад
|
||||
q-btn(@click="onSign" color="primary" :loading="isSubmitting") подписать
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { BaseDialog } from 'src/shared/ui/base/BaseDialog'
|
||||
import { Form } from 'src/shared/ui/Form'
|
||||
import { Loader } from 'src/shared/ui/Loader'
|
||||
import { DocumentHtmlReader } from 'src/shared/ui/DocumentHtmlReader'
|
||||
import { SuccessAlert, FailAlert } from 'src/shared/api'
|
||||
import { useConvertToBilling } from '../model'
|
||||
|
||||
const {
|
||||
isVisible,
|
||||
isLoading,
|
||||
isSubmitting,
|
||||
step,
|
||||
amountRub,
|
||||
generated,
|
||||
generate,
|
||||
sign,
|
||||
} = useConvertToBilling()
|
||||
|
||||
const onGenerate = async () => {
|
||||
try {
|
||||
await generate()
|
||||
} catch (e: any) {
|
||||
FailAlert(e)
|
||||
}
|
||||
}
|
||||
|
||||
const onSign = async () => {
|
||||
try {
|
||||
await sign()
|
||||
SuccessAlert('Паевой взнос сконвертирован в членский')
|
||||
} catch (e: any) {
|
||||
FailAlert(e)
|
||||
}
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
isVisible.value = false
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.digital-document .header {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,15 @@
|
||||
import { client } from 'src/shared/api/client'
|
||||
import { Mutations } from '@coopenomics/sdk'
|
||||
import type { IPaySubscriptionsInput } from '../model'
|
||||
|
||||
async function paySubscriptions(input: IPaySubscriptionsInput) {
|
||||
const { [Mutations.Billing.Pay.name]: result } = await client.Mutation(
|
||||
Mutations.Billing.Pay.mutation,
|
||||
{ variables: { input } },
|
||||
)
|
||||
return result
|
||||
}
|
||||
|
||||
export const api = {
|
||||
paySubscriptions,
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { usePaySubscriptions } from './model'
|
||||
export type { IPaySubscriptionsInput, IPaySubscriptionsOutput } from './model'
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { Mutations } from '@coopenomics/sdk'
|
||||
import { api } from '../api'
|
||||
|
||||
// Типы строго из SDK
|
||||
export type IPaySubscriptionsInput = Mutations.Billing.Pay.IInput['input']
|
||||
export type IPaySubscriptionsOutput =
|
||||
Mutations.Billing.Pay.IOutput[typeof Mutations.Billing.Pay.name]
|
||||
|
||||
/**
|
||||
* Списание стоимости подписок с биллинг-кошелька пайщика (Epic 12, billing::pay).
|
||||
*
|
||||
* ОТКРЫТО: `username` — пайщик-плательщик, чей USER_SHARED-кошелёк `w.wal.bill`
|
||||
* дебетуется. Реестр оператора не знает плательщика целевого коопа автоматически,
|
||||
* поэтому он указывается явно (та же конфиг-точка, что BILLING_CRON_PAYER в coopback).
|
||||
*/
|
||||
export function usePaySubscriptions() {
|
||||
const pay = async (input: IPaySubscriptionsInput): Promise<IPaySubscriptionsOutput> => {
|
||||
return await api.paySubscriptions(input)
|
||||
}
|
||||
|
||||
return { pay }
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from './ui'
|
||||
@@ -1,240 +0,0 @@
|
||||
<template lang="pug">
|
||||
div
|
||||
Form(:handler-submit="saveData" :showCancel="true" :button-cancel-txt="'Назад'" :button-submit-txt="'Продолжить'" @cancel="handleBack").q-gutter-md
|
||||
|
||||
//- Домен кооператива
|
||||
.form-section.q-mb-lg
|
||||
.section-header.q-mb-md
|
||||
.section-title
|
||||
q-icon(name="domain" size="20px" color="primary").q-mr-sm
|
||||
span.text-subtitle1.text-weight-medium Домен кооператива
|
||||
.section-description.text-body2.q-mt-sm
|
||||
| Укажите домен или поддомен, на котором будет работать ваш кооператив
|
||||
|
||||
q-input.form-input(
|
||||
standout="bg-teal text-white"
|
||||
hint="domovoy.com или coop.domovoy.com"
|
||||
label="Домен или поддомен для запуска"
|
||||
v-model="formData.announce"
|
||||
:rules="[val => notEmpty(val), val => isDomain(val)]"
|
||||
)
|
||||
|
||||
//- Финансовые параметры
|
||||
.form-section.q-mb-lg
|
||||
.section-header.q-mb-md
|
||||
.section-title
|
||||
q-icon(name="account_balance_wallet" size="20px" color="secondary").q-mr-sm
|
||||
span.text-subtitle1.text-weight-medium Финансовые параметры
|
||||
.section-description.text-body2.q-mt-sm
|
||||
| Введите вступительные и минимальные паевые взносы пайщиков. Эти параметры будут должны быть указаны в уставных документах кооператива.
|
||||
|
||||
.financial-grid
|
||||
.grid-section.q-mb-md
|
||||
.subsection-title.text-body1.text-weight-medium.q-mb-sm Для физических лиц и ИП
|
||||
.input-row.q-mb-sm
|
||||
q-input.form-input(
|
||||
standout="bg-teal text-white"
|
||||
placeholder="100"
|
||||
label="Вступительный взнос"
|
||||
v-model="formData.initial"
|
||||
type="number"
|
||||
:min="0"
|
||||
:rules="[val => notEmpty(val)]"
|
||||
)
|
||||
template(#append)
|
||||
span.text-overline RUB
|
||||
.input-row
|
||||
q-input.form-input(
|
||||
standout="bg-teal text-white"
|
||||
label="Минимальный паевый взнос"
|
||||
placeholder="300"
|
||||
v-model="formData.minimum"
|
||||
type="number"
|
||||
:min="0"
|
||||
:rules="[val => notEmpty(val)]"
|
||||
)
|
||||
template(#append)
|
||||
span.text-overline RUB
|
||||
|
||||
.grid-section
|
||||
.subsection-title.text-body1.text-weight-medium.q-mb-sm Для организаций
|
||||
.input-row.q-mb-sm
|
||||
q-input.form-input(
|
||||
standout="bg-teal text-white"
|
||||
placeholder="1000"
|
||||
label="Вступительный взнос"
|
||||
v-model="formData.org_initial"
|
||||
type="number"
|
||||
:min="0"
|
||||
:rules="[val => notEmpty(val)]"
|
||||
)
|
||||
template(#append)
|
||||
span.text-overline RUB
|
||||
.input-row
|
||||
q-input.form-input(
|
||||
standout="bg-teal text-white"
|
||||
placeholder="3000"
|
||||
label="Минимальный паевый взнос"
|
||||
v-model="formData.org_minimum"
|
||||
type="number"
|
||||
:min="0"
|
||||
:rules="[val => notEmpty(val)]"
|
||||
)
|
||||
template(#append)
|
||||
span.text-overline RUB
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { Form } from 'src/shared/ui/Form';
|
||||
import { notEmpty, isDomain } from 'src/shared/lib/utils';
|
||||
import { useConnectionAgreementStore } from 'src/entities/ConnectionAgreement';
|
||||
import type { ICooperativeFormData } from 'src/entities/ConnectionAgreement/model/types';
|
||||
|
||||
|
||||
const emit = defineEmits(['continue', 'back'])
|
||||
|
||||
const connectionAgreement = useConnectionAgreementStore()
|
||||
|
||||
// Локальное состояние формы для избежания проблем с реактивностью
|
||||
const formData = ref<ICooperativeFormData>({
|
||||
announce: connectionAgreement.formData.announce || '',
|
||||
initial: connectionAgreement.formData.initial || '',
|
||||
minimum: connectionAgreement.formData.minimum || '',
|
||||
org_initial: connectionAgreement.formData.org_initial || '',
|
||||
org_minimum: connectionAgreement.formData.org_minimum || ''
|
||||
})
|
||||
|
||||
// Синхронизируем локальное состояние с изменениями в store
|
||||
watch(() => connectionAgreement.formData, (newFormData) => {
|
||||
formData.value = {
|
||||
announce: newFormData.announce || '',
|
||||
initial: newFormData.initial || '',
|
||||
minimum: newFormData.minimum || '',
|
||||
org_initial: newFormData.org_initial || '',
|
||||
org_minimum: newFormData.org_minimum || ''
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
// Синхронизируем локальное состояние с store при изменении
|
||||
const syncFormData = () => {
|
||||
connectionAgreement.setFormData(formData.value)
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
emit('back')
|
||||
}
|
||||
|
||||
const saveData = async () => {
|
||||
console.log('📤 CooperativeDataForm: Данные формы:', formData.value)
|
||||
// Синхронизируем данные с store перед отправкой
|
||||
syncFormData()
|
||||
emit('continue', formData.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form-section {
|
||||
padding: 1.5rem;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||||
padding-bottom: 1rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.section-description {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.financial-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.grid-section {
|
||||
padding: 1rem;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.subsection-title {
|
||||
color: var(--q-primary);
|
||||
border-bottom: 2px solid var(--q-primary);
|
||||
padding-bottom: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.input-row {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.input-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Разделитель между секциями */
|
||||
.form-section + .form-section {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.financial-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.form-section {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
text-align: center;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.section-description {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.financial-grid {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.grid-section {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.subsection-title {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1 +0,0 @@
|
||||
export { default as CooperativeDataForm } from './CooperativeDataForm.vue'
|
||||
@@ -1,5 +1,7 @@
|
||||
import { RegistratorContract } from 'cooptypes';
|
||||
import { fetchTable } from 'src/shared/api';
|
||||
import { client } from 'src/shared/api/client';
|
||||
import { Queries } from '@coopenomics/sdk';
|
||||
|
||||
async function loadCoopByUsername(coopname: string): Promise<RegistratorContract.Tables.Cooperatives.ICooperative> {
|
||||
const coop = (await fetchTable(
|
||||
@@ -16,15 +18,17 @@ async function loadCoopByUsername(coopname: string): Promise<RegistratorContract
|
||||
}
|
||||
|
||||
|
||||
async function loadCoops(): Promise<RegistratorContract.Tables.Cooperatives.ICooperative[]> {
|
||||
const requests = (await fetchTable(
|
||||
RegistratorContract.contractName.production,
|
||||
RegistratorContract.contractName.production,
|
||||
RegistratorContract.Tables.Cooperatives.tableName
|
||||
)) as RegistratorContract.Tables.Cooperatives.ICooperative[];
|
||||
/**
|
||||
* Реестр кооперативов оператора грузится через бэкенд (coopback GraphQL),
|
||||
* а не напрямую из блокчейна: бэкенд сводит on-chain список кооперативов
|
||||
* с данными провайдера (подписки / инстанс / биллинг по coopname).
|
||||
*/
|
||||
async function loadCoops(): Promise<Queries.System.GetCooperativesRegistry.IOutput['getCooperativesRegistry']> {
|
||||
const { [Queries.System.GetCooperativesRegistry.name]: result } = await client.Query(
|
||||
Queries.System.GetCooperativesRegistry.query
|
||||
);
|
||||
|
||||
|
||||
return requests;
|
||||
return result;
|
||||
}
|
||||
|
||||
export const api = {
|
||||
|
||||
+39
-36
@@ -140,44 +140,50 @@ const init = async () => {
|
||||
connectionAgreement.setHasMatrixAccount(accountStatus.hasAccount);
|
||||
}
|
||||
|
||||
// Подтягиваем on-chain запись кооператива — её наличие означает, что
|
||||
// соглашение о подключении уже подписано (regcoop проходит только после
|
||||
// signagree). Используем как маркёр прохождения шагов 0–4.
|
||||
try {
|
||||
await connectionAgreement.reloadCooperative();
|
||||
} catch {
|
||||
// coop ещё не зарегистрирован — это валидное состояние, не падаем
|
||||
}
|
||||
|
||||
const instance = connectionAgreement.currentInstance;
|
||||
const hasInstanceError = connectionAgreement.currentInstanceError;
|
||||
const coop = connectionAgreement.coop;
|
||||
|
||||
// Определяем шаг на основе текущего прогресса установки (при каждом заходе)
|
||||
|
||||
// Сначала проверяем, была ли установка уже завершена (даже при ошибке загрузки)
|
||||
// Сначала проверяем, была ли установка уже завершена
|
||||
const isAlreadyCompleted = instance?.progress === 100 && instance?.status === Zeus.InstanceStatus.ACTIVE;
|
||||
if (isAlreadyCompleted) {
|
||||
console.log('✅ Установка уже завершена ранее, показываем дашборд');
|
||||
// Не меняем шаг, оставляем текущий (или устанавливаем специальный шаг для завершенной установки)
|
||||
isLoading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Определяем шаг на основе текущего состояния
|
||||
// Восстанавливаем фактический шаг по состоянию on-chain + provider.
|
||||
// Идея: каждый шаг имеет хорошо различимый внешний маркёр; при перезагрузке
|
||||
// не должно требоваться localStorage.
|
||||
let targetStep: number;
|
||||
|
||||
if (hasInstanceError) {
|
||||
// Если есть ошибка загрузки инстанса, определяем шаг на основе членства в союзе
|
||||
targetStep = system.info.is_unioned ? 0 : 1;
|
||||
} else if (instance && typeof instance.progress === 'number' && instance.progress > 0) {
|
||||
// Если установка уже идет (прогресс > 0), переходим к шагу установки
|
||||
targetStep = 6;
|
||||
} else {
|
||||
// Если инстанса нет ИЛИ его прогресс = 0, определяем шаг на основе членства в союзе
|
||||
if (system.info.is_unioned) {
|
||||
// Если кооператив не является членом союза, начинаем с нулевого шага
|
||||
targetStep = 0;
|
||||
if (coop) {
|
||||
// Соглашение точно подписано (coop появился в registrator.coops).
|
||||
if (instance && typeof instance.progress === 'number' && instance.progress > 0) {
|
||||
targetStep = 7; // установка идёт
|
||||
} else if (instance?.blockchain_status === 'active') {
|
||||
targetStep = 7; // союз подтвердил, провайдер вот-вот стартует
|
||||
} else if (instance?.is_delegated) {
|
||||
targetStep = 6; // домен делегирован, ждём подтверждения союза
|
||||
} else {
|
||||
// Если кооператив уже член союза, начинаем с первого шага
|
||||
targetStep = 1;
|
||||
targetStep = 5; // DNS-шаг: ждём делегирования или провайдер ещё не подхватил coop
|
||||
}
|
||||
} else if (system.info.is_unioned) {
|
||||
targetStep = 0; // нужна регистрация в мессенджере союза
|
||||
} else {
|
||||
targetStep = 1; // онбординг от выбора тарифа
|
||||
}
|
||||
|
||||
// Устанавливаем определенный шаг
|
||||
connectionAgreement.setCurrentStep(targetStep);
|
||||
|
||||
// Скрываем лоадер после загрузки данных
|
||||
isLoading.value = false;
|
||||
};
|
||||
|
||||
@@ -203,27 +209,24 @@ watch(
|
||||
status: instance.status,
|
||||
});
|
||||
|
||||
// Логика автоматических переходов (только для шагов 4, 5, 6)
|
||||
// Шаги 0, 1, 2, 3 не имеют автоматических переходов
|
||||
if (currentStep === 4) {
|
||||
// Шаг 4: Проверка домена
|
||||
// Автопереходы только для шагов 5 (dns), 6 (approval), 7 (installation).
|
||||
// Шаги 0..4 (union/intro/domain/financial/agreement) — пользовательский ввод.
|
||||
if (currentStep === 5) {
|
||||
// Шаг 5: Проверка делегирования домена.
|
||||
if (instance.is_valid && instance.is_delegated) {
|
||||
// Домен валиден и делегирован
|
||||
if (instance.blockchain_status === 'active') {
|
||||
// Можно переходить сразу к установке
|
||||
console.log('✅ Домен готов и blockchain_status активен → переход к шагу 6');
|
||||
connectionAgreement.setCurrentStep(6);
|
||||
console.log('✅ Домен готов и blockchain_status активен → переход к шагу 7 (installation)');
|
||||
connectionAgreement.setCurrentStep(7);
|
||||
} else {
|
||||
// Ожидаем подтверждения от союза
|
||||
console.log('⏳ Домен готов, но ожидаем подтверждения → переход к шагу 5');
|
||||
connectionAgreement.setCurrentStep(5);
|
||||
console.log('⏳ Домен готов, ждём подтверждения союза → переход к шагу 6 (approval)');
|
||||
connectionAgreement.setCurrentStep(6);
|
||||
}
|
||||
}
|
||||
} else if (currentStep === 5) {
|
||||
// Шаг 5: Ожидание подтверждения от союза
|
||||
} else if (currentStep === 6) {
|
||||
// Шаг 6: Ожидание подтверждения от союза.
|
||||
if (instance.blockchain_status === 'active') {
|
||||
console.log('✅ Подтверждение получено → переход к шагу 6');
|
||||
connectionAgreement.setCurrentStep(6);
|
||||
console.log('✅ Подтверждение получено → переход к шагу 7 (installation)');
|
||||
connectionAgreement.setCurrentStep(7);
|
||||
}
|
||||
}
|
||||
// Редирект на страницу завершения теперь делает только InstallationStep.vue
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './ui'
|
||||
+346
@@ -0,0 +1,346 @@
|
||||
<template lang="pug">
|
||||
.coop-detail.q-pa-md
|
||||
.coop-detail__back
|
||||
BaseButton(
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
type="button"
|
||||
@click="goBack"
|
||||
)
|
||||
q-icon(name="arrow_back" size="16px").q-mr-xs
|
||||
| К реестру
|
||||
|
||||
Loader(v-if="loading" :text="'Загрузка кооператива...'")
|
||||
|
||||
BaseBanner(v-else-if="!row" variant="warn")
|
||||
| Кооператив не найден в реестре.
|
||||
|
||||
template(v-else)
|
||||
BaseCard(
|
||||
:title="row.name || row.coopname"
|
||||
:subtitle="headerSubtitle"
|
||||
)
|
||||
template(#actions)
|
||||
.coop-detail__head-actions
|
||||
BaseChip(:variant="registryStatusVariant(row.status)" size="sm")
|
||||
span {{ registryStatusLabel(row.status) }}
|
||||
BaseButton(
|
||||
v-if="row.status !== 'active'"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
type="button"
|
||||
@click="activate"
|
||||
) Активировать
|
||||
BaseButton(
|
||||
v-if="row.status !== 'blocked'"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
type="button"
|
||||
@click="block"
|
||||
) Заблокировать
|
||||
|
||||
DataRow(v-if="row.announce" label="Сайт" :value="row.announce" mono)
|
||||
template(#value-override)
|
||||
a.coop-detail__link.t-mono(
|
||||
:href="resolveSiteUrl(row.announce)"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
) {{ row.announce }}
|
||||
|
||||
.coop-detail__section
|
||||
.coop-detail__section-title Подписки
|
||||
|
||||
EmptyState(
|
||||
v-if="!row.subscriptions || !row.subscriptions.length"
|
||||
title="Подписок нет"
|
||||
body="У кооператива пока нет активных подписок у провайдера"
|
||||
)
|
||||
|
||||
BaseCard(v-else variant="flat")
|
||||
BaseTable(
|
||||
:columns="subscriptionColumns"
|
||||
:rows="row.subscriptions"
|
||||
row-key="id"
|
||||
)
|
||||
template(#cell-name="{ row: sub }")
|
||||
.coop-detail__sub-name
|
||||
span {{ sub.subscription_type_name }}
|
||||
template(#cell-status="{ row: sub }")
|
||||
BaseChip(:variant="subscriptionStatusVariant(sub.status)" size="sm")
|
||||
span {{ subscriptionStatusLabel(sub.status) }}
|
||||
template(#cell-period="{ row: sub }")
|
||||
span.t-mono {{ sub.period_days }} дн.
|
||||
template(#cell-price="{ row: sub }")
|
||||
span.t-mono {{ formatMoney(sub.price) }} RUB
|
||||
template(#cell-next="{ row: sub }")
|
||||
span.t-mono(v-if="sub.next_payment_due") {{ formatDate(sub.next_payment_due) }}
|
||||
span.t-muted(v-else) —
|
||||
|
||||
.coop-detail__section
|
||||
.coop-detail__section-title Кошельки кооператива в Восходе
|
||||
|
||||
BaseBanner(v-if="walletError" variant="neg") {{ walletError }}
|
||||
|
||||
.coop-detail__wallets
|
||||
BaseCard(variant="flat" title="Паевой взнос")
|
||||
.coop-detail__wallet-body
|
||||
.coop-detail__wallet-amount.t-mono(v-if="walletLoading") …
|
||||
.coop-detail__wallet-amount.t-mono(v-else)
|
||||
| {{ formatAmount(walletAvailable) }} {{ walletDisplaySymbol }}
|
||||
.coop-detail__wallet-hint Возвратный взнос на счёте пайщика в Восходе.
|
||||
|
||||
BaseCard(variant="flat" title="Кошелёк членских взносов")
|
||||
.coop-detail__wallet-body
|
||||
.coop-detail__wallet-amount.t-mono(v-if="walletLoading") …
|
||||
.coop-detail__wallet-amount.t-mono(v-else)
|
||||
| {{ formatAmount(walletMembership) }} {{ walletDisplaySymbol }}
|
||||
.coop-detail__wallet-hint Списывается за инфраструктурные подписки.
|
||||
|
||||
.coop-detail__section
|
||||
.coop-detail__section-title История оплат
|
||||
|
||||
EmptyState(
|
||||
title="История пока недоступна"
|
||||
body="Раздел появится после реализации выгрузки операций биллинга по кооперативу."
|
||||
)
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import moment from 'src/shared/lib/utils/dates/moment'
|
||||
import {
|
||||
BaseBanner,
|
||||
BaseButton,
|
||||
BaseCard,
|
||||
BaseChip,
|
||||
BaseTable,
|
||||
EmptyState,
|
||||
} from 'src/shared/ui/base'
|
||||
import { DataRow } from 'src/shared/ui/domain'
|
||||
import Loader from 'src/shared/ui/Loader/Loader.vue'
|
||||
import { useLoadCooperatives } from 'src/features/Union/LoadCooperatives'
|
||||
import { useActivateCooperative } from 'src/features/Union/ActivateCooperative'
|
||||
import { useBlockCooperative } from 'src/features/Union/BlockCooperative'
|
||||
import { useUnionStore } from 'src/entities/Union/model'
|
||||
import type { ICooperativeRegistryItem } from 'src/entities/Union/model'
|
||||
import { useCooperativeMainWallet } from 'src/entities/Wallet/model'
|
||||
import { useSystemStore } from 'src/entities/System/model'
|
||||
import { FailAlert, SuccessAlert } from 'src/shared/api/alerts'
|
||||
|
||||
type BaseChipVariant = 'neutral' | 'accent' | 'pos' | 'neg' | 'warn' | 'info'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const union = useUnionStore()
|
||||
const system = useSystemStore()
|
||||
const { loadCooperatives } = useLoadCooperatives()
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const coopname = computed(() => String(route.params.detailCoopname || ''))
|
||||
|
||||
const row = computed<ICooperativeRegistryItem | undefined>(() =>
|
||||
union.coops.find((c) => c.coopname === coopname.value),
|
||||
)
|
||||
|
||||
const {
|
||||
available: walletAvailable,
|
||||
membership: walletMembership,
|
||||
symbol: walletSymbolRaw,
|
||||
loading: walletLoading,
|
||||
error: walletError,
|
||||
} = useCooperativeMainWallet(
|
||||
() => system.info.coopname || '',
|
||||
() => coopname.value,
|
||||
)
|
||||
|
||||
const walletDisplaySymbol = computed(
|
||||
() => walletSymbolRaw.value || system.info.symbols?.root_govern_symbol || 'RUB',
|
||||
)
|
||||
|
||||
const formatAmount = (amount: string): string =>
|
||||
new Intl.NumberFormat('ru-RU', {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(Number(amount))
|
||||
|
||||
const load = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
await loadCooperatives()
|
||||
} catch (e: any) {
|
||||
FailAlert(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
load()
|
||||
|
||||
const subscriptionColumns = [
|
||||
{ key: 'name', label: 'Подписка', align: 'left' as const },
|
||||
{ key: 'status', label: 'Статус', align: 'left' as const },
|
||||
{ key: 'period', label: 'Период', align: 'right' as const, numeric: true },
|
||||
{ key: 'price', label: 'Стоимость', align: 'right' as const, numeric: true },
|
||||
{ key: 'next', label: 'След. оплата', align: 'right' as const },
|
||||
]
|
||||
|
||||
const headerSubtitle = computed(() => {
|
||||
const r = row.value
|
||||
if (!r) return undefined
|
||||
const parts: string[] = [r.coopname]
|
||||
if (r.created_at) parts.push(`заявка от ${formatDateTime(r.created_at)}`)
|
||||
return parts.join(' · ')
|
||||
})
|
||||
|
||||
const formatDate = (d: string) => moment(d).format('DD.MM.YYYY')
|
||||
const formatDateTime = (d: string) => moment(d).format('DD.MM.YY HH:mm')
|
||||
const formatMoney = (value: number | string): string =>
|
||||
new Intl.NumberFormat('ru-RU', {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(Number(value))
|
||||
|
||||
const resolveSiteUrl = (announce: string): string =>
|
||||
/^https?:\/\//.test(announce) ? announce : `https://${announce}`
|
||||
|
||||
const registryStatusVariant = (status: string): BaseChipVariant => {
|
||||
switch (status) {
|
||||
case 'active':
|
||||
return 'pos'
|
||||
case 'pending':
|
||||
return 'warn'
|
||||
case 'blocked':
|
||||
return 'neg'
|
||||
default:
|
||||
return 'neutral'
|
||||
}
|
||||
}
|
||||
|
||||
const registryStatusLabel = (status: string): string => {
|
||||
switch (status) {
|
||||
case 'active':
|
||||
return 'активен'
|
||||
case 'pending':
|
||||
return 'на рассмотрении'
|
||||
case 'blocked':
|
||||
return 'заблокирован'
|
||||
default:
|
||||
return status
|
||||
}
|
||||
}
|
||||
|
||||
const subscriptionStatusVariant = (status: string): BaseChipVariant => {
|
||||
switch (status) {
|
||||
case 'ACTIVE':
|
||||
return 'pos'
|
||||
case 'TRIAL':
|
||||
return 'info'
|
||||
case 'EXPIRED':
|
||||
return 'warn'
|
||||
case 'CANCELLED':
|
||||
return 'neg'
|
||||
default:
|
||||
return 'neutral'
|
||||
}
|
||||
}
|
||||
|
||||
const subscriptionStatusLabel = (status: string): string => {
|
||||
switch (status) {
|
||||
case 'ACTIVE':
|
||||
return 'активна'
|
||||
case 'TRIAL':
|
||||
return 'триал'
|
||||
case 'EXPIRED':
|
||||
return 'истекла'
|
||||
case 'CANCELLED':
|
||||
return 'отменена'
|
||||
default:
|
||||
return status
|
||||
}
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
const params = { ...route.params }
|
||||
delete (params as any).detailCoopname
|
||||
router.push({ name: 'union-cooperatives', params })
|
||||
}
|
||||
|
||||
const activate = async () => {
|
||||
if (!row.value) return
|
||||
const { activateCooperative } = useActivateCooperative()
|
||||
try {
|
||||
await activateCooperative(row.value.coopname)
|
||||
await load()
|
||||
SuccessAlert('Кооператив активирован')
|
||||
} catch (e: any) {
|
||||
FailAlert(e)
|
||||
}
|
||||
}
|
||||
|
||||
const block = async () => {
|
||||
if (!row.value) return
|
||||
const { blockCooperative } = useBlockCooperative()
|
||||
try {
|
||||
await blockCooperative(row.value.coopname)
|
||||
await load()
|
||||
SuccessAlert('Кооператив заблокирован')
|
||||
} catch (e: any) {
|
||||
FailAlert(e)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.coop-detail__back {
|
||||
margin-bottom: var(--p-3);
|
||||
}
|
||||
.coop-detail__head-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--p-2);
|
||||
}
|
||||
.coop-detail__link {
|
||||
color: var(--p-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
.coop-detail__link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.coop-detail__section {
|
||||
margin-top: var(--p-5);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-3);
|
||||
}
|
||||
.coop-detail__section-title {
|
||||
font-size: var(--p-fs-h6);
|
||||
font-weight: 600;
|
||||
color: var(--p-ink);
|
||||
}
|
||||
.coop-detail__sub-name {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
.coop-detail__wallets {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: var(--p-3);
|
||||
}
|
||||
.coop-detail__wallet-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-1);
|
||||
padding: var(--p-2) 0;
|
||||
}
|
||||
.coop-detail__wallet-amount {
|
||||
font-size: var(--p-fs-h6);
|
||||
font-weight: 700;
|
||||
color: var(--p-ink);
|
||||
}
|
||||
.coop-detail__wallet-hint {
|
||||
font-size: var(--p-fs-caption);
|
||||
color: var(--p-ink-2);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as UnionPageCooperativeDetail } from './UnionPageCooperativeDetail.vue'
|
||||
+249
-116
@@ -1,141 +1,274 @@
|
||||
<template lang="pug">
|
||||
div
|
||||
q-table(
|
||||
v-if="coops"
|
||||
ref="tableRef" v-model:expanded="expanded"
|
||||
flat
|
||||
:rows="coops"
|
||||
:columns="columns"
|
||||
:table-colspan="9"
|
||||
row-key="username"
|
||||
:pagination="pagination"
|
||||
virtual-scroll
|
||||
:virtual-scroll-item-size="48"
|
||||
:rows-per-page-options="[10]"
|
||||
:loading="onLoading"
|
||||
:no-data-label="'Нет кооперативов'"
|
||||
).full-height
|
||||
template(#top)
|
||||
.coop-registry.q-pa-md
|
||||
.banner
|
||||
q-icon.banner__icon(name="info" size="18px")
|
||||
.banner__body
|
||||
| Заявки на подключение к платформе и состояние подписок у провайдера.
|
||||
|
||||
Loader(v-if="onLoading" :text="'Загрузка реестра...'")
|
||||
|
||||
template(#header="props")
|
||||
EmptyState(
|
||||
v-else-if="!coops || !coops.length"
|
||||
title="Нет кооперативов"
|
||||
body="В реестре пока нет заявок на подключение"
|
||||
)
|
||||
|
||||
q-tr(:props="props")
|
||||
q-th(auto-width)
|
||||
.coop-registry__list(v-else)
|
||||
BaseCard.coop-registry__card(
|
||||
v-for="row in coops"
|
||||
:key="row.coopname"
|
||||
:title="row.name || row.coopname"
|
||||
:subtitle="cardSubtitle(row)"
|
||||
@click="openDetail(row.coopname)"
|
||||
)
|
||||
template(#actions)
|
||||
.coop-registry__head-actions
|
||||
BaseChip(:variant="registryStatusVariant(row.status)" size="sm")
|
||||
span {{ registryStatusLabel(row.status) }}
|
||||
BaseButton(
|
||||
v-if="row.status !== 'active'"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
type="button"
|
||||
@click.stop="activate(row.coopname)"
|
||||
) Активировать
|
||||
BaseButton(
|
||||
v-if="row.status !== 'blocked'"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
type="button"
|
||||
@click.stop="block(row.coopname)"
|
||||
) Заблокировать
|
||||
|
||||
q-th(
|
||||
v-for="col in props.cols"
|
||||
:key="col.name"
|
||||
:props="props"
|
||||
) {{ col.label }}
|
||||
.coop-registry__summary
|
||||
.coop-registry__metric
|
||||
.coop-registry__metric-label Подписок
|
||||
.coop-registry__metric-value.t-mono {{ row.subscriptions?.length || 0 }}
|
||||
.coop-registry__metric
|
||||
.coop-registry__metric-label Сумма в месяц
|
||||
.coop-registry__metric-value.t-mono
|
||||
| {{ formatMoney(monthlyTotal(row)) }} RUB
|
||||
.coop-registry__metric
|
||||
.coop-registry__metric-label Следующая оплата
|
||||
.coop-registry__metric-value.t-mono {{ nextPaymentLabel(row) }}
|
||||
.coop-registry__metric
|
||||
.coop-registry__metric-label Хостинг
|
||||
.coop-registry__metric-value
|
||||
BaseChip(v-if="hostingStatus(row)" :variant="instanceStatusVariant(hostingStatus(row))" size="sm")
|
||||
span {{ instanceStatusLabel(hostingStatus(row)) }}
|
||||
span.t-muted(v-else) нет
|
||||
</template>
|
||||
|
||||
template(#body="props")
|
||||
q-tr(:key="`m_${props.row.username}`" :props="props")
|
||||
q-td(auto-width)
|
||||
q-btn(size="sm" color="primary" round dense :icon="props.expand ? 'remove' : 'add'" @click="props.expand = !props.expand")
|
||||
q-td {{ props.row.username }}
|
||||
q-td {{ props.row.announce }}
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import moment from 'src/shared/lib/utils/dates/moment'
|
||||
import {
|
||||
BaseButton,
|
||||
BaseCard,
|
||||
BaseChip,
|
||||
EmptyState,
|
||||
} from 'src/shared/ui/base'
|
||||
import Loader from 'src/shared/ui/Loader/Loader.vue'
|
||||
import { useLoadCooperatives } from 'src/features/Union/LoadCooperatives'
|
||||
import { useActivateCooperative } from 'src/features/Union/ActivateCooperative'
|
||||
import { useBlockCooperative } from 'src/features/Union/BlockCooperative'
|
||||
import { useUnionStore } from 'src/entities/Union/model'
|
||||
import type { ICooperativeRegistryItem } from 'src/entities/Union/model'
|
||||
import { FailAlert, SuccessAlert } from 'src/shared/api/alerts'
|
||||
|
||||
q-td
|
||||
q-badge(v-if="props.row.status === 'active'" color="teal") активен
|
||||
q-badge(v-if="props.row.status === 'pending'" color="orange") на рассмотрении
|
||||
q-badge(v-if="props.row.status === 'blocked'" color="red") заблокирован
|
||||
type ICooperativeSubscription = ICooperativeRegistryItem['subscriptions'][number]
|
||||
type BaseChipVariant = 'neutral' | 'accent' | 'pos' | 'neg' | 'warn' | 'info'
|
||||
|
||||
q-td {{ moment(props.row.created_at).format('DD.MM.YY HH:mm:ss') }}
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const union = useUnionStore()
|
||||
const { loadCooperatives } = useLoadCooperatives()
|
||||
|
||||
q-td
|
||||
q-btn-dropdown( label="действия" flat size="sm")
|
||||
q-list
|
||||
q-item(v-if="props.row.status !== 'active'" clickable v-close-popup @click="activate(props.row.username)")
|
||||
q-item-section
|
||||
q-item-label Активировать
|
||||
q-item(v-if="props.row.status !== 'blocked'" clickable v-close-popup @click="block(props.row.username)")
|
||||
q-item-section
|
||||
q-item-label Заблокировать
|
||||
//- q-item(clickable v-close-popup @click="deleteCoop(props.row.username)")
|
||||
//- q-item-section
|
||||
//- q-item-label Удалить
|
||||
const coops = computed(() => union.coops)
|
||||
|
||||
q-tr(v-show="props.expand" :key="`e_${props.row.username}`" :props="props" class="q-virtual-scroll--with-prev")
|
||||
q-td(colspan="100%")
|
||||
slot(:expand="props.expand" :receiver="props.row.username")
|
||||
ListOfDocumentsWidget(
|
||||
:username="props.row.username"
|
||||
:filter="{data: getDataFilter(props.row.document.hash)}"
|
||||
:expand="true"
|
||||
:documentType="'newsubmitted'"
|
||||
)
|
||||
const onLoading = ref(false)
|
||||
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { useLoadCooperatives } from 'src/features/Union/LoadCooperatives';
|
||||
const {loadCooperatives} = useLoadCooperatives()
|
||||
import { useUnionStore } from 'src/entities/Union/model';
|
||||
import { computed, ref } from 'vue';
|
||||
import moment from 'src/shared/lib/utils/dates/moment'
|
||||
import { useActivateCooperative } from 'src/features/Union/ActivateCooperative';
|
||||
import { FailAlert, SuccessAlert } from 'src/shared/api/alerts';
|
||||
import { useBlockCooperative } from 'src/features/Union/BlockCooperative';
|
||||
const union = useUnionStore()
|
||||
|
||||
import { ListOfDocumentsWidget } from 'src/widgets/Cooperative/Documents/ListOfDocuments';
|
||||
const getDataFilter = (hash: string) => {
|
||||
return {
|
||||
document: {
|
||||
hash: hash.toUpperCase()
|
||||
}
|
||||
}
|
||||
const load = async () => {
|
||||
onLoading.value = true
|
||||
try {
|
||||
await loadCooperatives()
|
||||
} catch (e: any) {
|
||||
FailAlert(e)
|
||||
} finally {
|
||||
onLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const coops = computed(() => union.coops)
|
||||
load()
|
||||
|
||||
loadCooperatives()
|
||||
const hostingSubscription = (row: ICooperativeRegistryItem): ICooperativeSubscription | undefined =>
|
||||
row.subscriptions?.find((s) => !!s.instance_status)
|
||||
|
||||
const activate = async (coopname: string) => {
|
||||
const {activateCooperative} = useActivateCooperative()
|
||||
const hostingStatus = (row: ICooperativeRegistryItem): string | null =>
|
||||
hostingSubscription(row)?.instance_status ?? null
|
||||
|
||||
try {
|
||||
await activateCooperative(coopname)
|
||||
loadCooperatives()
|
||||
SuccessAlert('Кооператив активирован')
|
||||
} catch(e: any) {
|
||||
FailAlert(e)
|
||||
}
|
||||
const monthlyTotal = (row: ICooperativeRegistryItem): number =>
|
||||
(row.subscriptions ?? []).reduce((sum, s) => sum + (Number(s.price) || 0), 0)
|
||||
|
||||
const nextPaymentDate = (row: ICooperativeRegistryItem): string | null => {
|
||||
const dates = (row.subscriptions ?? [])
|
||||
.map((s) => s.next_payment_due)
|
||||
.filter(Boolean) as string[]
|
||||
if (!dates.length) return null
|
||||
return dates.sort()[0]
|
||||
}
|
||||
|
||||
const nextPaymentLabel = (row: ICooperativeRegistryItem): string => {
|
||||
const d = nextPaymentDate(row)
|
||||
return d ? moment(d).format('DD.MM.YYYY') : '—'
|
||||
}
|
||||
|
||||
const formatDateTime = (value: string): string =>
|
||||
moment(value).format('DD.MM.YY HH:mm')
|
||||
|
||||
const formatMoney = (value: number | string): string =>
|
||||
new Intl.NumberFormat('ru-RU', {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(Number(value))
|
||||
|
||||
const cardSubtitle = (row: ICooperativeRegistryItem): string | undefined => {
|
||||
const parts: string[] = []
|
||||
if (row.name) parts.push(row.coopname)
|
||||
if (row.created_at) parts.push(`заявка от ${formatDateTime(row.created_at)}`)
|
||||
return parts.length ? parts.join(' · ') : undefined
|
||||
}
|
||||
|
||||
const registryStatusVariant = (status: string): BaseChipVariant => {
|
||||
switch (status) {
|
||||
case 'active':
|
||||
return 'pos'
|
||||
case 'pending':
|
||||
return 'warn'
|
||||
case 'blocked':
|
||||
return 'neg'
|
||||
default:
|
||||
return 'neutral'
|
||||
}
|
||||
}
|
||||
|
||||
const block = async (coopname: string) => {
|
||||
const {blockCooperative} = useBlockCooperative()
|
||||
|
||||
try {
|
||||
await blockCooperative(coopname)
|
||||
loadCooperatives()
|
||||
SuccessAlert('Кооператив заблокирован')
|
||||
} catch(e: any) {
|
||||
FailAlert(e)
|
||||
}
|
||||
const registryStatusLabel = (status: string): string => {
|
||||
switch (status) {
|
||||
case 'active':
|
||||
return 'активен'
|
||||
case 'pending':
|
||||
return 'на рассмотрении'
|
||||
case 'blocked':
|
||||
return 'заблокирован'
|
||||
default:
|
||||
return status
|
||||
}
|
||||
}
|
||||
|
||||
const instanceStatusVariant = (status: string | null): BaseChipVariant => {
|
||||
switch (status) {
|
||||
case 'active':
|
||||
return 'pos'
|
||||
case 'install':
|
||||
case 'rent':
|
||||
case 'pending':
|
||||
return 'warn'
|
||||
case 'error':
|
||||
case 'blocked':
|
||||
case 'requires_manual_review':
|
||||
return 'neg'
|
||||
default:
|
||||
return 'neutral'
|
||||
}
|
||||
}
|
||||
|
||||
const onLoading = ref(false)
|
||||
const instanceStatusLabel = (status: string | null): string => {
|
||||
switch (status) {
|
||||
case 'active':
|
||||
return 'активен'
|
||||
case 'install':
|
||||
return 'установка'
|
||||
case 'rent':
|
||||
return 'аренда'
|
||||
case 'pending':
|
||||
return 'ожидание'
|
||||
case 'error':
|
||||
return 'ошибка'
|
||||
case 'blocked':
|
||||
return 'заблокирован'
|
||||
case 'requires_manual_review':
|
||||
return 'нужна проверка'
|
||||
default:
|
||||
return status ?? '—'
|
||||
}
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ name: 'username', align: 'left', label: 'Аккаунт', field: 'username', sortable: true },
|
||||
{ name: 'announce', align: 'left', label: 'Сайт', field: 'announce', sortable: false },
|
||||
const openDetail = (coopname: string) => {
|
||||
const params = { ...route.params, detailCoopname: coopname }
|
||||
router.push({ name: 'union-cooperative-detail', params })
|
||||
}
|
||||
|
||||
{ name: 'status', align: 'left', label: 'Статус', field: 'status', sortable: true },
|
||||
{
|
||||
name: 'created_at',
|
||||
align: 'left',
|
||||
label: 'Дата заявки',
|
||||
field: 'created_at',
|
||||
sortable: true,
|
||||
},
|
||||
{ name: 'actions', align: 'center', label: '', field: 'actions', sortable: false },
|
||||
] as any
|
||||
const activate = async (coopname: string) => {
|
||||
const { activateCooperative } = useActivateCooperative()
|
||||
try {
|
||||
await activateCooperative(coopname)
|
||||
await load()
|
||||
SuccessAlert('Кооператив активирован')
|
||||
} catch (e: any) {
|
||||
FailAlert(e)
|
||||
}
|
||||
}
|
||||
|
||||
const expanded = ref([])
|
||||
const tableRef = ref(null)
|
||||
const pagination = ref({ rowsPerPage: 0 })
|
||||
const block = async (coopname: string) => {
|
||||
const { blockCooperative } = useBlockCooperative()
|
||||
try {
|
||||
await blockCooperative(coopname)
|
||||
await load()
|
||||
SuccessAlert('Кооператив заблокирован')
|
||||
} catch (e: any) {
|
||||
FailAlert(e)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
</script>
|
||||
<style scoped>
|
||||
.coop-registry__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-4);
|
||||
margin-top: var(--p-4);
|
||||
}
|
||||
.coop-registry__card {
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s ease;
|
||||
}
|
||||
.coop-registry__card:hover {
|
||||
border-color: var(--p-primary);
|
||||
}
|
||||
.coop-registry__head-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--p-2);
|
||||
}
|
||||
.coop-registry__summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
gap: var(--p-3);
|
||||
padding: var(--p-3) 0;
|
||||
}
|
||||
.coop-registry__metric {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-1);
|
||||
}
|
||||
.coop-registry__metric-label {
|
||||
font-size: var(--p-fs-caption);
|
||||
color: var(--p-ink-2);
|
||||
}
|
||||
.coop-registry__metric-value {
|
||||
font-size: var(--p-fs-body);
|
||||
font-weight: 600;
|
||||
color: var(--p-ink);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
.wallet-page
|
||||
WalletProgramWidget
|
||||
|
||||
//- Действия страницы в шапке — через canon Teleport в слот-host шапки.
|
||||
//- defer: target (#header-actions-host) живёт в layout-шапке, смонтированной
|
||||
//- раньше страницы. На мобильном — micro-вариант кнопок (иконка + tooltip),
|
||||
//- чтобы не раздувать узкую шапку; на десктопе — полные кнопки с подписью.
|
||||
//- Сами кнопки решают свою видимость (DepositButton скрыт, пока пайщик
|
||||
//- не принят — status !== 'active').
|
||||
Teleport(to='#header-actions-host', defer)
|
||||
DepositButton(:micro='isMobile')
|
||||
WithdrawButton(:micro='isMobile')
|
||||
//- Действия страницы в шапке — через canon Teleport в слот-host шапки.
|
||||
//- defer: target (#header-actions-host) живёт в layout-шапке, смонтированной
|
||||
//- раньше страницы. На мобильном — micro-вариант кнопок (иконка + tooltip),
|
||||
//- чтобы не раздувать узкую шапку; на десктопе — полные кнопки с подписью.
|
||||
//- Сами кнопки решают свою видимость (DepositButton скрыт, пока пайщик
|
||||
//- не принят — status !== 'active').
|
||||
Teleport(to='#header-actions-host', defer)
|
||||
DepositButton(:micro='isMobile')
|
||||
WithdrawButton(:micro='isMobile')
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
@@ -192,7 +192,7 @@ function labelClass(step: StepperStep): Record<string, boolean> {
|
||||
color: var(--p-ink-1);
|
||||
}
|
||||
.vertical-stepper__label--clickable:hover {
|
||||
color: var(--p-primary);
|
||||
color: var(--p-accent);
|
||||
}
|
||||
.vertical-stepper__label--active {
|
||||
color: var(--p-ink);
|
||||
|
||||
@@ -1,46 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import type { IStepProps } from '../model/types'
|
||||
import { BaseButton } from 'src/shared/ui/base/BaseButton'
|
||||
import { DocumentHtmlReader } from 'src/shared/ui/DocumentHtmlReader'
|
||||
import { Loader } from 'src/shared/ui/Loader'
|
||||
import { useConnectionAgreementStore } from 'src/entities/ConnectionAgreement'
|
||||
import { useAddCooperative } from 'src/features/Union/AddCooperative/model'
|
||||
import { useSessionStore } from 'src/entities/Session'
|
||||
|
||||
const props = withDefaults(defineProps<IStepProps & {
|
||||
defineProps<{
|
||||
html?: string
|
||||
}>(), {})
|
||||
}>()
|
||||
|
||||
const connectionAgreement = useConnectionAgreementStore()
|
||||
|
||||
const isActive = computed(() => props.isActive)
|
||||
const isDone = computed(() => props.isDone)
|
||||
const isSigning = ref(false)
|
||||
|
||||
// Проверяем, готов ли документ для подписания
|
||||
const isDocumentReady = computed(() => {
|
||||
return connectionAgreement.document && connectionAgreement.document.sign
|
||||
})
|
||||
const isDocumentReady = computed(
|
||||
() => !!(connectionAgreement.document && connectionAgreement.document.sign),
|
||||
)
|
||||
|
||||
const handleSign = async () => {
|
||||
if (isSigning.value) return // Предотвращаем повторные клики
|
||||
|
||||
// Дополнительная проверка готовности документа
|
||||
if (!isDocumentReady.value) {
|
||||
console.warn('⚠️ Документ еще не готов для подписания')
|
||||
return
|
||||
}
|
||||
|
||||
if (isSigning.value || !isDocumentReady.value) return
|
||||
isSigning.value = true
|
||||
try {
|
||||
console.log('📝 AgreementStep: Подписываем документ')
|
||||
await connectionAgreement.signDocument()
|
||||
|
||||
console.log('🔗 AgreementStep: Отправляем транзакцию registerCooperative в блокчейн')
|
||||
const { addCooperative } = useAddCooperative()
|
||||
const session = useSessionStore()
|
||||
|
||||
// Формируем данные для registerCooperative действия
|
||||
const registerData = {
|
||||
coopname: session.username,
|
||||
params: {
|
||||
@@ -51,36 +36,24 @@ const handleSign = async () => {
|
||||
initial: connectionAgreement.formData.initial,
|
||||
minimum: connectionAgreement.formData.minimum,
|
||||
org_initial: connectionAgreement.formData.org_initial,
|
||||
org_minimum: connectionAgreement.formData.org_minimum
|
||||
org_minimum: connectionAgreement.formData.org_minimum,
|
||||
},
|
||||
username: session.username,
|
||||
document: {
|
||||
...connectionAgreement.signedDocument,
|
||||
meta: JSON.stringify(connectionAgreement.signedDocument.meta)
|
||||
}
|
||||
meta: JSON.stringify(connectionAgreement.signedDocument.meta),
|
||||
},
|
||||
}
|
||||
|
||||
// Отправляем данные формы в блокчейн
|
||||
await addCooperative(registerData)
|
||||
|
||||
console.log('✅ AgreementStep: Транзакция успешно отправлена')
|
||||
|
||||
// Обновляем информацию о кооперативе из блокчейна
|
||||
console.log('🔄 AgreementStep: Обновляем информацию о кооперативе')
|
||||
await connectionAgreement.reloadCooperative()
|
||||
|
||||
// Обновляем информацию об инстансе
|
||||
console.log('🔄 AgreementStep: Обновляем информацию об инстансе')
|
||||
await connectionAgreement.loadCurrentInstance()
|
||||
|
||||
console.log('✅ AgreementStep: Информация обновлена')
|
||||
|
||||
// Переходим к следующему шагу после подписания и отправки в блокчейн
|
||||
if (connectionAgreement.currentStep < 5) {
|
||||
if (connectionAgreement.currentStep < 7) {
|
||||
connectionAgreement.setCurrentStep(connectionAgreement.currentStep + 1)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка при подписании или отправке в блокчейн:', error)
|
||||
console.error('Ошибка при подписании или отправке в блокчейн:', error)
|
||||
throw error
|
||||
} finally {
|
||||
isSigning.value = false
|
||||
@@ -88,47 +61,57 @@ const handleSign = async () => {
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
// Специальная логика для возврата - очищаем документы для показа Loader при повторном переходе
|
||||
console.log(`⬅️ AgreementStep: Возврат с шага ${connectionAgreement.currentStep}`)
|
||||
|
||||
// Очищаем документы, чтобы при повторном переходе вперед показался Loader
|
||||
connectionAgreement.setDocument(null)
|
||||
connectionAgreement.setSignedDocument(null)
|
||||
|
||||
if (connectionAgreement.currentStep > 1) {
|
||||
if (connectionAgreement.currentStep > 0) {
|
||||
connectionAgreement.setCurrentStep(connectionAgreement.currentStep - 1)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
q-step(
|
||||
:name="3"
|
||||
title="Соглашение о подключении к платформе"
|
||||
icon="description"
|
||||
:done="isDone"
|
||||
)
|
||||
.q-pa-md
|
||||
template(v-if="html")
|
||||
.agreement-step
|
||||
template(v-if="html")
|
||||
.agreement-step__doc
|
||||
DocumentHtmlReader(:html="html")
|
||||
template(v-else)
|
||||
Loader(:text='`Готовим соглашение...`')
|
||||
|
||||
q-stepper-navigation.q-gutter-sm(v-if="html")
|
||||
q-btn(
|
||||
v-if="isActive"
|
||||
color="grey-6"
|
||||
flat
|
||||
label="Назад"
|
||||
@click="handleBack"
|
||||
)
|
||||
|
||||
q-btn(
|
||||
v-if="isActive"
|
||||
color="primary"
|
||||
:loading="isSigning"
|
||||
:disable="!isDocumentReady"
|
||||
label="Подписать"
|
||||
@click="handleSign"
|
||||
)
|
||||
.agreement-step__actions
|
||||
BaseButton(
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@click="handleBack"
|
||||
) Назад
|
||||
.agreement-step__spacer
|
||||
BaseButton(
|
||||
variant="primary"
|
||||
size="sm"
|
||||
:loading="isSigning"
|
||||
:disabled="!isDocumentReady"
|
||||
@click="handleSign"
|
||||
) Подписать
|
||||
template(v-else)
|
||||
Loader(:text="'Готовим соглашение...'")
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.agreement-step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-5);
|
||||
}
|
||||
.agreement-step__doc {
|
||||
background: var(--p-surface);
|
||||
border: 1px solid var(--p-line-1);
|
||||
border-radius: var(--p-r-md);
|
||||
padding: var(--p-5);
|
||||
max-height: 540px;
|
||||
overflow: auto;
|
||||
}
|
||||
.agreement-step__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--p-2);
|
||||
}
|
||||
.agreement-step__spacer {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
+39
-210
@@ -1,70 +1,17 @@
|
||||
<template lang="pug">
|
||||
q-step(
|
||||
:name="5"
|
||||
title="Ожидание подтверждения"
|
||||
icon="schedule"
|
||||
:done="isDone"
|
||||
)
|
||||
.approval-waiting-container.q-pa-md
|
||||
|
||||
//- Основная карточка ожидания
|
||||
.waiting-card.bg-surface.q-mb-xl
|
||||
.waiting-header
|
||||
.waiting-icon-container
|
||||
q-icon.waiting-icon(
|
||||
:name="getWaitingIcon"
|
||||
:color="getWaitingIconColor"
|
||||
size="80px"
|
||||
@click="refreshStatus"
|
||||
clickable
|
||||
)
|
||||
.pulse-ring(v-if="instance?.blockchain_status !== 'active'")
|
||||
.waiting-title.text-h5.text-weight-medium.text-primary Ожидание подтверждения союза
|
||||
|
||||
//- Статусная информация
|
||||
.status-info.q-mt-lg
|
||||
.status-description.text-body1.text-on-surface
|
||||
| Все технические подготовки завершены. Ваш Цифровой Кооператив готов к установке и подключению к платформе Кооперативной Экономики.
|
||||
| Теперь мы ожидаем подтверждения от представителя союза о готовности произвести стандартизацию вашего документооборота.
|
||||
| Когда оно будет получено - установка продолжится автоматически.
|
||||
|
||||
//- Навигация
|
||||
q-stepper-navigation.q-gutter-sm
|
||||
q-btn(
|
||||
color="grey-6"
|
||||
flat
|
||||
label="Назад"
|
||||
@click="handleBack"
|
||||
)
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { IStepProps } from '../model/types'
|
||||
import {
|
||||
BaseButton,
|
||||
BaseCard,
|
||||
BaseChip,
|
||||
} from 'src/shared/ui/base'
|
||||
import { useConnectionAgreementStore } from 'src/entities/ConnectionAgreement'
|
||||
|
||||
const props = withDefaults(defineProps<IStepProps>(), {})
|
||||
|
||||
const connectionAgreement = useConnectionAgreementStore()
|
||||
|
||||
// Получаем данные напрямую из store
|
||||
const instance = computed(() => connectionAgreement.currentInstance)
|
||||
|
||||
const isDone = computed(() => props.isDone)
|
||||
const isApproved = computed(() => instance.value?.blockchain_status === 'active')
|
||||
|
||||
// Иконка ожидания
|
||||
const getWaitingIcon = computed(() => {
|
||||
if (instance.value?.blockchain_status === 'active') return 'check_circle'
|
||||
return 'schedule'
|
||||
})
|
||||
|
||||
// Цвет иконки ожидания
|
||||
const getWaitingIconColor = computed(() => {
|
||||
if (instance.value?.blockchain_status === 'active') return 'positive'
|
||||
return 'warning'
|
||||
})
|
||||
|
||||
// Функция обновления статуса
|
||||
const refreshStatus = async () => {
|
||||
try {
|
||||
await connectionAgreement.loadCurrentInstance()
|
||||
@@ -72,161 +19,43 @@ const refreshStatus = async () => {
|
||||
console.error('Ошибка обновления статуса:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
connectionAgreement.setCurrentStep(4)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
.approval-step
|
||||
BaseCard(
|
||||
title="Подтверждение союза"
|
||||
subtitle="Технические подготовки завершены. Цифровой Кооператив готов к установке и подключению к платформе Кооперативной Экономики."
|
||||
)
|
||||
p.approval-step__body
|
||||
| Ждём подтверждения от представителя союза о готовности произвести стандартизацию документооборота. Как только оно появится — установка продолжится автоматически.
|
||||
|
||||
.approval-step__status
|
||||
BaseChip(:variant="isApproved ? 'pos' : 'warn'")
|
||||
span {{ isApproved ? 'Подтверждено союзом' : 'Ожидаем подтверждения союза' }}
|
||||
BaseButton(
|
||||
v-if="!isApproved"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
type="button"
|
||||
@click="refreshStatus"
|
||||
) Проверить
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.approval-waiting-container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
.approval-step {
|
||||
max-width: 640px;
|
||||
}
|
||||
|
||||
/* Основная карточка ожидания */
|
||||
.waiting-card {
|
||||
border-radius: 20px;
|
||||
padding: 2.5rem;
|
||||
box-shadow:
|
||||
0 8px 32px rgba(0, 0, 0, 0.08),
|
||||
0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
.approval-step__body {
|
||||
margin: 0;
|
||||
color: var(--p-ink-1);
|
||||
font-size: var(--p-fs-body);
|
||||
line-height: var(--p-lh-body);
|
||||
}
|
||||
|
||||
.waiting-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, var(--q-primary) 0%, var(--q-secondary) 50%, var(--q-primary) 100%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 4s cubic-bezier(0.25, 0.46, 0.45, 0.94) infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { background-position: -100% 0; }
|
||||
100% { background-position: 100% 0; }
|
||||
}
|
||||
|
||||
/* Заголовок карточки */
|
||||
.waiting-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.waiting-icon-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.waiting-icon {
|
||||
filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.15));
|
||||
animation: gentle-pulse 3s ease-in-out infinite;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.waiting-icon:hover {
|
||||
transform: scale(1.1);
|
||||
filter: drop-shadow(0 6px 20px rgba(0, 0, 0, 0.25));
|
||||
}
|
||||
|
||||
@keyframes gentle-pulse {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
.pulse-ring {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
border: 1px solid rgba(255, 152, 0, 0.2);
|
||||
border-radius: 50%;
|
||||
animation: pulse-ring 2s cubic-bezier(0.455, 0.03, 0.515, 0.955) infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-ring {
|
||||
0% {
|
||||
transform: translate(-50%, -50%) scale(0.8);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translate(-50%, -50%) scale(1.4);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.waiting-title {
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
/* Статусная информация */
|
||||
.status-info {
|
||||
padding: 1.5rem;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(255, 152, 0, 0.1);
|
||||
}
|
||||
|
||||
.status-description {
|
||||
line-height: 1.6;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.waiting-card {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.pulse-ring {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.approval-waiting-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.waiting-card {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.waiting-icon {
|
||||
font-size: 60px;
|
||||
}
|
||||
|
||||
.pulse-ring {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.status-info {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.approval-step__status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--p-3);
|
||||
margin-top: var(--p-4);
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import {
|
||||
BaseInput,
|
||||
BaseButton,
|
||||
BaseCard,
|
||||
BaseForm,
|
||||
} from 'src/shared/ui/base'
|
||||
import { isDomain, notEmpty } from 'src/shared/lib/utils'
|
||||
import { useConnectionAgreementStore } from 'src/entities/ConnectionAgreement'
|
||||
|
||||
const connectionAgreement = useConnectionAgreementStore()
|
||||
|
||||
const announce = ref<string>(connectionAgreement.formData.announce || '')
|
||||
const errorMsg = ref<string>('')
|
||||
|
||||
watch(
|
||||
() => connectionAgreement.formData.announce,
|
||||
(v) => {
|
||||
announce.value = v || ''
|
||||
},
|
||||
)
|
||||
|
||||
const validate = (): boolean => {
|
||||
const v = (announce.value || '').trim()
|
||||
if (notEmpty(v) !== true) {
|
||||
errorMsg.value = 'Укажите домен или поддомен'
|
||||
return false
|
||||
}
|
||||
if (isDomain(v) !== true) {
|
||||
errorMsg.value = 'Введите корректное имя домена'
|
||||
return false
|
||||
}
|
||||
errorMsg.value = ''
|
||||
return true
|
||||
}
|
||||
|
||||
const handleContinue = () => {
|
||||
if (!validate()) return
|
||||
connectionAgreement.setFormData({
|
||||
...connectionAgreement.formData,
|
||||
announce: announce.value,
|
||||
})
|
||||
if (connectionAgreement.currentStep < 7) {
|
||||
connectionAgreement.setCurrentStep(connectionAgreement.currentStep + 1)
|
||||
}
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
if (connectionAgreement.currentStep > 1) {
|
||||
connectionAgreement.setCurrentStep(connectionAgreement.currentStep - 1)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
BaseForm.domain-form-step(@submit="handleContinue")
|
||||
BaseCard(title="Адрес кооператива в сети интернет")
|
||||
BaseInput(
|
||||
v-model="announce"
|
||||
label="Домен или поддомен"
|
||||
placeholder="domovoy.com или coop.domovoy.com"
|
||||
:error="errorMsg || undefined"
|
||||
)
|
||||
|
||||
template(#footer)
|
||||
.row.items-center.q-gutter-sm
|
||||
BaseButton(variant="ghost" size="sm" type="button" @click="handleBack") Назад
|
||||
q-space
|
||||
BaseButton(variant="primary" size="sm" type="submit") Дальше
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.domain-form-step {
|
||||
max-width: 640px;
|
||||
}
|
||||
</style>
|
||||
+53
-329
@@ -1,92 +1,24 @@
|
||||
<template lang="pug">
|
||||
q-step(
|
||||
:name="4"
|
||||
title="Настройка домена"
|
||||
icon="domain"
|
||||
:done="isDone"
|
||||
)
|
||||
.domain-validation-container.q-pa-md
|
||||
|
||||
//- Основная инструкция
|
||||
.dns-instruction-card.q-mb-xl
|
||||
.instruction-header
|
||||
.text-subtitle2.instruction-title Добавьте DNS-запись
|
||||
|
||||
//- Краткая инструкция
|
||||
.instruction-intro.q-mb-lg
|
||||
.text-body1.text-grey-8
|
||||
| Перейдите в панель управления вашим доменом и добавьте A-запись со следующими параметрами:
|
||||
|
||||
|
||||
.row.q-mb-md.justify-center
|
||||
.col-md-6.col-xs-12.q-pa-sm
|
||||
.dns-record-item
|
||||
.record-label Домен
|
||||
.record-value {{ coop?.announce }}
|
||||
|
||||
.col-md-6.col-xs-12.q-pa-sm
|
||||
.dns-record-item
|
||||
.record-label Адрес
|
||||
.record-value.record-highlight
|
||||
| {{ SERVER_IP }}
|
||||
|
||||
q-btn.copy-ip-btn(
|
||||
size="sm"
|
||||
flat
|
||||
round
|
||||
dense
|
||||
icon="content_copy"
|
||||
@click="copyIpAddress"
|
||||
)
|
||||
q-tooltip Копировать IP
|
||||
|
||||
.instruction-separator
|
||||
|
||||
.status-chips.row.justify-center.q-mb-md
|
||||
.chip-wrapper(v-if="instance?.is_valid")
|
||||
q-chip.status-chip(
|
||||
:class="{ 'delegated': instance?.is_delegated, 'waiting': !instance?.is_delegated }"
|
||||
text-color="white"
|
||||
size="md"
|
||||
clickable
|
||||
@click="handleReload"
|
||||
)
|
||||
q-spinner(size="16px" color="white")
|
||||
span.q-ml-xs {{ instance?.is_delegated ? 'Делегирован' : 'Ожидаем делегирования' }}
|
||||
|
||||
.instruction-footer
|
||||
| Мы автоматически проверим домен и активируем установку вашего Цифрового Кооператива
|
||||
|
||||
//- Навигация
|
||||
q-stepper-navigation.q-gutter-sm
|
||||
q-btn(
|
||||
color="grey-6"
|
||||
flat
|
||||
label="Назад"
|
||||
@click="handleBack"
|
||||
)
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { copyToClipboard } from 'quasar'
|
||||
import { FailAlert, SuccessAlert } from 'src/shared/api'
|
||||
import type { IStepProps } from '../model/types'
|
||||
import {
|
||||
BaseButton,
|
||||
BaseCard,
|
||||
BaseChip,
|
||||
BaseForm,
|
||||
} from 'src/shared/ui/base'
|
||||
import { DataRow } from 'src/shared/ui/domain'
|
||||
import { useConnectionAgreementStore } from 'src/entities/ConnectionAgreement'
|
||||
import { useProviderSubscriptions } from 'src/features/Provider/model'
|
||||
|
||||
const props = withDefaults(defineProps<IStepProps>(), {})
|
||||
|
||||
const connectionAgreement = useConnectionAgreementStore()
|
||||
const { loadCurrentInstance } = connectionAgreement
|
||||
const { SERVER_IP } = useProviderSubscriptions()
|
||||
|
||||
// Получаем данные напрямую из store
|
||||
const coop = computed(() => connectionAgreement.coop)
|
||||
const instance = computed(() => connectionAgreement.currentInstance)
|
||||
const isDelegated = computed(() => !!instance.value?.is_delegated)
|
||||
|
||||
// Загружаем кооператива при монтировании, если его нет в store
|
||||
const loadCoopIfNeeded = async () => {
|
||||
onMounted(async () => {
|
||||
if (!coop.value) {
|
||||
try {
|
||||
await connectionAgreement.reloadCooperative()
|
||||
@@ -94,17 +26,15 @@ const loadCoopIfNeeded = async () => {
|
||||
console.error('Ошибка загрузки кооператива:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadCoopIfNeeded()
|
||||
})
|
||||
|
||||
const isDone = computed(() => props.isDone)
|
||||
const handleBack = () => connectionAgreement.setCurrentStep(4)
|
||||
|
||||
|
||||
const handleBack = () => {
|
||||
connectionAgreement.setCurrentStep(2)
|
||||
const handleContinue = () => {
|
||||
if (!isDelegated.value) return
|
||||
if (connectionAgreement.currentStep < 7) {
|
||||
connectionAgreement.setCurrentStep(connectionAgreement.currentStep + 1)
|
||||
}
|
||||
}
|
||||
|
||||
const handleReload = async () => {
|
||||
@@ -114,254 +44,48 @@ const handleReload = async () => {
|
||||
console.error('Ошибка обновления инстанса:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const copyIpAddress = async () => {
|
||||
try {
|
||||
await copyToClipboard(SERVER_IP)
|
||||
SuccessAlert('IP адрес скопирован в буфер обмена')
|
||||
} catch (error) {
|
||||
console.error('Ошибка копирования:', error)
|
||||
FailAlert('Не удалось скопировать IP адрес')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
BaseForm.dns-step(@submit="handleContinue")
|
||||
BaseCard(
|
||||
title="Делегирование домена"
|
||||
subtitle="Откройте панель управления вашим доменом и добавьте A-запись со значениями ниже. Делегирование проверим автоматически."
|
||||
)
|
||||
DataRow(label="Домен" :value="coop?.announce" mono)
|
||||
DataRow(label="IP-адрес" :value="SERVER_IP" mono copyable)
|
||||
|
||||
.dns-step__status
|
||||
BaseChip(:variant="isDelegated ? 'pos' : 'warn'")
|
||||
span {{ isDelegated ? 'Домен делегирован' : 'Ожидаем делегирования' }}
|
||||
BaseButton(
|
||||
v-if="!isDelegated"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
type="button"
|
||||
@click="handleReload"
|
||||
) Проверить сейчас
|
||||
|
||||
template(#footer)
|
||||
.row.items-center.q-gutter-sm
|
||||
BaseButton(variant="ghost" size="sm" type="button" @click="handleBack") Назад
|
||||
q-space
|
||||
BaseButton(
|
||||
v-if="isDelegated"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
type="submit"
|
||||
) Дальше
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.domain-validation-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
.dns-step {
|
||||
max-width: 640px;
|
||||
}
|
||||
|
||||
/* Статус чипы */
|
||||
.status-header {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
.dns-step__status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
gap: var(--p-3);
|
||||
margin-top: var(--p-4);
|
||||
}
|
||||
|
||||
.status-chips {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chip-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.status-chip {
|
||||
border-radius: 20px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.5px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.status-chip:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.status-chip.valid {
|
||||
background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%);
|
||||
border: 1px solid rgba(76, 175, 80, 0.2);
|
||||
}
|
||||
|
||||
.status-chip.warning {
|
||||
background: linear-gradient(135deg, #FF9800 0%, #f57c00 100%);
|
||||
border: 1px solid rgba(255, 152, 0, 0.2);
|
||||
}
|
||||
|
||||
.status-chip.delegated {
|
||||
background: linear-gradient(135deg, #2196F3 0%, #1976D2 100%);
|
||||
border: 1px solid rgba(33, 150, 243, 0.2);
|
||||
}
|
||||
|
||||
.status-chip.waiting {
|
||||
background: linear-gradient(135deg, #9C27B0 0%, #7B1FA2 100%);
|
||||
border: 1px solid rgba(156, 39, 176, 0.2);
|
||||
}
|
||||
|
||||
/* DNS инструкция */
|
||||
.dns-instruction-card {
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
box-shadow:
|
||||
0 4px 20px rgba(0, 0, 0, 0.08),
|
||||
0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dns-instruction-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, #2196F3 0%, #21CBF3 50%, #2196F3 100%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 3s cubic-bezier(0.25, 0.46, 0.45, 0.94) infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { background-position: -100% 0; }
|
||||
100% { background-position: 100% 0; }
|
||||
}
|
||||
|
||||
.instruction-header {
|
||||
text-align: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.instruction-intro {
|
||||
text-align: center;
|
||||
padding: 1rem 1.5rem;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.instruction-title {
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.instruction-title::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -8px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 40px;
|
||||
height: 2px;
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
/* DNS записи */
|
||||
.dns-record-item {
|
||||
text-align: center;
|
||||
padding: 0.75rem;
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dns-record-item:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Кнопка копирования IP */
|
||||
.copy-ip-btn {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
border-radius: 50% !important;
|
||||
}
|
||||
|
||||
.record-highlight:hover .copy-ip-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.record-label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.8px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.record-value {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.record-highlight {
|
||||
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* Разделитель */
|
||||
.instruction-separator {
|
||||
height: 1px;
|
||||
margin: 2rem 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.instruction-separator::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* Футер инструкции */
|
||||
.instruction-footer {
|
||||
text-align: center;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.dns-record-item {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.status-chips {
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.dns-instruction-card {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.instruction-intro {
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.domain-validation-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.dns-instruction-card {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.instruction-title {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.instruction-intro {
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.record-value {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.copy-ip-btn {
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import {
|
||||
BaseInput,
|
||||
BaseButton,
|
||||
BaseCard,
|
||||
BaseForm,
|
||||
} from 'src/shared/ui/base'
|
||||
import { notEmpty } from 'src/shared/lib/utils'
|
||||
import { useConnectionAgreementStore } from 'src/entities/ConnectionAgreement'
|
||||
|
||||
const connectionAgreement = useConnectionAgreementStore()
|
||||
|
||||
const initial = ref<string>(connectionAgreement.formData.initial || '')
|
||||
const minimum = ref<string>(connectionAgreement.formData.minimum || '')
|
||||
const orgInitial = ref<string>(connectionAgreement.formData.org_initial || '')
|
||||
const orgMinimum = ref<string>(connectionAgreement.formData.org_minimum || '')
|
||||
const errors = ref<Record<string, string>>({})
|
||||
|
||||
watch(
|
||||
() => connectionAgreement.formData,
|
||||
(v) => {
|
||||
initial.value = v.initial || ''
|
||||
minimum.value = v.minimum || ''
|
||||
orgInitial.value = v.org_initial || ''
|
||||
orgMinimum.value = v.org_minimum || ''
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
const validate = (): boolean => {
|
||||
const e: Record<string, string> = {}
|
||||
if (notEmpty(initial.value) !== true) e.initial = 'Укажите сумму'
|
||||
if (notEmpty(minimum.value) !== true) e.minimum = 'Укажите сумму'
|
||||
if (notEmpty(orgInitial.value) !== true) e.orgInitial = 'Укажите сумму'
|
||||
if (notEmpty(orgMinimum.value) !== true) e.orgMinimum = 'Укажите сумму'
|
||||
errors.value = e
|
||||
return Object.keys(e).length === 0
|
||||
}
|
||||
|
||||
const handleContinue = () => {
|
||||
if (!validate()) return
|
||||
connectionAgreement.setFormData({
|
||||
...connectionAgreement.formData,
|
||||
initial: initial.value,
|
||||
minimum: minimum.value,
|
||||
org_initial: orgInitial.value,
|
||||
org_minimum: orgMinimum.value,
|
||||
})
|
||||
if (connectionAgreement.currentStep < 7) {
|
||||
connectionAgreement.setCurrentStep(connectionAgreement.currentStep + 1)
|
||||
}
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
if (connectionAgreement.currentStep > 1) {
|
||||
connectionAgreement.setCurrentStep(connectionAgreement.currentStep - 1)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
BaseForm.financial-step(@submit="handleContinue")
|
||||
BaseCard.q-mb-md(
|
||||
title="Финансовые параметры"
|
||||
subtitle="Установите вступительные и минимальные паевые взносы для физических лиц, индивидуальных предпринимателей и организаций"
|
||||
)
|
||||
|
||||
BaseCard.q-mb-md(title="Физические лица и ИП" subtitle="Применяется к пайщикам-гражданам при вступлении в кооператив")
|
||||
.row.q-col-gutter-md
|
||||
.col-12.col-md-6
|
||||
BaseInput(
|
||||
v-model="initial"
|
||||
label="Вступительный взнос"
|
||||
type="number"
|
||||
placeholder="100"
|
||||
suffix="RUB"
|
||||
:error="errors.initial || undefined"
|
||||
)
|
||||
.col-12.col-md-6
|
||||
BaseInput(
|
||||
v-model="minimum"
|
||||
label="Минимальный паевый взнос"
|
||||
type="number"
|
||||
placeholder="300"
|
||||
suffix="RUB"
|
||||
:error="errors.minimum || undefined"
|
||||
)
|
||||
|
||||
BaseCard(title="Организации" subtitle="Применяется к пайщикам-юрлицам при вступлении в кооператив")
|
||||
.row.q-col-gutter-md
|
||||
.col-12.col-md-6
|
||||
BaseInput(
|
||||
v-model="orgInitial"
|
||||
label="Вступительный взнос"
|
||||
type="number"
|
||||
placeholder="1000"
|
||||
suffix="RUB"
|
||||
:error="errors.orgInitial || undefined"
|
||||
)
|
||||
.col-12.col-md-6
|
||||
BaseInput(
|
||||
v-model="orgMinimum"
|
||||
label="Минимальный паевый взнос"
|
||||
type="number"
|
||||
placeholder="3000"
|
||||
suffix="RUB"
|
||||
:error="errors.orgMinimum || undefined"
|
||||
)
|
||||
|
||||
template(#footer)
|
||||
.row.items-center.q-gutter-sm
|
||||
BaseButton(variant="ghost" size="sm" type="button" @click="handleBack") Назад
|
||||
q-space
|
||||
BaseButton(variant="primary" size="sm" type="submit") Дальше
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.financial-step {
|
||||
max-width: 640px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,83 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { IStepProps } from '../model/types'
|
||||
import { CooperativeDataForm } from 'src/features/Union/CooperativeDataForm'
|
||||
import { useConnectionAgreementStore } from 'src/entities/ConnectionAgreement'
|
||||
|
||||
withDefaults(defineProps<IStepProps & {
|
||||
document?: any
|
||||
signedDocument?: any
|
||||
}>(), {})
|
||||
|
||||
const connectionAgreement = useConnectionAgreementStore()
|
||||
|
||||
const handleContinue = (formData?: any) => {
|
||||
console.log('📝 FormStep: Продолжаем с данными формы:', formData)
|
||||
|
||||
// Сохраняем данные формы в стор
|
||||
if (formData) {
|
||||
connectionAgreement.setFormData(formData)
|
||||
}
|
||||
|
||||
// Переходим к следующему шагу (документ сгенерируется в watch)
|
||||
if (connectionAgreement.currentStep < 5) {
|
||||
connectionAgreement.setCurrentStep(connectionAgreement.currentStep + 1)
|
||||
}
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
if (connectionAgreement.currentStep > 1) {
|
||||
connectionAgreement.setCurrentStep(connectionAgreement.currentStep - 1)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
q-step(
|
||||
:name="2"
|
||||
title="Параметры кооператива"
|
||||
icon="settings"
|
||||
:done="isDone"
|
||||
)
|
||||
.form-step-container.q-pa-md
|
||||
|
||||
//- Заголовок шага
|
||||
.step-header.q-mb-xl
|
||||
.text-h6.form-title Настройка параметров кооператива
|
||||
.subtitle.text-body2.text-grey-7.q-mt-sm
|
||||
| Заполните основные параметры для запуска вашего Цифрового Кооператива
|
||||
|
||||
//- Форма ввода данных
|
||||
CooperativeDataForm(
|
||||
@continue="handleContinue"
|
||||
@back="handleBack"
|
||||
)
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.form-step-container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.step-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.form-title {
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 480px) {
|
||||
.form-step-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.step-header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+69
-340
@@ -1,368 +1,97 @@
|
||||
<template lang="pug">
|
||||
q-step(
|
||||
:name="6"
|
||||
title="Поставка Цифрового Кооператива"
|
||||
icon="cloud_download"
|
||||
:done="isDone"
|
||||
)
|
||||
.installation-container.q-pa-md
|
||||
|
||||
//- Установка в процессе
|
||||
div
|
||||
//- Заголовок с градиентом
|
||||
|
||||
.installation-header
|
||||
.text-h6.installation-title Поставка Цифрового Кооператива
|
||||
.subtitle.text-body2.text-grey-7.q-mt-sm
|
||||
| с подключением к платформе Кооперативной Экономики
|
||||
|
||||
//- Основная карточка прогресса
|
||||
.progress-card.q-mb-xl
|
||||
.progress-header
|
||||
.progress-title
|
||||
q-icon(name="rocket_launch" size="24px" color="primary").q-mr-sm
|
||||
span.text-subtitle1.text-weight-medium Прогресс поставки
|
||||
|
||||
//- Живой прогресс-бар с анимацией
|
||||
.progress-bar-container.q-mt-lg
|
||||
.progress-bar-background
|
||||
.progress-bar-fill(
|
||||
:style="{ width: (instance?.progress || 0) + '%' }"
|
||||
)
|
||||
.progress-bar-shimmer
|
||||
.progress-bar-glow(:style="{ width: (instance?.progress || 0) + '%' }")
|
||||
|
||||
//- Оставшееся время
|
||||
.remaining-time.q-mt-md
|
||||
.time-display
|
||||
q-icon(name="schedule" size="18px" color="primary").q-mr-sm
|
||||
span.text-body2 Осталось: {{ remainingTime }} мин
|
||||
|
||||
//- Статус текущего этапа
|
||||
.current-stage.q-mt-lg
|
||||
.stage-info
|
||||
.stage-icon
|
||||
q-icon(:name="currentStageIcon" size="32px" :color="currentStageColor")
|
||||
.stage-details
|
||||
.stage-title.text-body1.text-weight-medium {{ currentStageTitle }}
|
||||
.stage-description.text-body2.text-grey-7.q-mt-xs {{ currentStageDescription }}
|
||||
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { IStepProps } from '../model/types'
|
||||
import { useConnectionAgreementStore } from 'src/entities/ConnectionAgreement'
|
||||
|
||||
const props = withDefaults(defineProps<IStepProps>(), {})
|
||||
|
||||
const connectionAgreement = useConnectionAgreementStore()
|
||||
|
||||
// Получаем данные напрямую из store
|
||||
const instance = computed(() => connectionAgreement.currentInstance)
|
||||
|
||||
const isDone = computed(() => props.isDone)
|
||||
const progress = computed(() => Math.max(0, Math.min(100, instance.value?.progress || 0)))
|
||||
|
||||
// Текущий этап (соответствует логике из startup-workflow.service.ts)
|
||||
const currentStageInfo = computed(() => {
|
||||
const progress = instance.value?.progress || 0
|
||||
|
||||
if (progress < 20) {
|
||||
return {
|
||||
icon: 'settings',
|
||||
color: 'primary',
|
||||
title: 'Подготовка серверного окружения',
|
||||
description: 'Настраиваем инфраструктуру, разворачиваем серверные компоненты'
|
||||
}
|
||||
} else if (progress < 40) {
|
||||
return {
|
||||
icon: 'download',
|
||||
color: 'info',
|
||||
title: 'Загрузка компонентов Цифрового Кооператива',
|
||||
description: 'Устанавливаем программное обеспечение и зависимости для работы платформы'
|
||||
}
|
||||
} else if (progress < 60) {
|
||||
return {
|
||||
icon: 'storage',
|
||||
color: 'warning',
|
||||
title: 'Настройка баз данных и хранилищ',
|
||||
description: 'Разворачиваем базы данных, инициализируем структуры и настраиваем резервное копирование'
|
||||
}
|
||||
} else if (progress < 80) {
|
||||
return {
|
||||
icon: 'security',
|
||||
color: 'secondary',
|
||||
title: 'Запуск блокчейн-узла',
|
||||
description: 'Разворачиваем и синхронизируем блокчейн-узел для подключения к Кооперативной Экономике'
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
icon: 'check_circle',
|
||||
color: 'positive',
|
||||
title: 'Финализация поставки',
|
||||
description: 'Выполняем заключительные настройки, проверяем работоспособность всех компонентов'
|
||||
}
|
||||
}
|
||||
const stage = computed(() => {
|
||||
const p = progress.value
|
||||
if (p < 20) return { title: 'Подготовка серверного окружения', body: 'Настраиваем инфраструктуру и развёртываем серверные компоненты.' }
|
||||
if (p < 40) return { title: 'Загрузка компонентов Цифрового Кооператива', body: 'Устанавливаем программное обеспечение и зависимости платформы.' }
|
||||
if (p < 60) return { title: 'Настройка баз данных и хранилищ', body: 'Разворачиваем базы данных, инициализируем структуры и резервное копирование.' }
|
||||
if (p < 80) return { title: 'Запуск блокчейн-узла', body: 'Разворачиваем и синхронизируем узел с сетью Кооперативной Экономики.' }
|
||||
return { title: 'Финализация поставки', body: 'Выполняем заключительные настройки и проверяем работоспособность.' }
|
||||
})
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const currentStageIcon = computed(() => currentStageInfo.value.icon)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const currentStageColor = computed(() => currentStageInfo.value.color)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const currentStageTitle = computed(() => currentStageInfo.value.title)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const currentStageDescription = computed(() => currentStageInfo.value.description)
|
||||
|
||||
// Расчет оставшегося времени (общая установка 100 минут)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const remainingTime = computed(() => {
|
||||
const progress = instance.value?.progress || 0
|
||||
const totalMinutes = 100
|
||||
const remaining = totalMinutes - progress
|
||||
return Math.max(0, remaining) // Не показываем отрицательные значения
|
||||
})
|
||||
|
||||
// Редирект теперь обрабатывается в ConnectionAgreementPage через реактивность
|
||||
// Здесь только отображение прогресса
|
||||
const remainingMinutes = computed(() => Math.max(0, 100 - progress.value))
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
.installation-step
|
||||
p.t-sm.t-muted.installation-step__lead Поставка Цифрового Кооператива с подключением к платформе Кооперативной Экономики.
|
||||
|
||||
.installation-step__progress
|
||||
.installation-step__progress-head
|
||||
span.t-eyebrow.t-muted Прогресс поставки
|
||||
span.t-mono.t-num.installation-step__pct {{ progress }}%
|
||||
.installation-step__bar
|
||||
.installation-step__bar-fill(:style="{ width: progress + '%' }")
|
||||
p.t-meta.t-muted.installation-step__remaining
|
||||
| Осталось примерно {{ remainingMinutes }} мин
|
||||
|
||||
.installation-step__stage
|
||||
.t-h3 {{ stage.title }}
|
||||
p.t-sm.t-muted.installation-step__stage-body {{ stage.body }}
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.installation-container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Заголовок с градиентом */
|
||||
.installation-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.installation-title {
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.5px;
|
||||
background: linear-gradient(135deg, var(--q-primary) 0%, rgba(25, 118, 210, 0.8) 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
text-shadow: 0 2px 4px rgba(25, 118, 210, 0.3);
|
||||
}
|
||||
|
||||
/* Основная карточка прогресса */
|
||||
.progress-card {
|
||||
border-radius: 20px;
|
||||
padding: 2.5rem;
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
box-shadow:
|
||||
0 8px 32px rgba(0, 0, 0, 0.08),
|
||||
0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, var(--q-primary) 0%, var(--q-secondary) 50%, var(--q-accent) 100%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 4s cubic-bezier(0.25, 0.46, 0.45, 0.94) infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { background-position: 100% 0; }
|
||||
100% { background-position: -100% 0; }
|
||||
}
|
||||
|
||||
.progress-header {
|
||||
.installation-step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-5);
|
||||
max-width: 640px;
|
||||
}
|
||||
.installation-step__lead {
|
||||
margin: 0;
|
||||
}
|
||||
.installation-step__progress {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-2);
|
||||
padding: var(--p-4);
|
||||
border: 1px solid var(--p-line-1);
|
||||
border-radius: var(--p-r-md);
|
||||
background: var(--p-surface);
|
||||
}
|
||||
.installation-step__progress-head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.progress-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.installation-step__pct {
|
||||
font-size: var(--p-fs-body);
|
||||
font-weight: 600;
|
||||
color: var(--q-primary);
|
||||
color: var(--p-ink);
|
||||
}
|
||||
|
||||
|
||||
/* Живой прогресс-бар */
|
||||
.progress-bar-container {
|
||||
position: relative;
|
||||
height: 12px;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.progress-bar-background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: linear-gradient(90deg, #e0e0e0 0%, #f0f0f0 100%);
|
||||
}
|
||||
|
||||
.progress-bar-fill {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, var(--q-primary) 0%, var(--q-secondary) 100%);
|
||||
border-radius: 20px;
|
||||
transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow:
|
||||
0 0 20px rgba(25, 118, 210, 0.3),
|
||||
inset 0 1px 2px rgba(255, 255, 255, 0.2);
|
||||
.installation-step__bar {
|
||||
position: relative;
|
||||
height: 4px;
|
||||
border-radius: var(--p-r-pill);
|
||||
background: var(--p-surface-3);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar-shimmer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
.installation-step__bar-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
rgba(255, 255, 255, 0.6) 30%,
|
||||
rgba(255, 255, 255, 0.8) 50%,
|
||||
rgba(255, 255, 255, 0.6) 70%,
|
||||
transparent 100%
|
||||
);
|
||||
animation: progress-shimmer 1.2s infinite cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
background: var(--p-primary);
|
||||
border-radius: var(--p-r-pill);
|
||||
transition: width var(--p-dur-slow) var(--p-ease-standard);
|
||||
}
|
||||
|
||||
@keyframes progress-shimmer {
|
||||
0% {
|
||||
left: -100%;
|
||||
opacity: 0;
|
||||
}
|
||||
10% {
|
||||
opacity: 1;
|
||||
}
|
||||
90% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
left: 100%;
|
||||
opacity: 0;
|
||||
}
|
||||
.installation-step__remaining {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.progress-bar-glow {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: 0;
|
||||
height: 16px;
|
||||
background: linear-gradient(135deg, var(--q-primary) 0%, rgba(25, 118, 210, 0.3) 100%);
|
||||
border-radius: 20px;
|
||||
filter: blur(4px);
|
||||
opacity: 0.6;
|
||||
transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* Текущий этап */
|
||||
.current-stage {
|
||||
margin-top: 2rem;
|
||||
padding: 1.5rem;
|
||||
background: rgba(25, 118, 210, 0.02);
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(25, 118, 210, 0.1);
|
||||
}
|
||||
|
||||
.stage-info {
|
||||
.installation-step__stage {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
flex-direction: column;
|
||||
gap: var(--p-2);
|
||||
padding: var(--p-4);
|
||||
border: 1px solid var(--p-line-1);
|
||||
border-radius: var(--p-r-md);
|
||||
background: var(--p-surface);
|
||||
}
|
||||
|
||||
.stage-icon {
|
||||
background: linear-gradient(135deg, var(--q-primary) 0%, rgba(25, 118, 210, 0.8) 100%);
|
||||
border-radius: 50%;
|
||||
padding: 0.75rem;
|
||||
box-shadow: 0 4px 12px rgba(25, 118, 210, 0.2);
|
||||
}
|
||||
|
||||
.stage-details {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stage-title {
|
||||
color: var(--q-primary);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.stage-description {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* Оставшееся время */
|
||||
.remaining-time {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.time-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: rgba(25, 118, 210, 0.05);
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(25, 118, 210, 0.1);
|
||||
}
|
||||
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.progress-card {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.progress-header {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stage-info {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.time-display {
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.installation-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.progress-card {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.time-display {
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.installation-step__stage-body {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { IStepProps } from '../model/types'
|
||||
import { BaseButton } from 'src/shared/ui/base/BaseButton'
|
||||
import { TariffSelector, type ITariff } from '../Tariffs'
|
||||
import { useConnectionAgreementStore } from 'src/entities/ConnectionAgreement'
|
||||
import { useSystemStore } from 'src/entities/System/model'
|
||||
|
||||
const props = withDefaults(defineProps<IStepProps & {
|
||||
const props = defineProps<{
|
||||
selectedTariff?: ITariff | null
|
||||
}>(), {})
|
||||
}>()
|
||||
|
||||
const connectionAgreement = useConnectionAgreementStore()
|
||||
const system = useSystemStore()
|
||||
|
||||
const isActive = computed(() => props.isActive)
|
||||
const isDone = computed(() => props.isDone)
|
||||
|
||||
const selectedTariff = computed({
|
||||
get: () => props.selectedTariff || connectionAgreement.selectedTariff,
|
||||
set: (value) => connectionAgreement.setSelectedTariff(value)
|
||||
set: (value) => connectionAgreement.setSelectedTariff(value),
|
||||
})
|
||||
|
||||
const canContinue = computed(() => selectedTariff.value !== null)
|
||||
@@ -37,41 +34,47 @@ const handleBack = () => {
|
||||
}
|
||||
|
||||
const handleContinue = () => {
|
||||
if (canContinue.value && connectionAgreement.currentStep < 5) {
|
||||
if (canContinue.value && connectionAgreement.currentStep < 7) {
|
||||
connectionAgreement.setCurrentStep(connectionAgreement.currentStep + 1)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
q-step(
|
||||
:name="1"
|
||||
title="Выбор тарифа подключения"
|
||||
icon="info"
|
||||
:done="isDone"
|
||||
)
|
||||
.q-pa-md
|
||||
|
||||
TariffSelector(
|
||||
:disabled="!isActive"
|
||||
:selected-tariff="selectedTariff"
|
||||
@tariff-selected="handleTariffSelected"
|
||||
@tariff-deselected="handleTariffDeselected"
|
||||
)
|
||||
|
||||
q-stepper-navigation.q-gutter-sm
|
||||
q-btn(
|
||||
v-if="isActive && system.info.is_unioned"
|
||||
color="grey-6"
|
||||
flat
|
||||
label="Назад"
|
||||
.intro-step
|
||||
TariffSelector(
|
||||
:selected-tariff="selectedTariff"
|
||||
@tariff-selected="handleTariffSelected"
|
||||
@tariff-deselected="handleTariffDeselected"
|
||||
)
|
||||
.intro-step__actions
|
||||
BaseButton(
|
||||
v-if="system.info.is_unioned"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@click="handleBack"
|
||||
)
|
||||
q-btn(
|
||||
v-if="isActive"
|
||||
color="primary"
|
||||
:disable="!canContinue"
|
||||
label="Продолжить"
|
||||
) Назад
|
||||
.intro-step__spacer
|
||||
BaseButton(
|
||||
variant="primary"
|
||||
size="sm"
|
||||
:disabled="!canContinue"
|
||||
@click="handleContinue"
|
||||
)
|
||||
) Дальше
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.intro-step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-5);
|
||||
}
|
||||
.intro-step__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--p-2);
|
||||
}
|
||||
.intro-step__spacer {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
+83
-392
@@ -1,436 +1,127 @@
|
||||
<template lang="pug">
|
||||
q-step(
|
||||
:name="0"
|
||||
title="Членство в союзе"
|
||||
icon="group"
|
||||
:done="isDone"
|
||||
)
|
||||
.union-membership-container.q-pa-md
|
||||
|
||||
//- Основная информационная карточка
|
||||
.membership-info-card.q-mb-xl
|
||||
.card-header
|
||||
.text-h6.membership-title Членство в Союзе Потребительских Обществ
|
||||
.subtitle.text-body2.text-grey-7.q-mt-sm
|
||||
| Необходимый шаг для полноценной работы на платформе Кооперативной Экономики
|
||||
|
||||
//- Что будет сделано
|
||||
.membership-benefits.q-mb-lg
|
||||
.benefit-section
|
||||
.section-title
|
||||
q-icon(name="check_circle" size="20px" color="positive").q-mr-sm
|
||||
span.text-subtitle1.text-weight-medium Подключение к платформе
|
||||
|
||||
.benefit-description.text-body2.q-mt-sm.q-ml-lg
|
||||
| Ваш кооператив будет подключен к платформе. Вы получите полный доступ ко всем сервисам цифрового кооператива.
|
||||
|
||||
.benefit-section.q-mt-md
|
||||
.section-title
|
||||
q-icon(name="group_add" size="20px" color="primary").q-mr-sm
|
||||
span.text-subtitle1.text-weight-medium Импорт участников и цифровые подписи
|
||||
|
||||
.benefit-description.text-body2.q-mt-sm.q-ml-lg
|
||||
| Вы сможете импортировать пайщиков и пригласите их для выпуска цифровых подписей.
|
||||
|
||||
.benefit-section.q-mt-md
|
||||
.section-title
|
||||
q-icon(name="event_available" size="20px" color="secondary").q-mr-sm
|
||||
span.text-subtitle1.text-weight-medium 30 дней на организацию
|
||||
|
||||
.benefit-description.text-body2.q-mt-sm.q-ml-lg
|
||||
| После подключения у вас будет 30 дней для проведения необходимых решений совета и общего собрания пайщиков в соответствии с требованиями союза.
|
||||
|
||||
//- Требование членства
|
||||
.membership-requirement.q-mt-lg
|
||||
.requirement-card
|
||||
.requirement-header
|
||||
q-icon(name="verified_user" size="24px" color="warning").q-mr-sm
|
||||
.text-subtitle1.text-weight-medium Членство в союзе
|
||||
|
||||
.requirement-description.text-body2.q-mt-sm
|
||||
| Членство в Союзе Потребительских Обществ является обязательным условием для работы на платформе. Союз обеспечивает стандартизацию вашего документооборота и приводит его в соответствие с требованиями законодательства, исключая проблемы с проверками и регуляторами.
|
||||
| Для связи с представителем союза и координации процесса подключения вам потребуется аккаунт в кооперативном мессенджере.
|
||||
|
||||
//- Информационная карточка если аккаунт уже есть
|
||||
.matrix-account-info-card.q-mb-xl(v-if="hasMatrixAccount")
|
||||
.card-header
|
||||
.text-h6.account-title У вас есть аккаунт
|
||||
.subtitle.text-body2.text-grey-7.q-mt-sm
|
||||
| Ваш аккаунт в кооперативном мессенджере активен
|
||||
|
||||
.account-info.q-mb-lg
|
||||
.info-section
|
||||
.section-title
|
||||
q-icon(name="message" size="20px" color="primary").q-mr-sm
|
||||
span.text-subtitle1.text-weight-medium Связь с представителем союза
|
||||
|
||||
.info-description.text-body2.q-mt-sm.q-ml-lg
|
||||
| Для связи с представителем союза перейдите на страницу
|
||||
q-btn.q-pa-none.q-ma-none.text-primary.text-weight-medium(
|
||||
flat
|
||||
dense
|
||||
no-caps
|
||||
label="Быстрый клиент"
|
||||
@click="goToQuickClient"
|
||||
).q-ml-sm
|
||||
| .
|
||||
|
||||
.info-description.text-body2.q-mt-sm.q-ml-lg
|
||||
| Там вы сможете начать общение и получить всю необходимую помощь в процессе подключения.
|
||||
|
||||
//- Регистрация в мессенджере (показываем только если аккаунта нет)
|
||||
.q-mt-xl(v-if="!hasMatrixAccount")
|
||||
div(@accountCreated="handleAccountCreated")
|
||||
slot(name="registration")
|
||||
|
||||
q-stepper-navigation.q-gutter-sm.q-pa-md
|
||||
.text-body2.text-grey-7(v-if="isActive")
|
||||
span(v-if="hasMatrixAccount") Отлично, связь установлена — можем продолжить подключение.
|
||||
span(v-else) Создайте аккаунт в кооперативном мессенджере, чтобы продолжить.
|
||||
q-btn(
|
||||
v-if="isActive"
|
||||
color="primary"
|
||||
label="Продолжить"
|
||||
:disable="!hasMatrixAccount"
|
||||
@click="handleContinue"
|
||||
)
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { BaseButton } from 'src/shared/ui/base/BaseButton'
|
||||
import { BaseChip } from 'src/shared/ui/base/BaseChip'
|
||||
import { useDesktopStore } from 'src/entities/Desktop'
|
||||
import type { IStepProps } from '../model/types'
|
||||
import { useConnectionAgreementStore } from 'src/entities/ConnectionAgreement'
|
||||
|
||||
const props = withDefaults(defineProps<IStepProps>(), {})
|
||||
|
||||
const connectionAgreement = useConnectionAgreementStore()
|
||||
const router = useRouter()
|
||||
const desktopStore = useDesktopStore()
|
||||
|
||||
const isActive = computed(() => props.isActive)
|
||||
const isDone = computed(() => props.isDone)
|
||||
const hasMatrixAccount = computed(() => connectionAgreement.hasMatrixAccount)
|
||||
|
||||
const handleContinue = () => {
|
||||
if (!hasMatrixAccount.value) {
|
||||
return
|
||||
}
|
||||
if (!hasMatrixAccount.value) return
|
||||
if (connectionAgreement.currentStep === 0) {
|
||||
connectionAgreement.setCurrentStep(1)
|
||||
}
|
||||
}
|
||||
|
||||
const goToQuickClient = () => {
|
||||
// Сначала переключаем рабочий стол на chatcoop
|
||||
desktopStore.selectWorkspace('chatcoop')
|
||||
// Затем переходим на страницу "Быстрый клиент"
|
||||
router.push({ name: 'chatcoop-chat' })
|
||||
}
|
||||
|
||||
const handleAccountCreated = () => {
|
||||
// После создания аккаунта обновляем флаг
|
||||
connectionAgreement.setHasMatrixAccount(true)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
.union-step
|
||||
p.t-body.union-step__lead
|
||||
| Членство в Союзе Потребительских Обществ — обязательное условие работы на платформе.
|
||||
| Союз стандартизирует документооборот вашего кооператива и приводит его в соответствие
|
||||
| с требованиями законодательства.
|
||||
|
||||
ul.union-step__list
|
||||
li
|
||||
strong Подключение к платформе.
|
||||
| Полный доступ ко всем сервисам Цифрового Кооператива.
|
||||
li
|
||||
strong Импорт пайщиков.
|
||||
| Пригласите участников и выпустите электронные подписи.
|
||||
li
|
||||
strong 30 дней на организацию.
|
||||
| Время для решений совета и общего собрания после подключения.
|
||||
|
||||
.union-step__matrix(v-if="hasMatrixAccount")
|
||||
BaseChip(variant="pos")
|
||||
span Аккаунт в кооперативном мессенджере создан
|
||||
p.t-sm.t-muted.union-step__matrix-body
|
||||
| Для связи с представителем союза откройте
|
||||
|
|
||||
a.union-step__link(href="#" @click.prevent="goToQuickClient") Быстрый клиент
|
||||
| . Там можно начать общение и получить помощь по подключению.
|
||||
|
||||
.union-step__matrix(v-else)
|
||||
p.t-sm.t-muted.union-step__matrix-body
|
||||
| Создайте аккаунт в кооперативном мессенджере — нужен для связи с представителем союза.
|
||||
div(@accountCreated="handleAccountCreated")
|
||||
slot(name="registration")
|
||||
|
||||
.union-step__actions
|
||||
BaseButton(
|
||||
variant="primary"
|
||||
size="sm"
|
||||
:disabled="!hasMatrixAccount"
|
||||
@click="handleContinue"
|
||||
) Продолжить
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.union-membership-container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Основная информационная карточка */
|
||||
.membership-info-card {
|
||||
border-radius: 20px;
|
||||
padding: 2.5rem;
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
box-shadow:
|
||||
0 8px 32px rgba(0, 0, 0, 0.08),
|
||||
0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.membership-info-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, var(--q-primary) 0%, var(--q-secondary) 50%, var(--q-accent) 100%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 4s cubic-bezier(0.25, 0.46, 0.45, 0.94) infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { background-position: 100% 0; }
|
||||
100% { background-position: -100% 0; }
|
||||
}
|
||||
|
||||
.card-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.membership-title {
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
/* Секции преимуществ */
|
||||
.membership-benefits {
|
||||
.union-step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
gap: var(--p-4);
|
||||
max-width: 640px;
|
||||
}
|
||||
|
||||
.benefit-section {
|
||||
padding: 1.5rem;
|
||||
border-radius: 16px;
|
||||
background: rgba(25, 118, 210, 0.02);
|
||||
border: 1px solid rgba(25, 118, 210, 0.1);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
.union-step__lead {
|
||||
margin: 0;
|
||||
color: var(--p-ink-1);
|
||||
}
|
||||
|
||||
.benefit-section:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(25, 118, 210, 0.1);
|
||||
background: rgba(25, 118, 210, 0.05);
|
||||
.union-step__list {
|
||||
margin: 0;
|
||||
padding: var(--p-4);
|
||||
border: 1px solid var(--p-line-1);
|
||||
border-radius: var(--p-r-md);
|
||||
background: var(--p-surface);
|
||||
font-size: var(--p-fs-body-sm);
|
||||
line-height: var(--p-lh-body-sm);
|
||||
color: var(--p-ink-1);
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
.union-step__list li + li {
|
||||
margin-top: var(--p-3);
|
||||
padding-top: var(--p-3);
|
||||
border-top: 1px solid var(--p-line);
|
||||
}
|
||||
|
||||
.benefit-description {
|
||||
line-height: 1.6;
|
||||
padding-left: 24px;
|
||||
position: relative;
|
||||
.union-step__list strong {
|
||||
color: var(--p-ink);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.benefit-description::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 2px;
|
||||
background: linear-gradient(180deg, var(--q-primary) 0%, rgba(25, 118, 210, 0.3) 100%);
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
/* Требование членства */
|
||||
.membership-requirement {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.08);
|
||||
padding-top: 2rem;
|
||||
}
|
||||
|
||||
.requirement-card {
|
||||
padding: 2rem;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(135deg, rgba(255, 152, 0, 0.02) 0%, rgba(255, 193, 7, 0.02) 100%);
|
||||
border: 1px solid rgba(255, 152, 0, 0.1);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.requirement-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, #FF9800 0%, #FFC107 50%, #FF9800 100%);
|
||||
border-radius: 16px 16px 0 0;
|
||||
}
|
||||
|
||||
.requirement-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.requirement-description {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Секция ссылки на союз */
|
||||
.union-link-section {
|
||||
padding: 1rem;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.union-link-btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 12px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.union-link-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.membership-info-card {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.benefit-section {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.requirement-card {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.benefit-description {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.benefit-description::before {
|
||||
width: 1.5px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.union-membership-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.membership-info-card {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.benefit-section {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.requirement-card {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.benefit-description {
|
||||
padding-left: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.benefit-description::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Карточка информации об аккаунте в мессенджере */
|
||||
.matrix-account-info-card {
|
||||
border-radius: 20px;
|
||||
padding: 2.5rem;
|
||||
border: 1px solid rgba(25, 118, 210, 0.1);
|
||||
background: linear-gradient(135deg, rgba(25, 118, 210, 0.02) 0%, rgba(66, 165, 245, 0.02) 100%);
|
||||
box-shadow:
|
||||
0 8px 32px rgba(25, 118, 210, 0.08),
|
||||
0 2px 8px rgba(25, 118, 210, 0.04);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.matrix-account-info-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, #2196F3 0%, #42A5F5 50%, #2196F3 100%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 4s cubic-bezier(0.25, 0.46, 0.45, 0.94) infinite;
|
||||
}
|
||||
|
||||
.account-title {
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.5px;
|
||||
color: var(--q-primary);
|
||||
}
|
||||
|
||||
.account-info {
|
||||
.union-step__matrix {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
gap: var(--p-3);
|
||||
padding: var(--p-4);
|
||||
border: 1px solid var(--p-line-1);
|
||||
border-radius: var(--p-r-md);
|
||||
background: var(--p-surface);
|
||||
}
|
||||
|
||||
.info-section {
|
||||
padding: 1.5rem;
|
||||
border-radius: 16px;
|
||||
background: rgba(25, 118, 210, 0.02);
|
||||
border: 1px solid rgba(25, 118, 210, 0.1);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
.union-step__matrix-body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.info-section:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(25, 118, 210, 0.1);
|
||||
background: rgba(25, 118, 210, 0.05);
|
||||
.union-step__link {
|
||||
color: var(--p-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.info-description {
|
||||
line-height: 1.6;
|
||||
padding-left: 24px;
|
||||
position: relative;
|
||||
.union-step__link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.info-description::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 2px;
|
||||
background: linear-gradient(180deg, var(--q-primary) 0%, rgba(25, 118, 210, 0.3) 100%);
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
/* Адаптивность для карточки аккаунта */
|
||||
@media (max-width: 768px) {
|
||||
.matrix-account-info-card {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.info-section {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.matrix-account-info-card {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.info-section {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.info-description {
|
||||
padding-left: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.info-description::before {
|
||||
display: none;
|
||||
}
|
||||
.union-step__actions {
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
// Этот файл больше не используется
|
||||
// Компоненты импортируются напрямую в ConnectionAgreementStepper.vue
|
||||
// Оставлен для обратной совместимости, если нужен где-то ещё
|
||||
// Реестр шагов ConnectionAgreement (не используется напрямую — шаги
|
||||
// импортируются по имени в ConnectionAgreementStepper.vue). Оставлен как
|
||||
// барель-экспорт на случай переиспользования отдельных шагов.
|
||||
|
||||
export { default as UnionMembershipStep } from './UnionMembershipStep.vue'
|
||||
export { default as IntroStep } from './IntroStep.vue'
|
||||
export { default as DomainStep } from './DomainStep.vue'
|
||||
export { default as FinancialParamsStep } from './FinancialParamsStep.vue'
|
||||
export { default as AgreementStep } from './AgreementStep.vue'
|
||||
export { default as FormStep } from './FormStep.vue'
|
||||
export { default as DomainValidationStep } from './DomainValidationStep.vue'
|
||||
export { default as ApprovalWaitingStep } from './ApprovalWaitingStep.vue'
|
||||
export { default as InstallationStep } from './InstallationStep.vue'
|
||||
|
||||
+128
-217
@@ -6,6 +6,7 @@ const props = defineProps<{
|
||||
tariff: ITariff
|
||||
selected?: boolean
|
||||
disabled?: boolean
|
||||
groupName?: string
|
||||
}>()
|
||||
|
||||
const emits = defineEmits<{
|
||||
@@ -13,256 +14,166 @@ const emits = defineEmits<{
|
||||
deselect: [tariffId: string]
|
||||
}>()
|
||||
|
||||
const isSelected = computed(() => props.selected)
|
||||
const isDisabled = computed(() => props.disabled)
|
||||
const isSelected = computed(() => !!props.selected)
|
||||
const isDisabled = computed(() => !!props.disabled)
|
||||
const isFree = computed(() => props.tariff.price === 'Бесплатно')
|
||||
|
||||
const cardClasses = computed(() => {
|
||||
const classes = ['tariff-card', 'cursor-pointer', 'transition-all', 'duration-200']
|
||||
const onChange = () => {
|
||||
if (isDisabled.value) return
|
||||
emits('select', props.tariff.id)
|
||||
}
|
||||
|
||||
if (isSelected.value) {
|
||||
classes.push('tariff-selected')
|
||||
}
|
||||
|
||||
if (isDisabled.value) {
|
||||
classes.push('tariff-disabled')
|
||||
}
|
||||
|
||||
return classes.join(' ')
|
||||
})
|
||||
|
||||
const handleSelect = () => {
|
||||
if (!isDisabled.value) {
|
||||
// Если тариф уже выбран, снимаем выбор, иначе выбираем
|
||||
if (isSelected.value) {
|
||||
emits('deselect', props.tariff.id)
|
||||
} else {
|
||||
emits('select', props.tariff.id)
|
||||
}
|
||||
}
|
||||
const onClick = () => {
|
||||
if (isDisabled.value) return
|
||||
if (isSelected.value) emits('deselect', props.tariff.id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
div.tariff-card-container
|
||||
.tariff-card(
|
||||
:class="cardClasses"
|
||||
@click="handleSelect"
|
||||
label.tariff-card(
|
||||
:class="{ 'is-selected': isSelected, 'is-disabled': isDisabled }"
|
||||
@click="onClick"
|
||||
)
|
||||
input.tariff-card__input(
|
||||
type="radio"
|
||||
:name="groupName || 'tariff'"
|
||||
:value="tariff.id"
|
||||
:checked="isSelected"
|
||||
:disabled="isDisabled"
|
||||
@change="onChange"
|
||||
)
|
||||
.tariff-header
|
||||
.tariff-checkmark(v-if="isSelected")
|
||||
q-icon(name="check_circle" size="24px" color="white")
|
||||
.tariff-title
|
||||
h6.text-h6 {{ tariff.name }}
|
||||
.tariff-card__head
|
||||
span.tariff-card__box(aria-hidden="true")
|
||||
.tariff-card__title {{ tariff.name }}
|
||||
|
||||
.tariff-content
|
||||
p.text-body2.text-grey-7.q-mb-md.text-center {{ tariff.description }}
|
||||
p.tariff-card__sub.t-sm.t-muted(v-if="tariff.description") {{ tariff.description }}
|
||||
|
||||
.tariff-features
|
||||
.feature-item(v-for="feature in tariff.features" :key="feature")
|
||||
q-icon(name="check" size="16px" color="positive").q-mr-xs
|
||||
span {{ feature }}
|
||||
ul.tariff-card__list
|
||||
li.tariff-card__item(v-for="feature in tariff.features" :key="feature") {{ feature }}
|
||||
|
||||
template(v-if="tariff.additionalCosts && tariff.additionalCosts.length")
|
||||
.tariff-additional.q-mt-md
|
||||
p.text-subtitle2.text-grey-8 Дополнительные расходы:
|
||||
.additional-item(v-for="cost in tariff.additionalCosts" :key="cost")
|
||||
q-icon(name="add_circle_outline" size="14px" color="info").q-mr-xs
|
||||
span.text-caption {{ cost }}
|
||||
|
||||
.tariff-footer
|
||||
.tariff-price
|
||||
.price-display {{ tariff.price }}
|
||||
.price-period(v-if="tariff.price !== 'Бесплатно'") в месяц
|
||||
.select-hint.text-caption.text-grey-6
|
||||
| Нажмите для выбора
|
||||
.tariff-card__foot
|
||||
.tariff-card__price
|
||||
span.tariff-card__price-value.t-num {{ tariff.price }}
|
||||
span.tariff-card__price-period(v-if="!isFree") / мес
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.tariff-card-container {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tariff-card {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
min-height: 360px; /* Уменьшенная минимальная высота */
|
||||
padding: 24px;
|
||||
border-radius: 16px;
|
||||
border: 2px solid #f0f0f0;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-5);
|
||||
padding: var(--p-6);
|
||||
border: 1px solid var(--p-line-1);
|
||||
border-radius: var(--p-r-lg);
|
||||
background: var(--p-surface);
|
||||
box-shadow: var(--p-shadow-card);
|
||||
cursor: pointer;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition:
|
||||
border-color var(--p-dur-fast) var(--p-ease-standard),
|
||||
background var(--p-dur-fast) var(--p-ease-standard),
|
||||
box-shadow var(--p-dur-fast) var(--p-ease-standard);
|
||||
}
|
||||
.tariff-card:hover:not(.is-disabled):not(.is-selected) {
|
||||
border-color: var(--p-line-2);
|
||||
box-shadow: var(--p-shadow-pop);
|
||||
}
|
||||
.tariff-card.is-selected {
|
||||
border-color: var(--p-primary);
|
||||
background: var(--p-primary-soft);
|
||||
box-shadow: var(--p-shadow-card);
|
||||
}
|
||||
.tariff-card.is-disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.tariff-card:hover:not(.tariff-disabled):not(.tariff-selected) {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||
border-color: #e0e0e0;
|
||||
}
|
||||
|
||||
.tariff-selected {
|
||||
border-color: var(--q-accent);
|
||||
background: linear-gradient(135deg, rgba(25, 118, 210, 0.05) 0%, rgba(25, 118, 210, 0.02) 100%);
|
||||
box-shadow:
|
||||
0 8px 32px rgba(25, 118, 210, 0.2),
|
||||
0 0 0 1px var(--q-accent);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.tariff-disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed !important;
|
||||
.tariff-card__input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.tariff-card__input:focus-visible ~ .tariff-card__head .tariff-card__box {
|
||||
box-shadow: var(--p-focus-ring);
|
||||
}
|
||||
|
||||
.tariff-header {
|
||||
position: relative;
|
||||
height: 40px; /* Фиксированная высота для предотвращения прыжков */
|
||||
.tariff-card__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 16px;
|
||||
gap: var(--p-3);
|
||||
}
|
||||
|
||||
.tariff-checkmark {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
transform: translateY(-50%) scale(0.8);
|
||||
background: var(--q-accent);
|
||||
border-radius: 50%;
|
||||
padding: 4px;
|
||||
box-shadow: 0 2px 8px rgba(25, 118, 210, 0.3);
|
||||
opacity: 0;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.tariff-selected .tariff-checkmark {
|
||||
opacity: 1;
|
||||
transform: translateY(-50%) scale(1);
|
||||
}
|
||||
|
||||
.tariff-title {
|
||||
text-align: center;
|
||||
margin-right: 36px; /* Отступ для иконки */
|
||||
}
|
||||
|
||||
.tariff-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tariff-features {
|
||||
flex: 1;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
padding: 4px 0;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.feature-item:hover {
|
||||
color: var(--q-primary);
|
||||
}
|
||||
|
||||
.tariff-additional {
|
||||
border-top: 1px solid #f5f5f5;
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.additional-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tariff-footer {
|
||||
position: relative;
|
||||
height: 80px; /* Увеличенная высота для богатого оформления */
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 20px 32px 20px 12px;
|
||||
}
|
||||
|
||||
.tariff-price {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 4px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.price-display {
|
||||
font-size: 36px;
|
||||
.tariff-card__title {
|
||||
font-size: var(--p-fs-h2);
|
||||
line-height: var(--p-lh-h2);
|
||||
letter-spacing: var(--p-ls-h2);
|
||||
font-weight: 600;
|
||||
letter-spacing: -1px;
|
||||
background: linear-gradient(135deg, var(--q-accent) 0%, rgba(25, 118, 210, 0.8) 100%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
line-height: 1;
|
||||
text-shadow: 0 2px 4px rgba(25, 118, 210, 0.3);
|
||||
color: var(--p-ink);
|
||||
}
|
||||
|
||||
.price-period {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #888;
|
||||
letter-spacing: 0.5px;
|
||||
text-transform: uppercase;
|
||||
opacity: 0.8;
|
||||
.tariff-card__box {
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 1.5px solid var(--p-line-2);
|
||||
border-radius: var(--p-r-pill);
|
||||
background: var(--p-surface);
|
||||
transition: border-color var(--p-dur-fast) var(--p-ease-standard);
|
||||
}
|
||||
|
||||
.select-hint {
|
||||
.tariff-card.is-selected .tariff-card__box {
|
||||
border-color: var(--p-primary);
|
||||
}
|
||||
.tariff-card.is-selected .tariff-card__box::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
opacity: 0;
|
||||
animation: pulse 2s infinite;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.3s ease;
|
||||
inset: 4px;
|
||||
border-radius: var(--p-r-pill);
|
||||
background: var(--p-primary);
|
||||
}
|
||||
|
||||
.tariff-card:not(.tariff-selected):not(.tariff-disabled) .select-hint {
|
||||
opacity: 1;
|
||||
.tariff-card__sub {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
.tariff-card__list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.tariff-card__item {
|
||||
position: relative;
|
||||
padding: var(--p-3) 0;
|
||||
font-size: var(--p-fs-body-sm);
|
||||
line-height: var(--p-lh-body-sm);
|
||||
color: var(--p-ink-1);
|
||||
border-bottom: 1px solid var(--p-line);
|
||||
}
|
||||
.tariff-card__item:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 599px) {
|
||||
.tariff-card {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.tariff-header {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.tariff-title h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
.tariff-card__foot {
|
||||
margin-top: auto;
|
||||
padding-top: var(--p-4);
|
||||
border-top: 1px solid var(--p-line);
|
||||
}
|
||||
.tariff-card__price {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: var(--p-1);
|
||||
color: var(--p-ink);
|
||||
}
|
||||
.tariff-card__price-value {
|
||||
font-family: var(--p-mono);
|
||||
font-size: var(--p-fs-h1);
|
||||
line-height: var(--p-lh-h1);
|
||||
letter-spacing: var(--p-ls-h1);
|
||||
font-weight: 600;
|
||||
}
|
||||
.tariff-card__price-period {
|
||||
font-size: var(--p-fs-body-sm);
|
||||
color: var(--p-ink-2);
|
||||
}
|
||||
</style>
|
||||
|
||||
+41
-51
@@ -1,27 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { TariffCard, type ITariff } from './index'
|
||||
|
||||
// Доступные тарифы
|
||||
const availableTariffs: ITariff[] = [
|
||||
{
|
||||
id: 'test',
|
||||
name: 'Тестовый',
|
||||
description: 'Единственный тариф на период бета-тестирования платформы',
|
||||
price: '1500 RUB',
|
||||
id: 'base',
|
||||
name: 'Базовый',
|
||||
description: 'Подключение кооператива к платформе Кооперативной Экономики',
|
||||
price: '3 000 ₽',
|
||||
features: [
|
||||
'150 AXON на счёт кооператива',
|
||||
'достаточно для 100 пакетов документов',
|
||||
'и 50 регистраций пайщиков',
|
||||
'50 пакетов документов в месяц',
|
||||
'50 регистраций пайщика в месяц',
|
||||
'Хостинг на изолированном сервере',
|
||||
'Техническая поддержка'
|
||||
'Техническая поддержка',
|
||||
],
|
||||
additionalCosts: [
|
||||
'5 AXON в день списывается со счёта кооператива',
|
||||
'1 AXON списывается за каждый пакет документов',
|
||||
'1 AXON списывается за регистрацию нового пайщика',
|
||||
]
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -36,16 +29,14 @@ const emits = defineEmits<{
|
||||
|
||||
const selectedTariffId = ref<string>(props.selectedTariff?.id || '')
|
||||
|
||||
const currentSelectedTariff = computed(() => {
|
||||
return availableTariffs.find(tariff => tariff.id === selectedTariffId.value)
|
||||
})
|
||||
const currentSelectedTariff = computed(() =>
|
||||
availableTariffs.find((tariff) => tariff.id === selectedTariffId.value),
|
||||
)
|
||||
|
||||
const handleTariffSelect = (tariffId: string) => {
|
||||
selectedTariffId.value = tariffId
|
||||
const tariff = availableTariffs.find(t => t.id === tariffId)
|
||||
if (tariff) {
|
||||
emits('tariffSelected', tariff)
|
||||
}
|
||||
const tariff = availableTariffs.find((t) => t.id === tariffId)
|
||||
if (tariff) emits('tariffSelected', tariff)
|
||||
}
|
||||
|
||||
const handleTariffDeselect = (tariffId: string) => {
|
||||
@@ -55,42 +46,41 @@ const handleTariffDeselect = (tariffId: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Экспортируем для использования в родительском компоненте
|
||||
// Если тариф один и ничего ещё не выбрано — отмечаем сразу. Канон-степпер
|
||||
// предполагает что пользователь делает явный выбор; экран с единственной
|
||||
// опцией не должен требовать лишнего клика.
|
||||
onMounted(() => {
|
||||
if (!selectedTariffId.value && availableTariffs.length === 1) {
|
||||
handleTariffSelect(availableTariffs[0].id)
|
||||
}
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
currentSelectedTariff,
|
||||
hasSelection: computed(() => !!selectedTariffId.value)
|
||||
hasSelection: computed(() => !!selectedTariffId.value),
|
||||
})
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
div
|
||||
.text-center.q-mb-lg
|
||||
//- h5.text-h5.q-mb-sm Выберите тариф
|
||||
p.text-body2.text-grey-7 Выберите подходящий тариф для вашего кооператива
|
||||
|
||||
.tariff-grid
|
||||
div(v-for="tariff in availableTariffs" :key="tariff.id")
|
||||
TariffCard(
|
||||
:tariff="tariff"
|
||||
:selected="selectedTariffId === tariff.id"
|
||||
:disabled="disabled"
|
||||
@select="handleTariffSelect"
|
||||
@deselect="handleTariffDeselect"
|
||||
)</template>
|
||||
.tariff-selector
|
||||
.tariff-selector__list
|
||||
TariffCard(
|
||||
v-for="tariff in availableTariffs"
|
||||
:key="tariff.id"
|
||||
:tariff="tariff"
|
||||
:selected="selectedTariffId === tariff.id"
|
||||
:disabled="disabled"
|
||||
group-name="connection-tariff"
|
||||
@select="handleTariffSelect"
|
||||
@deselect="handleTariffDeselect"
|
||||
)
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.tariff-grid {
|
||||
.tariff-selector__list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 24px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@media (max-width: 599px) {
|
||||
.tariff-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 300px));
|
||||
gap: var(--p-4);
|
||||
align-items: stretch;
|
||||
}
|
||||
</style>
|
||||
|
||||
+90
-74
@@ -1,70 +1,12 @@
|
||||
<template lang="pug">
|
||||
div
|
||||
q-stepper(
|
||||
:model-value="currentStep"
|
||||
flat
|
||||
vertical
|
||||
color="accent"
|
||||
animated
|
||||
done-color="teal"
|
||||
)
|
||||
UnionMembershipStep(
|
||||
v-if="system.info.is_unioned"
|
||||
:current-step="currentStep"
|
||||
:is-active="currentStep === 0"
|
||||
:is-done="currentStep > 0"
|
||||
)
|
||||
template(#registration)
|
||||
slot(name="union-registration")
|
||||
|
||||
IntroStep(
|
||||
:current-step="currentStep"
|
||||
:is-active="currentStep === 1"
|
||||
:is-done="currentStep > 1"
|
||||
:selected-tariff="selectedTariff"
|
||||
)
|
||||
|
||||
FormStep(
|
||||
:current-step="currentStep"
|
||||
:is-active="currentStep === 2"
|
||||
:is-done="currentStep > 2"
|
||||
:document="document"
|
||||
:signed-document="signedDocument"
|
||||
)
|
||||
|
||||
AgreementStep(
|
||||
:current-step="currentStep"
|
||||
:is-active="currentStep === 3"
|
||||
:is-done="currentStep > 3"
|
||||
:html="html"
|
||||
)
|
||||
|
||||
DomainValidationStep(
|
||||
:current-step="currentStep"
|
||||
:is-active="currentStep === 4"
|
||||
:is-done="currentStep > 4"
|
||||
)
|
||||
|
||||
ApprovalWaitingStep(
|
||||
:current-step="currentStep"
|
||||
:is-active="currentStep === 5"
|
||||
:is-done="currentStep > 5"
|
||||
)
|
||||
|
||||
InstallationStep(
|
||||
:current-step="currentStep"
|
||||
:is-active="currentStep === 6"
|
||||
:is-done="false"
|
||||
)
|
||||
</template>
|
||||
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, watch } from 'vue'
|
||||
import { VerticalStepper } from 'src/shared/ui/domain/VerticalStepper'
|
||||
import type { StepperStep } from 'src/shared/ui/domain/VerticalStepper'
|
||||
import UnionMembershipStep from '../Steps/UnionMembershipStep.vue'
|
||||
import IntroStep from '../Steps/IntroStep.vue'
|
||||
import DomainStep from '../Steps/DomainStep.vue'
|
||||
import FinancialParamsStep from '../Steps/FinancialParamsStep.vue'
|
||||
import AgreementStep from '../Steps/AgreementStep.vue'
|
||||
import FormStep from '../Steps/FormStep.vue'
|
||||
import DomainValidationStep from '../Steps/DomainValidationStep.vue'
|
||||
import ApprovalWaitingStep from '../Steps/ApprovalWaitingStep.vue'
|
||||
import InstallationStep from '../Steps/InstallationStep.vue'
|
||||
@@ -74,23 +16,97 @@ import { useSystemStore } from 'src/entities/System/model'
|
||||
const connectionAgreement = useConnectionAgreementStore()
|
||||
const system = useSystemStore()
|
||||
|
||||
// Данные из стора
|
||||
const currentStep = computed(() => connectionAgreement.currentStep)
|
||||
const selectedTariff = computed(() => connectionAgreement.selectedTariff)
|
||||
const document = computed(() => connectionAgreement.document)
|
||||
const signedDocument = computed(() => connectionAgreement.signedDocument)
|
||||
const html = computed(() => document.value?.data?.html)
|
||||
|
||||
// Watch для реактивной генерации документа при переходе к шагу 3
|
||||
watch(() => currentStep.value, async (newStep, oldStep) => {
|
||||
// Если переходим к шагу 3 (соглашение), всегда генерируем документ заново
|
||||
if (newStep === 3 && oldStep !== 3) {
|
||||
console.log('📝 Шаг 3: Генерируем соглашение')
|
||||
try {
|
||||
await connectionAgreement.generateDocument()
|
||||
} catch (error) {
|
||||
console.error('Ошибка генерации документа:', error)
|
||||
}
|
||||
}
|
||||
// Полный реестр шагов в порядке прохождения. Индекс в массиве = индекс в
|
||||
// connectionAgreement.currentStep. Все жёсткие переходы в шагах и в
|
||||
// ConnectionAgreementPage завязаны на эти числа — менять синхронно.
|
||||
const ALL_STEPS: Array<StepperStep & { index: number }> = [
|
||||
{ key: 'union', index: 0, label: 'Членство в союзе' },
|
||||
{ key: 'intro', index: 1, label: 'Тариф подключения' },
|
||||
{ key: 'domain', index: 2, label: 'Домен кооператива' },
|
||||
{ key: 'financial', index: 3, label: 'Финансовые параметры' },
|
||||
{ key: 'agreement', index: 4, label: 'Соглашение о подключении' },
|
||||
{ key: 'dns', index: 5, label: 'Делегирование домена' },
|
||||
{ key: 'approval', index: 6, label: 'Подтверждение союза' },
|
||||
{ key: 'installation', index: 7, label: 'Поставка инфраструктуры' },
|
||||
]
|
||||
|
||||
const visibleSteps = computed(() =>
|
||||
system.info.is_unioned ? ALL_STEPS : ALL_STEPS.filter((s) => s.key !== 'union'),
|
||||
)
|
||||
|
||||
const stepperSteps = computed<StepperStep[]>(() =>
|
||||
visibleSteps.value.map(({ key, label, description }) => ({ key, label, description })),
|
||||
)
|
||||
|
||||
const activeKey = computed(() => {
|
||||
const found = visibleSteps.value.find((s) => s.index === currentStep.value)
|
||||
return found?.key ?? visibleSteps.value[0]?.key ?? 'intro'
|
||||
})
|
||||
|
||||
const completedKeys = computed(() =>
|
||||
visibleSteps.value.filter((s) => s.index < currentStep.value).map((s) => s.key),
|
||||
)
|
||||
|
||||
function onStepperChange(key: string) {
|
||||
const found = ALL_STEPS.find((s) => s.key === key)
|
||||
if (!found) return
|
||||
if (found.index >= currentStep.value) return
|
||||
connectionAgreement.setCurrentStep(found.index)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => currentStep.value,
|
||||
async (newStep, oldStep) => {
|
||||
if (newStep === 4 && oldStep !== 4) {
|
||||
try {
|
||||
await connectionAgreement.generateDocument()
|
||||
} catch (error) {
|
||||
console.error('Ошибка генерации документа:', error)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
.connection-stepper
|
||||
VerticalStepper(
|
||||
:steps="stepperSteps"
|
||||
:active-key="activeKey"
|
||||
:completed="completedKeys"
|
||||
@change="onStepperChange"
|
||||
)
|
||||
template(#active="{ step }")
|
||||
UnionMembershipStep(v-if="step.key === 'union'")
|
||||
template(#registration)
|
||||
slot(name="union-registration")
|
||||
IntroStep(
|
||||
v-else-if="step.key === 'intro'"
|
||||
:selected-tariff="selectedTariff"
|
||||
)
|
||||
DomainStep(v-else-if="step.key === 'domain'")
|
||||
FinancialParamsStep(v-else-if="step.key === 'financial'")
|
||||
AgreementStep(
|
||||
v-else-if="step.key === 'agreement'"
|
||||
:html="html"
|
||||
:document="document"
|
||||
:signed-document="signedDocument"
|
||||
)
|
||||
DomainValidationStep(v-else-if="step.key === 'dns'")
|
||||
ApprovalWaitingStep(v-else-if="step.key === 'approval'")
|
||||
InstallationStep(v-else-if="step.key === 'installation'")
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.connection-stepper {
|
||||
max-width: 760px;
|
||||
margin: 0 auto;
|
||||
padding: var(--p-6) var(--p-4);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,87 +1,78 @@
|
||||
<template lang="pug">
|
||||
.axon-wallet
|
||||
ColorCard(color='purple', @click.stop)
|
||||
// Заголовок
|
||||
.wallet-header
|
||||
.wallet-title
|
||||
q-icon(name="account_balance_wallet" size="20px").q-mr-sm
|
||||
| Кошелек AXON
|
||||
BaseCard(
|
||||
title="Кошелёк AXON"
|
||||
subtitle="Для оплаты пакетов документов. Минимально 5 AXON в день, по факту — от использования."
|
||||
)
|
||||
.axon-wallet__metric
|
||||
.axon-wallet__metric-label Доступно
|
||||
.axon-wallet__metric-value.t-mono {{ formattedBalance }}
|
||||
|
||||
// Описание
|
||||
.wallet-description
|
||||
.text-body2.text-grey-7
|
||||
| AXON используется для оплаты пакетов документов. Минимально 5 AXON в день, по факту - от использования.
|
||||
|
||||
// Баланс
|
||||
.balance-section
|
||||
.balance-value {{ formattedBalance }}
|
||||
.balance-label Доступно
|
||||
|
||||
// Действия
|
||||
.actions-section
|
||||
.action-buttons.q-pa-sm
|
||||
q-btn(
|
||||
color="primary"
|
||||
icon="add"
|
||||
label="Пополнить"
|
||||
@click.stop="showDepositDialog = true"
|
||||
)
|
||||
.axon-wallet__actions
|
||||
BaseButton(
|
||||
variant="primary"
|
||||
size="md"
|
||||
type="button"
|
||||
@click="showDepositDialog = true"
|
||||
)
|
||||
q-icon(name="add" size="16px").q-mr-xs
|
||||
| Пополнить
|
||||
|
||||
BaseDialog(
|
||||
v-model='showDepositDialog',
|
||||
title='Пополнение кошелька AXON',
|
||||
size='md',
|
||||
@update:model-value='(v) => !v && clear()'
|
||||
v-model="showDepositDialog"
|
||||
title="Пополнение кошелька AXON"
|
||||
size="md"
|
||||
@update:model-value="(v) => !v && clear()"
|
||||
)
|
||||
.current-balance-section.q-mb-md
|
||||
.text-body2.text-grey-7.q-mb-xs
|
||||
| Текущий баланс: {{ formattedRubBalance }}.
|
||||
| Для оплаты AXON используется паевой взнос на вашем кошельке.
|
||||
| При недостатке средств на балансе совершите паевой взнос:
|
||||
q-btn(
|
||||
flat,
|
||||
dense,
|
||||
no-caps,
|
||||
color="primary",
|
||||
label="перейти в кошелек",
|
||||
@click="goToWallet"
|
||||
)
|
||||
BaseBanner(variant="info")
|
||||
| Текущий баланс паевого: <b>{{ formattedRubBalance }}</b>.
|
||||
| Для оплаты AXON используется паевой взнос на вашем кошельке.
|
||||
| При недостатке средств совершите паевой взнос в разделе «Кошелёк».
|
||||
|
||||
Form(
|
||||
:handler-submit="handlerSubmit",
|
||||
:is-submitting="isSubmitting",
|
||||
button-cancel-txt="Отменить",
|
||||
button-submit-txt="Пополнить",
|
||||
@cancel="clear"
|
||||
)
|
||||
q-input(
|
||||
v-model="depositAmount",
|
||||
standout="bg-teal text-white",
|
||||
placeholder="Введите сумму в RUB",
|
||||
type="number",
|
||||
:min="0",
|
||||
:step="10",
|
||||
:hint="depositHint",
|
||||
:rules="[(val) => val > 0 || 'Сумма должна быть положительной']"
|
||||
BaseForm.q-mt-md(:loading="isSubmitting" @submit="handlerSubmit")
|
||||
BaseInput(
|
||||
v-model="depositAmount"
|
||||
label="Сумма пополнения"
|
||||
type="number"
|
||||
:hint="depositHint"
|
||||
suffix="RUB"
|
||||
placeholder="Введите сумму в RUB"
|
||||
)
|
||||
template(#append)
|
||||
span.text-overline RUB
|
||||
|
||||
template(#footer="{ loading }")
|
||||
.axon-wallet__form-actions
|
||||
BaseButton(
|
||||
variant="ghost"
|
||||
size="md"
|
||||
type="button"
|
||||
:disabled="loading"
|
||||
@click="clear"
|
||||
) Отменить
|
||||
BaseButton(
|
||||
variant="primary"
|
||||
size="md"
|
||||
type="submit"
|
||||
:loading="loading"
|
||||
) Пополнить
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useSessionStore } from 'src/entities/Session';
|
||||
import { useWalletStore } from 'src/entities/Wallet';
|
||||
import { ColorCard } from 'src/shared/ui';
|
||||
import { Form } from 'src/shared/ui/Form';
|
||||
import { BaseDialog } from 'src/shared/ui/base/BaseDialog';
|
||||
import {
|
||||
BaseBanner,
|
||||
BaseButton,
|
||||
BaseCard,
|
||||
BaseDialog,
|
||||
BaseForm,
|
||||
BaseInput,
|
||||
} from 'src/shared/ui/base';
|
||||
import { formatAsset2Digits } from 'src/shared/lib/utils/formatAsset2Digits';
|
||||
import { formatToAsset } from 'src/shared/lib/utils/formatToAsset';
|
||||
import { useProviderAxonConvert, AXON_GOVERN_RATE } from 'src/features/Provider/model';
|
||||
import { useSystemStore } from 'src/entities/System/model';
|
||||
|
||||
const router = useRouter();
|
||||
const session = useSessionStore();
|
||||
const walletStore = useWalletStore();
|
||||
const system = useSystemStore();
|
||||
@@ -102,10 +93,7 @@ const formattedRubBalance = computed(() => {
|
||||
});
|
||||
|
||||
const depositHint = computed(() => {
|
||||
if (!depositAmount.value || parseFloat(depositAmount.value) <= 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!depositAmount.value || parseFloat(depositAmount.value) <= 0) return '';
|
||||
const rubAmount = parseFloat(depositAmount.value);
|
||||
const axonAmount = rubAmount / AXON_GOVERN_RATE;
|
||||
return `Будет зачислено: ${formatAsset2Digits(`${axonAmount} AXON`)} (курс: 1 AXON = ${AXON_GOVERN_RATE} RUB)`;
|
||||
@@ -117,110 +105,49 @@ const clear = () => {
|
||||
isSubmitting.value = false;
|
||||
};
|
||||
|
||||
const goToWallet = () => {
|
||||
router.push({ name: 'wallet' });
|
||||
};
|
||||
|
||||
const handlerSubmit = async () => {
|
||||
isSubmitting.value = true;
|
||||
try {
|
||||
const success = await convertToAxon({
|
||||
convertAmount: formatToAsset(depositAmount.value, system.info.symbols.root_govern_symbol, system.info.symbols.root_govern_precision),
|
||||
convertAmount: formatToAsset(
|
||||
depositAmount.value,
|
||||
system.info.symbols.root_govern_symbol,
|
||||
system.info.symbols.root_govern_precision,
|
||||
),
|
||||
username: session.username || '',
|
||||
coopname: system.info.coopname || ''
|
||||
coopname: system.info.coopname || '',
|
||||
});
|
||||
|
||||
if (success) {
|
||||
clear();
|
||||
} else {
|
||||
isSubmitting.value = false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (success) clear();
|
||||
else isSubmitting.value = false;
|
||||
} catch {
|
||||
isSubmitting.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.axon-wallet {
|
||||
padding: 8px;
|
||||
|
||||
:deep(.color-card) {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.wallet-header {
|
||||
margin-bottom: 8px;
|
||||
|
||||
.wallet-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.wallet-description {
|
||||
margin-bottom: 12px;
|
||||
|
||||
.text-body2 {
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
|
||||
.balance-section {
|
||||
padding: 12px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.balance-label {
|
||||
font-size: 11px;
|
||||
opacity: 0.8;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.balance-value {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.actions-section {
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
.action-btn {
|
||||
min-width: 120px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.current-balance-section {
|
||||
padding: 12px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
|
||||
.text-body2 {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.contribution-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
<style scoped>
|
||||
.axon-wallet__metric {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding: var(--p-3) 0;
|
||||
}
|
||||
.axon-wallet__metric-label {
|
||||
font-size: var(--p-fs-meta);
|
||||
color: var(--p-ink-2);
|
||||
}
|
||||
.axon-wallet__metric-value {
|
||||
font-size: var(--p-fs-h1);
|
||||
font-weight: 700;
|
||||
color: var(--p-ink);
|
||||
}
|
||||
.axon-wallet__actions {
|
||||
display: flex;
|
||||
gap: var(--p-2);
|
||||
}
|
||||
.axon-wallet__form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--p-2);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,24 +1,17 @@
|
||||
<template lang="pug">
|
||||
div.connection-dashboard
|
||||
//- Основная информация
|
||||
.row.q-mb-lg
|
||||
//- Карточка баланса AXON
|
||||
.row.q-col-gutter-md
|
||||
.col-12.col-md-4
|
||||
AxonWallet
|
||||
MembershipWallet.q-mt-md
|
||||
|
||||
//- Карточка домена
|
||||
.col-12.col-md-6
|
||||
.col-12.col-md-8
|
||||
DomainCard
|
||||
|
||||
SubscriptionsCard
|
||||
|
||||
|
||||
|
||||
SubscriptionsCard.q-mt-md
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { AxonWallet, DomainCard, SubscriptionsCard } from './index'
|
||||
|
||||
import { AxonWallet, MembershipWallet, DomainCard, SubscriptionsCard } from './index'
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -27,4 +20,3 @@ import { AxonWallet, DomainCard, SubscriptionsCard } from './index'
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,95 +1,61 @@
|
||||
<template lang="pug">
|
||||
.domain-card
|
||||
ColorCard(color='blue', @click.stop)
|
||||
// Заголовок
|
||||
.domain-header
|
||||
.domain-title
|
||||
q-icon(name="domain" size="20px").q-mr-sm
|
||||
| Подключение
|
||||
BaseCard(title="Подключение" subtitle="Доменное имя цифрового кооператива у провайдера")
|
||||
template(#actions)
|
||||
.domain-card__actions
|
||||
BaseChip(
|
||||
:variant="isDelegatingLoading ? 'neutral' : (instance?.is_delegated ? 'pos' : 'warn')"
|
||||
size="sm"
|
||||
)
|
||||
q-icon(v-if="isDelegatingLoading" name="autorenew" size="12px").q-mr-xs.domain-card__spin
|
||||
q-icon(v-else-if="instance?.is_delegated" name="check" size="12px").q-mr-xs
|
||||
q-icon(v-else name="schedule" size="12px").q-mr-xs
|
||||
span {{ delegationLabel }}
|
||||
BaseButton(
|
||||
v-if="!isEditing"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
type="button"
|
||||
aria-label="Изменить домен"
|
||||
@click="startEdit"
|
||||
)
|
||||
q-icon(name="edit" size="14px").q-mr-xs
|
||||
| Изменить
|
||||
|
||||
// Отображение или редактирование домена
|
||||
.domain-display
|
||||
.flex.items-center
|
||||
.domain-text.full-width
|
||||
template(v-if="!isEditing")
|
||||
div.q-pa-md.text-h6.q-mr-sm {{ coop.publicCooperativeData?.announce || '—' }}
|
||||
q-btn(
|
||||
flat
|
||||
round
|
||||
icon="edit"
|
||||
size="sm"
|
||||
color="primary"
|
||||
@click="startEdit"
|
||||
)
|
||||
q-tooltip Редактировать домен
|
||||
div(v-else).q-pa-sm
|
||||
.domain-warning.q-mb-md.full-width
|
||||
.text-caption.text-orange-8.q-mb-sm
|
||||
| ⚠️ Убедитесь, что домен делегирован IP-адрес: {{ SERVER_IP }}.
|
||||
| Обновление домена перезагрузит цифровой кооператив.
|
||||
| Все данные будут сохранены.
|
||||
template(v-if="!isEditing")
|
||||
.domain-card__view
|
||||
.domain-card__host.t-mono {{ coop.publicCooperativeData?.announce || '—' }}
|
||||
|
||||
q-input(
|
||||
v-model="domainValue"
|
||||
placeholder="Введите домен"
|
||||
outlined
|
||||
dense
|
||||
autofocus
|
||||
@keyup.enter="saveDomain"
|
||||
@keyup.escape="cancelEdit"
|
||||
:rules="[(val) => !!val || 'Домен обязателен']"
|
||||
).full-width.q-mt-md
|
||||
template(#prepend)
|
||||
q-btn(
|
||||
flat
|
||||
round
|
||||
icon="close"
|
||||
size="sm"
|
||||
color="negative"
|
||||
@click="cancelEdit"
|
||||
)
|
||||
q-tooltip Отменить
|
||||
template(#append)
|
||||
q-btn(
|
||||
flat
|
||||
round
|
||||
icon="check"
|
||||
size="sm"
|
||||
color="positive"
|
||||
@click="saveDomain"
|
||||
)
|
||||
q-tooltip Сохранить
|
||||
template(v-else)
|
||||
BaseBanner(variant="warn").q-mb-md
|
||||
| Убедитесь, что домен делегирован на IP {{ SERVER_IP }}.
|
||||
| Обновление перезагрузит цифровой кооператив; данные сохранятся.
|
||||
|
||||
BaseInput(
|
||||
v-model="domainValue"
|
||||
label="Доменное имя"
|
||||
placeholder="example.coopenomics.world"
|
||||
mono
|
||||
)
|
||||
|
||||
.row
|
||||
//- .col-6
|
||||
//- .text-caption.text-grey-7 Членство в союзе
|
||||
//- .text-body2.text-weight-medium
|
||||
//- q-chip(
|
||||
//- :color="getMembershipStatusColor"
|
||||
//- outline
|
||||
//- size="sm"
|
||||
//- ) {{ getMembershipStatusLabel }}
|
||||
.col-6
|
||||
.text-caption.text-grey-7 Домен делегирован
|
||||
.text-body2.text-weight-medium
|
||||
q-chip(
|
||||
:color="isDelegatingLoading ? 'grey' : (instance?.is_delegated ? 'positive' : 'grey')"
|
||||
outline
|
||||
size="sm"
|
||||
)
|
||||
template(v-if="isDelegatingLoading")
|
||||
q-spinner(color="white" size="16px")
|
||||
template(v-else)
|
||||
q-icon(
|
||||
v-if="!instance?.is_delegated"
|
||||
name="refresh"
|
||||
size="14px"
|
||||
class="q-ml-xs rotating-icon"
|
||||
color="grey-5"
|
||||
)
|
||||
span {{ instance?.is_delegated ? 'Да' : 'Обновляем' }}
|
||||
|
||||
.domain-card__edit-actions.q-mt-sm
|
||||
BaseButton(
|
||||
variant="primary"
|
||||
size="sm"
|
||||
type="button"
|
||||
:loading="isDelegatingLoading"
|
||||
@click="saveDomain"
|
||||
)
|
||||
q-icon(name="check" size="14px").q-mr-xs
|
||||
| Сохранить
|
||||
BaseButton(
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
type="button"
|
||||
@click="cancelEdit"
|
||||
)
|
||||
q-icon(name="close" size="14px").q-mr-xs
|
||||
| Отменить
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -99,76 +65,62 @@ import { useCooperativeStore } from 'src/entities/Cooperative'
|
||||
import { useUpdateCoop } from 'src/features/Cooperative/UpdateCoop'
|
||||
import { FailAlert, SuccessAlert } from 'src/shared/api'
|
||||
import { useSessionStore } from 'src/entities/Session'
|
||||
import { ColorCard } from 'src/shared/ui'
|
||||
import {
|
||||
BaseBanner,
|
||||
BaseButton,
|
||||
BaseCard,
|
||||
BaseChip,
|
||||
BaseInput,
|
||||
} from 'src/shared/ui/base'
|
||||
import { useProviderSubscriptions } from 'src/features/Provider/model'
|
||||
|
||||
const connectionAgreement = useConnectionAgreementStore()
|
||||
|
||||
const coop = useCooperativeStore()
|
||||
const { updateCoop } = useUpdateCoop()
|
||||
const session = useSessionStore()
|
||||
const { SERVER_IP } = useProviderSubscriptions()
|
||||
// Получаем instance напрямую из store
|
||||
const instance = computed(() => connectionAgreement.currentInstance)
|
||||
|
||||
// Цвет статуса членства
|
||||
// const getMembershipStatusColor = computed(() => {
|
||||
// if (instance.value?.blockchain_status === 'active') return 'positive'
|
||||
// if (instance.value?.blockchain_status === 'pending') return 'warning'
|
||||
// if (instance.value?.blockchain_status === 'blocked') return 'negative'
|
||||
// return 'grey'
|
||||
// })
|
||||
|
||||
// // Метка статуса членства
|
||||
// const getMembershipStatusLabel = computed(() => {
|
||||
// if (instance.value?.blockchain_status === 'active') return 'Активно'
|
||||
// if (instance.value?.blockchain_status === 'pending') return 'Ожидает подтверждения'
|
||||
// if (instance.value?.blockchain_status === 'blocked') return 'Заблокировано'
|
||||
// return 'Неизвестно'
|
||||
// })
|
||||
|
||||
// Состояние редактирования домена
|
||||
const isEditing = ref(false)
|
||||
const domainValue = ref('')
|
||||
const isDelegatingLoading = ref(false)
|
||||
|
||||
// Загружаем данные кооператива при монтировании
|
||||
const delegationLabel = computed(() => {
|
||||
if (isDelegatingLoading.value) return 'обновляем'
|
||||
return instance.value?.is_delegated ? 'домен делегирован' : 'ожидание делегирования'
|
||||
})
|
||||
|
||||
coop.loadPublicCooperativeData(session.username)
|
||||
|
||||
// Синхронизируем значение домена при изменении данных кооператива
|
||||
watch(() => coop?.publicCooperativeData?.announce, (newAnnounce) => {
|
||||
if (newAnnounce && !isEditing.value) {
|
||||
domainValue.value = newAnnounce
|
||||
}
|
||||
}, { immediate: true })
|
||||
watch(
|
||||
() => coop?.publicCooperativeData?.announce,
|
||||
(newAnnounce) => {
|
||||
if (newAnnounce && !isEditing.value) domainValue.value = newAnnounce
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// Начать редактирование
|
||||
const startEdit = () => {
|
||||
isEditing.value = true
|
||||
domainValue.value = coop?.publicCooperativeData?.announce || ''
|
||||
}
|
||||
|
||||
// Отменить редактирование
|
||||
const cancelEdit = () => {
|
||||
isEditing.value = false
|
||||
domainValue.value = coop?.publicCooperativeData?.announce || ''
|
||||
}
|
||||
|
||||
// Сохранить домен
|
||||
const saveDomain = async () => {
|
||||
if (!domainValue.value.trim()) {
|
||||
FailAlert('Домен не может быть пустым')
|
||||
return
|
||||
}
|
||||
|
||||
if (!coop.publicCooperativeData) {
|
||||
FailAlert('Не удалось получить данные кооператива')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
isDelegatingLoading.value = true
|
||||
|
||||
await updateCoop({
|
||||
coopname: session.username,
|
||||
username: session.username,
|
||||
@@ -176,16 +128,13 @@ const saveDomain = async () => {
|
||||
minimum: coop.publicCooperativeData.minimum,
|
||||
org_initial: coop.publicCooperativeData.org_initial,
|
||||
org_minimum: coop.publicCooperativeData.org_minimum,
|
||||
announce: domainValue.value.trim(), // Обновляем announce (домен)
|
||||
description: coop.publicCooperativeData.description
|
||||
announce: domainValue.value.trim(),
|
||||
description: coop.publicCooperativeData.description,
|
||||
})
|
||||
|
||||
// Перезагружаем данные
|
||||
await coop.loadPublicCooperativeData(session.username)
|
||||
await connectionAgreement.loadCurrentInstance()
|
||||
|
||||
isEditing.value = false
|
||||
SuccessAlert('Домен успешно обновлен')
|
||||
SuccessAlert('Домен успешно обновлён')
|
||||
} catch (error: any) {
|
||||
FailAlert(`Ошибка при обновлении домена: ${error.message}`)
|
||||
} finally {
|
||||
@@ -194,49 +143,33 @@ const saveDomain = async () => {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.domain-card {
|
||||
padding: 8px;
|
||||
|
||||
// Переопределяем отступ ColorCard только для этого виджета
|
||||
:deep(.color-card) {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.domain-header {
|
||||
margin-bottom: 12px;
|
||||
|
||||
.domain-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.domain-display {
|
||||
.domain-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.text-h6 {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Анимация вращения иконки обновления
|
||||
.rotating-icon {
|
||||
animation: rotate 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
<style scoped>
|
||||
.domain-card__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--p-2);
|
||||
}
|
||||
.domain-card__view {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--p-2);
|
||||
padding: var(--p-2) 0;
|
||||
}
|
||||
.domain-card__host {
|
||||
font-size: var(--p-fs-h2);
|
||||
font-weight: 600;
|
||||
color: var(--p-ink);
|
||||
word-break: break-all;
|
||||
flex: 1;
|
||||
}
|
||||
.domain-card__edit-actions {
|
||||
display: flex;
|
||||
gap: var(--p-2);
|
||||
}
|
||||
.domain-card__spin {
|
||||
animation: domain-card-rotate 1.6s linear infinite;
|
||||
}
|
||||
@keyframes domain-card-rotate {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
<template lang="pug">
|
||||
.membership-wallet
|
||||
BaseCard(
|
||||
title="Кошелёк членских взносов"
|
||||
subtitle="Списывается за инфраструктурные подписки кооператива у провайдера. Пополняется конвертацией паевого взноса."
|
||||
)
|
||||
.membership-wallet__metric
|
||||
.membership-wallet__metric-label Доступно
|
||||
.membership-wallet__metric-value.t-mono(v-if="initialLoading") …
|
||||
.membership-wallet__metric-value.t-mono(v-else) {{ formattedBalance }}
|
||||
|
||||
.membership-wallet__actions
|
||||
BaseButton(
|
||||
variant="primary"
|
||||
size="md"
|
||||
type="button"
|
||||
@click="openConvert"
|
||||
)
|
||||
q-icon(name="sync_alt" size="16px").q-mr-xs
|
||||
| Конвертировать в членский
|
||||
|
||||
ConvertToBillingDialog
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, watch } from 'vue'
|
||||
import { BaseButton, BaseCard } from 'src/shared/ui/base'
|
||||
import { useCooperativeMainWallet } from 'src/entities/Wallet/model'
|
||||
import { useSessionStore } from 'src/entities/Session'
|
||||
import { useSystemStore } from 'src/entities/System/model'
|
||||
import { formatAsset2Digits } from 'src/shared/lib/utils/formatAsset2Digits'
|
||||
import {
|
||||
ConvertToBillingDialog,
|
||||
useConvertToBillingVisibility,
|
||||
} from 'src/features/Billing/ConvertToBilling'
|
||||
|
||||
const session = useSessionStore()
|
||||
const system = useSystemStore()
|
||||
|
||||
const { membership, symbol, initialLoading, refresh } = useCooperativeMainWallet(
|
||||
() => system.info.coopname || '',
|
||||
() => session.username || '',
|
||||
)
|
||||
|
||||
const displaySymbol = computed(
|
||||
() => symbol.value || system.info.symbols?.root_govern_symbol || 'RUB',
|
||||
)
|
||||
|
||||
const formattedBalance = computed(() =>
|
||||
formatAsset2Digits(`${membership.value} ${displaySymbol.value}`),
|
||||
)
|
||||
|
||||
const { isVisible } = useConvertToBillingVisibility()
|
||||
const openConvert = () => {
|
||||
isVisible.value = true
|
||||
}
|
||||
watch(isVisible, (open, wasOpen) => {
|
||||
if (wasOpen && !open) void refresh()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.membership-wallet__metric {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding: var(--p-3) 0;
|
||||
}
|
||||
.membership-wallet__metric-label {
|
||||
font-size: var(--p-fs-meta);
|
||||
color: var(--p-ink-2);
|
||||
}
|
||||
.membership-wallet__metric-value {
|
||||
font-size: var(--p-fs-h1);
|
||||
font-weight: 700;
|
||||
color: var(--p-ink);
|
||||
min-height: 32px;
|
||||
}
|
||||
.membership-wallet__actions {
|
||||
display: flex;
|
||||
gap: var(--p-2);
|
||||
}
|
||||
</style>
|
||||
@@ -1,97 +1,68 @@
|
||||
<template lang="pug">
|
||||
.subscriptions-card
|
||||
ColorCard(color='orange', @click.stop)
|
||||
// Заголовок
|
||||
.subscriptions-header
|
||||
.subscriptions-title
|
||||
q-icon(name="subscriptions" size="20px").q-mr-sm
|
||||
| Подписки
|
||||
.subs-card
|
||||
BaseCard(title="Подписки" subtitle="Услуги провайдера, оплачиваемые членскими взносами")
|
||||
Loader(v-if="isLoading" :text="'Загрузка подписок...'")
|
||||
|
||||
// Список подписок
|
||||
.subscriptions-list
|
||||
template(v-if="isLoading")
|
||||
.text-center.q-pa-md
|
||||
q-spinner(color="orange" size="24px")
|
||||
.text-caption.text-grey-7.q-mt-sm Загрузка подписок...
|
||||
template(v-else-if="error")
|
||||
BaseBanner(variant="neg") Ошибка загрузки подписок: {{ error }}
|
||||
|
||||
template(v-else-if="subscriptions.length > 0")
|
||||
q-list(separator)
|
||||
q-item(
|
||||
v-for="subscription in subscriptions"
|
||||
:key="subscription.id"
|
||||
)
|
||||
q-item-section(avatar)
|
||||
q-icon(
|
||||
:name="getSubscriptionIcon(subscription)"
|
||||
:color="getSubscriptionColor(subscription)"
|
||||
size="20px"
|
||||
)
|
||||
template(v-else-if="subscriptions.length")
|
||||
.subs-card__list
|
||||
.subs-card__row(
|
||||
v-for="sub in subscriptions"
|
||||
:key="sub.id"
|
||||
)
|
||||
.subs-card__icon
|
||||
q-icon(
|
||||
:name="getSubscriptionIcon(sub)"
|
||||
size="20px"
|
||||
:class="iconColorClass(sub)"
|
||||
)
|
||||
.subs-card__body
|
||||
.subs-card__title {{ sub.subscription_type_name }}
|
||||
.subs-card__sub {{ sub.subscription_type_description }}
|
||||
.subs-card__price
|
||||
.subs-card__price-value.t-mono {{ formatPrice(sub.price) }}
|
||||
.subs-card__price-period {{ currencySymbol }}/месяц
|
||||
|
||||
q-item-section
|
||||
q-item-label {{ subscription.subscription_type_name }}
|
||||
q-item-label.caption.text-grey-7 {{ subscription.subscription_type_description }}
|
||||
|
||||
q-item-section(side)
|
||||
.text-weight-medium {{ formatPrice(subscription.price) }}
|
||||
.text-caption.text-grey-7 {{ currencySymbol }}/месяц
|
||||
|
||||
template(v-else-if="error")
|
||||
.text-center.q-pa-md
|
||||
.text-negative Ошибка загрузки подписок
|
||||
.text-caption.text-grey-7.q-mt-sm {{ error }}
|
||||
|
||||
template(v-else)
|
||||
.text-center.q-pa-md
|
||||
.text-grey-6 Нет активных подписок
|
||||
.text-caption.text-grey-7.q-mt-sm Подписки появятся после подключения услуг платформы
|
||||
EmptyState(
|
||||
v-else
|
||||
title="Нет активных подписок"
|
||||
body="Подписки появятся после подключения услуг платформы"
|
||||
)
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, computed } from 'vue'
|
||||
import { useProviderSubscriptions } from 'src/features/Provider/model'
|
||||
import { useSystemStore } from 'src/entities/System/model'
|
||||
import { ColorCard } from 'src/shared/ui'
|
||||
import { BaseBanner, BaseCard, EmptyState } from 'src/shared/ui/base'
|
||||
import Loader from 'src/shared/ui/Loader/Loader.vue'
|
||||
import { formatAsset2Digits } from 'src/shared/lib/utils/formatAsset2Digits'
|
||||
|
||||
const {
|
||||
subscriptions,
|
||||
isLoading,
|
||||
error,
|
||||
loadSubscriptions
|
||||
} = useProviderSubscriptions()
|
||||
const { subscriptions, isLoading, error, loadSubscriptions } = useProviderSubscriptions()
|
||||
const { info } = useSystemStore()
|
||||
|
||||
// Загружаем подписки при монтировании
|
||||
onMounted(async () => {
|
||||
await loadSubscriptions()
|
||||
})
|
||||
|
||||
// Форматирование цены
|
||||
const formatPrice = (price: number | string) => {
|
||||
const priceStr = typeof price === 'number' ? price.toString() : price
|
||||
const currencySymbol = info.symbols?.root_govern_symbol || 'AXON'
|
||||
return formatAsset2Digits(`${priceStr} ${currencySymbol}`)
|
||||
const sym = info.symbols?.root_govern_symbol || 'AXON'
|
||||
return formatAsset2Digits(`${priceStr} ${sym}`)
|
||||
}
|
||||
|
||||
// Получение символа валюты для отображения
|
||||
const currencySymbol = computed(() => info.symbols?.root_govern_symbol || 'AXON')
|
||||
|
||||
// Получение иконки для статуса подписки
|
||||
const getSubscriptionIcon = (subscription: any) => {
|
||||
// Если подписка триальная, показываем специальную иконку
|
||||
if (subscription.is_trial) {
|
||||
return 'local_offer'
|
||||
}
|
||||
|
||||
// Для хостинга проверяем specific_data
|
||||
if (subscription.is_trial) return 'local_offer'
|
||||
if (subscription.subscription_type_id === 1) {
|
||||
const specificData = subscription.specific_data
|
||||
if (specificData?.is_valid && specificData?.is_delegated) return 'check_circle'
|
||||
if (specificData?.progress > 0 && specificData?.progress < 100) return 'hourglass_top'
|
||||
const sd = subscription.specific_data
|
||||
if (sd?.is_valid && sd?.is_delegated) return 'check_circle'
|
||||
if (sd?.progress > 0 && sd?.progress < 100) return 'hourglass_top'
|
||||
return 'schedule'
|
||||
}
|
||||
|
||||
// Для других типов подписок проверяем instance_status
|
||||
switch (subscription.instance_status) {
|
||||
case 'active': return 'check_circle'
|
||||
case 'pending': return 'schedule'
|
||||
@@ -102,68 +73,64 @@ const getSubscriptionIcon = (subscription: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Получение цвета для статуса подписки
|
||||
const getSubscriptionColor = (subscription: any) => {
|
||||
// Если подписка триальная, показываем специальный цвет
|
||||
if (subscription.is_trial) {
|
||||
return 'info'
|
||||
}
|
||||
|
||||
// Для хостинга проверяем specific_data
|
||||
const iconColorClass = (subscription: any): string => {
|
||||
if (subscription.is_trial) return 'subs-card__icon--info'
|
||||
if (subscription.subscription_type_id === 1) {
|
||||
const specificData = subscription.specific_data
|
||||
if (specificData?.is_valid && specificData?.is_delegated) return 'positive'
|
||||
if (specificData?.progress > 0 && specificData?.progress < 100) return 'warning'
|
||||
return 'grey'
|
||||
const sd = subscription.specific_data
|
||||
if (sd?.is_valid && sd?.is_delegated) return 'subs-card__icon--pos'
|
||||
if (sd?.progress > 0 && sd?.progress < 100) return 'subs-card__icon--warn'
|
||||
return 'subs-card__icon--mute'
|
||||
}
|
||||
|
||||
// Для других типов подписок проверяем instance_status
|
||||
switch (subscription.instance_status) {
|
||||
case 'active': return 'positive'
|
||||
case 'pending': return 'grey'
|
||||
case 'installing': return 'warning'
|
||||
case 'error': return 'negative'
|
||||
case 'inactive': return 'grey'
|
||||
default: return 'grey'
|
||||
case 'active': return 'subs-card__icon--pos'
|
||||
case 'installing': return 'subs-card__icon--warn'
|
||||
case 'error': return 'subs-card__icon--neg'
|
||||
default: return 'subs-card__icon--mute'
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.subscriptions-card {
|
||||
padding: 8px;
|
||||
|
||||
// Переопределяем отступ ColorCard только для этого виджета
|
||||
:deep(.color-card) {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.subscriptions-header {
|
||||
margin-bottom: 16px;
|
||||
|
||||
.subscriptions-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.subscriptions-list {
|
||||
.q-list {
|
||||
background: transparent;
|
||||
border-radius: 8px;
|
||||
|
||||
.q-item {
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
<style scoped>
|
||||
.subs-card__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.subs-card__row {
|
||||
display: grid;
|
||||
grid-template-columns: 28px 1fr auto;
|
||||
align-items: center;
|
||||
gap: var(--p-3);
|
||||
padding: var(--p-3) 0;
|
||||
border-bottom: 1px solid var(--p-line);
|
||||
}
|
||||
.subs-card__row:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
.subs-card__icon--pos { color: var(--p-pos); }
|
||||
.subs-card__icon--neg { color: var(--p-neg); }
|
||||
.subs-card__icon--warn { color: var(--p-warn); }
|
||||
.subs-card__icon--info { color: var(--p-primary); }
|
||||
.subs-card__icon--mute { color: var(--p-ink-2); }
|
||||
.subs-card__title {
|
||||
font-size: var(--p-fs-body);
|
||||
font-weight: 600;
|
||||
color: var(--p-ink);
|
||||
}
|
||||
.subs-card__sub {
|
||||
font-size: var(--p-fs-meta);
|
||||
color: var(--p-ink-2);
|
||||
margin-top: 2px;
|
||||
}
|
||||
.subs-card__price {
|
||||
text-align: right;
|
||||
}
|
||||
.subs-card__price-value {
|
||||
font-size: var(--p-fs-body);
|
||||
font-weight: 700;
|
||||
color: var(--p-ink);
|
||||
}
|
||||
.subs-card__price-period {
|
||||
font-size: var(--p-fs-meta);
|
||||
color: var(--p-ink-2);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export { default as ConnectionDashboard } from './ConnectionDashboard.vue'
|
||||
export { default as AxonWallet } from './AxonWallet.vue'
|
||||
export { default as MembershipWallet } from './MembershipWallet.vue'
|
||||
export { default as DomainCard } from './DomainCard.vue'
|
||||
export { default as SubscriptionsCard } from './SubscriptionsCard.vue'
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { DraftContract } from 'cooptypes'
|
||||
import { BillingConversionStatement } from '../Templates'
|
||||
import { DocFactory } from '../Factory'
|
||||
import type { IGeneratedDocument, IGenerationOptions, IMetaDocument, ITemplate } from '../Interfaces'
|
||||
import type { MongoDBConnector } from '../Services/Databazor'
|
||||
|
||||
export { BillingConversionStatement as Template } from '../Templates'
|
||||
|
||||
export class Factory extends DocFactory<BillingConversionStatement.Action> {
|
||||
constructor(storage: MongoDBConnector) {
|
||||
super(storage)
|
||||
}
|
||||
|
||||
async generateDocument(data: BillingConversionStatement.Action, options?: IGenerationOptions): Promise<IGeneratedDocument> {
|
||||
let template: ITemplate<BillingConversionStatement.Model>
|
||||
|
||||
if (process.env.SOURCE === 'local') {
|
||||
template = BillingConversionStatement.Template
|
||||
}
|
||||
else {
|
||||
template = await this.getTemplate(DraftContract.contractName.production, BillingConversionStatement.registry_id, data.block_num)
|
||||
}
|
||||
const meta: IMetaDocument = await super.getMeta({ title: template.title, ...data })
|
||||
|
||||
const coop = await super.getCooperative(data.coopname, data.block_num)
|
||||
const vars = await super.getVars(data.coopname, data.block_num)
|
||||
const user = await super.getUser(data.username, data.block_num)
|
||||
const commonUser = super.getCommonUser(user)
|
||||
|
||||
const combinedData: BillingConversionStatement.Model = {
|
||||
meta,
|
||||
coop,
|
||||
vars,
|
||||
commonUser,
|
||||
}
|
||||
|
||||
await super.validate(combinedData, template.model)
|
||||
|
||||
const translation = template.translations[meta.lang]
|
||||
|
||||
const document: IGeneratedDocument = await super.generatePDF('', template.context, combinedData, translation, meta, options?.skip_save)
|
||||
|
||||
return document
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ export * as PrivacyPolicy from './3.PrivacyPolicy'
|
||||
export * as UserAgreement from './4.UserAgreement'
|
||||
export * as CoopenomicsAgreement from './50.CoopenomicsAgreement'
|
||||
export * as ConvertToAxonStatement from './51.ConvertToAxonStatement'
|
||||
export * as BillingConversionStatement from './1095.BillingConversionStatement'
|
||||
export * as SelectBranchStatement from './101.SelectBranchStatement'
|
||||
export * as ParticipantApplication from './100.ParticipantApplication'
|
||||
export * as DecisionOfParticipantApplication from './501.DecisionOfParticipantApplication'
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user