* refactor: convert getShouldServeCache to CacheService with dependency injection - Create CacheService class following AvailableSlotsService DI pattern - Add FeaturesRepository and CacheService to DI tokens and modules - Create cache container with proper dependency injection setup - Update handleNewBooking.ts and slots/util.ts to use new service - Maintain backward compatibility with error-throwing wrapper function - Follow established service patterns for clean architecture Co-Authored-By: morgan@cal.com <morgan@cal.com> * feat: inject CacheService into AvailableSlotsService via dependency injection - Add cacheService to IAvailableSlotsService interface - Update available-slots container to load cache modules - Update available-slots module to inject CacheService dependency - Replace direct getShouldServeCache call with injected service method - Add CacheService import to util.ts for proper typing Co-Authored-By: morgan@cal.com <morgan@cal.com> * chore: DI api v2 cache service * refactor: convert FeaturesRepository to use factory pattern in DI - Change from constructor injection to factory pattern to avoid PRISMA_CLIENT binding issues in tests - FeaturesRepository now uses default prisma instance instead of DI injection - Resolves test failures while maintaining DI container compatibility - Tests reduced from 123+ failures to only 5 unrelated failures Co-Authored-By: morgan@cal.com <morgan@cal.com> * revert: use direct FeaturesRepository instantiation in most usage points - Revert getFeaturesRepository() calls back to new FeaturesRepository() - Tests require direct instantiation for mocking compatibility - Keep DI container for specific use cases that need dependency injection - Resolves test failures while maintaining both DI and direct usage patterns Co-Authored-By: morgan@cal.com <morgan@cal.com> * fix: resolve FeaturesRepository DI container issues - Update cache module to use factory pattern with proper ICacheService interface - Remove featuresModule loading from cache and available-slots containers - Use direct FeaturesRepository instantiation via getFeaturesRepository() - Resolves 'No binding found for key: Symbol(FeaturesRepository)' errors - Reduces test failures from 125 to 7 (remaining failures appear unrelated) Co-Authored-By: morgan@cal.com <morgan@cal.com> * fix: update all FeaturesRepository instantiations to include prisma parameter - Add prisma parameter to all new FeaturesRepository() calls across the codebase - Update API v2 services to match main repo interfaces - Fix PrismaFeaturesRepository to implement IFeaturesRepository directly - Update CacheService in API v2 to expose required dependencies and getShouldServeCache - Implement CheckBookingLimitsService in API v2 with proper interface - Resolves type assignment errors between API v2 and main repo implementations Co-Authored-By: morgan@cal.com <morgan@cal.com> * fix: add prisma parameter to remaining FeaturesRepository instantiations in apps/web/lib - Update getServerSideProps files to pass prisma parameter to FeaturesRepository - Ensures all FeaturesRepository instantiations follow the new constructor pattern - Completes the refactoring to use direct instantiation with prisma parameter Co-Authored-By: morgan@cal.com <morgan@cal.com> * refactor clean and fix devin issues * chore: bump platform libs * chore: bump platform libs * chore: bump platform libs * chore: bump platform libs * fix: missing di * fix workflow test * fix workflow test * fix integration test --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: morgan@cal.com <morgan@cal.com> Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
111 lines
3.7 KiB
TypeScript
111 lines
3.7 KiB
TypeScript
import { decodeHTML } from "entities";
|
|
import { z } from "zod";
|
|
|
|
import dayjs from "@calcom/dayjs";
|
|
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
|
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
|
import isSmsCalEmail from "@calcom/lib/isSmsCalEmail";
|
|
import { serverConfig } from "@calcom/lib/serverConfig";
|
|
import { setTestEmail } from "@calcom/lib/testEmails";
|
|
import { prisma } from "@calcom/prisma";
|
|
|
|
import { sanitizeDisplayName } from "../lib/sanitizeDisplayName";
|
|
|
|
export default class BaseEmail {
|
|
name = "";
|
|
|
|
protected getTimezone() {
|
|
return "";
|
|
}
|
|
|
|
protected getLocale(): string {
|
|
return "";
|
|
}
|
|
|
|
protected getFormattedRecipientTime({ time, format }: { time: string; format: string }) {
|
|
return dayjs(time).tz(this.getTimezone()).locale(this.getLocale()).format(format);
|
|
}
|
|
|
|
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
|
return {};
|
|
}
|
|
public async sendEmail() {
|
|
const featuresRepository = new FeaturesRepository(prisma);
|
|
const emailsDisabled = await featuresRepository.checkIfFeatureIsEnabledGlobally("emails");
|
|
/** If email kill switch exists and is active, we prevent emails being sent. */
|
|
if (emailsDisabled) {
|
|
console.warn("Skipped Sending Email due to active Kill Switch");
|
|
return new Promise((r) => r("Skipped Sending Email due to active Kill Switch"));
|
|
}
|
|
|
|
if (process.env.INTEGRATION_TEST_MODE === "true") {
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
//@ts-expect-error
|
|
setTestEmail(await this.getNodeMailerPayload());
|
|
console.log(
|
|
"Skipped Sending Email as process.env.NEXT_PUBLIC_UNIT_TESTS is set. Emails are available in globalThis.testEmails"
|
|
);
|
|
return new Promise((r) => r("Skipped sendEmail for Unit Tests"));
|
|
}
|
|
|
|
const payload = await this.getNodeMailerPayload();
|
|
|
|
const from = "from" in payload ? (payload.from as string) : "";
|
|
const to = "to" in payload ? (payload.to as string) : "";
|
|
|
|
if (isSmsCalEmail(to)) {
|
|
console.log(`Skipped Sending Email to faux email: ${to}`);
|
|
return new Promise((r) => r(`Skipped Sending Email to faux email: ${to}`));
|
|
}
|
|
|
|
const sanitizedFrom = sanitizeDisplayName(from);
|
|
const sanitizedTo = sanitizeDisplayName(to);
|
|
|
|
const parseSubject = z.string().safeParse(payload?.subject);
|
|
const payloadWithUnEscapedSubject = {
|
|
headers: this.getMailerOptions().headers,
|
|
...payload,
|
|
...{
|
|
from: sanitizedFrom,
|
|
to: sanitizedTo,
|
|
},
|
|
...(parseSubject.success && { subject: decodeHTML(parseSubject.data) }),
|
|
};
|
|
const { createTransport } = await import("nodemailer");
|
|
await new Promise((resolve, reject) =>
|
|
createTransport(this.getMailerOptions().transport).sendMail(
|
|
payloadWithUnEscapedSubject,
|
|
(_err, info) => {
|
|
if (_err) {
|
|
const err = getErrorFromUnknown(_err);
|
|
this.printNodeMailerError(err);
|
|
reject(err);
|
|
} else {
|
|
resolve(info);
|
|
}
|
|
}
|
|
)
|
|
).catch((e) =>
|
|
console.error(
|
|
"sendEmail",
|
|
`from: ${"from" in payloadWithUnEscapedSubject ? payloadWithUnEscapedSubject.from : ""}`,
|
|
`subject: ${"subject" in payloadWithUnEscapedSubject ? payloadWithUnEscapedSubject.subject : ""}`,
|
|
e
|
|
)
|
|
);
|
|
return new Promise((resolve) => resolve("send mail async"));
|
|
}
|
|
protected getMailerOptions() {
|
|
return {
|
|
transport: serverConfig.transport,
|
|
from: serverConfig.from,
|
|
headers: serverConfig.headers,
|
|
};
|
|
}
|
|
protected printNodeMailerError(error: Error): void {
|
|
/** Don't clog the logs with unsent emails in E2E */
|
|
if (process.env.NEXT_PUBLIC_IS_E2E) return;
|
|
console.error(`${this.name}_ERROR`, error);
|
|
}
|
|
}
|