Files
Eunjae LeeandClaude Opus 4.6 98b6d63164 refactor: apply biome formatting to packages/features (#27844)
* refactor: apply biome formatting to packages/features (batch 1 - small subdirs)

Format small subdirectories in packages/features: di, flags, holidays, oauth,
settings, users, assignment-reason, selectedCalendar, hashedLink, host, form,
form-builder, availability, data-table, pbac, schedules, troubleshooter,
eventtypes, calendar-subscription, and root-level files.

Also includes straggler apps/web BookEventForm.tsx.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: apply biome formatting to packages/features (batch 2 - medium subdirs)

Format medium subdirectories in packages/features: auth, credentials,
calendars, routing-forms, routing-trace, attributes, watchlist, calAIPhone,
tasker, and webhooks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: apply biome formatting to packages/features (batch 3 - bookings + insights)

Format bookings and insights subdirectories in packages/features.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: apply biome formatting to packages/features (batch 4 - ee)

Format packages/features/ee subdirectory covering billing, workflows,
organizations, teams, managed-event-types, round-robin, dsync,
integration-attribute-sync, and payments.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: apply biome formatting to packages/features (batch 5 - booking-audit part 1)

Format booking-audit di, actions, common, dto, repository, and types
subdirectories in packages/features/booking-audit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: apply biome formatting to packages/features (batch 6 - booking-audit part 2)

Format booking-audit service subdirectory in packages/features/booking-audit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 15:47:14 +01:00

157 lines
4.6 KiB
TypeScript

import type { TFunction } from "i18next";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { prisma } from "@calcom/prisma";
import { getUserAndTeamWithBillingPermission } from "../../helpers/getUserAndTeamWithBillingPermission";
import { MonthlyProrationRepository } from "../../repository/proration/MonthlyProrationRepository";
const log = logger.getSubLogger({ prefix: ["ProrationEmailService"] });
interface UserWithBillingAccess {
id: number;
name: string | null;
email: string;
t: TFunction;
}
export interface SendInvoiceEmailParams {
prorationId: string;
teamId: number;
isAutoCharge: boolean;
}
export interface SendReminderEmailParams {
prorationId: string;
teamId: number;
}
export class ProrationEmailService {
private prorationRepository: MonthlyProrationRepository;
constructor() {
this.prorationRepository = new MonthlyProrationRepository();
}
async sendInvoiceEmail(params: SendInvoiceEmailParams): Promise<void> {
const { prorationId, teamId, isAutoCharge } = params;
log.debug(`Processing invoice email for prorationId ${prorationId}, teamId ${teamId}`);
const proration = await this.prorationRepository.findForEmail(prorationId);
if (!proration) {
log.warn(`Proration ${prorationId} not found, skipping invoice email`);
return;
}
const { team } = await getUserAndTeamWithBillingPermission({
teamId,
prismaClient: prisma,
});
if (!team) {
log.warn(`Team ${teamId} not found, skipping invoice email`);
return;
}
if (team.adminAndOwners.length === 0) {
log.warn(`No users with billing permission found for team ${teamId}, skipping invoice email`);
return;
}
const invoiceUrl = await this.getInvoiceUrl(proration.invoiceId);
const { sendProrationInvoiceEmails } = await import("@calcom/emails/billing-email-service");
await sendProrationInvoiceEmails({
team: { id: team.id, name: team.name },
proration: {
monthKey: proration.monthKey,
netSeatIncrease: proration.netSeatIncrease,
proratedAmount: proration.proratedAmount,
},
invoiceUrl,
isAutoCharge,
adminAndOwners: team.adminAndOwners,
});
log.debug(`Successfully sent proration invoice emails for prorationId ${prorationId}`);
}
async sendReminderEmail(params: SendReminderEmailParams): Promise<void> {
const { prorationId, teamId } = params;
log.debug(`Processing reminder email for prorationId ${prorationId}, teamId ${teamId}`);
const proration = await this.prorationRepository.findForEmail(prorationId);
if (!proration) {
log.warn(`Proration ${prorationId} not found, skipping reminder email`);
return;
}
if (proration.status === "CHARGED") {
log.debug(`Proration ${prorationId} already charged, skipping reminder email`);
return;
}
const { team } = await getUserAndTeamWithBillingPermission({
teamId,
prismaClient: prisma,
});
if (!team) {
log.warn(`Team ${teamId} not found, skipping reminder email`);
return;
}
if (team.adminAndOwners.length === 0) {
log.warn(`No users with billing permission found for team ${teamId}, skipping reminder email`);
return;
}
const invoiceUrl = await this.getInvoiceUrl(proration.invoiceId);
const { default: ProrationReminderEmail } = await import(
"@calcom/emails/templates/proration-reminder-email"
);
const emailPromises = team.adminAndOwners.map(async (user: UserWithBillingAccess) => {
const email = new ProrationReminderEmail({
user: {
name: user.name,
email: user.email,
t: user.t,
},
team: {
id: team.id,
name: team.name,
},
proration: {
monthKey: proration.monthKey,
netSeatIncrease: proration.netSeatIncrease,
proratedAmount: proration.proratedAmount,
},
invoiceUrl,
});
return email.sendEmail();
});
await Promise.all(emailPromises);
log.debug(`Successfully sent proration reminder emails for prorationId ${prorationId}`);
}
private async getInvoiceUrl(invoiceId: string | null): Promise<string | null> {
if (!invoiceId) return null;
try {
const { default: stripe } = await import("@calcom/features/ee/payments/server/stripe");
const invoice = await stripe.invoices.retrieve(invoiceId);
return invoice.hosted_invoice_url ?? null;
} catch (error) {
log.warn(`Failed to retrieve invoice URL for ${invoiceId}`, safeStringify(error));
return null;
}
}
}