From 992c260e1a57544cf522b74bd8ca386bf525213c Mon Sep 17 00:00:00 2001 From: isra el Date: Fri, 21 Feb 2025 20:21:36 +0300 Subject: [PATCH] chore(api): update polar sdk and improve billing and checkout logic --- api/package.json | 2 +- api/pnpm-lock.yaml | 10 +-- api/src/billing/billing.controller.ts | 8 +- api/src/billing/billing.dto.ts | 8 +- api/src/billing/billing.service.ts | 102 ++++++++++++------------- api/src/billing/schemas/plan.schema.ts | 18 +++++ api/src/gateway/gateway.service.ts | 2 +- api/tsconfig.json | 5 +- 8 files changed, 89 insertions(+), 66 deletions(-) diff --git a/api/package.json b/api/package.json index 1737884..81cd417 100644 --- a/api/package.json +++ b/api/package.json @@ -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", diff --git a/api/pnpm-lock.yaml b/api/pnpm-lock.yaml index 40a2d02..f606bcc 100644 --- a/api/pnpm-lock.yaml +++ b/api/pnpm-lock.yaml @@ -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 diff --git a/api/src/billing/billing.controller.ts b/api/src/billing/billing.controller.ts index bbf4910..56a37ca 100644 --- a/api/src/billing/billing.controller.ts +++ b/api/src/billing/billing.controller.ts @@ -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 diff --git a/api/src/billing/billing.dto.ts b/api/src/billing/billing.dto.ts index 13218bf..ce34868 100644 --- a/api/src/billing/billing.dto.ts +++ b/api/src/billing/billing.dto.ts @@ -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 diff --git a/api/src/billing/billing.service.ts b/api/src/billing/billing.service.ts index 145f378..3c4bd1b 100644 --- a/api/src/billing/billing.service.ts +++ b/api/src/billing/billing.service.ts @@ -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, ) { - 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 { 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 { diff --git a/api/src/billing/schemas/plan.schema.ts b/api/src/billing/schemas/plan.schema.ts index 584f970..b9c7dd9 100644 --- a/api/src/billing/schemas/plan.schema.ts +++ b/api/src/billing/schemas/plan.schema.ts @@ -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 } diff --git a/api/src/gateway/gateway.service.ts b/api/src/gateway/gateway.service.ts index 0091c28..ac2e5b9 100644 --- a/api/src/gateway/gateway.service.ts +++ b/api/src/gateway/gateway.service.ts @@ -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' diff --git a/api/tsconfig.json b/api/tsconfig.json index adb614c..c512908 100644 --- a/api/tsconfig.json +++ b/api/tsconfig.json @@ -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": "./",