feat: implement comprehensive notification engine with policies, preferences, and automated dispatching services
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
import mongoose, { Model } from "mongoose";
|
||||
import { INotificationJobDocument, NotificationJobType } from "./NotificationJob.Adapter.Mongoose.js";
|
||||
|
||||
const { Schema, model, models } = mongoose;
|
||||
|
||||
interface AppointmentDocument {
|
||||
_id: unknown;
|
||||
companyId: unknown;
|
||||
serviceId?: unknown;
|
||||
clientId?: unknown;
|
||||
userId?: unknown;
|
||||
start: Date;
|
||||
}
|
||||
|
||||
interface ClientDocument {
|
||||
_id: unknown;
|
||||
userId?: unknown;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
email?: string;
|
||||
}
|
||||
|
||||
interface UserDocument {
|
||||
_id: unknown;
|
||||
email?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
}
|
||||
|
||||
interface ServiceDocument {
|
||||
_id: unknown;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
interface CompanyDocument {
|
||||
_id: unknown;
|
||||
name?: string;
|
||||
ownerId?: unknown;
|
||||
}
|
||||
|
||||
export interface ResolvedNotificationContent {
|
||||
email?: string;
|
||||
phoneNumber?: string;
|
||||
userId?: string;
|
||||
companyOwnerId?: string;
|
||||
subject: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
const appointmentSchema = new Schema(
|
||||
{
|
||||
companyId: { type: Schema.Types.ObjectId, required: true, ref: "Companie" },
|
||||
serviceId: { type: Schema.Types.ObjectId, required: false, ref: "Service" },
|
||||
clientId: { type: Schema.Types.ObjectId, required: false, ref: "Client" },
|
||||
userId: { type: Schema.Types.ObjectId, required: false, ref: "User" },
|
||||
start: { type: Date, required: true },
|
||||
},
|
||||
{ strict: false }
|
||||
);
|
||||
|
||||
const clientSchema = new Schema(
|
||||
{
|
||||
userId: { type: Schema.Types.ObjectId, required: false, ref: "User" },
|
||||
firstName: { type: String, required: false },
|
||||
lastName: { type: String, required: false },
|
||||
email: { type: String, required: false },
|
||||
},
|
||||
{ strict: false }
|
||||
);
|
||||
|
||||
const userSchema = new Schema(
|
||||
{
|
||||
email: { type: String, required: false },
|
||||
firstName: { type: String, required: false },
|
||||
lastName: { type: String, required: false },
|
||||
},
|
||||
{ strict: false }
|
||||
);
|
||||
|
||||
const serviceSchema = new Schema(
|
||||
{
|
||||
name: { type: String, required: false },
|
||||
},
|
||||
{ strict: false }
|
||||
);
|
||||
|
||||
const companySchema = new Schema(
|
||||
{
|
||||
name: { type: String, required: false },
|
||||
ownerId: { type: Schema.Types.ObjectId, required: false, ref: "User" },
|
||||
},
|
||||
{ strict: false }
|
||||
);
|
||||
|
||||
const Appointment = (models.Appointment || model<AppointmentDocument>("Appointment", appointmentSchema)) as Model<AppointmentDocument>;
|
||||
const Client = (models.Client || model<ClientDocument>("Client", clientSchema)) as Model<ClientDocument>;
|
||||
const User = (models.User || model<UserDocument>("User", userSchema)) as Model<UserDocument>;
|
||||
const Service = (models.Service || model<ServiceDocument>("Service", serviceSchema)) as Model<ServiceDocument>;
|
||||
const Company = (models.Company || model<CompanyDocument>("Company", companySchema)) as Model<CompanyDocument>;
|
||||
|
||||
function compactName(...parts: Array<string | undefined>): string {
|
||||
return parts.filter(Boolean).join(" ").trim();
|
||||
}
|
||||
|
||||
function formatDateTime(value?: Date): string {
|
||||
if (!value) return "fecha pendiente";
|
||||
return new Intl.DateTimeFormat("es-AR", {
|
||||
dateStyle: "short",
|
||||
timeStyle: "short",
|
||||
timeZone: "America/Argentina/Buenos_Aires",
|
||||
}).format(value);
|
||||
}
|
||||
|
||||
export function buildNotificationText(params: {
|
||||
type: NotificationJobType;
|
||||
appointment?: AppointmentDocument | null;
|
||||
client?: ClientDocument | null;
|
||||
service?: ServiceDocument | null;
|
||||
company?: CompanyDocument | null;
|
||||
}): Pick<ResolvedNotificationContent, "subject" | "message"> {
|
||||
const companyName = params.company?.name || "TurnosXpress";
|
||||
const serviceName = params.service?.name || "tu turno";
|
||||
const appointmentDate = formatDateTime(params.appointment?.start);
|
||||
|
||||
switch (params.type) {
|
||||
case "creation":
|
||||
return {
|
||||
subject: `Alta de turno en ${companyName}`,
|
||||
message: `Tu turno para ${serviceName} en ${companyName} fue confirmado para el ${appointmentDate}.`,
|
||||
};
|
||||
case "reminder":
|
||||
return {
|
||||
subject: `Recordatorio de turno en ${companyName}`,
|
||||
message: `Te recordamos tu turno para ${serviceName} en ${companyName} el ${appointmentDate}.`,
|
||||
};
|
||||
case "update":
|
||||
return {
|
||||
subject: `Turno actualizado en ${companyName}`,
|
||||
message: `Tu turno para ${serviceName} en ${companyName} fue actualizado. Nueva fecha: ${appointmentDate}.`,
|
||||
};
|
||||
case "cancellation":
|
||||
return {
|
||||
subject: `Turno cancelado en ${companyName}`,
|
||||
message: `Tu turno para ${serviceName} en ${companyName} del ${appointmentDate} fue cancelado.`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveNotificationContent(job: INotificationJobDocument): Promise<ResolvedNotificationContent> {
|
||||
if ((job.type === "creation" || job.type === "reminder" || job.type === "cancellation") && job.payload) {
|
||||
const { email, phoneNumber, userId, companyOwnerId } = job.payload;
|
||||
const subject = job.channel === "email"
|
||||
? job.payload.emailSubject
|
||||
: job.channel === "system"
|
||||
? job.payload.systemSubject
|
||||
: job.payload.subject;
|
||||
const message = job.channel === "whatsapp"
|
||||
? job.payload.wapMessage
|
||||
: job.channel === "email"
|
||||
? job.payload.emailMessage
|
||||
: job.payload.systemMessage;
|
||||
const fallbackSubject = subject || job.payload.subject;
|
||||
const fallbackMessage = message || job.payload.message;
|
||||
if (fallbackSubject?.trim() && fallbackMessage?.trim()) {
|
||||
return {
|
||||
email: email?.trim(),
|
||||
phoneNumber: phoneNumber?.trim(),
|
||||
userId: userId ? String(userId) : undefined,
|
||||
companyOwnerId: companyOwnerId ? String(companyOwnerId) : undefined,
|
||||
subject: fallbackSubject.trim(),
|
||||
message: fallbackMessage.trim(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const appointment = await Appointment.findOne({ _id: job.appointmentId }).lean().exec();
|
||||
const client = await Client.findOne({ _id: job.clientId }).lean().exec();
|
||||
const company = await Company.findOne({ _id: job.companyId }).lean().exec();
|
||||
const service = appointment?.serviceId ? await Service.findOne({ _id: appointment.serviceId }).lean().exec() : null;
|
||||
const userId = client?.userId || appointment?.userId || company?.ownerId;
|
||||
const user = userId ? await User.findOne({ _id: userId }).lean().exec() : null;
|
||||
const text = buildNotificationText({ type: job.type, appointment, client, service, company });
|
||||
const email = client?.email || user?.email;
|
||||
|
||||
return {
|
||||
email: email?.trim(),
|
||||
userId: userId ? String(userId) : undefined,
|
||||
subject: text.subject.trim(),
|
||||
message: text.message.trim(),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user