feat: implement comprehensive notification engine with policies, preferences, and automated dispatching services
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
export enum NotificationJobStatus {
|
||||
PENDING = "pending",
|
||||
PROCESSING = "processing",
|
||||
SENT = "sent",
|
||||
FAILED = "failed",
|
||||
CANCELLED = "cancelled",
|
||||
}
|
||||
|
||||
export type NotificationChannel = "whatsapp" | "email" | "system";
|
||||
|
||||
export type NotificationJobType =
|
||||
| "creation"
|
||||
| "reminder"
|
||||
| "update"
|
||||
| "cancellation";
|
||||
|
||||
export interface NotificationJobPayload {
|
||||
email?: string;
|
||||
phoneNumber?: string;
|
||||
userId?: string;
|
||||
companyOwnerId?: string;
|
||||
subject?: string;
|
||||
message?: string;
|
||||
emailSubject?: string;
|
||||
emailMessage?: string;
|
||||
wapMessage?: string;
|
||||
systemSubject?: string;
|
||||
systemMessage?: string;
|
||||
}
|
||||
|
||||
export interface INotificationJob {
|
||||
id?: string;
|
||||
companyId: string;
|
||||
appointmentId: string;
|
||||
clientId: string;
|
||||
channel: NotificationChannel;
|
||||
type: NotificationJobType;
|
||||
scheduledAt: Date;
|
||||
status: NotificationJobStatus;
|
||||
attempts: number;
|
||||
maxAttempts: number;
|
||||
lastError?: string;
|
||||
nextRetryAt?: Date;
|
||||
providerResponse?: string;
|
||||
payload?: NotificationJobPayload;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
sentAt?: Date;
|
||||
}
|
||||
|
||||
export interface CreateJobParams {
|
||||
companyId: string;
|
||||
appointmentId: string;
|
||||
clientId: string;
|
||||
channel: NotificationChannel;
|
||||
type: NotificationJobType;
|
||||
scheduledAt: Date;
|
||||
payload?: NotificationJobPayload;
|
||||
}
|
||||
|
||||
export interface INotificationJobAdapter {
|
||||
create(data: CreateJobParams): Promise<INotificationJob>;
|
||||
findPendingDue(now: Date): Promise<INotificationJob[]>;
|
||||
updateStatus(
|
||||
jobId: string,
|
||||
status: NotificationJobStatus,
|
||||
data?: { lastError?: string; nextRetryAt?: Date; providerResponse?: string }
|
||||
): Promise<void>;
|
||||
cancelByAppointment(appointmentId: string): Promise<void>;
|
||||
findByAppointment(appointmentId: string): Promise<INotificationJob[]>;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
CreateJobParams,
|
||||
INotificationJob,
|
||||
} from "./NotificationJobs.Interface";
|
||||
import { NotificationJobsAdapterMongoose } from "./NotificationJobs.Adapter.Mongoose";
|
||||
|
||||
class NotificationJobManager {
|
||||
adapter: NotificationJobsAdapterMongoose;
|
||||
|
||||
constructor() {
|
||||
this.adapter = new NotificationJobsAdapterMongoose();
|
||||
}
|
||||
|
||||
public async createJob(data: CreateJobParams): Promise<INotificationJob> {
|
||||
return this.adapter.create(data);
|
||||
}
|
||||
|
||||
public async cancelByAppointment(appointmentId: string): Promise<void> {
|
||||
return this.adapter.cancelByAppointment(appointmentId);
|
||||
}
|
||||
|
||||
public async findByAppointment(appointmentId: string): Promise<INotificationJob[]> {
|
||||
return this.adapter.findByAppointment(appointmentId);
|
||||
}
|
||||
}
|
||||
|
||||
const NotificationJobsList = new NotificationJobManager();
|
||||
export default NotificationJobsList;
|
||||
Reference in New Issue
Block a user