feat: implement comprehensive notification engine with policies, preferences, and automated dispatching services

This commit is contained in:
2026-07-21 18:53:11 -03:00
parent 40090cdec5
commit 10ca449f88
82 changed files with 10249 additions and 171 deletions
@@ -0,0 +1,131 @@
import { Document, Model, Schema, model } from "mongoose";
import {
CreateJobParams,
INotificationJob,
INotificationJobAdapter,
NotificationJobStatus,
} from "./NotificationJobs.Interface";
export interface INotificationJobDocument
extends Omit<INotificationJob, "id">,
Document {}
export class NotificationJobsAdapterMongoose implements INotificationJobAdapter {
schema: Schema;
notificationJobList: Model<INotificationJobDocument>;
constructor() {
this.schema = new Schema(
{
companyId: {
type: Schema.Types.ObjectId,
required: true,
ref: "Company",
},
appointmentId: {
type: Schema.Types.ObjectId,
required: true,
ref: "Appointment",
},
clientId: {
type: Schema.Types.ObjectId,
required: true,
ref: "Client",
},
channel: {
type: String,
required: true,
enum: ["whatsapp", "email", "system"],
},
type: {
type: String,
required: true,
enum: ["creation", "reminder", "update", "cancellation"],
},
scheduledAt: { type: Date, required: true },
status: {
type: String,
required: true,
enum: Object.values(NotificationJobStatus),
default: NotificationJobStatus.PENDING,
},
attempts: { type: Number, required: true, default: 0 },
maxAttempts: { type: Number, required: true, default: 3 },
lastError: { type: String, required: false },
nextRetryAt: { type: Date, required: false },
providerResponse: { type: String, required: false },
payload: { type: Schema.Types.Mixed, required: false },
sentAt: { type: Date, required: false },
},
{
timestamps: true,
}
);
this.schema.index({ status: 1, scheduledAt: 1 });
this.schema.index({ companyId: 1, status: 1 });
this.schema.index({ appointmentId: 1 });
this.notificationJobList = model<INotificationJobDocument>(
"NotificationJob",
this.schema
);
}
public async create(data: CreateJobParams): Promise<INotificationJob> {
const doc = await this.notificationJobList.create({
...data,
status: NotificationJobStatus.PENDING,
attempts: 0,
maxAttempts: 3,
});
return doc.toObject() as INotificationJob;
}
public async findPendingDue(now: Date): Promise<INotificationJob[]> {
const docs = await this.notificationJobList
.find({
status: NotificationJobStatus.PENDING,
scheduledAt: { $lte: now },
})
.sort({ scheduledAt: 1 })
.exec();
return docs.map((d) => d.toObject() as INotificationJob);
}
public async updateStatus(
jobId: string,
status: NotificationJobStatus,
data?: { lastError?: string; nextRetryAt?: Date; providerResponse?: string }
): Promise<void> {
const update: Record<string, unknown> = { status };
if (status === NotificationJobStatus.SENT) {
update.sentAt = new Date();
}
if (data?.lastError !== undefined) update.lastError = data.lastError;
if (data?.nextRetryAt !== undefined) update.nextRetryAt = data.nextRetryAt;
if (data?.providerResponse !== undefined)
update.providerResponse = data.providerResponse;
await this.notificationJobList.updateOne({ _id: jobId }, { $set: update }).exec();
}
public async cancelByAppointment(appointmentId: string): Promise<void> {
await this.notificationJobList
.updateMany(
{
appointmentId,
status: NotificationJobStatus.PENDING,
},
{ $set: { status: NotificationJobStatus.CANCELLED } }
)
.exec();
}
public async findByAppointment(appointmentId: string): Promise<INotificationJob[]> {
const docs = await this.notificationJobList
.find({ appointmentId })
.exec();
return docs.map((d) => d.toObject() as INotificationJob);
}
}