feat(server): supplement team email remind (#9483)

fix PD-2047 AF-1996
This commit is contained in:
darkskygit
2025-01-21 05:18:02 +00:00
parent 42428fbf99
commit 1116a1d74e
13 changed files with 345 additions and 17 deletions

View File

@@ -17,6 +17,77 @@ export class SubscriptionCronJobs {
private readonly event: EventEmitter
) {}
private getDateRange(after: number, base: number | Date = Date.now()) {
const start = new Date(base);
start.setDate(start.getDate() + after);
start.setHours(0, 0, 0, 0);
const end = new Date(start);
end.setHours(23, 59, 59, 999);
return { start, end };
}
// TODO(@darkskygit): enable this after the cluster event system is ready
// @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT /* everyday at 12am */)
async notifyExpiredWorkspace() {
const { start: after30DayStart, end: after30DayEnd } =
this.getDateRange(30);
const { start: todayStart, end: todayEnd } = this.getDateRange(0);
const { start: before150DaysStart, end: before150DaysEnd } =
this.getDateRange(-150);
const { start: before180DaysStart, end: before180DaysEnd } =
this.getDateRange(-180);
const subscriptions = await this.db.subscription.findMany({
where: {
plan: SubscriptionPlan.Team,
OR: [
{
// subscription will cancel after 30 days
status: 'active',
canceledAt: { not: null },
end: { gte: after30DayStart, lte: after30DayEnd },
},
{
// subscription will cancel today
status: 'active',
canceledAt: { not: null },
end: { gte: todayStart, lte: todayEnd },
},
{
// subscription has been canceled for 150 days
// workspace becomes delete after 180 days
status: 'canceled',
canceledAt: { gte: before150DaysStart, lte: before150DaysEnd },
},
{
// subscription has been canceled for 180 days
// workspace becomes delete after 180 days
status: 'canceled',
canceledAt: { gte: before180DaysStart, lte: before180DaysEnd },
},
],
},
});
for (const subscription of subscriptions) {
const end = subscription.end;
if (!end) {
// should not reach here
continue;
}
this.event.emit('workspace.subscription.notify', {
workspaceId: subscription.targetId,
expirationDate: end,
deletionDate:
subscription.status === 'canceled'
? this.getDateRange(180, end).end
: undefined,
});
}
}
@Cron(CronExpression.EVERY_HOUR)
async cleanExpiredOnetimeSubscriptions() {
const subscriptions = await this.db.subscription.findMany({

View File

@@ -5,6 +5,7 @@ import { FeatureModule } from '../../core/features';
import { PermissionModule } from '../../core/permission';
import { QuotaModule } from '../../core/quota';
import { UserModule } from '../../core/user';
import { WorkspaceModule } from '../../core/workspaces';
import { Plugin } from '../registry';
import { StripeWebhookController } from './controller';
import { SubscriptionCronJobs } from './cron';
@@ -24,7 +25,13 @@ import { StripeWebhook } from './webhook';
@Plugin({
name: 'payment',
imports: [FeatureModule, QuotaModule, UserModule, PermissionModule],
imports: [
FeatureModule,
QuotaModule,
UserModule,
PermissionModule,
WorkspaceModule,
],
providers: [
StripeProvider,
SubscriptionService,

View File

@@ -1,15 +1,22 @@
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import type { EventPayload } from '../../base';
import { type EventPayload } from '../../base';
import { PermissionService } from '../../core/permission';
import { QuotaManagementService, QuotaType } from '../../core/quota';
import {
QuotaManagementService,
QuotaService,
QuotaType,
} from '../../core/quota';
import { WorkspaceService } from '../../core/workspaces/resolvers';
@Injectable()
export class TeamQuotaOverride {
constructor(
private readonly quota: QuotaService,
private readonly manager: QuotaManagementService,
private readonly permission: PermissionService
private readonly permission: PermissionService,
private readonly workspace: WorkspaceService
) {}
@OnEvent('workspace.subscription.activated')
@@ -20,7 +27,11 @@ export class TeamQuotaOverride {
quantity,
}: EventPayload<'workspace.subscription.activated'>) {
switch (plan) {
case 'team':
case 'team': {
const hasTeamWorkspace = await this.quota.hasWorkspaceQuota(
workspaceId,
QuotaType.TeamPlanV1
);
await this.manager.addTeamWorkspace(
workspaceId,
`${recurring} team subscription activated`
@@ -31,7 +42,13 @@ export class TeamQuotaOverride {
{ memberLimit: quantity }
);
await this.permission.refreshSeatStatus(workspaceId, quantity);
if (!hasTeamWorkspace) {
// this event will triggered when subscription is activated or changed
// we only send emails when the team workspace is activated
await this.workspace.sendTeamWorkspaceUpgradedEmail(workspaceId);
}
break;
}
default:
break;
}

View File

@@ -78,6 +78,11 @@ declare module '../../base/event/def' {
plan: SubscriptionPlan;
recurring: SubscriptionRecurring;
}>;
notify: Payload<{
workspaceId: Workspace['id'];
expirationDate: Date;
deletionDate: Date | undefined;
}>;
};
}
}