diff --git a/src/app/api/admin/StripeController.ts b/src/app/api/admin/StripeController.ts index cfbf0ad1..5586432c 100644 --- a/src/app/api/admin/StripeController.ts +++ b/src/app/api/admin/StripeController.ts @@ -1,7 +1,7 @@ import authHandler from "@App/middlewares/AuthHandler"; // import roleHandler from "@App/middlewares/RolesHandler"; import ApiController from "@Common/system/controller-pattern/ApiController"; -import { Controller, Get, Post } from "@ControllerPattern/index"; +import { Controller, Get, Post, Put } from "@ControllerPattern/index"; import StripeService from "@Services/common/StripeService/StripeService"; import { validateOrReject } from "class-validator"; import { Request, Response } from "express"; @@ -40,6 +40,33 @@ export default class StripeController extends ApiController { } } + @Put("/api/v1/admin/stripe/:uid") + protected async createStripeSubscriptionUpdateCheckout(req: Request, response: Response) { + try { + const uid = req.params["uid"]; + if (!uid) { + this.httpBadRequest(response, "No uid provided"); + return; + } + const officeId: string = req.body.user.office_Id; + + //add office id to request body + req.body.office = { uid: officeId }; + + //init Subscription resource with request body values + const subscriptionEntity = Subscription.hydrate(req.body, { strategy: "excludeAll" }); + + await validateOrReject(subscriptionEntity, { groups: ["updateSubscription"], forbidUnknownValues: false }); + + const stripeSession = await this.stripeService.createCheckoutSessionUpdate(uid, subscriptionEntity); + + this.httpCreated(response, stripeSession); + } catch (error) { + this.httpInternalError(response, error); + return; + } + } + @Get("/api/v1/admin/stripe/:uid", [authHandler]) protected async getClientPortalSession(req: Request, response: Response) { try { diff --git a/src/common/repositories/SubscriptionsRepository.ts b/src/common/repositories/SubscriptionsRepository.ts index ee5f787f..0b6fa624 100644 --- a/src/common/repositories/SubscriptionsRepository.ts +++ b/src/common/repositories/SubscriptionsRepository.ts @@ -85,8 +85,7 @@ export default class SubscriptionsRepository extends BaseRepository { /** * @description : update given subscription */ - public async update(uid: string, subscription: Subscription): Promise { - + public async update(uid: string, subscription: Subscription): Promise { if(subscription.type === "STANDARD") { const updateArgs: Prisma.SubscriptionsUpdateArgs = { @@ -98,16 +97,6 @@ export default class SubscriptionsRepository extends BaseRepository { type: ESubscriptionType.STANDARD, status: subscription.status as ESubscriptionStatus, nb_seats: subscription.nb_seats!, - seats: { - deleteMany: {}, - create: subscription.seats!.map(seat => ({ - user: { - connect: { - uid: seat.user!.uid, - }, - }, - })), - } }, }; return this.model.update(updateArgs); @@ -132,5 +121,16 @@ export default class SubscriptionsRepository extends BaseRepository { } } + /** + * @description : Delete a subscription + */ + public async delete(uid: string) { + return this.model.delete({ + where: { + uid: uid, + }, + }); + } + } diff --git a/src/common/webhooks/stripeWebhooks.ts b/src/common/webhooks/stripeWebhooks.ts index fee0e6bd..b0832ece 100644 --- a/src/common/webhooks/stripeWebhooks.ts +++ b/src/common/webhooks/stripeWebhooks.ts @@ -1,3 +1,4 @@ +import { BackendVariables } from "@Common/config/variables/Variables"; import ApiController from "@Common/system/controller-pattern/ApiController"; import { Controller, Post } from "@ControllerPattern/index"; import SubscriptionsService from "@Services/admin/SubscriptionsService/SubscriptionsService.ts"; @@ -10,7 +11,7 @@ import { Service } from "typedi"; @Controller() @Service() export default class StripeWebhooks extends ApiController { - constructor(private stripeService: StripeService, private subscriptionsService: SubscriptionsService) { + constructor(private stripeService: StripeService, private subscriptionsService: SubscriptionsService, private backendVariables: BackendVariables) { super(); } @@ -20,22 +21,32 @@ export default class StripeWebhooks extends ApiController { @Post("/api/v1/webhooks/stripe") protected async post(req: Request, response: Response) { try { - // const sig = req.headers["stripe-signature"]; - // const endpointSecret = "whsec_c4088876914bc166ff5c39253207f84900820b67f7bba3b2669c0ff392cbc838"; - // const stripe = this.stripeService.getClient(); - // let event: Stripe.Event; - - // if (!sig || !endpointSecret) { - // throw new Error("Signature verification failed"); - // } - // event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret); const event = req.body; switch (event.type) { + case "invoice.payment_succeeded": + if (event.data.object.billing_reason !== "subscription_update") break; + const stripeSubscription = await this.stripeService.getClient().subscriptions.retrieve(event.data.object.subscription); + const existingSubscription = await this.subscriptionsService.get({where : {stripe_subscription_id : stripeSubscription.id}}); + if(!existingSubscription[0]) break; + + const subscriptionUpdate: any = {}; + subscriptionUpdate.start_date = new Date(stripeSubscription.current_period_start * 1000); + subscriptionUpdate.end_date = new Date(stripeSubscription.current_period_end * 1000); + subscriptionUpdate.nb_seats = stripeSubscription.items.data[0]?.quantity; + subscriptionUpdate.type = stripeSubscription.items.data[0]?.price?.id === this.backendVariables.STRIPE_STANDARD_SUBSCRIPTION_PRICE_ID ? "STANDARD" : "UNLIMITED"; + + const subscriptionEntityUpdate = Subscription.hydrate(subscriptionUpdate); + + await validateOrReject(subscriptionEntityUpdate, { groups: ["updateSubscription"], forbidUnknownValues: false }); + + await this.subscriptionsService.update(existingSubscription[0].uid ,subscriptionEntityUpdate); + case "checkout.session.completed": - if (event.data.object.status !== "complete") break; + if (event.data.object.status !== "complete") break; const subscription = JSON.parse(event.data.object.metadata.subscription); + subscription.stripe_subscription_id = event.data.object.subscription; const subscriptionInfo = await this.stripeService @@ -48,9 +59,13 @@ export default class StripeWebhooks extends ApiController { const subscriptionEntity = Subscription.hydrate(subscription); await validateOrReject(subscriptionEntity, { groups: ["createSubscription"], forbidUnknownValues: false }); - await this.subscriptionsService.create(subscriptionEntity); break; + + case "customer.subscription.deleted": + const subscriptionToDelete = await this.subscriptionsService.get({where : {stripe_subscription_id : event.data.object.id}}); + if(!subscriptionToDelete[0]) break; + await this.subscriptionsService.delete(subscriptionToDelete[0].uid); default: break; } diff --git a/src/services/admin/SubscriptionsService/SubscriptionsService.ts.ts b/src/services/admin/SubscriptionsService/SubscriptionsService.ts.ts index 71e31fd9..0f311f03 100644 --- a/src/services/admin/SubscriptionsService/SubscriptionsService.ts.ts +++ b/src/services/admin/SubscriptionsService/SubscriptionsService.ts.ts @@ -39,7 +39,15 @@ export default class SubscriptionsService extends BaseService { * @description : Modify a subscription * @throws {Error} If subscription cannot be modified */ - public async update(uid: string, subscriptionEntity: Subscription): Promise { + public async update(uid: string, subscriptionEntity: Subscription): Promise { return this.subscriptionsRepository.update(uid, subscriptionEntity); } + + /** + * @description : Delete a subscription + * @throws {Error} If subscription cannot be deleted + */ + public async delete(uid: string) { + return this.subscriptionsRepository.delete(uid); + } } diff --git a/src/services/common/StripeService/StripeService.ts b/src/services/common/StripeService/StripeService.ts index a6163dfa..e8e1ee8e 100644 --- a/src/services/common/StripeService/StripeService.ts +++ b/src/services/common/StripeService/StripeService.ts @@ -15,26 +15,76 @@ export default class StripeService { } public async createCheckoutSession(subscription: Subscription) { - const priceId = - subscription.type === "STANDARD" - ? this.variables.STRIPE_STANDARD_SUBSCRIPTION_PRICE_ID - : this.variables.STRIPE_UNLIMITED_SUBSCRIPTION_PRICE_ID; - return this.client.checkout.sessions.create({ - mode: "subscription", - payment_method_types: ["card", "paypal"], - billing_address_collection: "auto", - line_items: [ - { - price: priceId, - quantity: subscription.type === "STANDARD" ? subscription.nb_seats : 1, + const priceId = subscription.type === "STANDARD" ? this.variables.STRIPE_STANDARD_SUBSCRIPTION_PRICE_ID : this.variables.STRIPE_UNLIMITED_SUBSCRIPTION_PRICE_ID; + return this.client.checkout.sessions.create({ + mode: "subscription", + payment_method_types: ["card", "paypal"], + billing_address_collection: "auto", + line_items: [ + { + price: priceId, + quantity: subscription.type === "STANDARD" ? subscription.nb_seats : 1, + }, + ], + success_url: this.variables.APP_HOST + "/subscription/success", + cancel_url: this.variables.APP_HOST + "/subscription/error", + metadata: { + subscription: JSON.stringify(subscription), }, - ], - success_url: this.variables.APP_HOST + "/subscription/success", - cancel_url: this.variables.APP_HOST + "/subscription/error", - metadata: { - subscription: JSON.stringify(subscription), - }, - }); + }); + + + + } + + public async createCheckoutSessionUpdate(uid: string, subscription: Subscription) { + + + + // return this.client.checkout.sessions.create({ + // mode: "payment", + // payment_method_types: ["card", "paypal"], + // billing_address_collection: "auto", + // success_url: this.variables.APP_HOST + "/subscription/success", + // cancel_url: this.variables.APP_HOST + "/subscription/error", + // metadata: { + // subscription: JSON.stringify(subscription), + // }, + // }); + + // const priceId = + // subscription.type === "STANDARD" + // ? this.variables.STRIPE_STANDARD_SUBSCRIPTION_PRICE_ID + // : this.variables.STRIPE_UNLIMITED_SUBSCRIPTION_PRICE_ID; + + // return this.client.checkout.sessions.create({ + // mode: "subscription", + // payment_method_types: ["card", "paypal"], + // billing_address_collection: "auto", + // line_items: [ + // { + // price: priceId, + // quantity: subscription.type === "STANDARD" ? subscription.nb_seats : 1, + // }, + // ], + // success_url: this.variables.APP_HOST + "/subscription/success", + // cancel_url: this.variables.APP_HOST + "/subscription/error", + // metadata: { + // subscription: JSON.stringify(subscription), + // }, + // }); + // const subscriptions = await this.client.subscriptions.retrieve(uid); + // const itemId = subscriptions.items.data[0]?.id; + + // return await this.client.subscriptions.update(uid, { + // items: [ + // { + // id: itemId, + // price: priceId, + // quantity: subscription.nb_seats, + // }, + // ], + // }); } public async createClientPortalSession(subscriptionId: string) {