mirror of
https://github.com/vernu/textbee.git
synced 2026-05-18 21:35:12 -04:00
fix billing issues and improve ui
This commit is contained in:
@@ -19,11 +19,10 @@ export class BillingController {
|
||||
return this.billingService.getPlans()
|
||||
}
|
||||
|
||||
|
||||
@Get('current-plan')
|
||||
@Get('current-subscription')
|
||||
@UseGuards(AuthGuard)
|
||||
async getCurrentPlan(@Request() req: any) {
|
||||
return this.billingService.getCurrentPlan(req.user)
|
||||
async getCurrentSubscription(@Request() req: any) {
|
||||
return this.billingService.getCurrentSubscription(req.user)
|
||||
}
|
||||
|
||||
@Post('checkout')
|
||||
|
||||
@@ -43,11 +43,13 @@ export class BillingService {
|
||||
})
|
||||
}
|
||||
|
||||
async getCurrentPlan(user: any) {
|
||||
const subscription = await this.subscriptionModel.findOne({
|
||||
user: user._id,
|
||||
isActive: true,
|
||||
})
|
||||
async getCurrentSubscription(user: any) {
|
||||
const subscription = await this.subscriptionModel
|
||||
.findOne({
|
||||
user: user._id,
|
||||
isActive: true,
|
||||
})
|
||||
.populate('plan')
|
||||
|
||||
let plan = null
|
||||
|
||||
@@ -240,7 +242,7 @@ export class BillingService {
|
||||
// Deactivate current active subscriptions
|
||||
const result = await this.subscriptionModel.updateMany(
|
||||
{ user: userObjectId, plan: { $ne: plan._id }, isActive: true },
|
||||
{ isActive: false, endDate: new Date() },
|
||||
{ isActive: false, subscriptionEndDate: new Date() },
|
||||
)
|
||||
console.log(`Deactivated subscriptions: ${result.modifiedCount}`)
|
||||
|
||||
|
||||
@@ -32,18 +32,6 @@ export class Plan {
|
||||
@Prop({ type: String, unique: true })
|
||||
polarYearlyProductId?: string
|
||||
|
||||
@Prop({ type: Date })
|
||||
subscriptionStartDate?: Date
|
||||
|
||||
@Prop({ type: Date })
|
||||
subscriptionEndDate?: Date
|
||||
|
||||
@Prop({ type: Date })
|
||||
currentPeriodStart?: Date
|
||||
|
||||
@Prop({ type: Date })
|
||||
currentPeriodEnd?: Date
|
||||
|
||||
@Prop({ type: Boolean, default: true })
|
||||
isActive: boolean
|
||||
}
|
||||
|
||||
@@ -3,6 +3,16 @@ import { Document, Types } from 'mongoose'
|
||||
import { User } from '../../users/schemas/user.schema'
|
||||
import { Plan } from './plan.schema'
|
||||
|
||||
export enum SubscriptionStatus {
|
||||
Incomplete = 'incomplete',
|
||||
IncompleteExpired = 'incomplete_expired',
|
||||
Trialing = 'trialing',
|
||||
Active = 'active',
|
||||
PastDue = 'past_due',
|
||||
Canceled = 'canceled',
|
||||
Unpaid = 'unpaid',
|
||||
}
|
||||
|
||||
export type SubscriptionDocument = Subscription & Document
|
||||
|
||||
@Schema({ timestamps: true })
|
||||
@@ -17,10 +27,19 @@ export class Subscription {
|
||||
// polarSubscriptionId?: string
|
||||
|
||||
@Prop({ type: Date })
|
||||
startDate: Date
|
||||
subscriptionStartDate?: Date
|
||||
|
||||
@Prop({ type: Date })
|
||||
endDate: Date
|
||||
subscriptionEndDate?: Date
|
||||
|
||||
@Prop({ type: Date })
|
||||
currentPeriodStart?: Date
|
||||
|
||||
@Prop({ type: Date })
|
||||
currentPeriodEnd?: Date
|
||||
|
||||
@Prop({ type: String })
|
||||
status: string
|
||||
|
||||
@Prop({ type: Boolean, default: true })
|
||||
isActive: boolean
|
||||
|
||||
@@ -269,16 +269,6 @@ export class GatewayService {
|
||||
)
|
||||
}
|
||||
|
||||
if (body.messages.map((m) => m.recipients).flat().length > 50) {
|
||||
throw new HttpException(
|
||||
{
|
||||
success: false,
|
||||
error: 'Maximum of 50 recipients per batch is allowed',
|
||||
},
|
||||
HttpStatus.BAD_REQUEST,
|
||||
)
|
||||
}
|
||||
|
||||
const { messageTemplate, messages } = body
|
||||
|
||||
const smsBatch = await this.smsBatchModel.create({
|
||||
|
||||
@@ -43,7 +43,12 @@ import { Textarea } from '@/components/ui/textarea'
|
||||
import axios from 'axios'
|
||||
import { useSession } from 'next-auth/react'
|
||||
import { Routes } from '@/config/routes'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip'
|
||||
|
||||
const updateProfileSchema = z.object({
|
||||
name: z.string().min(1, 'Name is required'),
|
||||
@@ -198,26 +203,29 @@ export default function AccountSettings() {
|
||||
},
|
||||
})
|
||||
|
||||
const CurrentPlan = () => {
|
||||
|
||||
const CurrentSubscription = () => {
|
||||
const {
|
||||
data: currentPlan,
|
||||
isLoading: isLoadingPlan,
|
||||
error: planError,
|
||||
data: currentSubscription,
|
||||
isLoading: isLoadingSubscription,
|
||||
error: subscriptionError,
|
||||
} = useQuery({
|
||||
queryKey: ['currentPlan'],
|
||||
queryKey: ['currentSubscription'],
|
||||
queryFn: () =>
|
||||
httpBrowserClient
|
||||
.get(ApiEndpoints.billing.currentPlan())
|
||||
.get(ApiEndpoints.billing.currentSubscription())
|
||||
.then((res) => res.data),
|
||||
})
|
||||
|
||||
|
||||
if (isLoadingPlan) return <div className='flex justify-center items-center h-full'><Spinner size='sm' /></div>
|
||||
if (planError)
|
||||
if (isLoadingSubscription)
|
||||
return (
|
||||
<div className='flex justify-center items-center h-full'>
|
||||
<Spinner size='sm' />
|
||||
</div>
|
||||
)
|
||||
if (subscriptionError)
|
||||
return (
|
||||
<p className='text-sm text-destructive'>
|
||||
Failed to load plan information
|
||||
Failed to load subscription information
|
||||
</p>
|
||||
)
|
||||
|
||||
@@ -226,7 +234,7 @@ export default function AccountSettings() {
|
||||
<div className='flex items-center justify-between mb-4'>
|
||||
<div>
|
||||
<h3 className='text-lg font-bold text-gray-900 dark:text-white'>
|
||||
{currentPlan?.name}
|
||||
{currentSubscription?.plan?.name}
|
||||
</h3>
|
||||
<p className='text-xs text-gray-500 dark:text-gray-400'>
|
||||
Current subscription
|
||||
@@ -234,7 +242,9 @@ export default function AccountSettings() {
|
||||
</div>
|
||||
<div className='flex items-center bg-green-50 dark:bg-green-900/30 px-2 py-0.5 rounded-full'>
|
||||
<Check className='h-3 w-3 text-green-600 dark:text-green-400 mr-1' />
|
||||
<span className='text-xs font-medium text-green-600 dark:text-green-400'>Active</span>
|
||||
<span className='text-xs font-medium text-green-600 dark:text-green-400'>
|
||||
Active
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -242,9 +252,15 @@ export default function AccountSettings() {
|
||||
<div className='flex items-center space-x-2 bg-white dark:bg-gray-800 p-2 rounded-md shadow-sm'>
|
||||
<Calendar className='h-4 w-4 text-blue-600 dark:text-blue-400' />
|
||||
<div>
|
||||
<p className='text-xs text-gray-500 dark:text-gray-400'>Next Payment</p>
|
||||
<p className='text-xs text-gray-500 dark:text-gray-400'>
|
||||
Next Payment
|
||||
</p>
|
||||
<p className='text-sm font-medium text-gray-900 dark:text-white'>
|
||||
{currentPlan?.nextPaymentDate ?? '-:-'}
|
||||
{currentSubscription?.nextPaymentDate
|
||||
? new Date(
|
||||
currentSubscription?.nextPaymentDate
|
||||
).toLocaleDateString()
|
||||
: '-:-'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -254,7 +270,7 @@ export default function AccountSettings() {
|
||||
<div>
|
||||
<p className='text-xs text-gray-500 dark:text-gray-400'>Quota</p>
|
||||
<p className='text-sm font-medium text-gray-900 dark:text-white'>
|
||||
{currentPlan?.quota}
|
||||
{currentSubscription?.quota}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -262,10 +278,14 @@ export default function AccountSettings() {
|
||||
<div className='col-span-2 bg-white dark:bg-gray-800 p-2 rounded-md shadow-sm'>
|
||||
<div className='grid grid-cols-3 gap-2'>
|
||||
<div>
|
||||
<p className='text-xs text-gray-500 dark:text-gray-400'>Daily</p>
|
||||
<p className='text-xs text-gray-500 dark:text-gray-400'>
|
||||
Daily
|
||||
</p>
|
||||
<p className='text-sm font-medium text-gray-900 dark:text-white'>
|
||||
{currentPlan?.dailyLimit === -1 ? 'Unlimited' : currentPlan?.dailyLimit}
|
||||
{currentPlan?.dailyLimit === -1 && (
|
||||
{currentSubscription?.dailyLimit === -1
|
||||
? 'Unlimited'
|
||||
: currentSubscription?.dailyLimit}
|
||||
{currentSubscription?.dailyLimit === -1 && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -282,10 +302,14 @@ export default function AccountSettings() {
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className='text-xs text-gray-500 dark:text-gray-400'>Monthly</p>
|
||||
<p className='text-xs text-gray-500 dark:text-gray-400'>
|
||||
Monthly
|
||||
</p>
|
||||
<p className='text-sm font-medium text-gray-900 dark:text-white'>
|
||||
{currentPlan?.monthlyLimit === -1 ? 'Unlimited' : currentPlan?.monthlyLimit.toLocaleString()}
|
||||
{currentPlan?.monthlyLimit === -1 && (
|
||||
{currentSubscription?.monthlyLimit === -1
|
||||
? 'Unlimited'
|
||||
: currentSubscription?.monthlyLimit.toLocaleString()}
|
||||
{currentSubscription?.monthlyLimit === -1 && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -304,8 +328,10 @@ export default function AccountSettings() {
|
||||
<div>
|
||||
<p className='text-xs text-gray-500 dark:text-gray-400'>Bulk</p>
|
||||
<p className='text-sm font-medium text-gray-900 dark:text-white'>
|
||||
{currentPlan?.bulkSendLimit === -1 ? 'Unlimited' : currentPlan?.bulkSendLimit}
|
||||
{currentPlan?.bulkSendLimit === -1 && (
|
||||
{currentSubscription?.bulkSendLimit === -1
|
||||
? 'Unlimited'
|
||||
: currentSubscription?.bulkSendLimit}
|
||||
{currentSubscription?.bulkSendLimit === -1 && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -326,16 +352,16 @@ export default function AccountSettings() {
|
||||
</div>
|
||||
|
||||
<div className='mt-3 flex justify-end gap-2'>
|
||||
{currentPlan?.name?.toLowerCase() === 'free' ? (
|
||||
<Link
|
||||
href="/checkout/pro"
|
||||
{currentSubscription?.plan?.name?.toLowerCase() === 'free' ? (
|
||||
<Link
|
||||
href='/checkout/pro'
|
||||
className='text-xs font-medium text-white bg-blue-600 hover:bg-blue-700 px-3 py-1.5 rounded-md transition-colors'
|
||||
>
|
||||
Upgrade to Pro →
|
||||
</Link>
|
||||
) : (
|
||||
<Link
|
||||
href="https://polar.sh/textbee/portal/"
|
||||
<Link
|
||||
href='https://polar.sh/textbee/portal/'
|
||||
className='text-xs font-medium text-gray-700 dark:text-gray-200 hover:text-gray-900 dark:hover:text-white'
|
||||
>
|
||||
Manage Subscription →
|
||||
@@ -355,7 +381,7 @@ export default function AccountSettings() {
|
||||
|
||||
return (
|
||||
<div className='grid gap-6 max-w-2xl mx-auto'>
|
||||
<CurrentPlan />
|
||||
<CurrentSubscription />
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className='flex items-center gap-2'>
|
||||
|
||||
@@ -28,8 +28,8 @@ import { useMutation, useQuery } from '@tanstack/react-query'
|
||||
import { Spinner } from '@/components/ui/spinner'
|
||||
import httpBrowserClient from '@/lib/httpBrowserClient'
|
||||
|
||||
const MAX_FILE_SIZE = 1024 * 1024 // 1 MB
|
||||
const MAX_ROWS = 50
|
||||
const DEFAULT_MAX_FILE_SIZE = 1024 * 1024 // 1 MB
|
||||
const DEFAULT_MAX_ROWS = 50
|
||||
|
||||
export default function BulkSMSSend() {
|
||||
const [csvData, setCsvData] = useState<any[]>([])
|
||||
@@ -39,9 +39,29 @@ export default function BulkSMSSend() {
|
||||
const [selectedRecipient, setSelectedRecipient] = useState<string>('')
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const {
|
||||
data: currentSubscription,
|
||||
isLoading: isLoadingSubscription,
|
||||
error: subscriptionError,
|
||||
} = useQuery({
|
||||
queryKey: ['currentSubscription'],
|
||||
queryFn: () =>
|
||||
httpBrowserClient
|
||||
.get(ApiEndpoints.billing.currentSubscription())
|
||||
.then((res) => res.data),
|
||||
})
|
||||
|
||||
const maxRows = useMemo(() => {
|
||||
if (currentSubscription?.plan?.bulkSendLimit == -1) {
|
||||
return 9999
|
||||
}
|
||||
|
||||
return currentSubscription?.plan?.bulkSendLimit || DEFAULT_MAX_ROWS
|
||||
}, [currentSubscription])
|
||||
|
||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||
const file = acceptedFiles[0]
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
if (file.size > DEFAULT_MAX_FILE_SIZE) {
|
||||
setError('File size exceeds 1 MB limit.')
|
||||
return
|
||||
}
|
||||
@@ -49,8 +69,8 @@ export default function BulkSMSSend() {
|
||||
Papa.parse(file, {
|
||||
complete: (results) => {
|
||||
if (results.data && results.data.length > 0) {
|
||||
if (results.data.length > MAX_ROWS) {
|
||||
setError(`CSV file exceeds ${MAX_ROWS} rows limit.`)
|
||||
if (results.data.length > maxRows) {
|
||||
setError(`CSV file exceeds ${maxRows} rows limit.`)
|
||||
return
|
||||
}
|
||||
setCsvData(results.data as any[])
|
||||
@@ -136,8 +156,8 @@ export default function BulkSMSSend() {
|
||||
<section>
|
||||
<h2 className='text-lg font-semibold mb-2'>1. Upload CSV</h2>
|
||||
<p className='text-sm text-gray-500 mb-4'>
|
||||
Upload a CSV file (max 1MB, {MAX_ROWS} rows) containing recipient
|
||||
information.
|
||||
Upload a CSV file (max {DEFAULT_MAX_FILE_SIZE} bytes, {maxRows}
|
||||
rows) containing recipient information.
|
||||
</p>
|
||||
<div
|
||||
{...getRootProps()}
|
||||
@@ -153,7 +173,8 @@ export default function BulkSMSSend() {
|
||||
Drag & drop a CSV file here, or click to select one
|
||||
</p>
|
||||
<p className='text-sm text-gray-500 mt-1'>
|
||||
Max file size: 1MB, Max rows: 50
|
||||
Max file size: {DEFAULT_MAX_FILE_SIZE} bytes, Max rows:{' '}
|
||||
{maxRows}
|
||||
</p>
|
||||
</div>
|
||||
{error && (
|
||||
|
||||
@@ -32,7 +32,7 @@ export const ApiEndpoints = {
|
||||
getStats: () => '/gateway/stats',
|
||||
},
|
||||
billing: {
|
||||
currentPlan: () => '/billing/current-plan',
|
||||
currentSubscription: () => '/billing/current-subscription',
|
||||
checkout: () => '/billing/checkout',
|
||||
plans: () => '/billing/plans',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user