refactor(server): saving past_due subscription in db (#10908)

close CLOUD-122
This commit is contained in:
forehalo
2025-03-17 10:17:14 +00:00
parent 9b5d12dc71
commit 8b67496951
5 changed files with 65 additions and 43 deletions
@@ -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()
@@ -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}`
);
}
}
}
@@ -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) {
@@ -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<typeof WorkspaceSubscriptionIdentity>) {
@@ -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);