UNPKG

@ideal-photography/shared

Version:

Shared MongoDB and utility logic for Ideal Photography PWAs: users, products, services, bookings, orders/cart, galleries, reviews, notifications, campaigns, settings, audit logs, minimart items/orders, and push notification subscriptions.

248 lines (220 loc) 7.13 kB
import mongoose from 'mongoose'; import { v4 as uuidv4 } from 'uuid'; const notificationSchema = new mongoose.Schema({ // Basic Information title: { type: String, required: [true, 'Notification title is required'], trim: true, maxlength: [100, 'Title must be less than 100 characters'] }, message: { type: String, required: [true, 'Notification message is required'], maxlength: [500, 'Message must be less than 500 characters'] }, // Notification Type & Priority type: { type: String, enum: ['announcement', 'booking', 'payment', 'promotion', 'system', 'verification', 'reminder', 'error', 'other'], required: [true, 'Notification type is required'] }, priority: { type: String, enum: ['low', 'normal', 'high', 'urgent'], default: 'normal' }, // Event trigger reference (system alerts) trigger: { type: String, default: null, trim: true, index: true }, // Timestamp when notification was actually sent sentAt: { type: Date, default: null, index: true }, // Targeting recipients: { // Specific users users: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }], // Role-based targeting roles: [{ type: String, enum: ['client', 'admin', 'manager', 'super_admin'] }], // Broadcast to all users broadcast: { type: Boolean, default: false }, // Additional filters filters: { verifiedOnly: { type: Boolean, default: false }, activeOnly: { type: Boolean, default: true }, newUsers: { type: Boolean, default: false }, hasBookings: { type: Boolean, default: false }, verifiedId: { type: Boolean, default: false }, unverifiedId: { type: Boolean, default: false }, verifiedEmail: { type: Boolean, default: false }, unverifiedEmail: { type: Boolean, default: false } } }, // Delivery Channels channels: { inApp: { type: Boolean, default: true }, push: { type: Boolean, default: false } // Only for important notifications }, // Status & Tracking status: { type: String, enum: ['draft', 'scheduled', 'sent', 'failed'], default: 'draft' }, // User interaction tracking isRead: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }], isHidden: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }], // Scheduling scheduledFor: { type: Date, default: Date.now }, expiresAt: { type: Date, default: function () { // Default to 30 days from creation return new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); } }, // Admin fields createdBy: { type: mongoose.Schema.Types.ObjectId, ref: 'Admin', required: false, default: null }, // UUID for references uuid: { type: String, default: () => uuidv4().replace(/-/g, '').substring(0, 32), unique: true }, // Metadata metadata: mongoose.Schema.Types.Mixed }, { timestamps: true, toJSON: { virtuals: true }, toObject: { virtuals: true } }); // Virtuals notificationSchema.virtual('isExpired').get(function () { return this.expiresAt && this.expiresAt < new Date(); }); notificationSchema.virtual('isScheduled').get(function () { return this.scheduledFor > new Date(); }); notificationSchema.virtual('recipientCount').get(function () { if (this.recipients.broadcast) { return 'all'; } return this.recipients.users.length + this.recipients.roles.length; }); // Indexes notificationSchema.index({ type: 1, priority: 1 }); notificationSchema.index({ status: 1 }); notificationSchema.index({ 'recipients.users': 1 }); notificationSchema.index({ 'recipients.roles': 1 }); notificationSchema.index({ 'recipients.broadcast': 1 }); notificationSchema.index({ createdBy: 1 }); notificationSchema.index({ scheduledFor: 1 }); notificationSchema.index({ createdAt: -1 }); // Methods notificationSchema.methods.markAsRead = function (userId) { if (!this.isRead.includes(userId)) { this.isRead.push(userId); } return this.save(); }; notificationSchema.methods.markAsHidden = function (userId) { if (!this.isHidden.includes(userId)) { this.isHidden.push(userId); } return this.save(); }; notificationSchema.methods.isReadByUser = function (userId) { return this.isRead.includes(userId); }; notificationSchema.methods.isHiddenByUser = function (userId) { return this.isHidden.includes(userId); }; notificationSchema.methods.markAsSent = function () { this.status = 'sent'; this.sentAt = new Date(); return this.save(); }; notificationSchema.methods.markAsFailed = function () { this.status = 'failed'; return this.save(); }; // Static methods notificationSchema.statics.getUserNotifications = async function (userId, limit = 20) { const userRoles = await this.getUserRoles(userId); return this.find({ $or: [ { 'recipients.users': userId }, { 'recipients.roles': { $in: userRoles } }, { 'recipients.broadcast': true } ], 'channels.inApp': true, status: 'sent', isHidden: { $ne: userId }, expiresAt: { $gt: new Date() } }) .sort({ createdAt: -1 }) .limit(limit); }; notificationSchema.statics.getUnreadCount = async function (userId) { const userRoles = await this.getUserRoles(userId); return this.countDocuments({ $or: [ { 'recipients.users': userId }, { 'recipients.roles': { $in: userRoles } }, { 'recipients.broadcast': true } ], 'channels.inApp': true, status: 'sent', isRead: { $ne: userId }, isHidden: { $ne: userId }, expiresAt: { $gt: new Date() } }); }; notificationSchema.statics.getPendingNotifications = function () { return this.find({ status: 'scheduled', scheduledFor: { $lte: new Date() } }).populate('recipients.users', 'name email'); }; // Helper method to get user roles notificationSchema.statics.getUserRoles = async function (userId) { try { const User = mongoose.model('User'); const user = await User.findById(userId).select('role'); return user ? [user.role] : ['client']; } catch (error) { console.error('Error getting user roles:', error); return ['client']; } }; export default mongoose.model('Notification', notificationSchema);