mirror of
https://github.com/vernu/textbee.git
synced 2026-05-25 00:44:15 -04:00
new pricing and billing
This commit is contained in:
@@ -8,6 +8,7 @@ import { APP_GUARD } from '@nestjs/core/constants'
|
||||
import { WebhookModule } from './webhook/webhook.module'
|
||||
import { ThrottlerByIpGuard } from './auth/guards/throttle-by-ip.guard'
|
||||
import { ScheduleModule } from '@nestjs/schedule'
|
||||
import { BillingModule } from './billing/billing.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -23,6 +24,7 @@ import { ScheduleModule } from '@nestjs/schedule'
|
||||
UsersModule,
|
||||
GatewayModule,
|
||||
WebhookModule,
|
||||
BillingModule,
|
||||
],
|
||||
controllers: [],
|
||||
providers: [
|
||||
|
||||
18
api/src/billing/billing.controller.spec.ts
Normal file
18
api/src/billing/billing.controller.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { BillingController } from './billing.controller';
|
||||
|
||||
describe('BillingController', () => {
|
||||
let controller: BillingController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [BillingController],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<BillingController>(BillingController);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
});
|
||||
65
api/src/billing/billing.controller.ts
Normal file
65
api/src/billing/billing.controller.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { Controller, Post, Body, Get, UseGuards, Request } from '@nestjs/common'
|
||||
import { BillingService } from './billing.service'
|
||||
import { AuthGuard } from 'src/auth/guards/auth.guard'
|
||||
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'
|
||||
import {
|
||||
CheckoutInputDTO,
|
||||
CheckoutResponseDTO,
|
||||
PlansResponseDTO,
|
||||
} from './billing.dto'
|
||||
|
||||
@ApiTags('billing')
|
||||
@ApiBearerAuth()
|
||||
@Controller('billing')
|
||||
export class BillingController {
|
||||
constructor(private billingService: BillingService) {}
|
||||
|
||||
@Get('plans')
|
||||
async getPlans(): Promise<PlansResponseDTO> {
|
||||
return this.billingService.getPlans()
|
||||
}
|
||||
|
||||
@Post('checkout')
|
||||
@UseGuards(AuthGuard)
|
||||
async getCheckoutUrl(
|
||||
@Body() payload: CheckoutInputDTO,
|
||||
@Request() req: any,
|
||||
): Promise<CheckoutResponseDTO> {
|
||||
return this.billingService.getCheckoutUrl({
|
||||
user: req.user,
|
||||
payload,
|
||||
req,
|
||||
})
|
||||
}
|
||||
|
||||
@Post('webhook/polar')
|
||||
async handlePolarWebhook(@Body() data: any, @Request() req: any) {
|
||||
const payload = await this.billingService.validatePolarWebhookPayload(
|
||||
data,
|
||||
req.headers,
|
||||
)
|
||||
|
||||
// Handle Polar.sh webhook events
|
||||
switch (payload.type) {
|
||||
case 'subscription.created':
|
||||
await this.billingService.switchPlan({
|
||||
userId: payload.data.userId,
|
||||
newPlanName: payload.data?.product?.name || 'pro',
|
||||
newPlanPolarProductId: payload.data?.product?.id,
|
||||
})
|
||||
break
|
||||
|
||||
// @ts-ignore
|
||||
case 'subscription.cancelled':
|
||||
await this.billingService.switchPlan({
|
||||
// @ts-ignore
|
||||
userId: payload?.data?.userId,
|
||||
newPlanName: 'free',
|
||||
})
|
||||
break
|
||||
default:
|
||||
console.log('Unhandled event type:', payload.type)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
36
api/src/billing/billing.dto.ts
Normal file
36
api/src/billing/billing.dto.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { ApiProperty } from '@nestjs/swagger'
|
||||
|
||||
export class PlanDTO {
|
||||
@ApiProperty({ type: String })
|
||||
name: string
|
||||
|
||||
@ApiProperty({ type: Number })
|
||||
monthlyPrice: number
|
||||
|
||||
@ApiProperty({ type: Number })
|
||||
yearlyPrice?: number
|
||||
|
||||
@ApiProperty({ type: String })
|
||||
polarProductId: string
|
||||
|
||||
@ApiProperty({ type: Boolean })
|
||||
isActive: boolean
|
||||
}
|
||||
|
||||
export class PlansResponseDTO extends Array<PlanDTO> {}
|
||||
|
||||
export class CheckoutInputDTO {
|
||||
@ApiProperty({ type: String, required: true })
|
||||
planName: string
|
||||
|
||||
@ApiProperty({ type: String })
|
||||
discountId?: string
|
||||
|
||||
@ApiProperty({ type: Boolean })
|
||||
isYearly?: boolean
|
||||
}
|
||||
|
||||
export class CheckoutResponseDTO {
|
||||
@ApiProperty({ type: String })
|
||||
redirectUrl: string
|
||||
}
|
||||
27
api/src/billing/billing.module.ts
Normal file
27
api/src/billing/billing.module.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Module, forwardRef } from '@nestjs/common'
|
||||
import { BillingController } from './billing.controller'
|
||||
import { BillingService } from './billing.service'
|
||||
import { PlanSchema } from './schemas/plan.schema'
|
||||
import { SubscriptionSchema } from './schemas/subscription.schema'
|
||||
import { Plan } from './schemas/plan.schema'
|
||||
import { Subscription } from './schemas/subscription.schema'
|
||||
import { MongooseModule } from '@nestjs/mongoose'
|
||||
import { AuthModule } from 'src/auth/auth.module'
|
||||
import { UsersModule } from 'src/users/users.module'
|
||||
import { GatewayModule } from 'src/gateway/gateway.module'
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
MongooseModule.forFeature([
|
||||
{ name: Plan.name, schema: PlanSchema },
|
||||
{ name: Subscription.name, schema: SubscriptionSchema },
|
||||
]),
|
||||
AuthModule,
|
||||
UsersModule,
|
||||
forwardRef(() => GatewayModule),
|
||||
],
|
||||
controllers: [BillingController],
|
||||
providers: [BillingService],
|
||||
exports: [BillingService],
|
||||
})
|
||||
export class BillingModule {}
|
||||
18
api/src/billing/billing.service.spec.ts
Normal file
18
api/src/billing/billing.service.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { BillingService } from './billing.service';
|
||||
|
||||
describe('BillingService', () => {
|
||||
let service: BillingService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [BillingService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<BillingService>(BillingService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
400
api/src/billing/billing.service.ts
Normal file
400
api/src/billing/billing.service.ts
Normal file
@@ -0,0 +1,400 @@
|
||||
import { HttpException, HttpStatus, Injectable } from '@nestjs/common'
|
||||
import { InjectModel } from '@nestjs/mongoose'
|
||||
import { Model } from 'mongoose'
|
||||
import { Plan, PlanDocument } from './schemas/plan.schema'
|
||||
import {
|
||||
Subscription,
|
||||
SubscriptionDocument,
|
||||
} from './schemas/subscription.schema'
|
||||
import { Polar } from '@polar-sh/sdk'
|
||||
import { User, UserDocument } from 'src/users/schemas/user.schema'
|
||||
import { CheckoutResponseDTO, PlanDTO } from './billing.dto'
|
||||
import { SMSDocument } from 'src/gateway/schemas/sms.schema'
|
||||
import { SMS } from 'src/gateway/schemas/sms.schema'
|
||||
import { validateEvent } from '@polar-sh/sdk/webhooks'
|
||||
|
||||
@Injectable()
|
||||
export class BillingService {
|
||||
private polarApi
|
||||
|
||||
constructor(
|
||||
@InjectModel(Plan.name) private planModel: Model<PlanDocument>,
|
||||
@InjectModel(Subscription.name)
|
||||
private subscriptionModel: Model<SubscriptionDocument>,
|
||||
@InjectModel(User.name) private userModel: Model<UserDocument>,
|
||||
@InjectModel(SMS.name) private smsModel: Model<SMSDocument>,
|
||||
) {
|
||||
this.initializePlans()
|
||||
this.polarApi = new Polar({
|
||||
accessToken: process.env.POLAR_ACCESS_TOKEN ?? '',
|
||||
server:
|
||||
process.env.POLAR_SERVER === 'production' ? 'production' : 'sandbox',
|
||||
})
|
||||
}
|
||||
|
||||
private async initializePlans() {
|
||||
const plans = await this.planModel.find()
|
||||
if (plans.length === 0) {
|
||||
await this.planModel.create([
|
||||
{
|
||||
name: 'free',
|
||||
dailyLimit: 50,
|
||||
monthlyLimit: 1000,
|
||||
bulkSendLimit: 50,
|
||||
monthlyPrice: 0,
|
||||
yearlyPrice: 0,
|
||||
},
|
||||
{
|
||||
name: 'pro',
|
||||
dailyLimit: -1, // -1 means unlimited
|
||||
monthlyLimit: 5000,
|
||||
bulkSendLimit: -1,
|
||||
monthlyPrice: 690, // $6.90
|
||||
yearlyPrice: 6900, // $69.00
|
||||
},
|
||||
{
|
||||
name: 'custom',
|
||||
dailyLimit: -1,
|
||||
monthlyLimit: -1,
|
||||
bulkSendLimit: -1,
|
||||
monthlyPrice: 0, // Custom pricing
|
||||
yearlyPrice: 0, // Custom pricing
|
||||
},
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
async getPlans(): Promise<PlanDTO[]> {
|
||||
return this.planModel.find({
|
||||
isActive: true,
|
||||
})
|
||||
}
|
||||
|
||||
async getCheckoutUrl({
|
||||
user,
|
||||
payload,
|
||||
req,
|
||||
}: {
|
||||
user: any
|
||||
payload: any
|
||||
req: any
|
||||
}): Promise<CheckoutResponseDTO> {
|
||||
const isYearly = payload.isYearly
|
||||
|
||||
const selectedPlan = await this.planModel.findOne({
|
||||
name: payload.planName,
|
||||
})
|
||||
|
||||
if (!selectedPlan?.polarProductId) {
|
||||
throw new Error('Plan cannot be purchased')
|
||||
}
|
||||
|
||||
// const product = await this.polarApi.products.get(selectedPlan.polarProductId)
|
||||
|
||||
const discountId =
|
||||
payload.discountId ?? '48f62ff7-3cd8-46ec-8ca7-2e570dc9c522'
|
||||
|
||||
try {
|
||||
const checkoutOptions: any = {
|
||||
productId: selectedPlan.polarProductId,
|
||||
// productPriceId: isYearly
|
||||
// ? selectedPlan.yearlyPolarProductId
|
||||
// : selectedPlan.monthlyPolarProductId,
|
||||
successUrl: `${process.env.FRONTEND_URL}/checkout-success?checkout_id={CHECKOUT_ID}`,
|
||||
cancelUrl: `${process.env.FRONTEND_URL}/checkout-cancel?checkout_id={CHECKOUT_ID}`,
|
||||
customerEmail: user.email,
|
||||
customerName: user.name,
|
||||
customerIpAddress: req.ip,
|
||||
metadata: {
|
||||
userId: user._id?.toString(),
|
||||
},
|
||||
}
|
||||
const discount = await this.polarApi.discounts.get({
|
||||
id: discountId,
|
||||
})
|
||||
if (discount) {
|
||||
checkoutOptions.discountId = discount.id
|
||||
}
|
||||
|
||||
const checkout =
|
||||
await this.polarApi.checkouts.custom.create(checkoutOptions)
|
||||
console.log(checkout)
|
||||
return { redirectUrl: checkout.url }
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
throw new Error('Failed to create checkout')
|
||||
}
|
||||
}
|
||||
|
||||
async getActiveSubscription(userId: string) {
|
||||
const user = await this.userModel.findById(userId)
|
||||
const plans = await this.planModel.find()
|
||||
|
||||
const customPlan = plans.find((plan) => plan.name === 'custom')
|
||||
const proPlan = plans.find((plan) => plan.name === 'pro')
|
||||
const freePlan = plans.find((plan) => plan.name === 'free')
|
||||
|
||||
const customPlanSubscription = await this.subscriptionModel.findOne({
|
||||
user: userId,
|
||||
plan: customPlan._id,
|
||||
isActive: true,
|
||||
})
|
||||
|
||||
if (customPlanSubscription) {
|
||||
return customPlanSubscription
|
||||
}
|
||||
|
||||
const proPlanSubscription = await this.subscriptionModel.findOne({
|
||||
user: userId,
|
||||
plan: proPlan._id,
|
||||
isActive: true,
|
||||
})
|
||||
|
||||
if (proPlanSubscription) {
|
||||
return proPlanSubscription
|
||||
}
|
||||
|
||||
const freePlanSubscription = await this.subscriptionModel.findOne({
|
||||
user: userId,
|
||||
plan: freePlan._id,
|
||||
isActive: true,
|
||||
})
|
||||
|
||||
if (freePlanSubscription) {
|
||||
return freePlanSubscription
|
||||
}
|
||||
|
||||
// create a new free plan subscription
|
||||
const newFreePlanSubscription = await this.subscriptionModel.create({
|
||||
user: userId,
|
||||
plan: freePlan._id,
|
||||
isActive: true,
|
||||
startDate: new Date(),
|
||||
})
|
||||
|
||||
return newFreePlanSubscription
|
||||
}
|
||||
|
||||
async getUserLimits(userId: string) {
|
||||
const subscription = await this.subscriptionModel
|
||||
.findOne({ user: userId, isActive: true })
|
||||
.populate('plan')
|
||||
|
||||
if (!subscription) {
|
||||
// Default to free plan limits
|
||||
const freePlan = await this.planModel.findOne({ name: 'free' })
|
||||
return {
|
||||
dailyLimit: freePlan.dailyLimit,
|
||||
monthlyLimit: freePlan.monthlyLimit,
|
||||
bulkSendLimit: freePlan.bulkSendLimit,
|
||||
}
|
||||
}
|
||||
|
||||
// For custom plans, use custom limits if set
|
||||
return {
|
||||
dailyLimit: subscription.customDailyLimit || subscription.plan.dailyLimit,
|
||||
monthlyLimit:
|
||||
subscription.customMonthlyLimit || subscription.plan.monthlyLimit,
|
||||
bulkSendLimit:
|
||||
subscription.customBulkSendLimit || subscription.plan.bulkSendLimit,
|
||||
}
|
||||
}
|
||||
|
||||
async switchPlan({
|
||||
userId,
|
||||
newPlanName,
|
||||
newPlanPolarProductId,
|
||||
}: {
|
||||
userId: string
|
||||
newPlanName?: string
|
||||
newPlanPolarProductId?: string
|
||||
}) {
|
||||
// switch the subscription to the new one
|
||||
// deactivate the current active subscription
|
||||
// activate the new subscription if it exists or create a new one
|
||||
|
||||
// get the plan from the polarProductId
|
||||
let plan: PlanDocument
|
||||
if (newPlanPolarProductId) {
|
||||
plan = await this.planModel.findOne({
|
||||
polarProductId: newPlanPolarProductId,
|
||||
})
|
||||
} else if (newPlanName) {
|
||||
plan = await this.planModel.findOne({ name: newPlanName })
|
||||
}
|
||||
|
||||
if (!plan) {
|
||||
throw new Error('Plan not found')
|
||||
}
|
||||
|
||||
// if any of the subscriptions that are not the new plan are active, deactivate them
|
||||
await this.subscriptionModel.updateMany(
|
||||
{ user: userId, plan: { $ne: plan._id }, isActive: true },
|
||||
{ isActive: false, endDate: new Date() },
|
||||
)
|
||||
|
||||
// create or update the new subscription
|
||||
await this.subscriptionModel.updateOne(
|
||||
{ user: userId, plan: plan._id },
|
||||
{ isActive: true },
|
||||
{ upsert: true },
|
||||
)
|
||||
}
|
||||
|
||||
async canPerformAction(
|
||||
userId: string,
|
||||
action: 'send_sms' | 'receive_sms' | 'bulk_send_sms',
|
||||
value: number,
|
||||
) {
|
||||
|
||||
|
||||
|
||||
// TODO: temporary allow all requests until march 15 2025
|
||||
if (new Date() < new Date('2025-03-15')) {
|
||||
return true
|
||||
}
|
||||
|
||||
const user = await this.userModel.findById(userId)
|
||||
if (user.isBanned) {
|
||||
throw new HttpException(
|
||||
{
|
||||
message: 'Sorry, we cannot process your request at the moment',
|
||||
},
|
||||
HttpStatus.BAD_REQUEST,
|
||||
)
|
||||
}
|
||||
|
||||
if (user.emailVerifiedAt === null) {
|
||||
// throw new HttpException(
|
||||
// {
|
||||
// message: 'Please verify your email to continue',
|
||||
// },
|
||||
// HttpStatus.BAD_REQUEST,
|
||||
// )
|
||||
}
|
||||
|
||||
let plan: PlanDocument
|
||||
const subscription = await this.subscriptionModel.findOne({
|
||||
user: userId,
|
||||
isActive: true,
|
||||
})
|
||||
|
||||
if (!subscription) {
|
||||
plan = await this.planModel.findOne({ name: 'free' })
|
||||
} else {
|
||||
plan = await this.planModel.findById(subscription.plan)
|
||||
}
|
||||
|
||||
if (plan.name === 'custom') {
|
||||
// TODO: for now custom plans are unlimited
|
||||
return true
|
||||
}
|
||||
|
||||
let hasReachedLimit = false
|
||||
let message = ''
|
||||
|
||||
const processedSmsToday = await this.smsModel.countDocuments({
|
||||
'device.user': userId,
|
||||
createdAt: { $gte: new Date(new Date().setHours(0, 0, 0, 0)) },
|
||||
})
|
||||
const processedSmsLastMonth = await this.smsModel.countDocuments({
|
||||
'device.user': userId,
|
||||
createdAt: {
|
||||
$gte: new Date(new Date().setMonth(new Date().getMonth() - 1)),
|
||||
},
|
||||
})
|
||||
|
||||
if (['send_sms', 'receive_sms', 'bulk_send_sms'].includes(action)) {
|
||||
// check daily limit
|
||||
if (
|
||||
plan.dailyLimit !== -1 &&
|
||||
processedSmsToday + value > plan.dailyLimit
|
||||
) {
|
||||
hasReachedLimit = true
|
||||
message = `You have reached your daily limit, you only have ${plan.dailyLimit - processedSmsToday} remaining`
|
||||
}
|
||||
|
||||
// check monthly limit
|
||||
if (
|
||||
plan.monthlyLimit !== -1 &&
|
||||
processedSmsLastMonth + value > plan.monthlyLimit
|
||||
) {
|
||||
hasReachedLimit = true
|
||||
message = `You have reached your monthly limit, you only have ${plan.monthlyLimit - processedSmsLastMonth} remaining`
|
||||
}
|
||||
|
||||
// check bulk send limit
|
||||
if (plan.bulkSendLimit !== -1 && value > plan.bulkSendLimit) {
|
||||
hasReachedLimit = true
|
||||
message = `You can only send ${plan.bulkSendLimit} sms at a time`
|
||||
}
|
||||
}
|
||||
|
||||
if (hasReachedLimit) {
|
||||
throw new HttpException(
|
||||
{
|
||||
message: message,
|
||||
hasReachedLimit: true,
|
||||
dailyLimit: plan.dailyLimit,
|
||||
dailyRemaining: plan.dailyLimit - processedSmsToday,
|
||||
monthlyRemaining: plan.monthlyLimit - processedSmsLastMonth,
|
||||
bulkSendLimit: plan.bulkSendLimit,
|
||||
monthlyLimit: plan.monthlyLimit,
|
||||
},
|
||||
HttpStatus.BAD_REQUEST,
|
||||
)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
async getUsage(userId: string) {
|
||||
const subscription = await this.subscriptionModel.findOne({
|
||||
user: userId,
|
||||
isActive: true,
|
||||
})
|
||||
|
||||
const plan = await this.planModel.findById(subscription.plan)
|
||||
|
||||
const processedSmsToday = await this.smsModel.countDocuments({
|
||||
'device.user': userId,
|
||||
createdAt: { $gte: new Date(new Date().setHours(0, 0, 0, 0)) },
|
||||
})
|
||||
|
||||
const processedSmsLastMonth = await this.smsModel.countDocuments({
|
||||
'device.user': userId,
|
||||
createdAt: {
|
||||
$gte: new Date(new Date().setMonth(new Date().getMonth() - 1)),
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
processedSmsToday,
|
||||
processedSmsLastMonth,
|
||||
dailyLimit: plan.dailyLimit,
|
||||
monthlyLimit: plan.monthlyLimit,
|
||||
bulkSendLimit: plan.bulkSendLimit,
|
||||
dailyRemaining: plan.dailyLimit - processedSmsToday,
|
||||
monthlyRemaining: plan.monthlyLimit - processedSmsLastMonth,
|
||||
}
|
||||
}
|
||||
|
||||
async validatePolarWebhookPayload(payload: any, headers: any) {
|
||||
const webhookHeaders = {
|
||||
'webhook-id': headers['webhook-id'] ?? '',
|
||||
'webhook-timestamp': headers['webhook-timestamp'] ?? '',
|
||||
'webhook-signature': headers['webhook-signature'] ?? '',
|
||||
}
|
||||
|
||||
try {
|
||||
const webhookPayload = validateEvent(
|
||||
payload,
|
||||
webhookHeaders,
|
||||
process.env.POLAR_WEBHOOK_SECRET,
|
||||
)
|
||||
return webhookPayload
|
||||
} catch (error) {
|
||||
throw new Error('Invalid webhook payload')
|
||||
}
|
||||
}
|
||||
}
|
||||
33
api/src/billing/schemas/plan.schema.ts
Normal file
33
api/src/billing/schemas/plan.schema.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'
|
||||
import { Document } from 'mongoose'
|
||||
|
||||
export type PlanDocument = Plan & Document
|
||||
|
||||
@Schema({ timestamps: true })
|
||||
export class Plan {
|
||||
@Prop({ required: true, unique: true })
|
||||
name: string // free, pro, custom
|
||||
|
||||
@Prop({ required: true })
|
||||
dailyLimit: number
|
||||
|
||||
@Prop({ required: true })
|
||||
monthlyLimit: number
|
||||
|
||||
@Prop({ required: true })
|
||||
bulkSendLimit: number
|
||||
|
||||
@Prop({ required: true })
|
||||
monthlyPrice: number // in cents
|
||||
|
||||
@Prop({})
|
||||
yearlyPrice: number // in cents
|
||||
|
||||
@Prop({ type: String, unique: true })
|
||||
polarProductId?: string
|
||||
|
||||
@Prop({ type: Boolean, default: true })
|
||||
isActive: boolean
|
||||
}
|
||||
|
||||
export const PlanSchema = SchemaFactory.createForClass(Plan)
|
||||
42
api/src/billing/schemas/subscription.schema.ts
Normal file
42
api/src/billing/schemas/subscription.schema.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'
|
||||
import { Document, Types } from 'mongoose'
|
||||
import { User } from '../../users/schemas/user.schema'
|
||||
import { Plan } from './plan.schema'
|
||||
|
||||
export type SubscriptionDocument = Subscription & Document
|
||||
|
||||
@Schema({ timestamps: true })
|
||||
export class Subscription {
|
||||
@Prop({ type: Types.ObjectId, ref: User.name, required: true })
|
||||
user: User
|
||||
|
||||
@Prop({ type: Types.ObjectId, ref: Plan.name, required: true })
|
||||
plan: Plan
|
||||
|
||||
// @Prop()
|
||||
// polarSubscriptionId?: string
|
||||
|
||||
@Prop({ type: Date })
|
||||
startDate: Date
|
||||
|
||||
@Prop({ type: Date })
|
||||
endDate: Date
|
||||
|
||||
@Prop({ type: Boolean, default: true })
|
||||
isActive: boolean
|
||||
|
||||
// Custom limits for custom plans
|
||||
@Prop({ type: Number })
|
||||
customDailyLimit?: number
|
||||
|
||||
@Prop({ type: Number })
|
||||
customMonthlyLimit?: number
|
||||
|
||||
@Prop({ type: Number })
|
||||
customBulkSendLimit?: number
|
||||
}
|
||||
|
||||
export const SubscriptionSchema = SchemaFactory.createForClass(Subscription)
|
||||
|
||||
// a user can only have one active subscription at a time
|
||||
SubscriptionSchema.index({ user: 1, isActive: 1 }, { unique: true })
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Module } from '@nestjs/common'
|
||||
import { forwardRef, Module } from '@nestjs/common'
|
||||
import { MongooseModule } from '@nestjs/mongoose'
|
||||
import { Device, DeviceSchema } from './schemas/device.schema'
|
||||
import { GatewayController } from './gateway.controller'
|
||||
@@ -8,6 +8,7 @@ import { UsersModule } from '../users/users.module'
|
||||
import { SMS, SMSSchema } from './schemas/sms.schema'
|
||||
import { SMSBatch, SMSBatchSchema } from './schemas/sms-batch.schema'
|
||||
import { WebhookModule } from 'src/webhook/webhook.module'
|
||||
import { BillingModule } from 'src/billing/billing.module'
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -28,6 +29,7 @@ import { WebhookModule } from 'src/webhook/webhook.module'
|
||||
AuthModule,
|
||||
UsersModule,
|
||||
WebhookModule,
|
||||
forwardRef(() => BillingModule),
|
||||
],
|
||||
controllers: [GatewayController],
|
||||
providers: [GatewayService],
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
} from 'firebase-admin/lib/messaging/messaging-api'
|
||||
import { WebhookEvent } from 'src/webhook/webhook-event.enum'
|
||||
import { WebhookService } from 'src/webhook/webhook.service'
|
||||
import { BillingService } from 'src/billing/billing.service'
|
||||
@Injectable()
|
||||
export class GatewayService {
|
||||
constructor(
|
||||
@@ -29,6 +30,7 @@ export class GatewayService {
|
||||
@InjectModel(SMSBatch.name) private smsBatchModel: Model<SMSBatch>,
|
||||
private authService: AuthService,
|
||||
private webhookService: WebhookService,
|
||||
private billingService: BillingService,
|
||||
) {}
|
||||
|
||||
async registerDevice(
|
||||
@@ -113,6 +115,12 @@ export class GatewayService {
|
||||
const message = smsData.message || smsData.smsBody
|
||||
const recipients = smsData.recipients || smsData.receivers
|
||||
|
||||
await this.billingService.canPerformAction(
|
||||
device.user.toString(),
|
||||
'send_sms',
|
||||
recipients.length,
|
||||
)
|
||||
|
||||
if (!message) {
|
||||
throw new HttpException(
|
||||
{
|
||||
@@ -241,6 +249,12 @@ export class GatewayService {
|
||||
)
|
||||
}
|
||||
|
||||
await this.billingService.canPerformAction(
|
||||
device.user.toString(),
|
||||
'bulk_send_sms',
|
||||
body.messages.map((m) => m.recipients).flat().length,
|
||||
)
|
||||
|
||||
if (
|
||||
!Array.isArray(body.messages) ||
|
||||
body.messages.length === 0 ||
|
||||
@@ -377,6 +391,12 @@ export class GatewayService {
|
||||
)
|
||||
}
|
||||
|
||||
await this.billingService.canPerformAction(
|
||||
device.user.toString(),
|
||||
'receive_sms',
|
||||
1,
|
||||
)
|
||||
|
||||
if (
|
||||
(!dto.receivedAt && !dto.receivedAtInMillis) ||
|
||||
!dto.sender ||
|
||||
@@ -414,9 +434,10 @@ export class GatewayService {
|
||||
console.log(e)
|
||||
})
|
||||
|
||||
this.webhookService.deliverNotification({
|
||||
sms,
|
||||
user: device.user,
|
||||
this.webhookService
|
||||
.deliverNotification({
|
||||
sms,
|
||||
user: device.user,
|
||||
event: WebhookEvent.MESSAGE_RECEIVED,
|
||||
})
|
||||
.catch((e) => {
|
||||
|
||||
@@ -34,6 +34,9 @@ export class User {
|
||||
|
||||
@Prop({ type: Date })
|
||||
emailVerifiedAt: Date
|
||||
|
||||
@Prop({ type: Boolean, default: false })
|
||||
isBanned: boolean
|
||||
}
|
||||
|
||||
export const UserSchema = SchemaFactory.createForClass(User)
|
||||
|
||||
Reference in New Issue
Block a user