mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-07 01:09:54 +08:00
@@ -31,6 +31,7 @@ import {
|
|||||||
leaveWorkspace,
|
leaveWorkspace,
|
||||||
PermissionEnum,
|
PermissionEnum,
|
||||||
revokeInviteLink,
|
revokeInviteLink,
|
||||||
|
revokeMember,
|
||||||
revokeUser,
|
revokeUser,
|
||||||
signUp,
|
signUp,
|
||||||
sleep,
|
sleep,
|
||||||
@@ -722,5 +723,33 @@ test('should be able to emit events', async t => {
|
|||||||
],
|
],
|
||||||
'should emit owner transferred event'
|
'should emit owner transferred event'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await revokeMember(app, read.token.token, tws.id, owner.id);
|
||||||
|
const [memberRemoved, memberUpdated] = event.emit
|
||||||
|
.getCalls()
|
||||||
|
.map(call => call.args)
|
||||||
|
.toReversed();
|
||||||
|
t.deepEqual(
|
||||||
|
memberRemoved,
|
||||||
|
[
|
||||||
|
'workspace.members.removed',
|
||||||
|
{
|
||||||
|
userId: owner.id,
|
||||||
|
workspaceId: tws.id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'should emit owner transferred event'
|
||||||
|
);
|
||||||
|
t.deepEqual(
|
||||||
|
memberUpdated,
|
||||||
|
[
|
||||||
|
'workspace.members.updated',
|
||||||
|
{
|
||||||
|
count: 3,
|
||||||
|
workspaceId: tws.id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'should emit role changed event'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -180,3 +180,27 @@ export async function grantMember(
|
|||||||
}
|
}
|
||||||
return res.body.data?.grantMember;
|
return res.body.data?.grantMember;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function revokeMember(
|
||||||
|
app: INestApplication,
|
||||||
|
token: string,
|
||||||
|
workspaceId: string,
|
||||||
|
userId: string
|
||||||
|
) {
|
||||||
|
const res = await request(app.getHttpServer())
|
||||||
|
.post(gql)
|
||||||
|
.auth(token, { type: 'bearer' })
|
||||||
|
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
|
||||||
|
.send({
|
||||||
|
query: `
|
||||||
|
mutation {
|
||||||
|
revoke(workspaceId: "${workspaceId}", userId: "${userId}")
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
.expect(200);
|
||||||
|
if (res.body.errors) {
|
||||||
|
throw new Error(res.body.errors[0].message);
|
||||||
|
}
|
||||||
|
return res.body.data?.revokeMember;
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export interface WorkspaceEvents {
|
|||||||
}>;
|
}>;
|
||||||
ownerTransferred: Payload<{ email: string; workspaceId: Workspace['id'] }>;
|
ownerTransferred: Payload<{ email: string; workspaceId: Workspace['id'] }>;
|
||||||
updated: Payload<{ workspaceId: Workspace['id']; count: number }>;
|
updated: Payload<{ workspaceId: Workspace['id']; count: number }>;
|
||||||
|
removed: Payload<{ workspaceId: Workspace['id']; userId: User['id'] }>;
|
||||||
};
|
};
|
||||||
deleted: Payload<Workspace['id']>;
|
deleted: Payload<Workspace['id']>;
|
||||||
blob: {
|
blob: {
|
||||||
|
|||||||
@@ -276,7 +276,39 @@ export class MailService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// =================== Team Workspace Mails ===================
|
// =================== Team Workspace Mails ===================
|
||||||
async sendReviewRequestMail(
|
async sendTeamWorkspaceUpgradedEmail(
|
||||||
|
to: string,
|
||||||
|
ws: { id: string; name: string; isOwner: boolean }
|
||||||
|
) {
|
||||||
|
const { id: workspaceId, name: workspaceName, isOwner } = ws;
|
||||||
|
|
||||||
|
const baseContent = {
|
||||||
|
subject: `${workspaceName} has been upgraded to team workspace! 🎉`,
|
||||||
|
title: 'Welcome to the team workspace!',
|
||||||
|
content: `Great news! ${workspaceName} has been upgraded to team workspace by the workspace owner. You now have access to the following enhanced features:`,
|
||||||
|
};
|
||||||
|
if (isOwner) {
|
||||||
|
baseContent.subject =
|
||||||
|
'Your workspace has been upgraded to team workspace! 🎉';
|
||||||
|
baseContent.title = 'Welcome to the team workspace!';
|
||||||
|
baseContent.content = `${workspaceName} has been upgraded to team workspace with the following benefits:`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = emailTemplate({
|
||||||
|
title: baseContent.title,
|
||||||
|
content: `${baseContent.content}
|
||||||
|
✓ 100 GB initial storage + 20 GB per seat
|
||||||
|
✓ 500 MB of maximum file size
|
||||||
|
✓ Unlimited team members (10+ seats)
|
||||||
|
✓ Multiple admin roles
|
||||||
|
✓ Priority customer support`,
|
||||||
|
buttonContent: 'Open Workspace',
|
||||||
|
buttonUrl: this.url.link(`/workspace/${workspaceId}`),
|
||||||
|
});
|
||||||
|
return this.sendMail({ to, subject: baseContent.subject, html });
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendReviewRequestEmail(
|
||||||
to: string,
|
to: string,
|
||||||
invitee: string,
|
invitee: string,
|
||||||
ws: { id: string; name: string }
|
ws: { id: string; name: string }
|
||||||
@@ -324,7 +356,7 @@ export class MailService {
|
|||||||
return this.sendMail({ to, subject, html });
|
return this.sendMail({ to, subject, html });
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendOwnerTransferred(to: string, ws: { name: string }) {
|
async sendOwnershipTransferredEmail(to: string, ws: { name: string }) {
|
||||||
const { name: workspaceName } = ws;
|
const { name: workspaceName } = ws;
|
||||||
const title = `Your ownership of ${workspaceName} has been transferred`;
|
const title = `Your ownership of ${workspaceName} has been transferred`;
|
||||||
|
|
||||||
@@ -334,4 +366,83 @@ export class MailService {
|
|||||||
});
|
});
|
||||||
return this.sendMail({ to, subject: title, html });
|
return this.sendMail({ to, subject: title, html });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async sendMemberRemovedEmail(to: string, ws: { name: string }) {
|
||||||
|
const { name: workspaceName } = ws;
|
||||||
|
const title = `You have been removed from ${workspaceName}`;
|
||||||
|
|
||||||
|
const html = emailTemplate({
|
||||||
|
title: 'Workspace access removed',
|
||||||
|
content: `You have been removed from {workspace name}. You no longer have access to this workspace.`,
|
||||||
|
});
|
||||||
|
return this.sendMail({ to, subject: title, html });
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendWorkspaceExpireRemindEmail(
|
||||||
|
to: string,
|
||||||
|
ws: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
expirationDate: Date;
|
||||||
|
deletionDate?: Date;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
id: workspaceId,
|
||||||
|
name: workspaceName,
|
||||||
|
expirationDate,
|
||||||
|
deletionDate,
|
||||||
|
} = ws;
|
||||||
|
const baseContent: {
|
||||||
|
subject: string;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
button?: { buttonContent: string; buttonUrl: string };
|
||||||
|
} = {
|
||||||
|
subject: `[Action Required] Your ${workspaceName} team workspace is expiring soon`,
|
||||||
|
title: 'Team workspace expiring soon',
|
||||||
|
content: `Your ${workspaceName} team workspace will expire on ${expirationDate}. After expiration, you won't be able to sync or collaborate with team members. Please renew your subscription to continue using all team features.`,
|
||||||
|
button: {
|
||||||
|
buttonContent: 'Go to Billing',
|
||||||
|
// TODO(@darkskygit): use real billing path
|
||||||
|
buttonUrl: this.url.link(`/workspace/${workspaceId}`),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (deletionDate) {
|
||||||
|
if (deletionDate.getTime() > Date.now()) {
|
||||||
|
// in 24 hours
|
||||||
|
if (deletionDate.getTime() - Date.now() < 24 * 60 * 60 * 1000) {
|
||||||
|
baseContent.subject = `[Action Required] Final warning: Your ${workspaceName} data will be deleted in 24 hours`;
|
||||||
|
baseContent.title = 'Urgent: Last chance to prevent data loss';
|
||||||
|
baseContent.content = `Your ${workspaceName} team workspace data will be permanently deleted in 24 hours on ${deletionDate}. To prevent data loss, please take immediate action:
|
||||||
|
<li>Renew your subscription to restore team features</li>
|
||||||
|
<li>Export your workspace data from Workspace Settings > Export Workspace</li>`;
|
||||||
|
} else {
|
||||||
|
baseContent.subject = `[Action Required] Important: Your ${workspaceName} data will be deleted soon`;
|
||||||
|
baseContent.title = 'Take action to prevent data loss';
|
||||||
|
baseContent.content = `Your ${workspaceName} team workspace expired on ${expirationDate}. All workspace data will be permanently deleted on ${deletionDate} (180 days after expiration). To prevent data loss, please either:
|
||||||
|
<li>Renew your subscription to restore team features</li>
|
||||||
|
<li>Export your workspace data from Workspace Settings > Export Workspace</li>`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
baseContent.subject = `Data deletion completed for ${workspaceName}`;
|
||||||
|
baseContent.title = 'Workspace data deleted';
|
||||||
|
baseContent.content = `All data in ${workspaceName} has been permanently deleted as the workspace remained expired for 180 days. This action cannot be undone.
|
||||||
|
Thank you for your support of AFFiNE. We hope to see you again in the future.`;
|
||||||
|
baseContent.button = undefined;
|
||||||
|
}
|
||||||
|
} else if (expirationDate.getTime() < Date.now()) {
|
||||||
|
baseContent.subject = `Your ${workspaceName} team workspace has expired`;
|
||||||
|
baseContent.title = 'Team workspace expired';
|
||||||
|
baseContent.content = `Your ${workspaceName} team workspace expired on ${expirationDate}. Your workspace can't sync or collaborate with team members. Please renew your subscription to restore all team features.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = emailTemplate({
|
||||||
|
title: baseContent.title,
|
||||||
|
content: baseContent.content,
|
||||||
|
...baseContent.button,
|
||||||
|
});
|
||||||
|
return this.sendMail({ to, subject: baseContent.subject, html });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -460,6 +460,10 @@ export class PermissionService {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
count,
|
count,
|
||||||
});
|
});
|
||||||
|
this.event.emit('workspace.members.removed', {
|
||||||
|
workspaceId,
|
||||||
|
userId: user,
|
||||||
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
permission.status === 'UnderReview' ||
|
permission.status === 'UnderReview' ||
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import {
|
|||||||
WorkspaceBlobResolver,
|
WorkspaceBlobResolver,
|
||||||
WorkspaceService,
|
WorkspaceService,
|
||||||
],
|
],
|
||||||
|
exports: [WorkspaceService],
|
||||||
})
|
})
|
||||||
export class WorkspaceModule {}
|
export class WorkspaceModule {}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,13 @@ import { Injectable, Logger } from '@nestjs/common';
|
|||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
import { getStreamAsBuffer } from 'get-stream';
|
import { getStreamAsBuffer } from 'get-stream';
|
||||||
|
|
||||||
import { Cache, MailService, UserNotFound } from '../../../base';
|
import {
|
||||||
|
Cache,
|
||||||
|
type EventPayload,
|
||||||
|
MailService,
|
||||||
|
OnEvent,
|
||||||
|
UserNotFound,
|
||||||
|
} from '../../../base';
|
||||||
import { Models } from '../../../models';
|
import { Models } from '../../../models';
|
||||||
import { DocContentService } from '../../doc-renderer';
|
import { DocContentService } from '../../doc-renderer';
|
||||||
import { Permission, PermissionService } from '../../permission';
|
import { Permission, PermissionService } from '../../permission';
|
||||||
@@ -131,7 +137,24 @@ export class WorkspaceService {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendReviewRequestedMail(inviteId: string) {
|
async sendTeamWorkspaceUpgradedEmail(workspaceId: string) {
|
||||||
|
const workspace = await this.getWorkspaceInfo(workspaceId);
|
||||||
|
const owner = await this.permission.getWorkspaceOwner(workspaceId);
|
||||||
|
const admin = await this.permission.getWorkspaceAdmin(workspaceId);
|
||||||
|
|
||||||
|
await this.mailer.sendTeamWorkspaceUpgradedEmail(owner.email, {
|
||||||
|
...workspace,
|
||||||
|
isOwner: true,
|
||||||
|
});
|
||||||
|
for (const user of admin) {
|
||||||
|
await this.mailer.sendTeamWorkspaceUpgradedEmail(user.email, {
|
||||||
|
...workspace,
|
||||||
|
isOwner: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendReviewRequestedEmail(inviteId: string) {
|
||||||
const { workspaceId, inviteeUserId } = await this.getInviteInfo(inviteId);
|
const { workspaceId, inviteeUserId } = await this.getInviteInfo(inviteId);
|
||||||
if (!inviteeUserId) {
|
if (!inviteeUserId) {
|
||||||
this.logger.error(`Invitee user not found for inviteId: ${inviteId}`);
|
this.logger.error(`Invitee user not found for inviteId: ${inviteId}`);
|
||||||
@@ -151,7 +174,7 @@ export class WorkspaceService {
|
|||||||
const admin = await this.permission.getWorkspaceAdmin(workspaceId);
|
const admin = await this.permission.getWorkspaceAdmin(workspaceId);
|
||||||
|
|
||||||
for (const user of [owner, ...admin]) {
|
for (const user of [owner, ...admin]) {
|
||||||
await this.mailer.sendReviewRequestMail(
|
await this.mailer.sendReviewRequestEmail(
|
||||||
user.email,
|
user.email,
|
||||||
invitee.email,
|
invitee.email,
|
||||||
workspace
|
workspace
|
||||||
@@ -159,7 +182,7 @@ export class WorkspaceService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendInviteMail(inviteId: string) {
|
async sendInviteEmail(inviteId: string) {
|
||||||
const target = await this.getInviteeEmailTarget(inviteId);
|
const target = await this.getInviteeEmailTarget(inviteId);
|
||||||
|
|
||||||
if (!target) {
|
if (!target) {
|
||||||
@@ -208,8 +231,43 @@ export class WorkspaceService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendOwnerTransferred(email: string, ws: { id: string }) {
|
async sendOwnerTransferredEmail(email: string, ws: { id: string }) {
|
||||||
const workspace = await this.getWorkspaceInfo(ws.id);
|
const workspace = await this.getWorkspaceInfo(ws.id);
|
||||||
await this.mailer.sendOwnerTransferred(email, { name: workspace.name });
|
await this.mailer.sendOwnershipTransferredEmail(email, {
|
||||||
|
name: workspace.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendMemberRemoved(email: string, ws: { id: string }) {
|
||||||
|
const workspace = await this.getWorkspaceInfo(ws.id);
|
||||||
|
await this.mailer.sendMemberRemovedEmail(email, {
|
||||||
|
name: workspace.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnEvent('workspace.members.removed')
|
||||||
|
async onMemberRemoved({
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
}: EventPayload<'workspace.members.requestDeclined'>) {
|
||||||
|
const user = await this.models.user.get(userId);
|
||||||
|
if (!user) return;
|
||||||
|
await this.sendMemberRemoved(user.email, { id: workspaceId });
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnEvent('workspace.subscription.notify')
|
||||||
|
async onSubscriptionNotify({
|
||||||
|
workspaceId,
|
||||||
|
expirationDate,
|
||||||
|
deletionDate,
|
||||||
|
}: EventPayload<'workspace.subscription.notify'>) {
|
||||||
|
const owner = await this.permission.getWorkspaceOwner(workspaceId);
|
||||||
|
if (!owner) return;
|
||||||
|
const workspace = await this.getWorkspaceInfo(workspaceId);
|
||||||
|
await this.mailer.sendWorkspaceExpireRemindEmail(owner.email, {
|
||||||
|
...workspace,
|
||||||
|
expirationDate,
|
||||||
|
deletionDate,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export class TeamWorkspaceResolver {
|
|||||||
// after user click the invite link, we can check again and reject if charge failed
|
// after user click the invite link, we can check again and reject if charge failed
|
||||||
if (sendInviteMail) {
|
if (sendInviteMail) {
|
||||||
try {
|
try {
|
||||||
await this.workspaceService.sendInviteMail(ret.inviteId);
|
await this.workspaceService.sendInviteEmail(ret.inviteId);
|
||||||
ret.sentSuccess = true;
|
ret.sentSuccess = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
@@ -344,7 +344,7 @@ export class TeamWorkspaceResolver {
|
|||||||
inviteId,
|
inviteId,
|
||||||
}: EventPayload<'workspace.members.reviewRequested'>) {
|
}: EventPayload<'workspace.members.reviewRequested'>) {
|
||||||
// send review request mail to owner and admin
|
// send review request mail to owner and admin
|
||||||
await this.workspaceService.sendReviewRequestedMail(inviteId);
|
await this.workspaceService.sendReviewRequestedEmail(inviteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnEvent('workspace.members.requestDeclined')
|
@OnEvent('workspace.members.requestDeclined')
|
||||||
@@ -388,7 +388,7 @@ export class TeamWorkspaceResolver {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
}: EventPayload<'workspace.members.ownerTransferred'>) {
|
}: EventPayload<'workspace.members.ownerTransferred'>) {
|
||||||
// send role changed mail
|
// send role changed mail
|
||||||
await this.workspaceService.sendOwnerTransferred(email, {
|
await this.workspaceService.sendOwnerTransferredEmail(email, {
|
||||||
id: workspaceId,
|
id: workspaceId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -433,7 +433,7 @@ export class WorkspaceResolver {
|
|||||||
);
|
);
|
||||||
if (sendInviteMail) {
|
if (sendInviteMail) {
|
||||||
try {
|
try {
|
||||||
await this.workspaceService.sendInviteMail(inviteId);
|
await this.workspaceService.sendInviteEmail(inviteId);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const ret = await this.permissions.revokeWorkspace(
|
const ret = await this.permissions.revokeWorkspace(
|
||||||
workspaceId,
|
workspaceId,
|
||||||
|
|||||||
@@ -17,6 +17,77 @@ export class SubscriptionCronJobs {
|
|||||||
private readonly event: EventEmitter
|
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)
|
@Cron(CronExpression.EVERY_HOUR)
|
||||||
async cleanExpiredOnetimeSubscriptions() {
|
async cleanExpiredOnetimeSubscriptions() {
|
||||||
const subscriptions = await this.db.subscription.findMany({
|
const subscriptions = await this.db.subscription.findMany({
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { FeatureModule } from '../../core/features';
|
|||||||
import { PermissionModule } from '../../core/permission';
|
import { PermissionModule } from '../../core/permission';
|
||||||
import { QuotaModule } from '../../core/quota';
|
import { QuotaModule } from '../../core/quota';
|
||||||
import { UserModule } from '../../core/user';
|
import { UserModule } from '../../core/user';
|
||||||
|
import { WorkspaceModule } from '../../core/workspaces';
|
||||||
import { Plugin } from '../registry';
|
import { Plugin } from '../registry';
|
||||||
import { StripeWebhookController } from './controller';
|
import { StripeWebhookController } from './controller';
|
||||||
import { SubscriptionCronJobs } from './cron';
|
import { SubscriptionCronJobs } from './cron';
|
||||||
@@ -24,7 +25,13 @@ import { StripeWebhook } from './webhook';
|
|||||||
|
|
||||||
@Plugin({
|
@Plugin({
|
||||||
name: 'payment',
|
name: 'payment',
|
||||||
imports: [FeatureModule, QuotaModule, UserModule, PermissionModule],
|
imports: [
|
||||||
|
FeatureModule,
|
||||||
|
QuotaModule,
|
||||||
|
UserModule,
|
||||||
|
PermissionModule,
|
||||||
|
WorkspaceModule,
|
||||||
|
],
|
||||||
providers: [
|
providers: [
|
||||||
StripeProvider,
|
StripeProvider,
|
||||||
SubscriptionService,
|
SubscriptionService,
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { OnEvent } from '@nestjs/event-emitter';
|
import { OnEvent } from '@nestjs/event-emitter';
|
||||||
|
|
||||||
import type { EventPayload } from '../../base';
|
import { type EventPayload } from '../../base';
|
||||||
import { PermissionService } from '../../core/permission';
|
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()
|
@Injectable()
|
||||||
export class TeamQuotaOverride {
|
export class TeamQuotaOverride {
|
||||||
constructor(
|
constructor(
|
||||||
|
private readonly quota: QuotaService,
|
||||||
private readonly manager: QuotaManagementService,
|
private readonly manager: QuotaManagementService,
|
||||||
private readonly permission: PermissionService
|
private readonly permission: PermissionService,
|
||||||
|
private readonly workspace: WorkspaceService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@OnEvent('workspace.subscription.activated')
|
@OnEvent('workspace.subscription.activated')
|
||||||
@@ -20,7 +27,11 @@ export class TeamQuotaOverride {
|
|||||||
quantity,
|
quantity,
|
||||||
}: EventPayload<'workspace.subscription.activated'>) {
|
}: EventPayload<'workspace.subscription.activated'>) {
|
||||||
switch (plan) {
|
switch (plan) {
|
||||||
case 'team':
|
case 'team': {
|
||||||
|
const hasTeamWorkspace = await this.quota.hasWorkspaceQuota(
|
||||||
|
workspaceId,
|
||||||
|
QuotaType.TeamPlanV1
|
||||||
|
);
|
||||||
await this.manager.addTeamWorkspace(
|
await this.manager.addTeamWorkspace(
|
||||||
workspaceId,
|
workspaceId,
|
||||||
`${recurring} team subscription activated`
|
`${recurring} team subscription activated`
|
||||||
@@ -31,7 +42,13 @@ export class TeamQuotaOverride {
|
|||||||
{ memberLimit: quantity }
|
{ memberLimit: quantity }
|
||||||
);
|
);
|
||||||
await this.permission.refreshSeatStatus(workspaceId, 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;
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,6 +78,11 @@ declare module '../../base/event/def' {
|
|||||||
plan: SubscriptionPlan;
|
plan: SubscriptionPlan;
|
||||||
recurring: SubscriptionRecurring;
|
recurring: SubscriptionRecurring;
|
||||||
}>;
|
}>;
|
||||||
|
notify: Payload<{
|
||||||
|
workspaceId: Workspace['id'];
|
||||||
|
expirationDate: Date;
|
||||||
|
deletionDate: Date | undefined;
|
||||||
|
}>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user