@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
JavaScript
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);