mirror of
https://github.com/vernu/textbee.git
synced 2026-03-03 03:47:01 +00:00
chore(api): update polar sdk and improve billing and checkout logic
This commit is contained in:
@@ -30,7 +30,7 @@
|
||||
"@nestjs/schedule": "^4.1.1",
|
||||
"@nestjs/swagger": "^7.4.2",
|
||||
"@nestjs/throttler": "^6.2.1",
|
||||
"@polar-sh/sdk": "^0.19.2",
|
||||
"@polar-sh/sdk": "^0.26.1",
|
||||
"axios": "^1.7.7",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"dotenv": "^16.4.5",
|
||||
|
||||
10
api/pnpm-lock.yaml
generated
10
api/pnpm-lock.yaml
generated
@@ -39,8 +39,8 @@ importers:
|
||||
specifier: ^6.2.1
|
||||
version: 6.2.1(@nestjs/common@10.4.5(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.5)(reflect-metadata@0.2.2)
|
||||
'@polar-sh/sdk':
|
||||
specifier: ^0.19.2
|
||||
version: 0.19.2(zod@3.24.1)
|
||||
specifier: ^0.26.1
|
||||
version: 0.26.1(zod@3.24.1)
|
||||
axios:
|
||||
specifier: ^1.7.7
|
||||
version: 1.7.7
|
||||
@@ -892,8 +892,8 @@ packages:
|
||||
resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==}
|
||||
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
||||
|
||||
'@polar-sh/sdk@0.19.2':
|
||||
resolution: {integrity: sha512-n1emRNmhcAzRfVAWBiVVCJ2krBSZ4wANVTRO7hCMchYCkxyV+kiRdjyvBdVG3JpRsS6SR7yBr2+CRJkuHPPeDg==}
|
||||
'@polar-sh/sdk@0.26.1':
|
||||
resolution: {integrity: sha512-OEaxiNJaxpeNi7LANHR5S71BAyORk6W0lwkfHcrGyMGS9VDdgXnZjB8QZ3tFSXbQvt3yZdHShX6pPC8xOxNvFw==}
|
||||
peerDependencies:
|
||||
zod: '>= 3'
|
||||
|
||||
@@ -5505,7 +5505,7 @@ snapshots:
|
||||
|
||||
'@pkgr/core@0.1.1': {}
|
||||
|
||||
'@polar-sh/sdk@0.19.2(zod@3.24.1)':
|
||||
'@polar-sh/sdk@0.26.1(zod@3.24.1)':
|
||||
dependencies:
|
||||
standardwebhooks: 1.0.0
|
||||
zod: 3.24.1
|
||||
|
||||
@@ -57,8 +57,14 @@ export class BillingController {
|
||||
console.log(payload)
|
||||
await this.billingService.switchPlan({
|
||||
userId: payload.data?.metadata?.userId as string,
|
||||
newPlanName: payload.data?.product?.name?.split(' ')[payload.data?.product?.name?.length - 1] || 'pro',
|
||||
// TODO: remove this after more plans are added
|
||||
newPlanName: 'pro',
|
||||
newPlanPolarProductId: payload.data?.product?.id,
|
||||
currentPeriodStart: payload.data?.currentPeriodStart,
|
||||
currentPeriodEnd: payload.data?.currentPeriodEnd,
|
||||
status: payload.data?.status,
|
||||
subscriptionStartDate: payload.data?.createdAt,
|
||||
subscriptionEndDate: payload.data?.canceledAt,
|
||||
})
|
||||
break
|
||||
|
||||
|
||||
@@ -11,7 +11,13 @@ export class PlanDTO {
|
||||
yearlyPrice?: number
|
||||
|
||||
@ApiProperty({ type: String })
|
||||
polarProductId: string
|
||||
polarProductId?: string
|
||||
|
||||
@ApiProperty({ type: String })
|
||||
polarMonthlyProductId?: string
|
||||
|
||||
@ApiProperty({ type: String })
|
||||
polarYearlyProductId?: string
|
||||
|
||||
@ApiProperty({ type: Boolean })
|
||||
isActive: boolean
|
||||
|
||||
@@ -12,7 +12,10 @@ 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'
|
||||
import { PolarWebhookPayload, PolarWebhookPayloadDocument } from './schemas/polar-webhook-payload.schema'
|
||||
import {
|
||||
PolarWebhookPayload,
|
||||
PolarWebhookPayloadDocument,
|
||||
} from './schemas/polar-webhook-payload.schema'
|
||||
|
||||
@Injectable()
|
||||
export class BillingService {
|
||||
@@ -27,7 +30,6 @@ export class BillingService {
|
||||
@InjectModel(PolarWebhookPayload.name)
|
||||
private polarWebhookPayloadModel: Model<PolarWebhookPayloadDocument>,
|
||||
) {
|
||||
this.initializePlans()
|
||||
this.polarApi = new Polar({
|
||||
accessToken: process.env.POLAR_ACCESS_TOKEN ?? '',
|
||||
server:
|
||||
@@ -35,38 +37,6 @@ export class BillingService {
|
||||
})
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -79,8 +49,6 @@ export class BillingService {
|
||||
isActive: true,
|
||||
})
|
||||
|
||||
|
||||
|
||||
let plan = null
|
||||
|
||||
if (!subscription) {
|
||||
@@ -107,7 +75,10 @@ export class BillingService {
|
||||
name: payload.planName,
|
||||
})
|
||||
|
||||
if (!selectedPlan?.polarProductId) {
|
||||
if (
|
||||
!selectedPlan?.polarMonthlyProductId &&
|
||||
!selectedPlan?.polarYearlyProductId
|
||||
) {
|
||||
throw new Error('Plan cannot be purchased')
|
||||
}
|
||||
|
||||
@@ -118,10 +89,11 @@ export class BillingService {
|
||||
|
||||
try {
|
||||
const checkoutOptions: any = {
|
||||
productId: selectedPlan.polarProductId,
|
||||
// productPriceId: isYearly
|
||||
// ? selectedPlan.yearlyPolarProductId
|
||||
// : selectedPlan.monthlyPolarProductId,
|
||||
// productId: selectedPlan.polarProductId, // deprecated
|
||||
products: [
|
||||
selectedPlan.polarMonthlyProductId,
|
||||
selectedPlan.polarYearlyProductId,
|
||||
],
|
||||
successUrl: `${process.env.FRONTEND_URL}/dashboard?checkout-success=1&checkout_id={CHECKOUT_ID}`,
|
||||
cancelUrl: `${process.env.FRONTEND_URL}/dashboard?checkout-cancel=1&checkout_id={CHECKOUT_ID}`,
|
||||
customerEmail: user.email,
|
||||
@@ -138,8 +110,7 @@ export class BillingService {
|
||||
checkoutOptions.discountId = discount.id
|
||||
}
|
||||
|
||||
const checkout =
|
||||
await this.polarApi.checkouts.custom.create(checkoutOptions)
|
||||
const checkout = await this.polarApi.checkouts.create(checkoutOptions)
|
||||
console.log(checkout)
|
||||
return { redirectUrl: checkout.url }
|
||||
} catch (error) {
|
||||
@@ -226,20 +197,35 @@ export class BillingService {
|
||||
userId,
|
||||
newPlanName,
|
||||
newPlanPolarProductId,
|
||||
currentPeriodStart,
|
||||
currentPeriodEnd,
|
||||
subscriptionStartDate,
|
||||
subscriptionEndDate,
|
||||
status,
|
||||
}: {
|
||||
userId: string
|
||||
newPlanName?: string
|
||||
newPlanPolarProductId?: string
|
||||
createdAt?: Date
|
||||
currentPeriodStart?: Date
|
||||
currentPeriodEnd?: Date
|
||||
subscriptionStartDate?: Date
|
||||
subscriptionEndDate?: Date
|
||||
status?: string
|
||||
}) {
|
||||
console.log(`Switching plan for user: ${userId}`);
|
||||
console.log(`Switching plan for user: ${userId}`)
|
||||
|
||||
// Convert userId to ObjectId
|
||||
const userObjectId = new Types.ObjectId(userId);
|
||||
const userObjectId = new Types.ObjectId(userId)
|
||||
|
||||
let plan: PlanDocument
|
||||
if (newPlanPolarProductId) {
|
||||
plan = await this.planModel.findOne({
|
||||
polarProductId: newPlanPolarProductId,
|
||||
$or: [
|
||||
// { polarProductId: newPlanPolarProductId }, // deprecated
|
||||
{ polarMonthlyProductId: newPlanPolarProductId },
|
||||
{ polarYearlyProductId: newPlanPolarProductId },
|
||||
],
|
||||
})
|
||||
} else if (newPlanName) {
|
||||
plan = await this.planModel.findOne({ name: newPlanName })
|
||||
@@ -249,24 +235,33 @@ export class BillingService {
|
||||
throw new Error('Plan not found')
|
||||
}
|
||||
|
||||
console.log(`Found plan: ${plan.name}`);
|
||||
console.log(`Found plan: ${plan.name}`)
|
||||
|
||||
// Deactivate current active subscriptions
|
||||
const result = await this.subscriptionModel.updateMany(
|
||||
{ user: userObjectId, plan: { $ne: plan._id }, isActive: true },
|
||||
{ isActive: false, endDate: new Date() },
|
||||
)
|
||||
console.log(`Deactivated subscriptions: ${result.modifiedCount}`);
|
||||
console.log(`Deactivated subscriptions: ${result.modifiedCount}`)
|
||||
|
||||
// Create or update the new subscription
|
||||
const updateResult = await this.subscriptionModel.updateOne(
|
||||
{ user: userObjectId, plan: plan._id },
|
||||
{ isActive: true },
|
||||
{
|
||||
isActive: true,
|
||||
currentPeriodStart,
|
||||
currentPeriodEnd,
|
||||
subscriptionStartDate,
|
||||
subscriptionEndDate,
|
||||
status,
|
||||
},
|
||||
{ upsert: true },
|
||||
)
|
||||
console.log(`Updated or created subscription: ${updateResult.upsertedCount > 0 ? 'Created' : 'Updated'}`);
|
||||
console.log(
|
||||
`Updated or created subscription: ${updateResult.upsertedCount > 0 ? 'Created' : 'Updated'}`,
|
||||
)
|
||||
|
||||
return { success: true, plan: plan.name };
|
||||
return { success: true, plan: plan.name }
|
||||
}
|
||||
|
||||
async canPerformAction(
|
||||
@@ -274,9 +269,6 @@ export class BillingService {
|
||||
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
|
||||
@@ -306,7 +298,7 @@ export class BillingService {
|
||||
user: userId,
|
||||
isActive: true,
|
||||
})
|
||||
|
||||
|
||||
if (!subscription) {
|
||||
plan = await this.planModel.findOne({ name: 'free' })
|
||||
} else {
|
||||
|
||||
@@ -26,6 +26,24 @@ export class Plan {
|
||||
@Prop({ type: String, unique: true })
|
||||
polarProductId?: string
|
||||
|
||||
@Prop({ type: String, unique: true })
|
||||
polarMonthlyProductId?: string
|
||||
|
||||
@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
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import { SMSBatch } from './schemas/sms-batch.schema'
|
||||
import {
|
||||
BatchResponse,
|
||||
Message,
|
||||
} from 'firebase-admin/lib/messaging/messaging-api'
|
||||
} from 'firebase-admin/messaging'
|
||||
import { WebhookEvent } from 'src/webhook/webhook-event.enum'
|
||||
import { WebhookService } from 'src/webhook/webhook.service'
|
||||
import { BillingService } from 'src/billing/billing.service'
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "ESNext",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "es2017",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
|
||||
Reference in New Issue
Block a user