Files
calendar/packages/emails/templates/proration-reminder-email.ts
sean-brydonGitHubClaude Opus 4.5Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
52f3ddaf74 feat: add tasker integration for proration email notifications (#27247)
* feat: add proration invoice and reminder email templates

Add email templates for monthly proration billing notifications:

- ProrationInvoiceEmail: Sent when invoice is created for additional seats
- ProrationReminderEmail: Sent 7 days later if invoice remains unpaid

Includes:
- React email templates using V2BaseEmailHtml
- BaseEmail classes for rendering
- Billing email service functions
- Translation keys for email content

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* feat: add tasker integration for proration email notifications

Add task handlers for proration billing email flow:

- sendProrationInvoiceEmail: Sends invoice email and schedules reminder
- sendProrationReminderEmail: Sends reminder if invoice still unpaid
- cancelProrationReminder: Cancels scheduled reminder on payment success

The calling job should trigger these tasks after:
1. MonthlyProrationService.createProrationForTeam() succeeds with invoice
2. handleProrationPaymentSuccess() is called (to cancel reminder)

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* feat: i18n + feedback from cubic + morgan

* fix: use default tasker import instead of non-existent getTasker

Co-Authored-By: unknown <>

* fix: add trigger tasks plus DI

* use for await

* fix query

* fix: remove duplicate JOIN alias in findTeamMembersWithPermission query

The raw SQL query had two INNER JOINs using the same alias 'u':
- INNER JOIN "users" u ON m."userId" = u.id
- INNER JOIN "User" u ON m."userId" = u.id

This would cause a SQL error at runtime. Removed the duplicate JOIN
with incorrect table name ("User" instead of "users").

Fixes issue identified by Cubic AI review.

Co-Authored-By: unknown <>

---------

Co-authored-by: Claude Opus 4.5 <[email protected]>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-03 08:29:36 +00:00

74 lines
1.8 KiB
TypeScript

import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
import type { TFunction } from "i18next";
import renderEmail from "../src/renderEmail";
import BaseEmail from "./_base-email";
export interface ProrationReminderEmailParams {
user: {
name: string | null;
email: string;
t: TFunction;
};
team: {
id: number;
name: string | null;
};
proration: {
monthKey: string;
netSeatIncrease: number;
proratedAmount: number;
};
invoiceUrl?: string | null;
}
export default class ProrationReminderEmail extends BaseEmail {
user: {
name: string;
email: string;
t: TFunction;
};
team: {
id: number;
name: string;
};
proration: {
monthKey: string;
netSeatIncrease: number;
proratedAmount: number;
};
invoiceUrl?: string | null;
constructor(params: ProrationReminderEmailParams) {
super();
this.user = { ...params.user, name: params.user.name || "" };
this.team = { ...params.team, name: params.team.name || "" };
this.proration = params.proration;
this.invoiceUrl = params.invoiceUrl;
}
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
return {
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
to: this.user.email,
subject: this.user.t("proration_reminder_subject", {
teamName: this.team.name,
}),
html: await renderEmail("ProrationReminderEmail", {
user: this.user,
team: this.team,
proration: this.proration,
invoiceUrl: this.invoiceUrl,
}),
text: this.getTextBody(),
};
}
protected getTextBody(): string {
const formattedAmount = (this.proration.proratedAmount / 100).toFixed(2);
return this.user.t("proration_reminder_text", {
amount: formattedAmount,
teamName: this.team.name,
});
}
}