diff --git a/src/models/User.js b/src/models/User.js index 3c22781..e7db380 100644 --- a/src/models/User.js +++ b/src/models/User.js @@ -1,48 +1,58 @@ -import mongoose from 'mongoose'; +import mongoose from "mongoose"; const timeRegex = /^([01]\d|2[0-3]):([0-5]\d)$/; -const slotSchema = new mongoose.Schema({ - startTime: { - type: String, - required: true, - match: timeRegex +const slotSchema = new mongoose.Schema( + { + startTime: { + type: String, + required: true, + match: timeRegex, + }, + endTime: { + type: String, + required: true, + match: timeRegex, + }, }, - endTime: { - type: String, - required: true, - match: timeRegex - } -}, { _id: false }); - -const workingDaySchema = new mongoose.Schema({ - day: { - type: String, - required: true, - enum: ["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"] + { _id: false }, +); + +const workingDaySchema = new mongoose.Schema( + { + day: { + type: String, + required: true, + enum: [ + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday", + ], + }, + isAvailable: { + type: Boolean, + default: true, + }, + slots: { + type: [slotSchema], + default: [], + }, }, - isAvailable: { - type: Boolean, - default: true - }, - slots: { - type: [slotSchema], - default: [] - } -}, { _id: false }); - -workingDaySchema.pre('validate', function() { + { _id: false }, +); +workingDaySchema.pre("validate", function () { if (!this.isAvailable && this.slots.length > 0) { return new Error("Slots cannot exist when doctor is unavailable"); } - const sorted = [...this.slots].sort((a, b) => - a.startTime.localeCompare(b.startTime) - ); + const sorted = [...this.slots].sort((a, b) => a.startTime.localeCompare(b.startTime)); for (let i = 0; i < sorted.length; i++) { - if (sorted[i].startTime >= sorted[i].endTime) { return new Error("Start time must be before end time"); } @@ -55,68 +65,80 @@ workingDaySchema.pre('validate', function() { const DEFAULT_WORKING_HOURS = [ { day: "Monday", isAvailable: true, slots: [{ startTime: "09:00", endTime: "17:00" }] }, - { day: "Tuesday", isAvailable: true, slots: [{ startTime: "09:00", endTime: "17:00" }] }, - { day: "Wednesday", isAvailable: true, slots: [{ startTime: "09:00", endTime: "17:00" }] }, - { day: "Thursday", isAvailable: true, slots: [{ startTime: "09:00", endTime: "17:00" }] }, + { + day: "Tuesday", + isAvailable: true, + slots: [{ startTime: "09:00", endTime: "17:00" }], + }, + { + day: "Wednesday", + isAvailable: true, + slots: [{ startTime: "09:00", endTime: "17:00" }], + }, + { + day: "Thursday", + isAvailable: true, + slots: [{ startTime: "09:00", endTime: "17:00" }], + }, { day: "Friday", isAvailable: true, slots: [{ startTime: "09:00", endTime: "17:00" }] }, { day: "Saturday", isAvailable: false, slots: [] }, { day: "Sunday", isAvailable: false, slots: [] }, ]; -const userSchema = new mongoose.Schema({ - - email: { - type: String, - required: true, - unique: true, - match: /.+@.+\..+/ - }, - - password: { - type: String, - required: true +const userSchema = new mongoose.Schema( + { + email: { + type: String, + required: true, + unique: true, + match: /.+@.+\..+/, + }, + + password: { + type: String, + required: true, + }, + + role: { + type: String, + required: true, + enum: ["admin", "doctor", "receptionist", "billing", "patient"], + }, + + name: { type: String }, + + phno: { + type: String, + match: /^[0-9]{10}$/, + unique: true, + }, + + spec: { type: String }, + + dept: { + type: mongoose.Schema.Types.ObjectId, + ref: "Department", + }, + + exp: { type: String }, + qual: { type: String }, + + status: { + type: String, + enum: ["Active", "Inactive"], + default: "Active", + }, + + workingHours: { + type: [workingDaySchema], + }, }, + { timestamps: true }, +); - role: { - type: String, - required: true, - enum: ['admin', 'doctor','receptionist','billing', 'patient'] - }, - - name: { type: String }, - - phno: { - type: String, - match: /^[0-9]{10}$/ - }, - - spec: { type: String }, - - dept: { - type: mongoose.Schema.Types.ObjectId, - ref: 'Department' - }, - - exp: { type: String }, - qual: { type: String }, - - status: { - type: String, - enum: ['Active', 'Inactive'], - default: 'Active' - }, - - workingHours: { - type: [workingDaySchema] - } - -}, { timestamps: true }); - - -userSchema.pre('validate', function() { - - if (this.role === 'doctor' && this.workingHours) { - const days = this.workingHours.map(d => d.day); +userSchema.pre("validate", function () { + if (this.role === "doctor" && this.workingHours) { + const days = this.workingHours.map((d) => d.day); const uniqueDays = new Set(days); if (days.length !== uniqueDays.size) { @@ -125,12 +147,11 @@ userSchema.pre('validate', function() { } }); -userSchema.pre('save', function() { - - if (this.role === 'doctor' && (!this.workingHours || this.workingHours.length === 0)) { +userSchema.pre("save", function () { + if (this.role === "doctor" && (!this.workingHours || this.workingHours.length === 0)) { this.workingHours = DEFAULT_WORKING_HOURS; } }); -const User = mongoose.models.User || mongoose.model('User', userSchema); -export default User; \ No newline at end of file +const User = mongoose.models.User || mongoose.model("User", userSchema); +export default User; diff --git a/src/models/patient.js b/src/models/patient.js index dea1f1d..f270027 100644 --- a/src/models/patient.js +++ b/src/models/patient.js @@ -1,58 +1,57 @@ -import mongoose from 'mongoose'; +import mongoose from "mongoose"; -const Patientschema=new mongoose.Schema({ -name:{ type:String, required:true }, -email:{ - type: String, - required:true, - match: /.+@.+\..+/ - }, -phno:{ - type: String, - required: true, - match:/^[0-9]{10}$/ - }, -age:{ - type:Number, - required: true, - min: [0, "Age cannot be negative"], - max: [150, "Age cannot exceed 150"], - validate: { +const Patientschema = new mongoose.Schema( + { + name: { type: String, required: true }, + email: { + type: String, + required: true, + match: /.+@.+\..+/, + }, + phno: { + type: String, + required: true, + match: /^[0-9]{10}$/, + }, + age: { + type: Number, + required: true, + min: [0, "Age cannot be negative"], + max: [150, "Age cannot exceed 150"], + validate: { validator: Number.isInteger, - message: "Age must be a whole number" - } -}, -gender: { - type:String, - enum:['male','female','other'], - required:true}, -status:{ - type:String, - enum:['active','cancelled'], - default:'active', - required:true -}, - paymentMethod: { - type: String, - enum: ['cash', 'online'], - default: 'cash' + message: "Age must be a whole number", + }, + }, + gender: { + type: String, + enum: ["male", "female", "other"], + required: true, + }, + status: { + type: String, + enum: ["active", "cancelled"], + default: "active", + required: true, + }, + bg: { + type: String, + enum: ["A+", "A-", "B+", "B-", "AB+", "AB-", "O+", "O-"], + }, + address: { type: String }, + emerno: { + type: String, + match: /^[0-9]{10}$/, + }, + medical_history: { type: String }, + paymentMode: { + type: String, + enum: ["cash", "online"], + default: "cash", + }, }, -bg:{ - type: String, - enum:['A+','A-','B+','B-','AB+','AB-','O+','O-'] - }, -address: {type:String}, -emerno:{ - type:String, - match:/^[0-9]{10}$/ - }, -medical_history: {type:String}, -paymentMode: { - type: String, - enum: ['cash', 'online'], - default: 'cash' - } -},{ timestamps: true}); + { timestamps: true }, +); -const Patient= mongoose.model('Patient', Patientschema); -export default Patient; \ No newline at end of file +const Patient = mongoose.model("Patient", Patientschema); +export default Patient; diff --git a/src/services/doctor.service.js b/src/services/doctor.service.js index e724b1e..b2aff4b 100644 --- a/src/services/doctor.service.js +++ b/src/services/doctor.service.js @@ -1,92 +1,134 @@ -import User from '../models/User.js'; -import bcrypt from 'bcrypt'; +import User from "../models/User.js"; +import bcrypt from "bcrypt"; +import { checkUniqueUser } from "../utils/uniqueness.js"; const createDoctor = async (data) => { - const password = data.password || 'doctor@123'; - const hashedPassword = await bcrypt.hash(password, 10); + const email = normalizeEmail(data.email); + const phno = sanitizePhone(data.phno); + await checkUniqueUser({ email, phno }); + + const password = data.password || "doctor@123"; + const hashedPassword = await bcrypt.hash(password, 10); + + try { const doctor = new User({ - ...data, - password: hashedPassword, - role: 'doctor', + ...data, + email, + phno, + password: hashedPassword, + role: "doctor", }); await doctor.save(); return { - success: true, - message: 'Doctor created successfully', + success: true, + message: "Doctor created successfully", }; + } catch (err) { + if (err.code === 11000) { + if (err.keyPattern?.email) throw new Error("Email already exists"); + if (err.keyPattern?.phno) throw new Error("Phone number already exists"); + } + throw err; + } }; - const changePassword = async (userId, oldPassword, newPassword) => { - const doctor = await User.findById(userId); - if(!doctor) throw new Error('Doctor not found'); + const doctor = await User.findById(userId); + if (!doctor) throw new Error("Doctor not found"); - const isMatch = await bcrypt.compare(oldPassword, doctor.password); - if(!isMatch) throw new Error('Old password is incorrect'); + const isMatch = await bcrypt.compare(oldPassword, doctor.password); + if (!isMatch) throw new Error("Old password is incorrect"); - doctor.password = await bcrypt.hash(newPassword, 10); - await doctor.save(); + doctor.password = await bcrypt.hash(newPassword, 10); + await doctor.save(); - return { message: 'Password changed successfully' }; + return { message: "Password changed successfully" }; }; const getDoctors = async () => { - return await User.find({ role: 'doctor' }); + return await User.find({ role: "doctor" }); }; const updateDoctor = async (id, data) => { + if (data.workingHours) { + throw new Error("Use dedicated endpoint to update working hours"); + } - if(data.workingHours){ - throw new Error('User dedicated endpoint to update working hours'); - } - if(data.password) { - data.password = await bcrypt.hash(data.password, 10); - } + let email, phno; + + if (data.email) { + email = normalizeEmail(data.email); + } + + if (data.phno) { + phno = sanitizePhone(data.phno); + } - const doctor = await User.findOneAndUpdate( - { _id: id, role: 'doctor' }, - data, - { new: true, runValidators: true } - ); + if (email || phno) { + await checkUniqueUser({ + email: email || undefined, + phno: phno || undefined, + excludeId: id, + }); + } + + if (data.password) { + data.password = await bcrypt.hash(data.password, 10); + } - if(!doctor) throw new Error('Doctor not found'); + if (email) data.email = email; + if (phno) data.phno = phno; + + try { + const doctor = await User.findOneAndUpdate({ _id: id, role: "doctor" }, data, { + new: true, + runValidators: true, + }); + + if (!doctor) throw new Error("Doctor not found"); return doctor; + } catch (err) { + if (err.code === 11000) { + if (err.keyPattern?.email) throw new Error("Email already exists"); + if (err.keyPattern?.phno) throw new Error("Phone number already exists"); + } + throw err; + } }; const deleteDoctor = async (id) => { - const doctor = await User.findOneAndDelete({ _id: id, role: 'doctor' }); - if(!doctor) throw new Error('Doctor not found'); + const doctor = await User.findOneAndDelete({ _id: id, role: "doctor" }); + if (!doctor) throw new Error("Doctor not found"); - return { message: 'Doctor deleted successfully' }; + return { message: "Doctor deleted successfully" }; }; const updateWorkingHours = async (doctorId, workingHours) => { + const doctor = await User.findOne({ + _id: doctorId, + role: "doctor", + }); - const doctor = await User.findOne({ - _id: doctorId, - role: 'doctor' - }); + if (!doctor) throw new Error("Doctor not found"); - if (!doctor) throw new Error("Doctor not found"); + doctor.workingHours = workingHours; - doctor.workingHours = workingHours; + await doctor.save(); - await doctor.save(); - - return { - message: "Working hours updated successfully" - }; + return { + message: "Working hours updated successfully", + }; }; -export default{ +export default { createDoctor, changePassword, getDoctors, updateDoctor, deleteDoctor, - updateWorkingHours -}; \ No newline at end of file + updateWorkingHours, +}; diff --git a/src/utils/uniqueness.js b/src/utils/uniqueness.js new file mode 100644 index 0000000..865b704 --- /dev/null +++ b/src/utils/uniqueness.js @@ -0,0 +1,22 @@ +const checkUniqueUser = async ({ email, phno, excludeId = null }) => { + const query = { + $or: [{ email }, { phno }], + }; + + if (excludeId) { + query._id = { $ne: excludeId }; + } + + const existing = await User.findOne(query); + + if (existing) { + if (existing.email === email) { + throw new Error("Email already exists"); + } + if (existing.phno === phno) { + throw new Error("Phone number already exists"); + } + } +}; + +export { checkUniqueUser };