Browse Source

fix billing issues and improve ui

pull/52/head
isra el 1 year ago
parent
commit
087bdcc9d6
  1. 7
      api/src/billing/billing.controller.ts
  2. 14
      api/src/billing/billing.service.ts
  3. 12
      api/src/billing/schemas/plan.schema.ts
  4. 23
      api/src/billing/schemas/subscription.schema.ts
  5. 10
      api/src/gateway/gateway.service.ts
  6. 84
      web/app/(app)/dashboard/(components)/account-settings.tsx
  7. 37
      web/app/(app)/dashboard/(components)/bulk-sms-send.tsx
  8. 2
      web/config/api.ts

7
api/src/billing/billing.controller.ts

@ -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')

14
api/src/billing/billing.service.ts

@ -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}`)

12
api/src/billing/schemas/plan.schema.ts

@ -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
}

23
api/src/billing/schemas/subscription.schema.ts

@ -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 })
subscriptionEndDate?: Date
@Prop({ type: Date })
endDate: Date
currentPeriodStart?: Date
@Prop({ type: Date })
currentPeriodEnd?: Date
@Prop({ type: String })
status: string
@Prop({ type: Boolean, default: true })
isActive: boolean

10
api/src/gateway/gateway.service.ts

@ -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({

84
web/app/(app)/dashboard/(components)/account-settings.tsx

@ -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' ? (
{currentSubscription?.plan?.name?.toLowerCase() === 'free' ? (
<Link
href="/checkout/pro"
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/"
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'>

37
web/app/(app)/dashboard/(components)/bulk-sms-send.tsx

@ -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 &amp; 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 && (

2
web/config/api.ts

@ -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',
},

Loading…
Cancel
Save