diff --git a/packages/twenty-emails/src/constants/billing-settings-url.constant.ts b/packages/twenty-emails/src/constants/billing-settings-url.constant.ts deleted file mode 100644 index 08092a8be0..0000000000 --- a/packages/twenty-emails/src/constants/billing-settings-url.constant.ts +++ /dev/null @@ -1 +0,0 @@ -export const BILLING_SETTINGS_URL = 'https://app.twenty.com/settings/billing'; diff --git a/packages/twenty-emails/src/emails/billing-subscription-renewing.email.tsx b/packages/twenty-emails/src/emails/billing-subscription-renewing.email.tsx index b1946f20a6..5e3b4ea7b2 100644 --- a/packages/twenty-emails/src/emails/billing-subscription-renewing.email.tsx +++ b/packages/twenty-emails/src/emails/billing-subscription-renewing.email.tsx @@ -3,7 +3,6 @@ import { BaseEmail } from 'src/components/BaseEmail'; import { CallToAction } from 'src/components/CallToAction'; import { MainText } from 'src/components/MainText'; import { Title } from 'src/components/Title'; -import { BILLING_SETTINGS_URL } from 'src/constants/billing-settings-url.constant'; import { createI18nInstance } from 'src/utils/i18n.utils'; import { type APP_LOCALES } from 'twenty-shared/translations'; @@ -11,6 +10,7 @@ type BillingSubscriptionRenewingEmailProps = { userName: string; workspaceDisplayName: string | undefined; renewsAt: Date; + link: string; locale: keyof typeof APP_LOCALES; }; @@ -21,6 +21,7 @@ export const BillingSubscriptionRenewingEmail = ({ userName, workspaceDisplayName, renewsAt, + link, locale, }: BillingSubscriptionRenewingEmailProps) => { const i18n = createI18nInstance(locale); @@ -54,10 +55,7 @@ export const BillingSubscriptionRenewingEmail = ({
- +

@@ -68,6 +66,7 @@ BillingSubscriptionRenewingEmail.PreviewProps = { userName: 'John Doe', workspaceDisplayName: 'Acme Inc.', renewsAt: new Date('2027-07-02'), + link: 'https://acme.twenty.com/settings/billing', locale: 'en', } as BillingSubscriptionRenewingEmailProps; diff --git a/packages/twenty-emails/src/emails/billing-trial-converting.email.tsx b/packages/twenty-emails/src/emails/billing-trial-converting.email.tsx index 894f7e2fca..0940417c78 100644 --- a/packages/twenty-emails/src/emails/billing-trial-converting.email.tsx +++ b/packages/twenty-emails/src/emails/billing-trial-converting.email.tsx @@ -3,7 +3,6 @@ import { BaseEmail } from 'src/components/BaseEmail'; import { CallToAction } from 'src/components/CallToAction'; import { MainText } from 'src/components/MainText'; import { Title } from 'src/components/Title'; -import { BILLING_SETTINGS_URL } from 'src/constants/billing-settings-url.constant'; import { createI18nInstance } from 'src/utils/i18n.utils'; import { type APP_LOCALES } from 'twenty-shared/translations'; @@ -12,6 +11,7 @@ type BillingTrialConvertingEmailProps = { workspaceDisplayName: string | undefined; trialEndsAt: Date; interval: 'month' | 'year'; + link: string; locale: keyof typeof APP_LOCALES; }; @@ -23,6 +23,7 @@ export const BillingTrialConvertingEmail = ({ workspaceDisplayName, trialEndsAt, interval, + link, locale, }: BillingTrialConvertingEmailProps) => { const i18n = createI18nInstance(locale); @@ -64,10 +65,7 @@ export const BillingTrialConvertingEmail = ({
- +

@@ -79,6 +77,7 @@ BillingTrialConvertingEmail.PreviewProps = { workspaceDisplayName: 'Acme Inc.', trialEndsAt: new Date('2026-07-02'), interval: 'month', + link: 'https://acme.twenty.com/settings/billing', locale: 'en', } as BillingTrialConvertingEmailProps; diff --git a/packages/twenty-emails/src/emails/billing-trial-ending.email.tsx b/packages/twenty-emails/src/emails/billing-trial-ending.email.tsx index d6f980f5da..e28ee4d907 100644 --- a/packages/twenty-emails/src/emails/billing-trial-ending.email.tsx +++ b/packages/twenty-emails/src/emails/billing-trial-ending.email.tsx @@ -3,7 +3,6 @@ import { BaseEmail } from 'src/components/BaseEmail'; import { CallToAction } from 'src/components/CallToAction'; import { MainText } from 'src/components/MainText'; import { Title } from 'src/components/Title'; -import { BILLING_SETTINGS_URL } from 'src/constants/billing-settings-url.constant'; import { createI18nInstance } from 'src/utils/i18n.utils'; import { type APP_LOCALES } from 'twenty-shared/translations'; @@ -12,6 +11,7 @@ type BillingTrialEndingEmailProps = { workspaceDisplayName: string | undefined; trialEndsAt: Date; dataRetentionDays: number; + link: string; locale: keyof typeof APP_LOCALES; }; @@ -23,6 +23,7 @@ export const BillingTrialEndingEmail = ({ workspaceDisplayName, trialEndsAt, dataRetentionDays, + link, locale, }: BillingTrialEndingEmailProps) => { const i18n = createI18nInstance(locale); @@ -59,10 +60,7 @@ export const BillingTrialEndingEmail = ({ />
- +

@@ -74,6 +72,7 @@ BillingTrialEndingEmail.PreviewProps = { workspaceDisplayName: 'Acme Inc.', trialEndsAt: new Date('2026-07-02'), dataRetentionDays: 14, + link: 'https://acme.twenty.com/settings/billing', locale: 'en', } as BillingTrialEndingEmailProps; diff --git a/packages/twenty-emails/src/emails/warn-suspended-workspace.email.tsx b/packages/twenty-emails/src/emails/warn-suspended-workspace.email.tsx index 7bfb618674..9443a4fcfb 100644 --- a/packages/twenty-emails/src/emails/warn-suspended-workspace.email.tsx +++ b/packages/twenty-emails/src/emails/warn-suspended-workspace.email.tsx @@ -3,7 +3,6 @@ import { BaseEmail } from 'src/components/BaseEmail'; import { CallToAction } from 'src/components/CallToAction'; import { MainText } from 'src/components/MainText'; import { Title } from 'src/components/Title'; -import { BILLING_SETTINGS_URL } from 'src/constants/billing-settings-url.constant'; import { createI18nInstance } from 'src/utils/i18n.utils'; import { type APP_LOCALES } from 'twenty-shared/translations'; @@ -12,6 +11,7 @@ type WarnSuspendedWorkspaceEmailProps = { inactiveDaysBeforeDelete: number; userName: string; workspaceDisplayName: string | undefined; + link: string; locale: keyof typeof APP_LOCALES; }; @@ -20,6 +20,7 @@ export const WarnSuspendedWorkspaceEmail = ({ inactiveDaysBeforeDelete, userName, workspaceDisplayName, + link, locale, }: WarnSuspendedWorkspaceEmailProps) => { const i18n = createI18nInstance(locale); @@ -54,10 +55,7 @@ export const WarnSuspendedWorkspaceEmail = ({
- +

@@ -69,6 +67,7 @@ WarnSuspendedWorkspaceEmail.PreviewProps = { inactiveDaysBeforeDelete: 14, userName: 'John Doe', workspaceDisplayName: 'Acme Inc.', + link: 'https://acme.twenty.com/settings/billing', locale: 'en', }; diff --git a/packages/twenty-server/src/engine/core-modules/billing/reminders/billing-reminder.module.ts b/packages/twenty-server/src/engine/core-modules/billing/reminders/billing-reminder.module.ts index 8b268c24cf..92696aeaeb 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/reminders/billing-reminder.module.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/reminders/billing-reminder.module.ts @@ -4,6 +4,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { BillingSubscriptionEntity } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; import { BillingReminderCronCommand } from 'src/engine/core-modules/billing/reminders/crons/commands/billing-reminder.cron.command'; import { BillingReminderService } from 'src/engine/core-modules/billing/reminders/services/billing-reminder.service'; +import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module'; import { EmailModule } from 'src/engine/core-modules/email/email.module'; import { UserVarsModule } from 'src/engine/core-modules/user/user-vars/user-vars.module'; import { UserModule } from 'src/engine/core-modules/user/user.module'; @@ -15,6 +16,7 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent EmailModule, UserModule, UserVarsModule, + WorkspaceDomainsModule, ], providers: [BillingReminderService, BillingReminderCronCommand], exports: [BillingReminderService, BillingReminderCronCommand], diff --git a/packages/twenty-server/src/engine/core-modules/billing/reminders/services/__tests__/billing-reminder.service.spec.ts b/packages/twenty-server/src/engine/core-modules/billing/reminders/services/__tests__/billing-reminder.service.spec.ts index dfead99c9d..15a25c745e 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/reminders/services/__tests__/billing-reminder.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/reminders/services/__tests__/billing-reminder.service.spec.ts @@ -78,6 +78,11 @@ const buildService = ({ const emailService = { send: emailSend }; const i18nService = { getI18nInstance: () => ({ _: () => 'subject' }) }; const twentyConfigService = { get: (key: string) => CONFIG[key] }; + const workspaceDomainsService = { + buildWorkspaceURL: jest.fn( + () => new URL('https://acme.twenty.com/settings/billing'), + ), + }; const service = new BillingReminderService( // oxlint-disable-next-line typescript/no-explicit-any @@ -94,6 +99,8 @@ const buildService = ({ emailService as any, // oxlint-disable-next-line typescript/no-explicit-any i18nService as any, + // oxlint-disable-next-line typescript/no-explicit-any + workspaceDomainsService as any, ); return { service, emailSend, userVarsSet }; @@ -121,7 +128,11 @@ describe('BillingReminderService', () => { expect(BillingTrialEndingEmail).toHaveBeenCalledTimes(1); expect(BillingTrialEndingEmail).toHaveBeenCalledWith( - expect.objectContaining({ trialEndsAt: trialEnd, dataRetentionDays: 14 }), + expect.objectContaining({ + trialEndsAt: trialEnd, + dataRetentionDays: 14, + link: 'https://acme.twenty.com/settings/billing', + }), ); expect(BillingTrialConvertingEmail).not.toHaveBeenCalled(); expect(emailSend).toHaveBeenCalledTimes(1); diff --git a/packages/twenty-server/src/engine/core-modules/billing/reminders/services/billing-reminder.service.ts b/packages/twenty-server/src/engine/core-modules/billing/reminders/services/billing-reminder.service.ts index b052933958..8fc040b6ed 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/reminders/services/billing-reminder.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/reminders/services/billing-reminder.service.ts @@ -11,7 +11,8 @@ import { BillingTrialConvertingEmail, BillingTrialEndingEmail, } from 'twenty-emails'; -import { isDefined } from 'twenty-shared/utils'; +import { SettingsPath } from 'twenty-shared/types'; +import { getSettingsPath, isDefined } from 'twenty-shared/utils'; import { WorkspaceActivationStatus } from 'twenty-shared/workspace'; import { Between, Repository } from 'typeorm'; @@ -22,6 +23,7 @@ import { BILLING_RENEWAL_REMINDER_SENT_KEY, BILLING_TRIAL_REMINDER_SENT_KEY, } from 'src/engine/core-modules/billing/reminders/constants/billing-reminder-sent-keys.constant'; +import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; import { EmailService } from 'src/engine/core-modules/email/email.service'; import { I18nService } from 'src/engine/core-modules/i18n/i18n.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; @@ -52,6 +54,7 @@ export class BillingReminderService { private readonly userVarsService: UserVarsService, private readonly emailService: EmailService, private readonly i18nService: I18nService, + private readonly workspaceDomainsService: WorkspaceDomainsService, ) {} async processReminders(): Promise { @@ -235,10 +238,18 @@ export class BillingReminderService { const workspaceMembers = await this.userService.loadWorkspaceMembers(workspace); + const billingSettingsUrl = this.workspaceDomainsService + .buildWorkspaceURL({ + workspace, + pathname: getSettingsPath(SettingsPath.Billing), + }) + .toString(); + for (const workspaceMember of workspaceMembers) { await this.sendReminderEmail({ workspaceMember, workspaceDisplayName: workspace.displayName, + billingSettingsUrl, reminder, }); } @@ -258,10 +269,12 @@ export class BillingReminderService { private async sendReminderEmail({ workspaceMember, workspaceDisplayName, + billingSettingsUrl, reminder, }: { workspaceMember: WorkspaceMemberWorkspaceEntity; workspaceDisplayName: string | undefined; + billingSettingsUrl: string; reminder: BillingReminderEmail; }): Promise { if (!isDefined(workspaceMember.userEmail)) { @@ -276,6 +289,7 @@ export class BillingReminderService { reminder, userName, workspaceDisplayName, + billingSettingsUrl, locale, }); @@ -297,11 +311,13 @@ export class BillingReminderService { reminder, userName, workspaceDisplayName, + billingSettingsUrl, locale, }: { reminder: BillingReminderEmail; userName: string; workspaceDisplayName: string | undefined; + billingSettingsUrl: string; locale: WorkspaceMemberWorkspaceEntity['locale']; }) { switch (reminder.type) { @@ -315,6 +331,7 @@ export class BillingReminderService { dataRetentionDays: this.twentyConfigService.get( 'WORKSPACE_INACTIVE_DAYS_BEFORE_SOFT_DELETION', ), + link: billingSettingsUrl, locale, }), }; @@ -326,6 +343,7 @@ export class BillingReminderService { workspaceDisplayName, trialEndsAt: reminder.trialEndsAt, interval: reminder.interval, + link: billingSettingsUrl, locale, }), }; @@ -336,6 +354,7 @@ export class BillingReminderService { userName, workspaceDisplayName, renewsAt: reminder.renewsAt, + link: billingSettingsUrl, locale, }), }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/services/cleaner.workspace-service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/services/cleaner.workspace-service.ts index 4c609d57ac..cf6f1692b2 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/services/cleaner.workspace-service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/services/cleaner.workspace-service.ts @@ -8,13 +8,15 @@ import { CleanSuspendedWorkspaceEmail, WarnSuspendedWorkspaceEmail, } from 'twenty-emails'; -import { isDefined } from 'twenty-shared/utils'; +import { SettingsPath } from 'twenty-shared/types'; +import { getSettingsPath, isDefined } from 'twenty-shared/utils'; import { WorkspaceActivationStatus } from 'twenty-shared/workspace'; import { In, Repository } from 'typeorm'; import { BillingSubscriptionEntity } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum'; import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; +import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; import { EmailService } from 'src/engine/core-modules/email/email.service'; import { I18nService } from 'src/engine/core-modules/i18n/i18n.service'; import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service'; @@ -67,6 +69,7 @@ export class CleanerWorkspaceService { private readonly userWorkspaceRepository: Repository, private readonly i18nService: I18nService, private readonly metricsService: MetricsService, + private readonly workspaceDomainsService: WorkspaceDomainsService, ) { this.inactiveDaysBeforeSoftDelete = this.twentyConfigService.get( 'WORKSPACE_INACTIVE_DAYS_BEFORE_SOFT_DELETION', @@ -115,6 +118,7 @@ export class CleanerWorkspaceService { async sendWarningEmail( workspaceMember: WorkspaceMemberWorkspaceEntity, workspaceDisplayName: string | undefined, + billingSettingsUrl: string, daysSinceInactive: number, ) { const emailData = { @@ -122,6 +126,7 @@ export class CleanerWorkspaceService { inactiveDaysBeforeDelete: this.inactiveDaysBeforeSoftDelete, userName: `${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`, workspaceDisplayName: `${workspaceDisplayName}`, + link: billingSettingsUrl, locale: workspaceMember.locale, }; const emailTemplate = WarnSuspendedWorkspaceEmail(emailData); @@ -177,11 +182,19 @@ export class CleanerWorkspaceService { .join(', ')}']`, ); + const billingSettingsUrl = this.workspaceDomainsService + .buildWorkspaceURL({ + workspace, + pathname: getSettingsPath(SettingsPath.Billing), + }) + .toString(); + if (!dryRun) { for (const workspaceMember of workspaceMembers) { await this.sendWarningEmail( workspaceMember, workspace.displayName, + billingSettingsUrl, daysSinceInactive, ); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/workspace-cleaner.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/workspace-cleaner.module.ts index bec06c151f..10b0b7f645 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/workspace-cleaner.module.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/workspace-cleaner.module.ts @@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { BillingModule } from 'src/engine/core-modules/billing/billing.module'; import { BillingSubscriptionEntity } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; +import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module'; import { EmailModule } from 'src/engine/core-modules/email/email.module'; import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module'; import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; @@ -31,6 +32,7 @@ import { CleanerWorkspaceService } from 'src/engine/workspace-manager/workspace- EmailModule, BillingModule, MetricsModule, + WorkspaceDomainsModule, ], providers: [ DestroyWorkspaceCommand,