Skip to content

Commit

Permalink
add two new emails in suspended workspace cleaning
Browse files Browse the repository at this point in the history
  • Loading branch information
etiennejou committed Jan 24, 2025
1 parent 235bc71 commit 44b03ea
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 15 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { BaseEmail } from 'src/components/BaseEmail';
import { CallToAction } from 'src/components/CallToAction';
import { MainText } from 'src/components/MainText';
import { Title } from 'src/components/Title';

type CleanSuspendedWorkspaceEmailProps = {
inactiveDaysBeforeDelete: number;
userName: string;
workspaceDisplayName: string | undefined;
};

export const CleanSuspendedWorkspaceEmail = ({
inactiveDaysBeforeDelete,
userName,
workspaceDisplayName,
}: CleanSuspendedWorkspaceEmailProps) => {
const helloString = userName?.length > 1 ? `Hello ${userName}` : 'Hello';

return (
<BaseEmail width={333}>
<Title value="Deleted Workspace 🥺" />
<MainText>
{helloString},
<br />
<br />
We wanted to inform you that your workspace{''}
<b>{workspaceDisplayName}</b> has been deleted as it remained suspended
for {inactiveDaysBeforeDelete} days following your subscription
expiration.
<br />
<br />
All associated data within this workspace has been permanently deleted.
<br />
<br />
If you wish to continue using Twenty, you can create a new workspace.
</MainText>
<CallToAction
href="https://app.twenty.com/"
value="Create a new workspace"
/>
</BaseEmail>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { BaseEmail } from 'src/components/BaseEmail';
import { CallToAction } from 'src/components/CallToAction';
import { MainText } from 'src/components/MainText';
import { Title } from 'src/components/Title';

type WarnSuspendedWorkspaceEmailProps = {
daysSinceInactive: number;
inactiveDaysBeforeDelete: number;
userName: string;
workspaceDisplayName: string | undefined;
};

export const WarnSuspendedWorkspaceEmail = ({
daysSinceInactive,
inactiveDaysBeforeDelete,
userName,
workspaceDisplayName,
}: WarnSuspendedWorkspaceEmailProps) => {
const daysLeft = inactiveDaysBeforeDelete - daysSinceInactive;
const dayOrDays = daysLeft > 1 ? 'days' : 'day';
const remainingDays = daysLeft > 1 ? `${daysLeft} ` : '';

const helloString = userName?.length > 1 ? `Hello ${userName}` : 'Hello';

return (
<BaseEmail width={333}>
<Title value="Suspended Workspace 😴" />
<MainText>
{helloString},
<br />
<br />
It appears that your <b>{workspaceDisplayName}</b> workspace has been
suspended for {daysSinceInactive} days due to an expired subscription.
<br />
<br />
Please note that the workspace is due for deactivation soon, and all
associated data within this workspace will be deleted.
<br />
<br />
If you wish to continue using Twenty, please update your subscription
within the next {remainingDays} {dayOrDays}
{''}
to retain access to your workspace and data.
</MainText>
<CallToAction
href="https://app.twenty.com/settings/billing"
value="Update your subscription"
/>
</BaseEmail>
);
};
3 changes: 3 additions & 0 deletions packages/twenty-emails/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export * from './emails/clean-suspended-workspace.email';
export * from './emails/password-reset-link.email';
export * from './emails/password-update-notify.email';
export * from './emails/send-email-verification-link.email';
export * from './emails/send-invite-link.email';
export * from './emails/warn-suspended-workspace.email';

Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const cleanSuspendedWorkspaceCronPattern = '0 22 * * *'; // Every day at 10pm
export const cleanSuspendedWorkspaceCronPattern = '*/1 * * * *'; // Every day at 10pm
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';

import { render } from '@react-email/components';
import chunk from 'lodash.chunk';
import {
CleanSuspendedWorkspaceEmail,
WarnSuspendedWorkspaceEmail,
} from 'twenty-emails';
import { WorkspaceActivationStatus } from 'twenty-shared';
import { Repository } from 'typeorm';

import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
import { EmailService } from 'src/engine/core-modules/email/email.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
Expand All @@ -31,6 +37,7 @@ export class CleanSuspendedWorkspacesJob {
private readonly environmentService: EnvironmentService,
private readonly userService: UserService,
private readonly userVarsService: UserVarsService,
private readonly emailService: EmailService,
@InjectRepository(BillingSubscription, 'core')
private readonly billingSubscriptionRepository: Repository<BillingSubscription>,
@InjectRepository(Workspace, 'core')
Expand Down Expand Up @@ -92,7 +99,38 @@ export class CleanSuspendedWorkspacesJob {
return false;
}

async warnWorkspaceMembers(workspace: Workspace) {
async sendWarningEmail(
workspaceMember: WorkspaceMemberWorkspaceEntity,
workspaceDisplayName: string | undefined,
daysSinceInactive: number,
) {
const emailData = {
daysSinceInactive,
inactiveDaysBeforeDelete: this.inactiveDaysBeforeDelete,
userName: `${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`,
workspaceDisplayName: `${workspaceDisplayName}`,
};
const emailTemplate = WarnSuspendedWorkspaceEmail(emailData);
const html = render(emailTemplate, {
pretty: true,
});
const text = render(emailTemplate, {
plainText: true,
});

this.emailService.send({
to: workspaceMember.userEmail,
bcc: this.environmentService.get('EMAIL_SYSTEM_ADDRESS'),
from: `${this.environmentService.get(
'EMAIL_FROM_NAME',
)} <${this.environmentService.get('EMAIL_FROM_ADDRESS')}>`,
subject: 'Action needed to prevent workspace deletion',
html,
text,
});
}

async warnWorkspaceMembers(workspace: Workspace, daysSinceInactive: number) {
const workspaceMembers =
await this.userService.loadWorkspaceMembers(workspace);

Expand All @@ -108,6 +146,14 @@ export class CleanSuspendedWorkspacesJob {

return;
} else {
this.logger.log(
`Sending ${workspace.id} ${
workspace.displayName
} suspended since ${daysSinceInactive} days emails to users ['${workspaceMembers
.map((workspaceUser) => workspaceUser.userEmail)
.join(', ')}']`,
);

const workspaceMembersChunks = chunk(workspaceMembers, 5);

for (const workspaceMembersChunk of workspaceMembersChunks) {
Expand All @@ -119,25 +165,61 @@ export class CleanSuspendedWorkspacesJob {
key: USER_WORKSPACE_DELETION_WARNING_SENT_KEY,
value: true,
});

this.sendWarningEmail(
workspaceMember,
workspace.displayName,
daysSinceInactive,
);
}),
);
}

// TODO: issue #284
// send email warning for deletion in (this.inactiveDaysBeforeDelete - this.inactiveDaysBeforeWarn) days (cci @twenty.com)

this.logger.log(
`Warning Workspace ${workspace.id} ${workspace.displayName}`,
);

return;
}
}

async sendCleaningEmail(
workspaceMember: WorkspaceMemberWorkspaceEntity,
workspaceDisplayName: string | undefined,
) {
const emailData = {
inactiveDaysBeforeDelete: this.inactiveDaysBeforeDelete,
userName: `${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`,
workspaceDisplayName: `${workspaceDisplayName}`,
};
const emailTemplate = CleanSuspendedWorkspaceEmail(emailData);
const html = render(emailTemplate, {
pretty: true,
});
const text = render(emailTemplate, {
plainText: true,
});

this.emailService.send({
to: workspaceMember.userEmail,
bcc: this.environmentService.get('EMAIL_SYSTEM_ADDRESS'),
from: `${this.environmentService.get(
'EMAIL_FROM_NAME',
)} <${this.environmentService.get('EMAIL_FROM_ADDRESS')}>`,
subject: 'Your workspace has been deleted',
html,
text,
});
}

async informWorkspaceMembersAndDeleteWorkspace(workspace: Workspace) {
const workspaceMembers =
await this.userService.loadWorkspaceMembers(workspace);

this.logger.log(
`Sending workspace ${workspace.id} ${
workspace.displayName
} deletion emails to users ['${workspaceMembers
.map((workspaceUser) => workspaceUser.userEmail)
.join(', ')}']`,
);

const workspaceMembersChunks = chunk(workspaceMembers, 5);

for (const workspaceMembersChunk of workspaceMembersChunks) {
Expand All @@ -148,12 +230,9 @@ export class CleanSuspendedWorkspacesJob {
workspaceId: workspace.id,
key: USER_WORKSPACE_DELETION_WARNING_SENT_KEY,
});
}),

// TODO: issue #285
// send email informing about deletion (cci @twenty.com)
// remove clean-inactive-workspace.job.ts and .. files
// add new env var in infra
await this.sendCleaningEmail(workspaceMember, workspace.displayName);
}),
);
}

Expand Down Expand Up @@ -197,7 +276,7 @@ export class CleanSuspendedWorkspacesJob {
workspaceInactivity > this.inactiveDaysBeforeWarn &&
workspaceInactivity <= this.inactiveDaysBeforeDelete
) {
await this.warnWorkspaceMembers(workspace);
await this.warnWorkspaceMembers(workspace, workspaceInactivity);

return;
}
Expand Down

0 comments on commit 44b03ea

Please sign in to comment.