11import { AppConfig } from "@/config/type" ;
2+ import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository" ;
23import { BILLING_QUEUE , INCREMENT_JOB , IncrementJobDataType } from "@/modules/billing/billing.processor" ;
34import { BillingRepository } from "@/modules/billing/billing.repository" ;
45import { BillingConfigService } from "@/modules/billing/services/billing.config.service" ;
56import { PlatformPlan } from "@/modules/billing/types" ;
7+ import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository" ;
68import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository" ;
79import { StripeService } from "@/modules/stripe/stripe.service" ;
10+ import { UsersRepository } from "@/modules/users/users.repository" ;
811import { InjectQueue } from "@nestjs/bull" ;
912import {
1013 BadRequestException ,
@@ -30,6 +33,9 @@ export class BillingService implements OnModuleDestroy {
3033 private readonly billingRepository : BillingRepository ,
3134 private readonly configService : ConfigService < AppConfig > ,
3235 private readonly billingConfigService : BillingConfigService ,
36+ private readonly usersRepository : UsersRepository ,
37+ private readonly oAuthClientRepository : OAuthClientRepository ,
38+ private readonly bookingsRepository : BookingsRepository_2024_08_13 ,
3339 @InjectQueue ( BILLING_QUEUE ) private readonly billingQueue : Queue
3440 ) {
3541 this . webAppUrl = this . configService . get ( "app.baseUrl" , { infer : true } ) ?? "https://app.cal.com" ;
@@ -119,7 +125,7 @@ export class BillingService implements OnModuleDestroy {
119125 return url ;
120126 }
121127
122- async setSubscriptionForTeam ( teamId : number , subscriptionId : string , plan : PlatformPlan ) {
128+ async setPerBookingSubscriptionForTeam ( teamId : number , subscriptionId : string , plan : PlatformPlan ) {
123129 const billingCycleStart = DateTime . now ( ) . get ( "day" ) ;
124130 const billingCycleEnd = DateTime . now ( ) . plus ( { month : 1 } ) . get ( "day" ) ;
125131
@@ -132,6 +138,25 @@ export class BillingService implements OnModuleDestroy {
132138 ) ;
133139 }
134140
141+ async setPerActiveUserSubscriptionForTeam (
142+ teamId : number ,
143+ subscriptionId : string ,
144+ plan : PlatformPlan ,
145+ priceId : string
146+ ) {
147+ const billingCycleStart = DateTime . now ( ) . get ( "day" ) ;
148+ const billingCycleEnd = DateTime . now ( ) . plus ( { month : 1 } ) . get ( "day" ) ;
149+
150+ return this . billingRepository . updateTeamBilling (
151+ teamId ,
152+ billingCycleStart ,
153+ billingCycleEnd ,
154+ plan ,
155+ subscriptionId ,
156+ priceId
157+ ) ;
158+ }
159+
135160 async handleStripeSubscriptionDeleted ( event : Stripe . Event ) {
136161 const subscription = event . data . object as Stripe . Subscription ;
137162 const teamId = subscription ?. metadata ?. teamId ;
@@ -204,9 +229,19 @@ export class BillingService implements OnModuleDestroy {
204229 this . logger . log ( "Webhook received but not pertaining to Platform, discarding." ) ;
205230 return ;
206231 }
232+ const isPriceIdPresent = Boolean ( checkoutSession . metadata ?. priceId ) ;
207233
208- if ( checkoutSession . mode === "subscription" ) {
209- await this . setSubscriptionForTeam (
234+ if ( checkoutSession . mode === "subscription" && isPriceIdPresent ) {
235+ await this . setPerActiveUserSubscriptionForTeam (
236+ teamId ,
237+ checkoutSession . subscription as string ,
238+ PlatformPlan [ plan . toUpperCase ( ) as keyof typeof PlatformPlan ] ,
239+ checkoutSession . metadata ?. priceId
240+ ) ;
241+ }
242+
243+ if ( checkoutSession . mode === "subscription" && ! isPriceIdPresent ) {
244+ await this . setPerBookingSubscriptionForTeam (
210245 teamId ,
211246 checkoutSession . subscription as string ,
212247 PlatformPlan [ plan . toUpperCase ( ) as keyof typeof PlatformPlan ]
@@ -220,6 +255,67 @@ export class BillingService implements OnModuleDestroy {
220255 return ;
221256 }
222257
258+ async handleStripeSubscriptionForActiveManagedUsers ( event : Stripe . Event ) {
259+ const invoice = event . data . object as Stripe . Invoice ;
260+ const subscriptionId = this . getSubscriptionIdFromInvoice ( invoice ) ;
261+
262+ if ( ! subscriptionId ) {
263+ throw new NotFoundException ( "No subscription found for team" ) ;
264+ }
265+
266+ const teamWithBilling = await this . billingRepository . getBillingForTeamBySubscriptionId ( subscriptionId ) ;
267+
268+ if ( teamWithBilling ?. plan === "PER_ACTIVE_USER" ) {
269+ const activeManagedUsersCount = await this . getActiveManagedUsersCount (
270+ subscriptionId ,
271+ new Date ( invoice . period_start * 1000 ) ,
272+ new Date ( invoice . period_end * 1000 )
273+ ) ;
274+
275+ await this . stripeService . getStripe ( ) . subscriptions . update ( subscriptionId , {
276+ items : [
277+ {
278+ quantity : activeManagedUsersCount > 0 ? activeManagedUsersCount : 1 ,
279+ } ,
280+ ] ,
281+ } ) ;
282+ }
283+ }
284+
285+ async getActiveManagedUsersCount ( subscriptionId : string , invoiceStart : Date , invoiceEnd : Date ) {
286+ const managedUsersEmails = await this . usersRepository . getOrgsManagedUserEmailsBySubscriptionId (
287+ subscriptionId
288+ ) ;
289+
290+ if ( ! managedUsersEmails ) return 0 ;
291+
292+ if ( ! invoiceStart || ! invoiceEnd ) {
293+ this . logger . log ( "Invoice period start or end date is null" ) ;
294+ return 0 ;
295+ }
296+
297+ const activeManagedUserEmailsAsHost = await this . usersRepository . getActiveManagedUsersAsHost (
298+ subscriptionId ,
299+ invoiceStart ,
300+ invoiceEnd
301+ ) ;
302+
303+ const activeHostEmails = activeManagedUserEmailsAsHost . map ( ( email ) => email . email ) ;
304+ const notActiveHostEmails = managedUsersEmails
305+ . filter ( ( email ) => ! activeHostEmails . includes ( email . email ) )
306+ . map ( ( email ) => email . email ) ;
307+
308+ if ( notActiveHostEmails . length === 0 ) return activeManagedUserEmailsAsHost . length ;
309+
310+ const activeManagedUserEmailsAsAttendee = await this . usersRepository . getActiveManagedUsersAsAttendee (
311+ notActiveHostEmails ,
312+ invoiceStart ,
313+ invoiceEnd
314+ ) ;
315+
316+ return activeManagedUserEmailsAsAttendee . length + activeManagedUserEmailsAsHost . length ;
317+ }
318+
223319 async updateStripeSubscriptionForTeam ( teamId : number , plan : PlatformPlan ) {
224320 const teamWithBilling = await this . teamsRepository . findByIdIncludeBilling ( teamId ) ;
225321
@@ -263,7 +359,7 @@ export class BillingService implements OnModuleDestroy {
263359 proration_behavior : "create_prorations" ,
264360 } ) ;
265361
266- await this . setSubscriptionForTeam (
362+ await this . setPerBookingSubscriptionForTeam (
267363 teamId ,
268364 teamWithBilling ?. platformBilling ?. subscriptionId ,
269365 PlatformPlan [ plan . toUpperCase ( ) as keyof typeof PlatformPlan ]
0 commit comments