From 0931705d6a13c8ac756361620656cb89db33d962 Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Fri, 17 Jan 2025 12:23:01 +0000 Subject: [PATCH] feat: implement subscription status refresh logic in BillingInvoiceService and UserAPI --- Common/Server/API/BillingInvoiceAPI.ts | 34 +----- .../Server/Services/BillingInvoiceService.ts | 109 ++++++++++++++++++ Common/Server/Services/ProjectService.ts | 2 + 3 files changed, 113 insertions(+), 32 deletions(-) diff --git a/Common/Server/API/BillingInvoiceAPI.ts b/Common/Server/API/BillingInvoiceAPI.ts index 095e8a4ffac..c6af6701306 100644 --- a/Common/Server/API/BillingInvoiceAPI.ts +++ b/Common/Server/API/BillingInvoiceAPI.ts @@ -14,7 +14,6 @@ import { import Response from "../Utils/Response"; import BaseAPI from "./BaseAPI"; import BaseModel from "Common/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel"; -import SubscriptionStatus from "Common/Types/Billing/SubscriptionStatus"; import BadDataException from "Common/Types/Exception/BadDataException"; import { JSONObject } from "Common/Types/JSON"; import Permission, { UserPermission } from "Common/Types/Permission"; @@ -127,37 +126,8 @@ export default class UserAPI extends BaseAPI< }, }); - // refresh subscription status. - const subscriptionState: SubscriptionStatus = - await BillingService.getSubscriptionStatus( - project.paymentProviderSubscriptionId as string, - ); - - const meteredSubscriptionState: SubscriptionStatus = - await BillingService.getSubscriptionStatus( - project.paymentProviderMeteredSubscriptionId as string, - ); - - // if subscription is cancelled, create a new subscription and update project. - - if ( - meteredSubscriptionState === SubscriptionStatus.Canceled || - subscriptionState === SubscriptionStatus.Canceled - ) { - await ProjectService.reactiveSubscription(project.id!); - } - - await ProjectService.updateOneById({ - id: project.id!, - data: { - paymentProviderSubscriptionStatus: subscriptionState, - paymentProviderMeteredSubscriptionStatus: - meteredSubscriptionState, - }, - props: { - isRoot: true, - ignoreHooks: true, - }, + await BillingInvoiceService.refreshSubscriptionStatus({ + projectId: project.id!, }); return Response.sendEmptySuccessResponse(req, res); diff --git a/Common/Server/Services/BillingInvoiceService.ts b/Common/Server/Services/BillingInvoiceService.ts index 17aaa87a75f..7b42adf5397 100644 --- a/Common/Server/Services/BillingInvoiceService.ts +++ b/Common/Server/Services/BillingInvoiceService.ts @@ -10,6 +10,8 @@ import Model, { InvoiceStatus, } from "Common/Models/DatabaseModels/BillingInvoice"; import Project from "Common/Models/DatabaseModels/Project"; +import SubscriptionStatus from "../../Types/Billing/SubscriptionStatus"; +import ObjectID from "../../Types/ObjectID"; export class Service extends DatabaseService { public constructor() { @@ -17,6 +19,107 @@ export class Service extends DatabaseService { this.setDoNotAllowDelete(true); } + + public async refreshSubscriptionStatus(data: { + projectId: ObjectID; + }) { + let project: Project | null = await ProjectService.findOneById({ + id: data.projectId, + props: { + isRoot: true, + }, + select: { + _id: true, + paymentProviderCustomerId: true, + paymentProviderSubscriptionId: true, + paymentProviderMeteredSubscriptionId: true, + }, + }); + + // refresh the subscription status. This is a hack to ensure that the subscription status is always up to date. + // This is because the subscription status can change at any time and we need to ensure that the subscription status is always up to date. + + if (!project) { + throw new BadDataException("Project not found"); + } + + if (!project.paymentProviderCustomerId) { + throw new BadDataException("Payment provider customer id not found."); + } + + let subscriptionState: SubscriptionStatus = + await BillingService.getSubscriptionStatus( + project.paymentProviderSubscriptionId as string, + ); + + let meteredSubscriptionState: SubscriptionStatus = + await BillingService.getSubscriptionStatus( + project.paymentProviderMeteredSubscriptionId as string, + ); + + if ( + meteredSubscriptionState === SubscriptionStatus.Canceled || + subscriptionState === SubscriptionStatus.Canceled + ) { + + // check if all invoices are paid. If yes, then reactivate the subscription. + + const invoices: Array = await BillingService.getInvoices( + project.paymentProviderCustomerId, + ); + + let allInvoicesPaid: boolean = true; + + for (const invoice of invoices) { + if (invoice.status === InvoiceStatus.Open || invoice.status === InvoiceStatus.Uncollectible) { + allInvoicesPaid = false; + break; + } + } + + if (allInvoicesPaid) { + + await ProjectService.reactiveSubscription(project.id!); + project = await ProjectService.findOneById({ + id: data.projectId, + props: { + isRoot: true, + }, + select: { + _id: true, + paymentProviderCustomerId: true, + paymentProviderSubscriptionId: true, + paymentProviderMeteredSubscriptionId: true, + }, + }); + + if (!project) { + throw new BadDataException("Project not found"); + } + + subscriptionState = await BillingService.getSubscriptionStatus( + project.paymentProviderSubscriptionId as string, + ); + + meteredSubscriptionState = await BillingService.getSubscriptionStatus( + project.paymentProviderMeteredSubscriptionId as string, + ); + } + } + + await ProjectService.updateOneById({ + id: project.id!, + data: { + paymentProviderSubscriptionStatus: subscriptionState, + paymentProviderMeteredSubscriptionStatus: meteredSubscriptionState, + }, + props: { + isRoot: true, + ignoreHooks: true, + }, + }); + } + protected override async onBeforeFind( findBy: FindBy, ): Promise> { @@ -37,6 +140,11 @@ export class Service extends DatabaseService { }, }); + // refresh the subscription status. This is a hack to ensure that the subscription status is always up to date. + // This is because the subscription status can change at any time and we need to ensure that the subscription status is always up to date. + + await this.refreshSubscriptionStatus({ projectId: findBy.props.tenantId! }); + if (!project) { throw new BadDataException("Project not found"); } @@ -45,6 +153,7 @@ export class Service extends DatabaseService { throw new BadDataException("Payment provider customer id not found."); } + const invoices: Array = await BillingService.getInvoices( project.paymentProviderCustomerId, ); diff --git a/Common/Server/Services/ProjectService.ts b/Common/Server/Services/ProjectService.ts index 8403dd4eda5..073436e2794 100755 --- a/Common/Server/Services/ProjectService.ts +++ b/Common/Server/Services/ProjectService.ts @@ -1224,6 +1224,8 @@ export class ProjectService extends DatabaseService { isRoot: true, }, }); + + } public getActiveProjectStatusQuery(): Query {