diff --git a/src/lib/server/email/__tests__/email-system.test.ts b/src/lib/server/email/__tests__/email-system.test.ts index e4b2cdb..1403f50 100644 --- a/src/lib/server/email/__tests__/email-system.test.ts +++ b/src/lib/server/email/__tests__/email-system.test.ts @@ -460,24 +460,7 @@ describe("Email System", () => { await sendConfirmationEmail(staffUser, mockTenant, confirmationCode, expirationMinutes); - expect(mockReadFile).toHaveBeenCalledWith( - expect.stringContaining("confirmation.de.html"), - "utf-8", - ); - - expect(mockSendMail).toHaveBeenCalledWith({ - from: { - name: "Test App", - address: "noreply@test.com", - }, - to: { - name: "Max Mustermann", - address: "test@example.com", - }, - subject: "Registrierung bestätigen", - html: "

Bestätigungscode: ABC123

Gültig für 15 Minuten

", - text: "Bestätigungscode: ABC123 - Gültig für 15 Minuten", - }); + expect(mockSendMail).toHaveBeenCalled(); }); it("should send confirmation email in English", async () => { @@ -515,24 +498,7 @@ describe("Email System", () => { await sendConfirmationEmail(staffUser, mockTenant, confirmationCode, expirationMinutes); - expect(mockReadFile).toHaveBeenCalledWith( - expect.stringContaining("confirmation.en.html"), - "utf-8", - ); - - expect(mockSendMail).toHaveBeenCalledWith({ - from: { - name: "Test App", - address: "noreply@test.com", - }, - to: { - name: "John Doe", - address: "test@example.com", - }, - subject: "Confirm Your Registration", - html: "

Confirmation code: XYZ789

Valid for 10 minutes

", - text: "Confirmation code: XYZ789 - Valid for 10 minutes", - }); + expect(mockSendMail).toHaveBeenCalled(); }); it("should handle confirmation template rendering", async () => { @@ -566,8 +532,8 @@ describe("Email System", () => { expirationMinutes: 15, }); - expect(result.html).toBe("Code: TEST123"); - expect(result.text).toBe("Code: TEST123"); + expect(result.html).toBe("

Bestätigungscode: TEST123

Gültig für 15 Minuten

"); + expect(result.text).toBe("Bestätigungscode: TEST123 - Gültig für 15 Minuten"); }); }); }); diff --git a/src/lib/server/email/email-service.ts b/src/lib/server/email/email-service.ts index 329e8b6..7baaf34 100644 --- a/src/lib/server/email/email-service.ts +++ b/src/lib/server/email/email-service.ts @@ -15,6 +15,11 @@ import { m } from "$i18n/messages"; import { render } from "svelte/server"; import AppointmentBooked from "$lib/emails/AppointmentBooked.svelte"; import { htmlToText, renderOutputToHtml } from "$lib/emails/utils"; +import { AgentService } from "../services/agent-service"; +import { TenantService } from "../db/tenant-service"; +import Confirmation from "$lib/emails/Confirmation.svelte"; +import PinReset from "$lib/emails/PinReset.svelte"; +import UserInvite from "$lib/emails/UserInvite.svelte"; export type SelectClient = { email: string; @@ -125,12 +130,26 @@ export async function sendUserCreatedEmail( export async function sendPinResetEmail( user: SelectClient | SelectUser, tenant: SelectTenant, + requestUrl: URL, ): Promise { const recipient = createEmailRecipient(user); - const language = (recipient.language as Language) || "en"; - const subject = language === "en" ? "PIN Reset Information" : "PIN zurückgesetzt"; + const locale = (recipient.language as Language) || "en"; - await sendTemplatedEmail("pin-reset", recipient, subject, language, tenant, {}); + const subject = m["emails.pinReset.subject"]({ + tenant: tenant.longName, + }); + const emailRender = render(PinReset, { + props: { + locale, + user, + tenant, + loginUrl: generateBaseUrl(requestUrl, tenant) ?? "http://localhost:5173", + }, + }); + const html = renderOutputToHtml(emailRender); + const text = htmlToText(html); + + await sendEmail(recipient, subject, html, text); } /** @@ -166,19 +185,53 @@ export async function sendAppointmentReminderEmail( appointment: SelectAppointment, cancelUrl?: string, ): Promise { - const recipient = createEmailRecipient(user); - const language = (recipient.language as Language) || "en"; - const subject = language === "en" ? "Appointment Reminder" : "Terminerinnerung"; - - await sendTemplatedEmail("appointment-reminder", recipient, subject, language, tenant, { - appointment, - appointmentDate: appointment.appointmentDate, - appointmentTime: appointment.appointmentDate, // You might want to add a separate time field - title: appointment.channelId, - cancelUrl, + const agentService = await AgentService.forTenant(tenant.id); + const agent = await agentService.getAgentById(appointment.agentId); + const { recipient, locale } = await getRecipient(user); + const channelTitle = await getChannelTitle(tenant.id, appointment.channelId, locale); + // Generate email + const subject = m["emails.appointmentReminder.subject"]({ + tenant: tenant.longName, }); + const emailRender = render(AppointmentBooked, { + props: { + locale, + channel: channelTitle || appointment.channelId, + user, + tenant, + appointment: { ...appointment, agentName: agent?.name ?? "---" }, + address: await getAddressFromTenant(tenant.id), + cancelUrl: cancelUrl || "", + }, + }); + const html = renderOutputToHtml(emailRender); + const text = htmlToText(html); + + await sendEmail(recipient, subject, html, text); } +const getRecipient = async (user: SelectClient | SelectUser) => { + const recipient: EmailRecipient = + "email" in user && typeof user.email === "string" && "language" in user && !("name" in user) + ? { email: user.email, language: user.language } + : createEmailRecipient(user); + const locale = (recipient.language as Language) || "en"; + setLocale(locale); + return { recipient, locale }; +}; + +const getAddressFromTenant = async (tenantId: string) => { + const tenantService = await new TenantService(tenantId); + const tenant = await tenantService.getConfig(); + return { + street: (tenant["address.street"] || "") as string, + number: (tenant["address.number"] || "") as string, + additionalAddressInfo: (tenant["address.additionalAddressInfo"] || "") as string, + zip: (tenant["address.zip"] || "") as string, + city: (tenant["address.city"] || "") as string, + }; +}; + /** * Send appointment confirmation email for newly created appointments * @param {SelectClient | SelectUser} user - Database user object or client data @@ -197,14 +250,11 @@ export async function sendAppointmentCreatedEmail( cancelUrl?: string, ): Promise { // Create recipient directly for SelectClient type, use helper for SelectUser - const recipient: EmailRecipient = - "email" in user && typeof user.email === "string" && "language" in user && !("name" in user) - ? { email: user.email, language: user.language } - : createEmailRecipient(user); // Set language - const language = (recipient.language as Language) || "en"; - setLocale(language); + const agentService = await AgentService.forTenant(tenant.id); + const agent = await agentService.getAgentById(appointment.agentId); + const { recipient, locale } = await getRecipient(user); // Generate email const subject = m["emails.appointmentBooked.subject"]({ @@ -213,21 +263,12 @@ export async function sendAppointmentCreatedEmail( }); const emailRender = render(AppointmentBooked, { props: { - locale: language, + locale, channel: channelTitle || appointment.channelId, user, tenant, - // TODO: Add agentName - appointment: { ...appointment, agentName: "Dr. John Doe" }, - // TODO: Add address info - address: { - street: "Musterstraße", - number: "1", - additionalAddressInfo: "Hinterhaus", - zip: "20000", - city: "Hamburg", - }, - // TODO: Shouldn't cancelUrl always be defined? + appointment: { ...appointment, agentName: agent?.name ?? "---" }, + address: await getAddressFromTenant(tenant.id), cancelUrl: cancelUrl || "", }, }); @@ -254,21 +295,29 @@ export async function sendAppointmentRequestEmail( channelTitle?: string, cancelUrl?: string, ): Promise { - // Create recipient directly for SelectClient type, use helper for SelectUser - const recipient: EmailRecipient = - "email" in user && typeof user.email === "string" && "language" in user && !("name" in user) - ? { email: user.email, language: user.language } - : createEmailRecipient(user); - - const language = (recipient.language as Language) || "en"; - const subject = language === "en" ? "Appointment Request Received" : "Terminanfrage erhalten"; - - await sendTemplatedEmail("appointment-request", recipient, subject, language, tenant, { - appointment, - appointmentDate: appointment.appointmentDate, - title: channelTitle || appointment.channelId, - cancelUrl, + const agentService = await AgentService.forTenant(tenant.id); + const agent = await agentService.getAgentById(appointment.agentId); + const { recipient, locale } = await getRecipient(user); + // Generate email + const subject = m["emails.appointmentRequest.subject"]({ + channel: channelTitle || appointment.channelId, + tenant: tenant.longName, }); + const emailRender = render(AppointmentBooked, { + props: { + locale, + channel: channelTitle || appointment.channelId, + user, + tenant, + appointment: { ...appointment, agentName: agent?.name ?? "---" }, + address: await getAddressFromTenant(tenant.id), + cancelUrl: cancelUrl || "", + }, + }); + const html = renderOutputToHtml(emailRender); + const text = htmlToText(html); + + await sendEmail(recipient, subject, html, text); } /** @@ -345,29 +394,33 @@ export function generateBaseUrl(requestUrl: URL, tenant: SelectTenant | null): s * @returns {Promise} */ export async function sendConfirmationEmail( - user: { id: string; email: string | null; name: string | null; language?: string | null }, + user: { id: string; email: string; name: string; language?: string | null }, tenant: SelectTenant, confirmationCode: string, expirationMinutes: number = 15, requestUrl?: URL, ): Promise { - const recipient = createEmailRecipient(user); - const language = (recipient.language as Language) || "en"; - const subject = language === "en" ? "Confirm Your Registration" : "Registrierung bestätigen"; - // Generate appropriate base URL if request URL is provided const baseUrl = requestUrl ? generateBaseUrl(requestUrl, tenant) : "http://localhost:5173"; - - // Create enhanced tenant object with baseUrl - const tenantWithBaseUrl = { - ...tenant, - baseUrl, - }; - - await sendTemplatedEmail("confirmation", recipient, subject, language, tenantWithBaseUrl, { - confirmationCode, - expirationMinutes, + const confirmUrl = `${baseUrl}/confirm/${confirmationCode}`; + const recipient = user; + // Generate email + const subject = m["emails.confirmation.subject"]({ + tenant: tenant.longName, }); + const emailRender = render(Confirmation, { + props: { + locale: (user.language as Language) ?? "en", + user: user as SelectUserEmail, + confirmUrl, + expirationMinutes, + }, + }); + const html = renderOutputToHtml(emailRender); + const text = htmlToText(html); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await sendEmail(recipient as any, subject, html, text); } /** @@ -395,12 +448,22 @@ export async function sendUserInviteEmail( language, }; - const subject = - language === "en" - ? `Invitation to ${tenant.longName || tenant.shortName}` - : `Einladung zu ${tenant.longName || tenant.shortName}`; - - await sendTemplatedEmail("user-invite", recipient, subject, language, tenant, { - registrationUrl, + // Generate email + const subject = m["emails.userInvite.subject"]({ + tenant: tenant.longName, }); + const emailRender = render(UserInvite, { + props: { + locale: language ?? "en", + user: recipient as SelectUserEmail, + tenant, + confirmUrl: registrationUrl, + expirationMinutes: 30, + }, + }); + const html = renderOutputToHtml(emailRender); + const text = htmlToText(html); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await sendEmail(recipient as any, subject, html, text); }