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,