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

View File

@@ -107,7 +107,7 @@ export class UserFeatureModel extends BaseModel {
} }
async remove(userId: string, featureName: UserFeatureName) { async remove(userId: string, featureName: UserFeatureName) {
await this.db.userFeature.updateMany({ const { count } = await this.db.userFeature.updateMany({
where: { where: {
userId, userId,
name: featureName, name: featureName,
@@ -117,9 +117,11 @@ export class UserFeatureModel extends BaseModel {
}, },
}); });
this.logger.verbose( if (count > 0) {
`Feature ${featureName} deactivated for user ${userId}` this.logger.verbose(
); `Feature ${featureName} deactivated for user ${userId}`
);
}
} }
@Transactional() @Transactional()

View File

@@ -194,15 +194,17 @@ export class WorkspaceFeatureModel extends BaseModel {
} }
async remove(workspaceId: string, featureName: WorkspaceFeatureName) { async remove(workspaceId: string, featureName: WorkspaceFeatureName) {
await this.db.workspaceFeature.deleteMany({ const { count } = await this.db.workspaceFeature.deleteMany({
where: { where: {
workspaceId, workspaceId,
name: featureName, name: featureName,
}, },
}); });
this.logger.verbose( if (count > 0) {
`Feature ${featureName} removed from workspace ${workspaceId}` this.logger.verbose(
); `Feature ${featureName} removed from workspace ${workspaceId}`
);
}
} }
} }

View File

@@ -213,14 +213,22 @@ export class UserSubscriptionManager extends SubscriptionManager {
// update features first, features modify are idempotent // update features first, features modify are idempotent
// so there is no need to skip if a subscription already exists. // so there is no need to skip if a subscription already exists.
// TODO(@forehalo): if (
// we should move the subscription feature updating logic back to payment module, stripeSubscription.status === SubscriptionStatus.Active ||
// because quota or feature module themself should not be aware of what payment or subscription is. stripeSubscription.status === SubscriptionStatus.Trialing
this.event.emit('user.subscription.activated', { ) {
userId, this.event.emit('user.subscription.activated', {
plan: lookupKey.plan, userId,
recurring: lookupKey.recurring, 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); const subscriptionData = this.transformSubscription(subscription);
@@ -247,20 +255,17 @@ export class UserSubscriptionManager extends SubscriptionManager {
stripeSubscription, stripeSubscription,
}: KnownStripeSubscription) { }: KnownStripeSubscription) {
this.assertUserIdExists(userId); 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: { where: {
stripeSubscriptionId: stripeSubscription.id, stripeSubscriptionId: stripeSubscription.id,
}, },
}); });
if (deleted.count > 0) {
this.event.emit('user.subscription.canceled', {
userId,
plan: lookupKey.plan,
recurring: lookupKey.recurring,
});
}
} }
async cancelSubscription(subscription: Subscription) { async cancelSubscription(subscription: Subscription) {

View File

@@ -137,12 +137,23 @@ export class WorkspaceSubscriptionManager extends SubscriptionManager {
const subscriptionData = this.transformSubscription(subscription); const subscriptionData = this.transformSubscription(subscription);
this.event.emit('workspace.subscription.activated', { if (
workspaceId, stripeSubscription.status === SubscriptionStatus.Active ||
plan: lookupKey.plan, stripeSubscription.status === SubscriptionStatus.Trialing
recurring: lookupKey.recurring, ) {
quantity: subscriptionData.quantity, 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({ return this.db.subscription.upsert({
where: { where: {
@@ -176,17 +187,15 @@ export class WorkspaceSubscriptionManager extends SubscriptionManager {
); );
} }
const deleted = await this.db.subscription.deleteMany({ this.event.emit('workspace.subscription.canceled', {
where: { stripeSubscriptionId: stripeSubscription.id }, workspaceId,
plan: lookupKey.plan,
recurring: lookupKey.recurring,
}); });
if (deleted.count > 0) { await this.db.subscription.deleteMany({
this.event.emit('workspace.subscription.canceled', { where: { stripeSubscriptionId: stripeSubscription.id },
workspaceId, });
plan: lookupKey.plan,
recurring: lookupKey.recurring,
});
}
} }
getSubscription(identity: z.infer<typeof WorkspaceSubscriptionIdentity>) { getSubscription(identity: z.infer<typeof WorkspaceSubscriptionIdentity>) {

View File

@@ -467,13 +467,17 @@ export class SubscriptionService implements OnApplicationBootstrap {
throw new InternalServerError('Failed to parse stripe subscription.'); throw new InternalServerError('Failed to parse stripe subscription.');
} }
const isPlanActive = const shouldSave =
subscription.status === SubscriptionStatus.Active || 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); 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); await manager.deleteStripeSubscription(knownSubscription);
} else { } else {
await manager.saveStripeSubscription(knownSubscription); await manager.saveStripeSubscription(knownSubscription);