From 8b67496951a1209f45dcca256ba1ee8487afe699 Mon Sep 17 00:00:00 2001 From: forehalo Date: Mon, 17 Mar 2025 10:17:14 +0000 Subject: [PATCH] refactor(server): saving past_due subscription in db (#10908) close CLOUD-122 --- .../backend/server/src/models/user-feature.ts | 10 +++-- .../server/src/models/workspace-feature.ts | 10 +++-- .../src/plugins/payment/manager/user.ts | 39 +++++++++++-------- .../src/plugins/payment/manager/workspace.ts | 39 ++++++++++++------- .../server/src/plugins/payment/service.ts | 10 +++-- 5 files changed, 65 insertions(+), 43 deletions(-) diff --git a/packages/backend/server/src/models/user-feature.ts b/packages/backend/server/src/models/user-feature.ts index d8bc5a6ec..8904521db 100644 --- a/packages/backend/server/src/models/user-feature.ts +++ b/packages/backend/server/src/models/user-feature.ts @@ -107,7 +107,7 @@ export class UserFeatureModel extends BaseModel { } async remove(userId: string, featureName: UserFeatureName) { - await this.db.userFeature.updateMany({ + const { count } = await this.db.userFeature.updateMany({ where: { userId, name: featureName, @@ -117,9 +117,11 @@ export class UserFeatureModel extends BaseModel { }, }); - this.logger.verbose( - `Feature ${featureName} deactivated for user ${userId}` - ); + if (count > 0) { + this.logger.verbose( + `Feature ${featureName} deactivated for user ${userId}` + ); + } } @Transactional() diff --git a/packages/backend/server/src/models/workspace-feature.ts b/packages/backend/server/src/models/workspace-feature.ts index 90d82b6be..b066f3466 100644 --- a/packages/backend/server/src/models/workspace-feature.ts +++ b/packages/backend/server/src/models/workspace-feature.ts @@ -194,15 +194,17 @@ export class WorkspaceFeatureModel extends BaseModel { } async remove(workspaceId: string, featureName: WorkspaceFeatureName) { - await this.db.workspaceFeature.deleteMany({ + const { count } = await this.db.workspaceFeature.deleteMany({ where: { workspaceId, name: featureName, }, }); - this.logger.verbose( - `Feature ${featureName} removed from workspace ${workspaceId}` - ); + if (count > 0) { + this.logger.verbose( + `Feature ${featureName} removed from workspace ${workspaceId}` + ); + } } } diff --git a/packages/backend/server/src/plugins/payment/manager/user.ts b/packages/backend/server/src/plugins/payment/manager/user.ts index 070a3254a..403d06c85 100644 --- a/packages/backend/server/src/plugins/payment/manager/user.ts +++ b/packages/backend/server/src/plugins/payment/manager/user.ts @@ -213,14 +213,22 @@ export class UserSubscriptionManager extends SubscriptionManager { // update features first, features modify are idempotent // so there is no need to skip if a subscription already exists. - // TODO(@forehalo): - // we should move the subscription feature updating logic back to payment module, - // because quota or feature module themself should not be aware of what payment or subscription is. - this.event.emit('user.subscription.activated', { - userId, - plan: lookupKey.plan, - recurring: lookupKey.recurring, - }); + if ( + stripeSubscription.status === SubscriptionStatus.Active || + stripeSubscription.status === SubscriptionStatus.Trialing + ) { + this.event.emit('user.subscription.activated', { + userId, + plan: lookupKey.plan, + recurring: lookupKey.recurring, + }); + } else { + this.event.emit('user.subscription.canceled', { + userId, + plan: lookupKey.plan, + recurring: lookupKey.recurring, + }); + } const subscriptionData = this.transformSubscription(subscription); @@ -247,20 +255,17 @@ export class UserSubscriptionManager extends SubscriptionManager { stripeSubscription, }: KnownStripeSubscription) { this.assertUserIdExists(userId); + this.event.emit('user.subscription.canceled', { + userId, + plan: lookupKey.plan, + recurring: lookupKey.recurring, + }); - const deleted = await this.db.subscription.deleteMany({ + await this.db.subscription.deleteMany({ where: { stripeSubscriptionId: stripeSubscription.id, }, }); - - if (deleted.count > 0) { - this.event.emit('user.subscription.canceled', { - userId, - plan: lookupKey.plan, - recurring: lookupKey.recurring, - }); - } } async cancelSubscription(subscription: Subscription) { diff --git a/packages/backend/server/src/plugins/payment/manager/workspace.ts b/packages/backend/server/src/plugins/payment/manager/workspace.ts index 3e5d28c45..40fbeb0a4 100644 --- a/packages/backend/server/src/plugins/payment/manager/workspace.ts +++ b/packages/backend/server/src/plugins/payment/manager/workspace.ts @@ -137,12 +137,23 @@ export class WorkspaceSubscriptionManager extends SubscriptionManager { const subscriptionData = this.transformSubscription(subscription); - this.event.emit('workspace.subscription.activated', { - workspaceId, - plan: lookupKey.plan, - recurring: lookupKey.recurring, - quantity: subscriptionData.quantity, - }); + if ( + stripeSubscription.status === SubscriptionStatus.Active || + stripeSubscription.status === SubscriptionStatus.Trialing + ) { + this.event.emit('workspace.subscription.activated', { + workspaceId, + plan: lookupKey.plan, + recurring: lookupKey.recurring, + quantity: subscriptionData.quantity, + }); + } else { + this.event.emit('workspace.subscription.canceled', { + workspaceId, + plan: lookupKey.plan, + recurring: lookupKey.recurring, + }); + } return this.db.subscription.upsert({ where: { @@ -176,17 +187,15 @@ export class WorkspaceSubscriptionManager extends SubscriptionManager { ); } - const deleted = await this.db.subscription.deleteMany({ - where: { stripeSubscriptionId: stripeSubscription.id }, + this.event.emit('workspace.subscription.canceled', { + workspaceId, + plan: lookupKey.plan, + recurring: lookupKey.recurring, }); - if (deleted.count > 0) { - this.event.emit('workspace.subscription.canceled', { - workspaceId, - plan: lookupKey.plan, - recurring: lookupKey.recurring, - }); - } + await this.db.subscription.deleteMany({ + where: { stripeSubscriptionId: stripeSubscription.id }, + }); } getSubscription(identity: z.infer) { diff --git a/packages/backend/server/src/plugins/payment/service.ts b/packages/backend/server/src/plugins/payment/service.ts index 9587b2de7..178ff8bac 100644 --- a/packages/backend/server/src/plugins/payment/service.ts +++ b/packages/backend/server/src/plugins/payment/service.ts @@ -467,13 +467,17 @@ export class SubscriptionService implements OnApplicationBootstrap { throw new InternalServerError('Failed to parse stripe subscription.'); } - const isPlanActive = + const shouldSave = subscription.status === SubscriptionStatus.Active || - subscription.status === SubscriptionStatus.Trialing; + subscription.status === SubscriptionStatus.Trialing || + // PastDue is a temporary status, it will be cancelled after all recurring payments retries failed. + // Saved in db to let users be able to cancel further retries manually. + subscription.status === SubscriptionStatus.PastDue; const manager = this.select(knownSubscription.lookupKey.plan); - if (!isPlanActive) { + // TODO(@forehalo): trigger 'subscription.status.changed' event to let strategy handle them. after migrated to Model + if (!shouldSave) { await manager.deleteStripeSubscription(knownSubscription); } else { await manager.saveStripeSubscription(knownSubscription);