Implement billing functionality and enhance role management
CI / Backend (build + test) (push) Successful in 1m22s
CI / Frontend (lint + typecheck + build) (push) Successful in 33s

- Introduced billing capabilities, allowing users to request payments for subscription periods (3/6/12 months) with admin approval via Telegram.
- Updated role management to include a `BillingEnabled` property, preventing billing for admin roles.
- Enhanced the `CreateRoleCommand` and `UpdateRoleCommand` to accept billing parameters, ensuring proper handling during role creation and updates.
- Added new endpoints for billing management and integrated billing checks into VPN config creation to enforce payment requirements.
- Updated related services, models, and tests to support the new billing features, ensuring comprehensive coverage and functionality.
- Enhanced documentation to reflect the new billing processes and role management changes.
This commit is contained in:
Leonid Pershin
2026-07-19 01:38:16 +03:00
parent b980dc6cef
commit b2ae358250
106 changed files with 6018 additions and 66 deletions
+1
View File
@@ -3360,6 +3360,7 @@ export interface components {
/** Format: int32 */
maxIpLimit: number | string;
isSystem: boolean;
billingEnabled: boolean;
};
StatsDto: {
/** Format: int32 */
+43
View File
@@ -173,6 +173,7 @@ export type RoleDto = {
maxConfigs: number
maxIpLimit: number
isSystem: boolean
billingEnabled: boolean
}
/** Глобальная справочная цена за конфиг (видна только админу) — одна на весь сервис, не per-роль.
@@ -183,6 +184,48 @@ export type PricingSettingsDto = {
pricePerConfigPerYear: number | null
}
export type PaymentPeriod = 'Quarter' | 'HalfYear' | 'Year'
export type PaymentRequestStatus =
| 'AwaitingPayment'
| 'AwaitingConfirmation'
| 'Confirmed'
| 'Rejected'
| 'Cancelled'
export type PaymentRequestDto = {
id: string
period: PaymentPeriod
amountSnapshot: number
status: PaymentRequestStatus
createdAt: string
}
export type BillingStatusDto = {
billingEnabled: boolean
paidUntil: string | null
suspended: boolean
requisitesText: string
activeRequest: PaymentRequestDto | null
}
/** Реквизиты + грейс-период для новых billing-пользователей (см. BillingSettings.DefaultGraceDays). */
export type BillingSettingsDto = {
requisitesText: string
graceDays: number
/** Начальное состояние чекбокса "Включить биллинг" в диалоге создания новой роли. */
defaultBillingEnabledForNewRoles: boolean
}
export type AdminPaymentRequestDto = {
id: string
userId: string
userName: string
period: PaymentPeriod
amountSnapshot: number
status: PaymentRequestStatus
createdAt: string
}
export type ActivationRequestAdminDto = {
id: string
userId: string
+134
View File
@@ -50,6 +50,7 @@ const resources = {
instructions: 'Инструкции',
news: 'Новости',
support: 'Поддержка',
billing: 'Оплата',
settings: 'Настройки',
admin: 'Админка',
logout: 'Выйти',
@@ -100,6 +101,40 @@ const resources = {
},
},
billing: {
title: 'Оплата подписки',
notApplicable: 'Биллинг не применяется к вашей роли.',
status: 'Статус оплаты',
suspended: 'Приостановлено',
paidUntil: 'Оплачено до {{date}}',
neverPaid: 'Оплата ещё не производилась.',
newRequest: 'Оформить оплату',
period: 'Период',
periods: {
Quarter: '3 месяца',
HalfYear: 'Полгода',
Year: 'Год',
},
createRequest: 'Создать заявку',
requestCreated: 'Заявка создана.',
activeRequest: 'Заявка на оплату',
requestStatus: {
AwaitingPayment: 'Ожидает оплаты',
AwaitingConfirmation: 'На проверке у администратора',
},
requisites: 'Реквизиты для оплаты',
requisitesMissing: 'Реквизиты ещё не настроены администратором.',
sendToTelegram: 'Отправить в Telegram',
requisitesSent: 'Реквизиты отправлены в Telegram.',
telegramNotLinked: 'Сначала привяжите Telegram в настройках.',
iPaid: 'Я оплатил',
markedPaid: 'Отмечено — администратор проверит оплату.',
cancelRequest: 'Отменить',
confirmCancel: 'Отменить заявку на оплату?',
requestCancelled: 'Заявка отменена.',
awaitingAdminHint: 'Администратор уведомлён и проверит оплату. Конфиги не отключатся, пока заявка не решена.',
},
instructions: {
title: 'Инструкции по подключению',
appsTitle: 'Приложения',
@@ -197,6 +232,7 @@ const resources = {
configs: 'Все конфиги',
roles: 'Роли',
pricing: 'Цены',
billing: 'Оплата',
nodes: 'Ноды',
apps: 'Приложения',
instructions: 'Инструкции',
@@ -271,6 +307,8 @@ const resources = {
created: 'Роль создана.',
updated: 'Квота обновлена.',
deleted: 'Роль удалена.',
billingEnabledLabel: 'Включить биллинг для этой роли',
billingBadge: 'биллинг',
},
pricing: {
title: 'Справочная цена конфига',
@@ -285,6 +323,35 @@ const resources = {
yearCheaperThanQuarter: 'Цена за год (в пересчёте на 12 месяцев) не может быть меньше цены за 3 месяца.',
updated: 'Цена обновлена.',
},
billing: {
settingsTitle: 'Настройки биллинга',
requisitesText: 'Реквизиты для оплаты',
requisitesTextHint: 'Показываются пользователю вместе с заявкой на оплату.',
graceDays: 'Грейс-период (дней)',
graceDaysHint: 'Сколько дней доступ активен после назначения billing-роли, пока не поступит первая оплата.',
defaultBillingEnabledForNewRolesLabel: 'Новые роли по умолчанию с включённым биллингом',
settingsUpdated: 'Настройки биллинга обновлены.',
requestsTitle: 'Заявки на оплату',
allStatuses: 'Все статусы',
user: 'Пользователь',
period: 'Период',
amount: 'Сумма',
statusLabel: 'Статус',
created: 'Создана',
empty: 'Заявок не найдено.',
confirm: 'Подтвердить',
reject: 'Отклонить',
confirmed: 'Оплата подтверждена.',
rejected: 'Заявка отклонена.',
rejectReasonPrompt: 'Причина отклонения (необязательно):',
status: {
AwaitingPayment: 'Ожидает оплаты',
AwaitingConfirmation: 'На проверке',
Confirmed: 'Подтверждена',
Rejected: 'Отклонена',
Cancelled: 'Отменена',
},
},
nodes: {
create: 'Добавить ноду',
name: 'Название',
@@ -492,6 +559,7 @@ const resources = {
instructions: 'Instructions',
news: 'News',
support: 'Support',
billing: 'Billing',
settings: 'Settings',
admin: 'Admin',
logout: 'Log out',
@@ -542,6 +610,40 @@ const resources = {
},
},
billing: {
title: 'Subscription billing',
notApplicable: 'Billing does not apply to your role.',
status: 'Payment status',
suspended: 'Suspended',
paidUntil: 'Paid until {{date}}',
neverPaid: 'No payment has been made yet.',
newRequest: 'Set up payment',
period: 'Period',
periods: {
Quarter: '3 months',
HalfYear: 'Half a year',
Year: 'A year',
},
createRequest: 'Create request',
requestCreated: 'Request created.',
activeRequest: 'Payment request',
requestStatus: {
AwaitingPayment: 'Awaiting payment',
AwaitingConfirmation: 'Under admin review',
},
requisites: 'Payment details',
requisitesMissing: 'The administrator has not set up payment details yet.',
sendToTelegram: 'Send to Telegram',
requisitesSent: 'Payment details sent to Telegram.',
telegramNotLinked: 'Link Telegram in settings first.',
iPaid: 'I paid',
markedPaid: 'Marked — the administrator will verify the payment.',
cancelRequest: 'Cancel',
confirmCancel: 'Cancel this payment request?',
requestCancelled: 'Request cancelled.',
awaitingAdminHint: 'The administrator has been notified and will verify the payment. Configs stay active until the request is decided.',
},
instructions: {
title: 'Connection instructions',
appsTitle: 'Apps',
@@ -639,6 +741,7 @@ const resources = {
configs: 'All configs',
roles: 'Roles',
pricing: 'Pricing',
billing: 'Billing',
nodes: 'Nodes',
apps: 'Apps',
instructions: 'Instructions',
@@ -713,6 +816,8 @@ const resources = {
created: 'Role created.',
updated: 'Quota updated.',
deleted: 'Role deleted.',
billingEnabledLabel: 'Enable billing for this role',
billingBadge: 'billing',
},
pricing: {
title: 'Reference config price',
@@ -727,6 +832,35 @@ const resources = {
yearCheaperThanQuarter: 'The annual price (over 12 months) cannot be lower than the 3-month price.',
updated: 'Pricing updated.',
},
billing: {
settingsTitle: 'Billing settings',
requisitesText: 'Payment details',
requisitesTextHint: 'Shown to the user alongside their payment request.',
graceDays: 'Grace period (days)',
graceDaysHint: 'How many days access stays active after a billing role is assigned, before the first payment is due.',
defaultBillingEnabledForNewRolesLabel: 'New roles default to billing enabled',
settingsUpdated: 'Billing settings updated.',
requestsTitle: 'Payment requests',
allStatuses: 'All statuses',
user: 'User',
period: 'Period',
amount: 'Amount',
statusLabel: 'Status',
created: 'Created',
empty: 'No requests found.',
confirm: 'Confirm',
reject: 'Reject',
confirmed: 'Payment confirmed.',
rejected: 'Request rejected.',
rejectReasonPrompt: 'Rejection reason (optional):',
status: {
AwaitingPayment: 'Awaiting payment',
AwaitingConfirmation: 'Under review',
Confirmed: 'Confirmed',
Rejected: 'Rejected',
Cancelled: 'Cancelled',
},
},
nodes: {
create: 'Add node',
name: 'Name',