Implement billing functionality and enhance role management
- 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:
@@ -0,0 +1,153 @@
|
||||
import { useState } from 'react'
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from '@/shared/ui/toast-store'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/shared/ui/card'
|
||||
import { Badge } from '@/shared/ui/badge'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
import { Label } from '@/shared/ui/label'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/ui/select'
|
||||
import { HttpError } from '@/shared/api/client'
|
||||
import type { BillingStatusDto, PaymentPeriod } from '@/shared/api/types'
|
||||
import { cancelPaymentRequest, createPaymentRequest, markPaymentSent, sendRequisitesToTelegram } from './api'
|
||||
|
||||
const PERIODS: PaymentPeriod[] = ['Quarter', 'HalfYear', 'Year']
|
||||
|
||||
export function PaymentRequestPanel({ status }: { status: BillingStatusDto }) {
|
||||
const { t } = useTranslation()
|
||||
const queryClient = useQueryClient()
|
||||
const [period, setPeriod] = useState<PaymentPeriod>('Quarter')
|
||||
|
||||
const invalidate = () => queryClient.invalidateQueries({ queryKey: ['my-billing-status'] })
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: () => createPaymentRequest(period),
|
||||
onSuccess: async () => {
|
||||
toast.success(t('billing.requestCreated'))
|
||||
await invalidate()
|
||||
},
|
||||
onError: (error) => toast.error(error instanceof HttpError ? error.detail : t('auth.genericError')),
|
||||
})
|
||||
|
||||
const cancelMutation = useMutation({
|
||||
mutationFn: (id: string) => cancelPaymentRequest(id),
|
||||
onSuccess: async () => {
|
||||
toast.success(t('billing.requestCancelled'))
|
||||
await invalidate()
|
||||
},
|
||||
onError: (error) => toast.error(error instanceof HttpError ? error.detail : t('auth.genericError')),
|
||||
})
|
||||
|
||||
const markPaidMutation = useMutation({
|
||||
mutationFn: (id: string) => markPaymentSent(id),
|
||||
onSuccess: async () => {
|
||||
toast.success(t('billing.markedPaid'))
|
||||
await invalidate()
|
||||
},
|
||||
onError: (error) => toast.error(error instanceof HttpError ? error.detail : t('auth.genericError')),
|
||||
})
|
||||
|
||||
const sendToTelegramMutation = useMutation({
|
||||
mutationFn: (id: string) => sendRequisitesToTelegram(id),
|
||||
onSuccess: () => toast.success(t('billing.requisitesSent')),
|
||||
onError: (error) => {
|
||||
const message =
|
||||
error instanceof HttpError && error.status === 409 ? t('billing.telegramNotLinked') : t('auth.genericError')
|
||||
toast.error(message)
|
||||
},
|
||||
})
|
||||
|
||||
const request = status.activeRequest
|
||||
|
||||
if (!request) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">{t('billing.newRequest')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label>{t('billing.period')}</Label>
|
||||
<Select value={period} onValueChange={(v) => setPeriod(v as PaymentPeriod)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{PERIODS.map((p) => (
|
||||
<SelectItem key={p} value={p}>
|
||||
{t(`billing.periods.${p}`)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Button onClick={() => createMutation.mutate()} disabled={createMutation.isPending}>
|
||||
{t('billing.createRequest')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const isAwaitingConfirmation = request.status === 'AwaitingConfirmation'
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex-row items-center justify-between gap-2 space-y-0">
|
||||
<CardTitle className="text-base">{t('billing.activeRequest')}</CardTitle>
|
||||
<Badge variant={isAwaitingConfirmation ? 'warning' : 'outline'}>
|
||||
{t(`billing.requestStatus.${request.status}`)}
|
||||
</Badge>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<p className="text-sm">
|
||||
{t(`billing.periods.${request.period}`)} — <span className="font-medium">{request.amountSnapshot} ₽</span>
|
||||
</p>
|
||||
|
||||
{!isAwaitingConfirmation && (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label>{t('billing.requisites')}</Label>
|
||||
<p className="whitespace-pre-wrap rounded-md border border-border bg-muted px-3 py-2 text-sm">
|
||||
{status.requisitesText || t('billing.requisitesMissing')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => sendToTelegramMutation.mutate(request.id)}
|
||||
disabled={sendToTelegramMutation.isPending}
|
||||
>
|
||||
{t('billing.sendToTelegram')}
|
||||
</Button>
|
||||
{!isAwaitingConfirmation && (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => markPaidMutation.mutate(request.id)}
|
||||
disabled={markPaidMutation.isPending}
|
||||
>
|
||||
{t('billing.iPaid')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
if (confirm(t('billing.confirmCancel'))) cancelMutation.mutate(request.id)
|
||||
}}
|
||||
disabled={cancelMutation.isPending}
|
||||
>
|
||||
{t('billing.cancelRequest')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{isAwaitingConfirmation && <p className="text-xs text-muted-foreground">{t('billing.awaitingAdminHint')}</p>}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user