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);
}