alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
140 lines (122 loc) • 4.09 kB
text/typescript
import { $inject, Alepha, AlephaError } from "alepha";
import { EmailProvider } from "alepha/email";
import { $logger } from "alepha/logger";
import { SmsProvider } from "alepha/sms";
import { $notification } from "../primitives/$notification.ts";
import type { NotificationPayload } from "../schemas/notificationPayloadSchema.ts";
type NotificationMessageEmail = {
subject: string;
body: string | ((variables: any) => string);
};
type NotificationMessageSms = {
message: string | ((variables: any) => string);
};
export class NotificationSenderService {
protected readonly alepha = $inject(Alepha);
protected readonly log = $logger();
protected readonly emailProvider = $inject(EmailProvider);
protected readonly smsProvider = $inject(SmsProvider);
public async send(payload: NotificationPayload) {
this.log.debug("Processing notification", {
type: payload.type,
template: payload.template,
contact: payload.contact,
});
if (payload.type === "email") {
const rendered = this.renderEmail(payload);
await this.emailProvider.send(rendered);
this.log.info("Email notification sent", {
template: payload.template,
contact: payload.contact,
});
return {
type: "email" as const,
to: rendered.to,
subject: rendered.subject,
body: rendered.body,
};
}
if (payload.type === "sms") {
const rendered = this.renderSms(payload);
await this.smsProvider.send(rendered);
this.log.info("SMS notification sent", {
template: payload.template,
contact: payload.contact,
});
return {
type: "sms" as const,
to: rendered.to,
message: rendered.message,
};
}
}
public renderSms(payload: NotificationPayload) {
const { variables, contact, template } = this.load(payload);
const sms =
this.translation(template.options, payload.lang)?.sms ??
template.options.sms;
if (!sms) {
throw new AlephaError(
`Notification template ${payload.template} has no sms defined`,
);
}
const message =
typeof sms.message === "function"
? sms.message(variables as any)
: sms.message;
return { to: contact, message };
}
public renderEmail(payload: NotificationPayload) {
const { variables, contact, template } = this.load(payload);
const email =
this.translation(template.options, payload.lang)?.email ??
template.options.email;
if (!email) {
throw new AlephaError(
`Notification template ${payload.template} has no email defined`,
);
}
const subject = email.subject;
const body =
typeof email.body === "function"
? email.body(variables as any)
: email.body;
return { to: contact, subject, body };
}
/**
* Pick the template's `translations` entry for the payload language:
* exact match ("fr-FR") first, then the base language ("fr"). Returns
* undefined when there is no match — callers fall back to the default
* (template-level) message.
*/
protected translation(
options: {
translations?: Record<
string,
{ email?: unknown; sms?: unknown } | undefined
>;
},
lang?: string,
):
| { email?: NotificationMessageEmail; sms?: NotificationMessageSms }
| undefined {
if (!lang || !options.translations) return undefined;
const exact = options.translations[lang];
if (exact) return exact as any;
const base = lang.split("-")[0]?.toLowerCase();
return base ? (options.translations[base] as any) : undefined;
}
protected load(payload: NotificationPayload) {
const variables = payload.variables || {};
const contact = payload.contact;
const template = this.alepha
.primitives($notification)
.find((it) => it.name === payload.template);
if (!template) {
throw new AlephaError(
`No notification template found for ${payload.template}`,
);
}
return { template, variables, contact };
}
}