Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 98 additions & 25 deletions backend/src/controllers/bookingController.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import User from "../models/user.model.js";
import Staff from "../models/staff.model.js";
import { sendWhatsappOtp } from "../utils/WhatsApp.js"; // We might need a separate template for confirmation
import mongoose from "mongoose";
import Offer from "../models/offer.model.js";

// Helper: Get Time Slots (10 AM - 8 PM)
const generateTimeSlots = () => {
Expand Down Expand Up @@ -93,7 +94,27 @@ export const createBooking = async (req, res) => {
const session = await mongoose.startSession();
session.startTransaction();
try {
let { userId, date, timeSlot, services, products, staffId } = req.body;
let { userId, date, timeSlot, services, products, staffId, couponCode } = req.body;

const user = await User.findById(userId);
if (!user) {
return res.status(404).json({ message: "User not found" });
}

const startOfToday = new Date();
startOfToday.setHours(0, 0, 0, 0);

const activeUserBooking = await Appointment.findOne({
userId: userId,
date: { $gte: startOfToday }, // Future or Today
status: { $in: ["confirmed", "booked", "pending"] } // Active Statuses
});

if (activeUserBooking) {
return res.status(409).json({
message: "You already have an upcoming appointment. Please complete it before booking a new one."
});
}

if (!staffId) {
// Find a staff member who is active AND free at this time
Expand All @@ -107,10 +128,7 @@ export const createBooking = async (req, res) => {
}).distinct("staff"); // Get list of busy staff IDs

// Finding someone who is NOT in the busy list
const availableStaff = activeStaff.find(
(s) =>
!busyStaffIds.map((id) => id.toString()).includes(s._id.toString()),
);
const availableStaff = activeStaff.find((s) => !busyStaffIds.map((id) => id.toString()).includes(s._id.toString()));

if (!availableStaff) {
return res
Expand Down Expand Up @@ -142,43 +160,98 @@ export const createBooking = async (req, res) => {
});
}

// Calculate totlal Price (Securely from Backend)
let totalAmount = 0;

// Services
// if (!Array.isArray(services) || services.length === 0) {
// return res
// .status(400)
// .json({ message: "Services must be a non-empty array" });
// }
const hasGeneralMembership = user.subscriptions && user.subscriptions.some(
(sub) => sub.name === "Salon Membership" && sub.isActive && new Date(sub.expiry) > new Date()
);

// Process Services
if (Array.isArray(services) && services.length > 0) {
for (const item of services) {
const service = await Service.findOne({
_id: item.serviceId,
isActive: true,
});
const service = await Service.findOne({ _id: item.serviceId, isActive: true });
if (!service) throw new Error(`Service not available: ${item.serviceId}`);

totalAmount +=
item.variant === "male" ? service.prices.male : service.prices.female;
let price = item.variant === "male" ? service.prices.male : service.prices.female;

// CHECK SUBSCRIPTIONS
const hasSpecificSub = user.subscriptions && user.subscriptions.some(
(sub) => sub.isActive &&
new Date(sub.expiry) > new Date() &&
service.name.toLowerCase() === sub.name.toLowerCase()
);

if (hasSpecificSub) {
price = 0; // Free if covered by specific subscription
} else if (hasGeneralMembership) {
price = price * 0.80; // Apply 20% discount
}

totalAmount += price;
}
}

// Products
if (products && products.length > 0) {
for (const prodId of products) {
const product = await Product.findOne({ _id: prodId, isActive: true });
if (!product) throw new Error(`Product not available: ${prodId}`);

totalAmount += product.price;
await Product.findByIdAndUpdate(prodId, { $inc: { stock: -1 } }, { session });
}
}

await Product.findByIdAndUpdate(
prodId,
{ $inc: { stock: -1 } },
{ session },
);
if (couponCode) {
const offer = await Offer.findOne({ code: couponCode.toUpperCase(), isActive: true });

// Validate Expiry
if (offer) {
const isExpired = offer.expiryDate && new Date(offer.expiryDate) < new Date();

if (!isExpired) {
let discountAmount = 0;
if (offer.type === "percentage") {
discountAmount = (totalAmount * offer.value) / 100;
} else {
discountAmount = offer.value;
}
// Apply discount (also ensuring total doesn't go below 0)
totalAmount = Math.max(0, totalAmount - discountAmount);
}
}
}


// let totalAmount = 0;

// if (Array.isArray(services) && services.length > 0) {
// for (const item of services) {
// const service = await Service.findOne({
// _id: item.serviceId,
// isActive: true,
// });
// if (!service) throw new Error(`Service not available: ${item.serviceId}`);

// totalAmount +=
// item.variant === "male" ? service.prices.male : service.prices.female;
// }
// }

// // Products
// if (products && products.length > 0) {
// for (const prodId of products) {
// const product = await Product.findOne({ _id: prodId, isActive: true });
// if (!product) throw new Error(`Product not available: ${prodId}`);
// totalAmount += product.price;

// await Product.findByIdAndUpdate(
// prodId,
// { $inc: { stock: -1 } },
// { session },
// );
// }
// }

const appointment = await Appointment.create(
[
{
Expand All @@ -188,7 +261,7 @@ export const createBooking = async (req, res) => {
services,
products,
staff: staffId,
totalAmount,
totalAmount: Math.round(totalAmount), // Round to avoid decimals
status: "confirmed",
},
],
Expand Down
6 changes: 6 additions & 0 deletions backend/src/models/user.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ const userSchema = new mongoose.Schema({
email: { type: String, unique: true, sparse: true }, // for booking notification and staff/admin login
password: { type: String }, // Only for Admin/Staff
role: { type: String, enum: ["user", "admin", "staff"], default: "user" },
subscriptions: [{
name: { type: String }, // e.g., "Gold Membership", "Gel Polish Yearly"
type: { type: String, enum: ["general_discount", "service_specific"] },
expiry: { type: Date },
isActive: { type: Boolean, default: true }
}],
name: { type: String },
staffProfile: {type: mongoose.Schema.Types.ObjectId, ref: "Staff"},
createdAt: { type: Date, default: Date.now },
Expand Down
34 changes: 34 additions & 0 deletions backend/src/routes/publicRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,38 @@ router.get("/categories", async (req, res) => {
}
});

// 7. Verify Coupon Endpoint
router.post("/verify-coupon", async (req, res) => {
try {
const { code } = req.body;
if (!code) return res.status(400).json({ message: "Code is required" });

// Finding active offer matching the code
const offer = await Offer.findOne({
code: code.toUpperCase(),
isActive: true
});

if (!offer) {
return res.status(404).json({ message: "Invalid or inactive coupon code." });
}

// Check Expiry
if (offer.expiryDate && new Date(offer.expiryDate) < new Date()) {
return res.status(400).json({ message: "This coupon has expired." });
}

// Return offer details needed for calculation
return res.json({
code: offer.code,
type: offer.type, // "percentage" or "flat"
value: offer.value,
title: offer.title
});

} catch (error) {
res.status(500).json({ message: error.message });
}
});

export default router;
Loading
Loading