feat: add proration invoice and reminder email templates (#27246)
* 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 <noreply@anthropic.com> * fix: use allSettled --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
bf39489899
commit
cb76e8bde0
@@ -3791,6 +3791,18 @@
|
||||
"cal_ai_credit_limit_reached_message": "Your Cal.com team {{teamName}} has run out of credits. As a result, Cal.ai phone calls are now disabled. To resume using Cal.ai phone calls, please purchase additional credits.",
|
||||
"cal_ai_credit_limit_reached_message_user": "Your Cal.com account has run out of credits. As a result, Cal.ai phone calls are now disabled. To resume using Cal.ai phone calls, please purchase additional credits.",
|
||||
"current_credit_balance": "Current balance: {{balance}} credits",
|
||||
"proration_invoice_subject": "Invoice for additional seats - {{teamName}} (${{amount}})",
|
||||
"proration_invoice_auto_charge_message": "An invoice for ${{amount}} has been created for {{seats}} additional seat(s) added to your team {{teamName}} during {{month}}. This amount will be automatically charged to your default payment method.",
|
||||
"proration_invoice_manual_payment_message": "An invoice for ${{amount}} has been created for {{seats}} additional seat(s) added to your team {{teamName}} during {{month}}. Please pay this invoice to complete the billing for your additional seats.",
|
||||
"proration_description": "Description",
|
||||
"proration_line_item": "{{seats}} additional seat(s) - {{month}}",
|
||||
"amount": "Amount",
|
||||
"pay_invoice": "Pay Invoice",
|
||||
"pay_invoice_now": "Pay Invoice Now",
|
||||
"view_billing_settings": "View Billing Settings",
|
||||
"proration_reminder_subject": "Payment Reminder - {{teamName}}",
|
||||
"proration_reminder_message": "This is a reminder that your invoice of ${{amount}} for team {{teamName}} is still unpaid.",
|
||||
"proration_reminder_warning": "If this invoice remains unpaid, you will not be able to add new users to your team until payment is received.",
|
||||
"current_balance": "Current balance:",
|
||||
"notification_about_your_booking": "Notification about your booking",
|
||||
"monthly_credits": "Monthly credits",
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import type { TFunction } from "i18next";
|
||||
|
||||
import type BaseEmail from "@calcom/emails/templates/_base-email";
|
||||
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||
import type { CreditUsageType } from "@calcom/prisma/enums";
|
||||
import type { EventTypeMetadata } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import OrganizerPaymentRefundFailedEmail from "./templates/organizer-payment-refund-failed-email";
|
||||
import NoShowFeeChargedEmail from "./templates/no-show-fee-charged-email";
|
||||
import CreditBalanceLowWarningEmail from "./templates/credit-balance-low-warning-email";
|
||||
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||
import type { TFunction } from "i18next";
|
||||
import CreditBalanceLimitReachedEmail from "./templates/credit-balance-limit-reached-email";
|
||||
import CreditBalanceLowWarningEmail from "./templates/credit-balance-low-warning-email";
|
||||
import NoShowFeeChargedEmail from "./templates/no-show-fee-charged-email";
|
||||
import OrganizerPaymentRefundFailedEmail from "./templates/organizer-payment-refund-failed-email";
|
||||
import ProrationInvoiceEmail from "./templates/proration-invoice-email";
|
||||
import ProrationReminderEmail from "./templates/proration-reminder-email";
|
||||
|
||||
const sendEmail = (prepare: () => BaseEmail) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -127,3 +127,102 @@ export const sendCreditBalanceLimitReachedEmails = async ({
|
||||
await sendEmail(() => new CreditBalanceLimitReachedEmail({ user, creditFor }));
|
||||
}
|
||||
};
|
||||
|
||||
export const sendProrationInvoiceEmails = async ({
|
||||
team,
|
||||
proration,
|
||||
invoiceUrl,
|
||||
isAutoCharge,
|
||||
adminAndOwners,
|
||||
}: {
|
||||
team: {
|
||||
id: number;
|
||||
name: string | null;
|
||||
};
|
||||
proration: {
|
||||
monthKey: string;
|
||||
netSeatIncrease: number;
|
||||
proratedAmount: number;
|
||||
};
|
||||
invoiceUrl?: string | null;
|
||||
isAutoCharge: boolean;
|
||||
adminAndOwners: {
|
||||
id: number;
|
||||
name: string | null;
|
||||
email: string;
|
||||
t: TFunction;
|
||||
}[];
|
||||
}) => {
|
||||
if (!adminAndOwners.length) return;
|
||||
|
||||
const emailsToSend: Promise<unknown>[] = [];
|
||||
|
||||
for (const admin of adminAndOwners) {
|
||||
emailsToSend.push(
|
||||
sendEmail(
|
||||
() =>
|
||||
new ProrationInvoiceEmail({
|
||||
user: admin,
|
||||
team,
|
||||
proration,
|
||||
invoiceUrl,
|
||||
isAutoCharge,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const results = await Promise.allSettled(emailsToSend);
|
||||
const failures = results.filter((r) => r.status === "rejected");
|
||||
if (failures.length > 0) {
|
||||
console.error(`${failures.length} email(s) failed to send`, failures);
|
||||
}
|
||||
};
|
||||
|
||||
export const sendProrationReminderEmails = async ({
|
||||
team,
|
||||
proration,
|
||||
invoiceUrl,
|
||||
adminAndOwners,
|
||||
}: {
|
||||
team: {
|
||||
id: number;
|
||||
name: string | null;
|
||||
};
|
||||
proration: {
|
||||
monthKey: string;
|
||||
netSeatIncrease: number;
|
||||
proratedAmount: number;
|
||||
};
|
||||
invoiceUrl?: string | null;
|
||||
adminAndOwners: {
|
||||
id: number;
|
||||
name: string | null;
|
||||
email: string;
|
||||
t: TFunction;
|
||||
}[];
|
||||
}) => {
|
||||
if (!adminAndOwners.length) return;
|
||||
|
||||
const emailsToSend: Promise<unknown>[] = [];
|
||||
|
||||
for (const admin of adminAndOwners) {
|
||||
emailsToSend.push(
|
||||
sendEmail(
|
||||
() =>
|
||||
new ProrationReminderEmail({
|
||||
user: admin,
|
||||
team,
|
||||
proration,
|
||||
invoiceUrl,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const results = await Promise.allSettled(emailsToSend);
|
||||
const failures = results.filter((r) => r.status === "rejected");
|
||||
if (failures.length > 0) {
|
||||
console.error(`${failures.length} email(s) failed to send`, failures);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import type { TFunction } from "i18next";
|
||||
|
||||
import { CallToAction, V2BaseEmailHtml } from "../components";
|
||||
|
||||
export interface ProrationInvoiceEmailProps {
|
||||
user: {
|
||||
name: string;
|
||||
email: string;
|
||||
t: TFunction;
|
||||
};
|
||||
team: {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
proration: {
|
||||
monthKey: string;
|
||||
netSeatIncrease: number;
|
||||
proratedAmount: number;
|
||||
};
|
||||
invoiceUrl?: string | null;
|
||||
isAutoCharge: boolean;
|
||||
}
|
||||
|
||||
export const ProrationInvoiceEmail = (props: ProrationInvoiceEmailProps) => {
|
||||
const { user, team, proration, invoiceUrl, isAutoCharge } = props;
|
||||
const { t } = user;
|
||||
const formattedAmount = (proration.proratedAmount / 100).toFixed(2);
|
||||
|
||||
return (
|
||||
<V2BaseEmailHtml
|
||||
subject={t("proration_invoice_subject", {
|
||||
teamName: team.name,
|
||||
amount: formattedAmount,
|
||||
interpolation: { escapeValue: false },
|
||||
})}>
|
||||
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
|
||||
{t("hi_user_name", { name: user.name, interpolation: { escapeValue: false } })},
|
||||
</p>
|
||||
<p style={{ fontWeight: 400, lineHeight: "24px", marginBottom: "24px" }}>
|
||||
{isAutoCharge
|
||||
? t("proration_invoice_auto_charge_message", {
|
||||
teamName: team.name,
|
||||
seats: proration.netSeatIncrease,
|
||||
amount: formattedAmount,
|
||||
month: proration.monthKey,
|
||||
interpolation: { escapeValue: false },
|
||||
})
|
||||
: t("proration_invoice_manual_payment_message", {
|
||||
teamName: team.name,
|
||||
seats: proration.netSeatIncrease,
|
||||
amount: formattedAmount,
|
||||
month: proration.monthKey,
|
||||
interpolation: { escapeValue: false },
|
||||
})}
|
||||
</p>
|
||||
|
||||
{/* Invoice Details Table */}
|
||||
<div style={{ marginBottom: "24px" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
borderBottom: "1px solid #E5E7EB",
|
||||
padding: "12px 0",
|
||||
}}>
|
||||
<span style={{ fontWeight: 500, color: "#374151" }}>{t("proration_description")}</span>
|
||||
<span style={{ fontWeight: 500, color: "#374151" }}>{t("amount")}</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
borderBottom: "1px solid #E5E7EB",
|
||||
padding: "12px 0",
|
||||
}}>
|
||||
<span style={{ color: "#6B7280" }}>
|
||||
{t("proration_line_item", {
|
||||
seats: proration.netSeatIncrease,
|
||||
month: proration.monthKey,
|
||||
})}
|
||||
</span>
|
||||
<span style={{ color: "#6B7280" }}>${formattedAmount}</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
padding: "12px 0",
|
||||
}}>
|
||||
<span style={{ fontWeight: 600, color: "#111827" }}>{t("total")}</span>
|
||||
<span style={{ fontWeight: 600, color: "#111827" }}>${formattedAmount}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}>
|
||||
<a
|
||||
href={`${WEBAPP_URL}/settings/teams/${team.id}/billing`}
|
||||
style={{ color: "#6B7280", fontSize: "14px", textDecoration: "none" }}>
|
||||
{t("view_billing_settings")}
|
||||
</a>
|
||||
{!isAutoCharge && invoiceUrl && <CallToAction label={t("pay_invoice")} href={invoiceUrl} />}
|
||||
</div>
|
||||
</V2BaseEmailHtml>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,113 @@
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import type { TFunction } from "i18next";
|
||||
|
||||
import { CallToAction, V2BaseEmailHtml } from "../components";
|
||||
|
||||
export interface ProrationReminderEmailProps {
|
||||
user: {
|
||||
name: string;
|
||||
email: string;
|
||||
t: TFunction;
|
||||
};
|
||||
team: {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
proration: {
|
||||
monthKey: string;
|
||||
netSeatIncrease: number;
|
||||
proratedAmount: number;
|
||||
};
|
||||
invoiceUrl?: string | null;
|
||||
}
|
||||
|
||||
export const ProrationReminderEmail = (props: ProrationReminderEmailProps) => {
|
||||
const { user, team, proration, invoiceUrl } = props;
|
||||
const { t } = user;
|
||||
const formattedAmount = (proration.proratedAmount / 100).toFixed(2);
|
||||
|
||||
return (
|
||||
<V2BaseEmailHtml
|
||||
subject={t("proration_reminder_subject", {
|
||||
teamName: team.name,
|
||||
interpolation: { escapeValue: false },
|
||||
})}>
|
||||
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
|
||||
{t("hi_user_name", { name: user.name, interpolation: { escapeValue: false } })},
|
||||
</p>
|
||||
<p style={{ fontWeight: 400, lineHeight: "24px", marginBottom: "24px" }}>
|
||||
{t("proration_reminder_message", {
|
||||
teamName: team.name,
|
||||
amount: formattedAmount,
|
||||
interpolation: { escapeValue: false },
|
||||
})}
|
||||
</p>
|
||||
|
||||
{/* Warning Banner */}
|
||||
<p
|
||||
style={{
|
||||
backgroundColor: "#FEF3C7",
|
||||
borderLeft: "4px solid #F59E0B",
|
||||
padding: "12px 16px",
|
||||
marginBottom: "24px",
|
||||
color: "#92400E",
|
||||
fontWeight: 500,
|
||||
lineHeight: "20px",
|
||||
}}>
|
||||
{t("proration_reminder_warning")}
|
||||
</p>
|
||||
|
||||
{/* Invoice Details Table */}
|
||||
<div style={{ marginBottom: "24px" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
borderBottom: "1px solid #E5E7EB",
|
||||
padding: "12px 0",
|
||||
}}>
|
||||
<span style={{ fontWeight: 500, color: "#374151" }}>{t("proration_description")}</span>
|
||||
<span style={{ fontWeight: 500, color: "#374151" }}>{t("amount")}</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
borderBottom: "1px solid #E5E7EB",
|
||||
padding: "12px 0",
|
||||
}}>
|
||||
<span style={{ color: "#6B7280" }}>
|
||||
{t("proration_line_item", {
|
||||
seats: proration.netSeatIncrease,
|
||||
month: proration.monthKey,
|
||||
})}
|
||||
</span>
|
||||
<span style={{ color: "#6B7280" }}>${formattedAmount}</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
padding: "12px 0",
|
||||
}}>
|
||||
<span style={{ fontWeight: 600, color: "#111827" }}>{t("total")}</span>
|
||||
<span style={{ fontWeight: 600, color: "#111827" }}>${formattedAmount}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}>
|
||||
<a
|
||||
href={`${WEBAPP_URL}/settings/teams/${team.id}/billing`}
|
||||
style={{ color: "#6B7280", fontSize: "14px", textDecoration: "none" }}>
|
||||
{t("view_billing_settings")}
|
||||
</a>
|
||||
{invoiceUrl && <CallToAction label={t("pay_invoice_now")} href={invoiceUrl} />}
|
||||
</div>
|
||||
</V2BaseEmailHtml>
|
||||
);
|
||||
};
|
||||
@@ -1,48 +1,50 @@
|
||||
export * from "@calcom/app-store/routing-forms/emails/components";
|
||||
export { AdminOAuthClientNotificationEmail } from "./AdminOAuthClientNotificationEmail";
|
||||
export { AdminOrganizationNotificationEmail } from "./AdminOrganizationNotificationEmail";
|
||||
export { AttendeeAddGuestsEmail } from "./AttendeeAddGuestsEmail";
|
||||
export { AttendeeAwaitingPaymentEmail } from "./AttendeeAwaitingPaymentEmail";
|
||||
export { AttendeeCancelledEmail } from "./AttendeeCancelledEmail";
|
||||
export { AttendeeCancelledSeatEmail } from "./AttendeeCancelledSeatEmail";
|
||||
export { AttendeeDeclinedEmail } from "./AttendeeDeclinedEmail";
|
||||
export { AttendeeLocationChangeEmail } from "./AttendeeLocationChangeEmail";
|
||||
export { AttendeeRequestEmail } from "./AttendeeRequestEmail";
|
||||
export { AttendeeWasRequestedToRescheduleEmail } from "./AttendeeWasRequestedToRescheduleEmail";
|
||||
export { AttendeeRescheduledEmail } from "./AttendeeRescheduledEmail";
|
||||
export { AttendeeUpdatedEmail } from "./AttendeeUpdatedEmail";
|
||||
export { AttendeeScheduledEmail } from "./AttendeeScheduledEmail";
|
||||
export { DisabledAppEmail } from "./DisabledAppEmail";
|
||||
export { SlugReplacementEmail } from "./SlugReplacementEmail";
|
||||
export { FeedbackEmail } from "./FeedbackEmail";
|
||||
export { ForgotPasswordEmail } from "./ForgotPasswordEmail";
|
||||
export { OrganizerCancelledEmail } from "./OrganizerCancelledEmail";
|
||||
export { OrganizerReassignedEmail } from "./OrganizerReassignedEmail";
|
||||
export { OrganizerLocationChangeEmail } from "./OrganizerLocationChangeEmail";
|
||||
export { OrganizerPaymentRefundFailedEmail } from "./OrganizerPaymentRefundFailedEmail";
|
||||
export { OrganizerRequestEmail } from "./OrganizerRequestEmail";
|
||||
export { OrganizerRequestEmailV2 } from "./OrganizerRequestEmailV2";
|
||||
export { OrganizerRequestReminderEmail } from "./OrganizerRequestReminderEmail";
|
||||
export { OrganizerRequestedToRescheduleEmail } from "./OrganizerRequestedToRescheduleEmail";
|
||||
export { OrganizerRescheduledEmail } from "./OrganizerRescheduledEmail";
|
||||
export { OrganizerScheduledEmail } from "./OrganizerScheduledEmail";
|
||||
export { TeamInviteEmail } from "./TeamInviteEmail";
|
||||
export { AttendeeUpdatedEmail } from "./AttendeeUpdatedEmail";
|
||||
export { AttendeeWasRequestedToRescheduleEmail } from "./AttendeeWasRequestedToRescheduleEmail";
|
||||
export { BookingRedirectEmailNotification } from "./BookingRedirectEmailNotification";
|
||||
export { BrokenIntegrationEmail } from "./BrokenIntegrationEmail";
|
||||
export { CreditBalanceLowWarningEmail } from "./CreditBalanceLowWarningEmail";
|
||||
export { CreditBalanceLimitReachedEmail } from "./CreditBalanceLimitReachedEmail";
|
||||
export { OrganizerAttendeeCancelledSeatEmail } from "./OrganizerAttendeeCancelledSeatEmail";
|
||||
export { NoShowFeeChargedEmail } from "./NoShowFeeChargedEmail";
|
||||
export { VerifyAccountEmail } from "./VerifyAccountEmail";
|
||||
export { VerifyEmailByCode } from "./VerifyEmailByCode";
|
||||
export * from "@calcom/app-store/routing-forms/emails/components";
|
||||
export { CreditBalanceLowWarningEmail } from "./CreditBalanceLowWarningEmail";
|
||||
export { DailyVideoDownloadRecordingEmail } from "./DailyVideoDownloadRecordingEmail";
|
||||
export { DailyVideoDownloadTranscriptEmail } from "./DailyVideoDownloadTranscriptEmail";
|
||||
export { OrganisationAccountVerifyEmail } from "./OrganizationAccountVerifyEmail";
|
||||
export { OrgAutoInviteEmail } from "./OrgAutoInviteEmail";
|
||||
export { DisabledAppEmail } from "./DisabledAppEmail";
|
||||
export { FeedbackEmail } from "./FeedbackEmail";
|
||||
export { ForgotPasswordEmail } from "./ForgotPasswordEmail";
|
||||
export { MonthlyDigestEmail } from "./MonthlyDigestEmail";
|
||||
export { AdminOrganizationNotificationEmail } from "./AdminOrganizationNotificationEmail";
|
||||
export { AdminOAuthClientNotificationEmail } from "./AdminOAuthClientNotificationEmail";
|
||||
export { NoShowFeeChargedEmail } from "./NoShowFeeChargedEmail";
|
||||
export { OAuthClientApprovedNotificationEmail } from "./OAuthClientApprovedNotificationEmail";
|
||||
export { OAuthClientRejectedNotificationEmail } from "./OAuthClientRejectedNotificationEmail";
|
||||
export { BookingRedirectEmailNotification } from "./BookingRedirectEmailNotification";
|
||||
export { VerifyEmailChangeEmail } from "./VerifyEmailChangeEmail";
|
||||
export { OrgAutoInviteEmail } from "./OrgAutoInviteEmail";
|
||||
export { OrganisationAccountVerifyEmail } from "./OrganizationAccountVerifyEmail";
|
||||
export { OrganizationAdminNoSlotsEmail } from "./OrganizationAdminNoSlots";
|
||||
export { OrganizationCreationEmail } from "./OrganizationCreationEmail";
|
||||
export { OrganizerAddGuestsEmail } from "./OrganizerAddGuestsEmail";
|
||||
export { AttendeeAddGuestsEmail } from "./AttendeeAddGuestsEmail";
|
||||
export { OrganizationAdminNoSlotsEmail } from "./OrganizationAdminNoSlots";
|
||||
export { OrganizerAttendeeCancelledSeatEmail } from "./OrganizerAttendeeCancelledSeatEmail";
|
||||
export { OrganizerCancelledEmail } from "./OrganizerCancelledEmail";
|
||||
export { OrganizerLocationChangeEmail } from "./OrganizerLocationChangeEmail";
|
||||
export { OrganizerPaymentRefundFailedEmail } from "./OrganizerPaymentRefundFailedEmail";
|
||||
export { OrganizerReassignedEmail } from "./OrganizerReassignedEmail";
|
||||
export { OrganizerRequestEmail } from "./OrganizerRequestEmail";
|
||||
export { OrganizerRequestEmailV2 } from "./OrganizerRequestEmailV2";
|
||||
export { OrganizerRequestedToRescheduleEmail } from "./OrganizerRequestedToRescheduleEmail";
|
||||
export { OrganizerRequestReminderEmail } from "./OrganizerRequestReminderEmail";
|
||||
export { OrganizerRescheduledEmail } from "./OrganizerRescheduledEmail";
|
||||
export { OrganizerScheduledEmail } from "./OrganizerScheduledEmail";
|
||||
export { ProrationInvoiceEmail } from "./ProrationInvoiceEmail";
|
||||
export { ProrationReminderEmail } from "./ProrationReminderEmail";
|
||||
export { SlugReplacementEmail } from "./SlugReplacementEmail";
|
||||
export { TeamInviteEmail } from "./TeamInviteEmail";
|
||||
export { VerifyAccountEmail } from "./VerifyAccountEmail";
|
||||
export { VerifyEmailByCode } from "./VerifyEmailByCode";
|
||||
export { VerifyEmailChangeEmail } from "./VerifyEmailChangeEmail";
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
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 ProrationInvoiceEmailParams {
|
||||
user: {
|
||||
name: string | null;
|
||||
email: string;
|
||||
t: TFunction;
|
||||
};
|
||||
team: {
|
||||
id: number;
|
||||
name: string | null;
|
||||
};
|
||||
proration: {
|
||||
monthKey: string;
|
||||
netSeatIncrease: number;
|
||||
proratedAmount: number;
|
||||
};
|
||||
invoiceUrl?: string | null;
|
||||
isAutoCharge: boolean;
|
||||
}
|
||||
|
||||
export default class ProrationInvoiceEmail extends BaseEmail {
|
||||
user: {
|
||||
name: string;
|
||||
email: string;
|
||||
t: TFunction;
|
||||
};
|
||||
team: {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
proration: {
|
||||
monthKey: string;
|
||||
netSeatIncrease: number;
|
||||
proratedAmount: number;
|
||||
};
|
||||
invoiceUrl?: string | null;
|
||||
isAutoCharge: boolean;
|
||||
|
||||
constructor(params: ProrationInvoiceEmailParams) {
|
||||
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;
|
||||
this.isAutoCharge = params.isAutoCharge;
|
||||
}
|
||||
|
||||
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||
const formattedAmount = (this.proration.proratedAmount / 100).toFixed(2);
|
||||
|
||||
return {
|
||||
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
|
||||
to: this.user.email,
|
||||
subject: this.user.t("proration_invoice_subject", {
|
||||
teamName: this.team.name,
|
||||
amount: formattedAmount,
|
||||
}),
|
||||
html: await renderEmail("ProrationInvoiceEmail", {
|
||||
user: this.user,
|
||||
team: this.team,
|
||||
proration: this.proration,
|
||||
invoiceUrl: this.invoiceUrl,
|
||||
isAutoCharge: this.isAutoCharge,
|
||||
}),
|
||||
text: this.getTextBody(),
|
||||
};
|
||||
}
|
||||
|
||||
protected getTextBody(): string {
|
||||
const formattedAmount = (this.proration.proratedAmount / 100).toFixed(2);
|
||||
return `An invoice for $${formattedAmount} has been created for ${this.proration.netSeatIncrease} additional seat(s) for your team ${this.team.name}.`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
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 `Reminder: Your invoice of $${formattedAmount} for team ${this.team.name} is still unpaid. Please pay to avoid restrictions on adding new users.`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user