@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.
224 lines (199 loc) • 5.79 kB
JavaScript
import mongoose from 'mongoose';
const makeoverOfferSchema = new mongoose.Schema({
name: {
type: String,
required: true,
trim: true,
maxlength: 100
},
description: {
type: String,
required: true,
trim: true,
maxlength: 500
},
offerType: {
type: String,
required: true,
trim: true,
maxlength: 50
},
basePrice: {
type: Number,
required: true,
min: 0
},
currency: {
type: String,
default: 'NGN',
enum: ['NGN', 'USD', 'EUR']
},
// Duration and timing
defaultDuration: {
type: Number,
required: true,
min: 30, // minimum 30 minutes
default: 120 // 2 hours default
},
minDuration: {
type: Number,
min: 30,
validate: {
validator: function (value) {
return !value || value <= this.defaultDuration;
},
message: 'Minimum duration cannot be greater than default duration'
}
},
maxDuration: {
type: Number,
min: 30,
validate: {
validator: function (value) {
return !value || value >= this.defaultDuration;
},
message: 'Maximum duration cannot be less than default duration'
}
},
// Availability and rules
status: {
type: String,
enum: ['active', 'inactive', 'expired'],
default: 'active'
},
isFeatured: {
type: Boolean,
default: false
},
isPopular: {
type: Boolean,
default: false
},
// Business rules
advanceBookingHours: {
type: Number,
default: 24,
min: 1
},
// Media - Array of image URLs
images: [String], // Array of Cloudinary image URLs
// Metadata
createdBy: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Admin',
required: true
},
lastModifiedBy: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Admin'
},
// Analytics (computed fields)
bookingCount: {
type: Number,
default: 0,
min: 0
},
totalRevenue: {
type: Number,
default: 0,
min: 0
},
averageRating: {
type: Number,
min: 0,
max: 5
},
popularityScore: {
type: Number,
default: 0,
min: 0
}
}, {
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true }
});
// Indexes for performance
makeoverOfferSchema.index({ offerType: 1, status: 1 });
makeoverOfferSchema.index({ status: 1, createdAt: -1 });
makeoverOfferSchema.index({ basePrice: 1 });
makeoverOfferSchema.index({ bookingCount: -1 });
// Text search index
makeoverOfferSchema.index({
name: 'text',
description: 'text'
});
// Virtual for formatted price
makeoverOfferSchema.virtual('formattedPrice').get(function () {
return new Intl.NumberFormat('en-NG', {
style: 'currency',
currency: this.currency,
minimumFractionDigits: 0
}).format(this.basePrice);
});
// Virtual for duration in hours
makeoverOfferSchema.virtual('durationInHours').get(function () {
return Math.round(this.defaultDuration / 60 * 10) / 10; // Round to 1 decimal place
});
// Static methods
makeoverOfferSchema.statics.getActiveOffers = function () {
return this.find({ status: 'active' })
.sort({ bookingCount: -1, createdAt: -1 });
};
makeoverOfferSchema.statics.getActiveOffersByType = function (offerType) {
return this.find({
status: 'active',
offerType: offerType
}).sort({ bookingCount: -1, createdAt: -1 });
};
makeoverOfferSchema.statics.getPopularOffers = function (limit = 5) {
return this.find({
status: 'active',
isPopular: true
})
.sort({ bookingCount: -1, averageRating: -1 })
.limit(limit);
};
makeoverOfferSchema.statics.getFeaturedOffers = function (limit = 3) {
return this.find({
status: 'active',
isFeatured: true
})
.sort({ createdAt: -1 })
.limit(limit);
};
// Instance methods
makeoverOfferSchema.methods.incrementBookingCount = function () {
this.bookingCount += 1;
return this.save();
};
makeoverOfferSchema.methods.addRevenue = function (amount) {
this.totalRevenue += amount;
return this.save();
};
makeoverOfferSchema.methods.updateRating = function (newRating) {
if (this.averageRating === undefined || this.averageRating === null) {
this.averageRating = newRating;
} else {
// Simple average - in production, you might want to store individual ratings
this.averageRating = (this.averageRating + newRating) / 2;
}
return this.save();
};
// Pre-save middleware to calculate popularity
makeoverOfferSchema.pre('save', function (next) {
// Mark as popular if booking count is above threshold
this.isPopular = this.bookingCount >= 10;
next();
});
// Post-save middleware to update popularity score
makeoverOfferSchema.post('save', function (doc) {
// Calculate popularity score based on booking count and rating
const popularityScore = (doc.bookingCount * 0.7) + ((doc.averageRating || 0) * 10 * 0.3);
if (Math.abs(doc.popularityScore - popularityScore) > 0.1) {
doc.popularityScore = popularityScore;
doc.save();
}
});
const MakeoverOffer = mongoose.model('MakeoverOffer', makeoverOfferSchema);
export default MakeoverOffer;