@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.
335 lines (308 loc) • 9.91 kB
JavaScript
import mongoose from 'mongoose';
/**
* Simple Cart Model - Organized structure matching our payload
* Ready for both authenticated and anonymous users
*/
const simpleCartSchema = new mongoose.Schema({
// User identification (either userId or sessionId for anonymous)
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: false // Allow anonymous carts
},
// Cart items with organized structure
items: [{
// Product details (matches our payload structure)
productDetails: {
productId: {
type: String,
required: true
},
productType: {
type: String,
required: true,
enum: ['equipment_rental', 'minimart', 'makeover_session', 'studio_session']
},
name: {
type: String,
required: true
},
description: {
type: String,
default: ''
},
shortDescription: {
type: String,
default: ''
},
price: {
type: Number,
required: true,
min: 0
},
originalPrice: {
type: Number,
min: 0
},
quantity: {
type: Number,
required: true,
min: 1,
default: 1
},
// Product images
images: [{
url: String,
alt: String,
isPrimary: { type: Boolean, default: false }
}],
// Product category and classification
category: String,
subcategory: String,
tags: [String],
// Product specifications and features
specifications: {
type: Map,
of: mongoose.Schema.Types.Mixed,
default: {}
},
features: [String],
// Inventory and availability
stock: {
type: Number,
default: 0
},
isActive: {
type: Boolean,
default: true
},
// Pricing details
pricing: {
baseRate: Number,
rateType: {
type: String,
enum: ['hourly', 'daily', 'weekly', 'monthly', 'fixed'],
default: 'daily'
},
currency: {
type: String,
default: 'NGN'
}
},
// Additional metadata
sku: String,
barcode: String,
isFeatured: {
type: Boolean,
default: false
},
// Store the original product data snapshot
productSnapshot: {
type: Map,
of: mongoose.Schema.Types.Mixed,
default: {}
}
},
// Booking details (for equipment rentals)
bookingDetails: {
startDate: {
type: String, // ISO date string
required: false
},
endDate: {
type: String, // ISO date string
required: false
},
pickupTime: {
type: String, // HH:MM format
required: false
},
returnTime: {
type: String, // HH:MM format
required: false
},
duration: {
type: Number, // in days or hours
required: false
},
specialRequests: [{
type: String
}]
},
// Session details (for makeover and studio sessions)
sessionDetails: {
sessionDate: {
type: String, // ISO date string
required: false
},
sessionTime: {
type: String, // HH:MM format
required: false
},
duration: {
type: Number, // in minutes
required: false
},
specialRequests: [{
type: String
}]
},
// Referee details (for equipment rentals)
refereeDetails: {
name: {
type: String,
required: false
},
email: {
type: String,
required: false
},
phone: {
type: String,
required: false
}
},
// Calculated fields
calculatedTotal: {
type: Number,
default: 0
},
// Metadata
addedAt: {
type: Date,
default: Date.now
},
lastUpdated: {
type: Date,
default: Date.now
}
}],
// Cart totals
totals: {
subtotal: {
type: Number,
default: 0
},
discount: {
type: Number,
default: 0
},
total: {
type: Number,
default: 0
}
},
// Cart status
status: {
type: String,
enum: ['active', 'abandoned', 'converted'],
default: 'active'
},
// Expiration for abandoned carts
expiresAt: {
type: Date,
default: function () {
return new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days
}
}
}, {
timestamps: true
});
// Indexes for performance
simpleCartSchema.index({ userId: 1 });
simpleCartSchema.index({ status: 1 });
simpleCartSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 });
// Virtual for item count
simpleCartSchema.virtual('itemCount').get(function () {
return this.items.reduce((total, item) => total + item.productDetails.quantity, 0);
});
// Virtual for unique item count
simpleCartSchema.virtual('uniqueItemCount').get(function () {
return this.items.length;
});
// Method to calculate totals
simpleCartSchema.methods.calculateTotals = function () {
this.totals.subtotal = this.items.reduce((total, item) => {
return total + item.calculatedTotal;
}, 0);
this.totals.total = this.totals.subtotal - this.totals.discount;
return {
subtotal: this.totals.subtotal,
discount: this.totals.discount,
total: this.totals.total
};
};
// Method to add item to cart
simpleCartSchema.methods.addItem = function (itemData) {
const { productDetails, bookingDetails, sessionDetails, refereeDetails } = itemData;
// Calculate total for this item
const calculatedTotal = productDetails.price * productDetails.quantity;
// Check if item already exists based on product type
let existingItemIndex = -1;
if (productDetails.productType === 'equipment_rental' && bookingDetails) {
// For equipment rentals, check by productId + bookingDetails
existingItemIndex = this.items.findIndex(item =>
item.productDetails.productId === productDetails.productId &&
item.bookingDetails?.startDate === bookingDetails.startDate &&
item.bookingDetails?.endDate === bookingDetails.endDate
);
} else if ((productDetails.productType === 'makeover' || productDetails.productType === 'studio') && sessionDetails) {
// For makeover and studio sessions, check by productId + sessionDetails
existingItemIndex = this.items.findIndex(item =>
item.productDetails.productId === productDetails.productId &&
item.sessionDetails?.sessionDate === sessionDetails.sessionDate &&
item.sessionDetails?.sessionTime === sessionDetails.sessionTime
);
} else {
// For other product types (minimart), check by productId only
existingItemIndex = this.items.findIndex(item =>
item.productDetails.productId === productDetails.productId
);
}
if (existingItemIndex !== -1) {
// Update existing item
this.items[existingItemIndex].productDetails.quantity += productDetails.quantity;
this.items[existingItemIndex].calculatedTotal = this.items[existingItemIndex].productDetails.price * this.items[existingItemIndex].productDetails.quantity;
this.items[existingItemIndex].lastUpdated = new Date();
} else {
// Add new item
this.items.push({
productDetails,
bookingDetails: bookingDetails || {},
sessionDetails: sessionDetails || {},
refereeDetails: refereeDetails || {},
calculatedTotal,
addedAt: new Date(),
lastUpdated: new Date()
});
}
// Recalculate totals
this.calculateTotals();
return this;
};
// Method to remove item from cart
simpleCartSchema.methods.removeItem = function (itemId) {
this.items = this.items.filter(item => item._id.toString() !== itemId);
this.calculateTotals();
return this;
};
// Method to update item quantity
simpleCartSchema.methods.updateItemQuantity = function (itemId, quantity) {
const item = this.items.id(itemId);
if (item) {
item.productDetails.quantity = quantity;
item.calculatedTotal = item.productDetails.price * quantity;
item.lastUpdated = new Date();
this.calculateTotals();
}
return this;
};
// Method to clear cart
simpleCartSchema.methods.clearCart = function () {
this.items = [];
this.totals.subtotal = 0;
this.totals.discount = 0;
this.totals.total = 0;
return this;
};
export default mongoose.model('Cart', simpleCartSchema);