Enhance user plan management and update related endpoints
- Added new configuration options for user plans in `.env.example`, including `Plans__MaxCustomConfigCount` and `Plans__MinCustomConfigCount`. - Introduced `MapPlanEndpoints` in `Program.cs` to handle plan-related API routes. - Implemented `SetUserPlan` endpoint in `RoleEndpoints` to allow admins to assign plans to users. - Removed deprecated role request approval endpoints from `AdminSupportEndpoints`. - Updated `ITelegramNotifier` and related classes to reflect changes in role request handling and payment notifications. - Refactored role management commands to remove `MaxConfigs` and focus on `MaxIpLimit` and billing settings. - Enhanced billing request handling to accommodate plan changes instead of role changes. - Updated various interfaces and command handlers to support new plan management features.
This commit is contained in:
1 parent
2c5b730500
commit
fad03c2834
152 files changed
+4060
-2240
No files matched your search
@@ -93,7 +93,8 @@ export type MySubscriptionDto = {
|
||||
|
||||
export type GetMyConfigsResult = {
|
||||
configs: VpnConfigDto[]
|
||||
maxConfigs: number
|
||||
configQuota: number
|
||||
planId: string | null
|
||||
}
|
||||
|
||||
export type ClientAppDto = {
|
||||
@@ -175,13 +176,40 @@ export type UserSummaryDto = {
|
||||
export type RoleDto = {
|
||||
id: string
|
||||
name: string
|
||||
maxConfigs: number
|
||||
maxIpLimit: number
|
||||
isSystem: boolean
|
||||
billingEnabled: boolean
|
||||
}
|
||||
|
||||
/** Одна ступень скидочной лесенки за объём: роль с квотой maxConfigs >= minConfigs получает скидку
|
||||
export type PlanDto = {
|
||||
id: string
|
||||
name: string
|
||||
configCount: number
|
||||
}
|
||||
|
||||
export type AdminPlanDto = {
|
||||
id: string
|
||||
name: string
|
||||
configCount: number
|
||||
sortOrder: number
|
||||
isEnabled: boolean
|
||||
}
|
||||
|
||||
export type MyPlanStatusDto = {
|
||||
configQuota: number
|
||||
planId: string | null
|
||||
activeConfigCount: number
|
||||
billingEnabled: boolean
|
||||
billingPaidUntil: string | null
|
||||
}
|
||||
|
||||
export type ChangePlanResultDto = {
|
||||
configQuota: number
|
||||
planId: string | null
|
||||
topUpAmount: number | null
|
||||
}
|
||||
|
||||
/** Одна ступень скидочной лесенки за объём: тариф с количеством конфигов >= minConfigs получает скидку
|
||||
* discountPercent от итоговой цены периода. Действует наивысший подходящий порог (не суммируется). */
|
||||
export type DiscountTierDto = {
|
||||
minConfigs: number
|
||||
@@ -205,9 +233,9 @@ export type PaymentRequestStatus =
|
||||
| 'Confirmed'
|
||||
| 'Rejected'
|
||||
| 'Cancelled'
|
||||
/** Subscription — оплата за период (period задан). RoleChangeTopUp — доплата разницы в цене при
|
||||
* апгрейде роли с активным оплаченным периодом (period null, не продлевает paidUntil). */
|
||||
export type PaymentRequestKind = 'Subscription' | 'RoleChangeTopUp'
|
||||
/** Subscription — оплата за период (period задан). PlanChangeTopUp — доплата разницы в цене при
|
||||
* увеличении тарифа с активным оплаченным периодом (period null, не продлевает paidUntil). */
|
||||
export type PaymentRequestKind = 'Subscription' | 'PlanChangeTopUp'
|
||||
|
||||
export type PaymentRequestDto = {
|
||||
id: string
|
||||
@@ -328,7 +356,7 @@ export type AuditLogDto = {
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export type TicketType = 'BugReport' | 'RoleRequest' | 'ExtensionRequest'
|
||||
export type TicketType = 'BugReport' | 'ExtensionRequest'
|
||||
export type TicketStatus = 'Open' | 'Resolved' | 'Closed'
|
||||
|
||||
export type TicketAttachmentDto = {
|
||||
@@ -369,11 +397,6 @@ export type TicketDetailDto = {
|
||||
userName: string
|
||||
type: TicketType
|
||||
status: TicketStatus
|
||||
requestedRoleId: string | null
|
||||
requestedRoleName: string | null
|
||||
proposedRoleName: string | null
|
||||
proposedMaxConfigs: number | null
|
||||
proposedMaxIpLimit: number | null
|
||||
requestedDays: number | null
|
||||
createdAt: string
|
||||
comments: TicketCommentDto[]
|
||||
|
||||
@@ -81,7 +81,7 @@ const resources = {
|
||||
billingRequiredNotice: 'Оплата подписки истекла — создание новых конфигов недоступно, пока не продлите доступ.',
|
||||
goToBilling: 'Перейти к оплате',
|
||||
created: 'Конфиг создан.',
|
||||
quotaExceeded: 'Достигнут лимит конфигов для вашей роли.',
|
||||
quotaExceeded: 'Достигнут лимит конфигов для вашего тарифа.',
|
||||
showLink: 'Ссылка / QR',
|
||||
rotate: 'Перевыпустить',
|
||||
rotated: 'Конфиг перевыпущен.',
|
||||
@@ -103,6 +103,26 @@ const resources = {
|
||||
},
|
||||
},
|
||||
|
||||
plan: {
|
||||
title: 'Тариф',
|
||||
changePlan: 'Изменить тариф',
|
||||
current: 'Текущая квота: {{count}} конфигов',
|
||||
currentUnlimited: 'Текущая квота: без лимита',
|
||||
custom: 'Своё количество',
|
||||
customConfigCount: 'Количество конфигов',
|
||||
submit: 'Применить',
|
||||
changed: 'Тариф изменён.',
|
||||
downgradeNotice: 'Новая квота меньше текущего числа активных конфигов — выберите, какие отозвать:',
|
||||
selectExactly: 'Нужно выбрать ровно {{count}} конфиг(ов) для отзыва.',
|
||||
topUpCreated: 'Тариф увеличен. Создана доплата за оставшийся оплаченный период.',
|
||||
goToBilling: 'Перейти к оплате',
|
||||
mustSelectConfigs: 'Выберите конфиги для отзыва.',
|
||||
perQuarter: '3 мес: {{price}}',
|
||||
perHalfYear: 'Полгода: {{price}}',
|
||||
perYear: 'Год: {{price}}',
|
||||
noPrice: '—',
|
||||
},
|
||||
|
||||
billing: {
|
||||
title: 'Оплата подписки',
|
||||
notApplicable: 'Биллинг не применяется к вашей роли.',
|
||||
@@ -147,8 +167,8 @@ const resources = {
|
||||
confirmCancel: 'Отменить заявку на оплату?',
|
||||
requestCancelled: 'Заявка отменена.',
|
||||
awaitingAdminHint: 'Администратор уведомлён и проверит оплату. Конфиги не отключатся, пока заявка не решена.',
|
||||
roleChangeTopUp: 'Доплата за смену роли',
|
||||
roleChangeTopUpHint: 'Новая роль дороже прежней — эта сумма покрывает разницу в цене за оставшуюся часть уже оплаченного периода, срок подписки при этом не меняется.',
|
||||
planChangeTopUp: 'Доплата за смену тарифа',
|
||||
planChangeTopUpHint: 'Новый тариф дороже прежнего — эта сумма покрывает разницу в цене за оставшуюся часть уже оплаченного периода, срок подписки при этом не меняется.',
|
||||
subscription: 'Подписка',
|
||||
},
|
||||
|
||||
@@ -175,7 +195,6 @@ const resources = {
|
||||
title: 'Поддержка',
|
||||
empty: 'У вас пока нет обращений.',
|
||||
reportBug: 'Сообщить об ошибке',
|
||||
requestRole: 'Запросить роль',
|
||||
requestExtension: 'Попросить о продлении',
|
||||
requestedDaysLabel: 'Сколько дней нужно',
|
||||
requestedExtension: 'Запрошено продление на {{days}} дн.',
|
||||
@@ -184,32 +203,15 @@ const resources = {
|
||||
messageLabel: 'Опишите проблему или предложение',
|
||||
attachmentsLabel: 'Скриншоты (необязательно, до 5)',
|
||||
filesSelected: '{{count}} файл(ов) выбрано',
|
||||
existingRole: 'Существующая роль',
|
||||
newRole: 'Новая роль',
|
||||
selectRole: 'Выберите роль',
|
||||
roleOption: '{{name}} — конфигов: {{configs}}, IP: {{ip}}',
|
||||
newRoleName: 'Название роли',
|
||||
newRoleMaxConfigs: 'Количество конфигов (-1 — без лимита)',
|
||||
newRoleMaxIpLimit: 'Количество IP (-1 — без лимита)',
|
||||
pricingTitle: 'Ориентировочная стоимость',
|
||||
pricingQuarter: '3 месяца: {{price}}',
|
||||
pricingHalfYear: 'Полгода: {{price}}',
|
||||
pricingYear: 'Год: {{price}}',
|
||||
pricingDisclaimer: 'Цены на данный момент ознакомительные.',
|
||||
pricingDiscounted: '{{price}} ₽ (вместо {{original}} ₽, скидка {{percent}}%)',
|
||||
justification: 'Обоснование',
|
||||
ticketCreated: 'Обращение отправлено.',
|
||||
roleRequestPending: 'У вас уже есть необработанная заявка на роль.',
|
||||
reply: 'Ответить',
|
||||
replyPlaceholder: 'Написать комментарий…',
|
||||
reopen: 'Переоткрыть',
|
||||
reopened: 'Тикет переоткрыт.',
|
||||
lastActivity: 'Последняя активность: {{date}}',
|
||||
requestedExistingRole: 'Запрошена роль: {{role}}',
|
||||
requestedNewRole: 'Запрошена новая роль «{{name}}» (конфигов: {{configs}}, IP: {{ip}})',
|
||||
type: {
|
||||
BugReport: 'Ошибка/предложение',
|
||||
RoleRequest: 'Заявка на роль',
|
||||
ExtensionRequest: 'Заявка на продление',
|
||||
},
|
||||
status: {
|
||||
@@ -254,6 +256,7 @@ const resources = {
|
||||
users: 'Пользователи',
|
||||
configs: 'Все конфиги',
|
||||
roles: 'Роли',
|
||||
plans: 'Тарифы',
|
||||
pricing: 'Цены',
|
||||
billing: 'Оплата',
|
||||
nodes: 'Ноды',
|
||||
@@ -331,25 +334,39 @@ const resources = {
|
||||
roles: {
|
||||
create: 'Создать роль',
|
||||
name: 'Название',
|
||||
maxConfigs: 'Квота конфигов',
|
||||
maxConfigsHint: '−1 = без лимита.',
|
||||
maxIpLimit: 'Лимит IP на конфиг',
|
||||
maxIpLimitHint: '−1 = без лимита. Применяется только к новым конфигам.',
|
||||
totalPerQuarter: 'Итого / 3 мес',
|
||||
totalHalfYear: 'Итого / полгода',
|
||||
totalPerYear: 'Итого / год',
|
||||
noPrice: '—',
|
||||
system: 'системная',
|
||||
edit: 'Изменить',
|
||||
save: 'Сохранить',
|
||||
delete: 'Удалить',
|
||||
confirmDelete: 'Удалить роль? Это действие необратимо.',
|
||||
created: 'Роль создана.',
|
||||
updated: 'Квота обновлена.',
|
||||
updated: 'Роль обновлена.',
|
||||
deleted: 'Роль удалена.',
|
||||
billingEnabledLabel: 'Включить биллинг для этой роли',
|
||||
billingBadge: 'биллинг',
|
||||
},
|
||||
plans: {
|
||||
create: 'Создать тариф',
|
||||
name: 'Название',
|
||||
configCount: 'Количество конфигов',
|
||||
sortOrder: 'Порядок сортировки',
|
||||
enabled: 'Включён',
|
||||
totalPerQuarter: 'Итого / 3 мес',
|
||||
totalHalfYear: 'Итого / полгода',
|
||||
totalPerYear: 'Итого / год',
|
||||
noPrice: '—',
|
||||
edit: 'Изменить',
|
||||
save: 'Сохранить',
|
||||
delete: 'Удалить',
|
||||
confirmDelete: 'Удалить тариф? Это действие необратимо.',
|
||||
created: 'Тариф создан.',
|
||||
updated: 'Тариф обновлён.',
|
||||
deleted: 'Тариф удалён.',
|
||||
empty: 'Тарифы пока не созданы.',
|
||||
disabled: 'выключен',
|
||||
},
|
||||
pricing: {
|
||||
title: 'Справочная цена конфига',
|
||||
pricePerConfigPerQuarter: 'Цена за конфиг в месяц / оплата раз в 3 мес',
|
||||
@@ -651,7 +668,7 @@ const resources = {
|
||||
billingRequiredNotice: 'Your subscription has expired — creating new configs is unavailable until you renew.',
|
||||
goToBilling: 'Go to payment',
|
||||
created: 'Config created.',
|
||||
quotaExceeded: 'Config quota reached for your role.',
|
||||
quotaExceeded: 'Config quota reached for your plan.',
|
||||
showLink: 'Link / QR',
|
||||
rotate: 'Rotate',
|
||||
rotated: 'Config rotated.',
|
||||
@@ -673,6 +690,26 @@ const resources = {
|
||||
},
|
||||
},
|
||||
|
||||
plan: {
|
||||
title: 'Plan',
|
||||
changePlan: 'Change plan',
|
||||
current: 'Current quota: {{count}} configs',
|
||||
currentUnlimited: 'Current quota: unlimited',
|
||||
custom: 'Custom amount',
|
||||
customConfigCount: 'Number of configs',
|
||||
submit: 'Apply',
|
||||
changed: 'Plan changed.',
|
||||
downgradeNotice: 'The new quota is lower than your current number of active configs — choose which to revoke:',
|
||||
selectExactly: 'Select exactly {{count}} config(s) to revoke.',
|
||||
topUpCreated: 'Plan increased. A top-up was created for the remaining paid period.',
|
||||
goToBilling: 'Go to payment',
|
||||
mustSelectConfigs: 'Select configs to revoke.',
|
||||
perQuarter: '3 months: {{price}}',
|
||||
perHalfYear: '6 months: {{price}}',
|
||||
perYear: 'Year: {{price}}',
|
||||
noPrice: '—',
|
||||
},
|
||||
|
||||
billing: {
|
||||
title: 'Subscription billing',
|
||||
notApplicable: 'Billing does not apply to your role.',
|
||||
@@ -717,8 +754,8 @@ const resources = {
|
||||
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.',
|
||||
roleChangeTopUp: 'Role change top-up',
|
||||
roleChangeTopUpHint: "Your new role costs more than the old one — this amount covers the price difference for the remaining part of your already-paid period; your subscription end date doesn't change.",
|
||||
planChangeTopUp: 'Plan change top-up',
|
||||
planChangeTopUpHint: "Your new plan costs more than the old one — this amount covers the price difference for the remaining part of your already-paid period; your subscription end date doesn't change.",
|
||||
subscription: 'Subscription',
|
||||
},
|
||||
|
||||
@@ -745,7 +782,6 @@ const resources = {
|
||||
title: 'Support',
|
||||
empty: 'You have no tickets yet.',
|
||||
reportBug: 'Report a bug',
|
||||
requestRole: 'Request a role',
|
||||
requestExtension: 'Request an extension',
|
||||
requestedDaysLabel: 'How many days you need',
|
||||
requestedExtension: 'Requested a {{days}}-day extension',
|
||||
@@ -754,32 +790,15 @@ const resources = {
|
||||
messageLabel: 'Describe the issue or suggestion',
|
||||
attachmentsLabel: 'Screenshots (optional, up to 5)',
|
||||
filesSelected: '{{count}} file(s) selected',
|
||||
existingRole: 'Existing role',
|
||||
newRole: 'New role',
|
||||
selectRole: 'Select a role',
|
||||
roleOption: '{{name}} — configs: {{configs}}, IP: {{ip}}',
|
||||
newRoleName: 'Role name',
|
||||
newRoleMaxConfigs: 'Max configs (-1 = unlimited)',
|
||||
newRoleMaxIpLimit: 'Max IPs (-1 = unlimited)',
|
||||
pricingTitle: 'Estimated cost',
|
||||
pricingQuarter: '3 months: {{price}}',
|
||||
pricingHalfYear: '6 months: {{price}}',
|
||||
pricingYear: 'Year: {{price}}',
|
||||
pricingDisclaimer: 'Prices are indicative only at this time.',
|
||||
pricingDiscounted: '{{price}} ₽ (instead of {{original}} ₽, {{percent}}% off)',
|
||||
justification: 'Justification',
|
||||
ticketCreated: 'Ticket submitted.',
|
||||
roleRequestPending: 'You already have a pending role request.',
|
||||
reply: 'Reply',
|
||||
replyPlaceholder: 'Write a comment…',
|
||||
reopen: 'Reopen',
|
||||
reopened: 'Ticket reopened.',
|
||||
lastActivity: 'Last activity: {{date}}',
|
||||
requestedExistingRole: 'Requested role: {{role}}',
|
||||
requestedNewRole: 'Requested new role "{{name}}" (configs: {{configs}}, IPs: {{ip}})',
|
||||
type: {
|
||||
BugReport: 'Bug/suggestion',
|
||||
RoleRequest: 'Role request',
|
||||
ExtensionRequest: 'Extension request',
|
||||
},
|
||||
status: {
|
||||
@@ -824,6 +843,7 @@ const resources = {
|
||||
users: 'Users',
|
||||
configs: 'All configs',
|
||||
roles: 'Roles',
|
||||
plans: 'Plans',
|
||||
pricing: 'Pricing',
|
||||
billing: 'Billing',
|
||||
nodes: 'Nodes',
|
||||
@@ -901,25 +921,39 @@ const resources = {
|
||||
roles: {
|
||||
create: 'Create role',
|
||||
name: 'Name',
|
||||
maxConfigs: 'Config quota',
|
||||
maxConfigsHint: '−1 = unlimited.',
|
||||
maxIpLimit: 'IP limit per config',
|
||||
maxIpLimitHint: '−1 = unlimited. Applies to new configs only.',
|
||||
totalPerQuarter: 'Total / 3 months',
|
||||
totalHalfYear: 'Total / 6 months',
|
||||
totalPerYear: 'Total / year',
|
||||
noPrice: '—',
|
||||
system: 'system',
|
||||
edit: 'Edit',
|
||||
save: 'Save',
|
||||
delete: 'Delete',
|
||||
confirmDelete: 'Delete this role? This cannot be undone.',
|
||||
created: 'Role created.',
|
||||
updated: 'Quota updated.',
|
||||
updated: 'Role updated.',
|
||||
deleted: 'Role deleted.',
|
||||
billingEnabledLabel: 'Enable billing for this role',
|
||||
billingBadge: 'billing',
|
||||
},
|
||||
plans: {
|
||||
create: 'Create plan',
|
||||
name: 'Name',
|
||||
configCount: 'Number of configs',
|
||||
sortOrder: 'Sort order',
|
||||
enabled: 'Enabled',
|
||||
totalPerQuarter: 'Total / 3 months',
|
||||
totalHalfYear: 'Total / 6 months',
|
||||
totalPerYear: 'Total / year',
|
||||
noPrice: '—',
|
||||
edit: 'Edit',
|
||||
save: 'Save',
|
||||
delete: 'Delete',
|
||||
confirmDelete: 'Delete this plan? This cannot be undone.',
|
||||
created: 'Plan created.',
|
||||
updated: 'Plan updated.',
|
||||
deleted: 'Plan deleted.',
|
||||
empty: 'No plans created yet.',
|
||||
disabled: 'disabled',
|
||||
},
|
||||
pricing: {
|
||||
title: 'Reference config price',
|
||||
pricePerConfigPerQuarter: 'Price per config per month / billed every 3 months',
|
||||
|
||||
Reference in new issue
Block a user