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
11 changes: 11 additions & 0 deletions server/config/multer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import multer from "multer";

const storage = multer.memoryStorage();

const upload= multer({
storage,
limits:{fileSize: 5*1024*1024}// 5MB limit

})

export default upload
348 changes: 348 additions & 0 deletions server/controllers/ownerController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,348 @@
import { Response } from "express";
import { AuthRequest } from "../middlewares/auth.js";
import Restaurant from "../models/Restaurants.js";
import {v2 as cloudinary} from 'cloudinary'
import Booking from "../models/Booking.js";

// Helper Function to upload buffer to cloudinary
const uploadToCloudinary = (fileBuffer: Buffer ): Promise<{secure_url: string}>=> {
return new Promise((resolve, reject)=>{
const stream= cloudinary.uploader.upload_stream({folder: "Quickdine"}, (error,result)=>{
if(error ) return reject(error )
if(!result) return reject(new Error("Upload failed"));
resolve({secure_url: result.secure_url})
})
stream.end(fileBuffer)
})

}

// ======================================================
// Get owner's restaurant
// GET /api/owner/restaurant
// ======================================================
export const getOwnerRestaurant = async (
req: AuthRequest,
res: Response
): Promise<void> => {
try {
const ownerId = req.user?.id;

if (!ownerId) {
res.status(401).json({
success: false,
message: "Unauthorized",
});
return;
}

const restaurant = await Restaurant.findOne({
owner: ownerId,
});

if (!restaurant) {
res.status(404).json({
success: false,
message: "Restaurant not found",
});
return;
}

res.status(200).json({
success: true,
restaurant,
});
} catch (error: any) {
console.error(error);

res.status(500).json({
success: false,
message: error.message,
});
}
};

// ======================================================
// Create owner's restaurant
// POST /api/owner/restaurant
// ======================================================
export const createRestaurant = async (
req: AuthRequest,
res: Response
): Promise<void> => {
try {
const ownerId = req.user?.id;

if (!ownerId) {
res.status(401).json({
success: false,
message: "Unauthorized",
});
return;
}

// Check if owner already has a restaurant
const existingRestaurant = await Restaurant.findOne({
owner: ownerId,
});

if (existingRestaurant) {
res.status(400).json({
success: false,
message: "You already own a restaurant.",
});
return;
}

const {
name,
description,
cuisine,
priceRange,
location,
address,
image,
chef,
tags,
availableSlots,
totalSeats,
} = req.body;

const slug = name
.toLowerCase()
.trim()
.replace(/\s+/g, "-");
Comment on lines +111 to +114

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Missing validation for required name before slug generation.

If name is omitted from req.body, name.toLowerCase() throws a TypeError, caught by the outer catch and returned as a generic 500 instead of a proper 400 "name is required" response.

✅ Suggested guard
     } = req.body;
 
+    if (!name) {
+      res.status(400).json({ success: false, message: "Restaurant name is required." });
+      return;
+    }
+
     const slug = name
       .toLowerCase()
       .trim()
       .replace(/\s+/g, "-");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const slug = name
.toLowerCase()
.trim()
.replace(/\s+/g, "-");
if (!name) {
res.status(400).json({ success: false, message: "Restaurant name is required." });
return;
}
const slug = name
.toLowerCase()
.trim()
.replace(/\s+/g, "-");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/controllers/ownerController.ts` around lines 111 - 114, Validate that
the required name field is present before the slug generation chain in the owner
controller, returning a 400 response with “name is required” when omitted; only
call toLowerCase, trim, and replace after validation.

// handle image
let imageUrl ="";
if(req.file){
const result = await uploadToCloudinary(req.file.buffer);
imageUrl = result.secure_url;

}

// setup parsed tags and slots
const parsedTags = typeof tags === "string" ? tags.split(",").map((t)=> t.trim()) : tags || [];
const parsedSlots = typeof availableSlots === "string" ? availableSlots.split(",").map((s)=> s.trim()) : availableSlots || ["17:00", "18:00","19:00", "20:00", "21:00"];

const restaurant = await Restaurant.create({
name,
slug,
description,
cuisine,
priceRange,
location,
address,
image: imageUrl,
chef,
tags: parsedTags,
availableSlots: parsedSlots,
totalSeats,
owner: ownerId,
featured: false,
exclusive: false,
rating: 0,
reviewCount: 0,
status: "pending",
});

res.status(201).json({
success: true,
message: "Restaurant submitted successfully. Waiting for admin approval.",
restaurant,
});
} catch (error: any) {
console.error(error);

res.status(500).json({
success: false,
message: error.message,
});
}
};

// ======================================================
// Update owner's restaurant
// PUT /api/owner/restaurant
// ======================================================
export const updateOwnerRestaurant = async (
req: AuthRequest,
res: Response
): Promise<void> => {
try {
const restaurant = await Restaurant.findOne({
owner: req.user?.id,
});

if (!restaurant) {
res.status(404).json({
success: false,
message: "Restaurant profile not found",
});
return;
}

const {
name,
description,
cuisine,
priceRange,
location,
address,
chef,
tags,
availableSlots,
totalSeats,
} = req.body;

// Upload image if provided
let imageUrl = restaurant.image;

if (req.file) {
const result = await uploadToCloudinary(req.file.buffer);
imageUrl = result.secure_url;
}

// Parse tags and slots
const parsedTags =
typeof tags === "string"
? tags.split(",").map((t: string) => t.trim())
: tags;

const parsedSlots =
typeof availableSlots === "string"
? availableSlots.split(",").map((s: string) => s.trim())
: availableSlots;

// Update fields
if (name) {
restaurant.name = name;
restaurant.slug = name
.toLowerCase()
.trim()
.replace(/\s+/g, "-");
}

if (description) restaurant.description = description;
if (cuisine) restaurant.cuisine = cuisine;
if (priceRange) restaurant.priceRange = priceRange;
if (location) restaurant.location = location;
if (address) restaurant.address = address;
if (chef) restaurant.chef = chef;
if (totalSeats) restaurant.totalSeats = totalSeats;

if (parsedTags) restaurant.tags = parsedTags;
if (parsedSlots) restaurant.availableSlots = parsedSlots;
if(tags){
restaurant.tags = typeof tags === "string" ? tags.split(",").map((t)=> t.trim()): tags;
}
if(availableSlots){
restaurant.availableSlots = typeof availableSlots === "string" ? availableSlots.split(",").map((s)=> s.trim()): availableSlots;
}


// Handle new image upload if any

// Handle new image upload if any


if (req.file) {
const result = await uploadToCloudinary(req.file.buffer);
imageUrl = result.secure_url;
}

const updated = await restaurant.save()
res.json(updated);

restaurant.image = imageUrl;

await restaurant.save();

res.status(200).json({
success: true,
message: "Restaurant updated successfully",
restaurant,
});
Comment on lines +197 to +264

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Duplicate save/response logic will crash the request — res.json/res.status().json() called twice.

This function saves and responds twice: res.json(updated) at line 254 sends the response, then execution continues to mutate restaurant.image, save again, and call res.status(200).json(...) a second time at line 260. The second response attempt will throw ERR_HTTP_HEADERS_SENT, and since this happens inside the try block, the catch handler will then attempt a third response send. There's also a duplicated Cloudinary upload block (lines 200-203 and 248-251) that will upload the same file twice when req.file is present, and duplicated tag/slot reassignment (lines 233-240).

🐛 Proposed fix — remove duplicate save/response/upload logic
     if (parsedTags) restaurant.tags = parsedTags;
     if (parsedSlots) restaurant.availableSlots = parsedSlots;
-    if(tags){
-      restaurant.tags = typeof tags === "string" ? tags.split(",").map((t)=> t.trim()): tags;
-    }
-    if(availableSlots){
-      restaurant.availableSlots = typeof availableSlots === "string" ? availableSlots.split(",").map((s)=> s.trim()): availableSlots;
-    }
-
- 
-   // Handle new image upload if any
-    
-    // Handle new image upload if any
-    
-
-    if (req.file) {
-      const result = await uploadToCloudinary(req.file.buffer);
-      imageUrl = result.secure_url;
-    }
-
-    const updated = await restaurant.save()
-    res.json(updated);
-
-    restaurant.image = imageUrl;
-
-    await restaurant.save();
+
+    restaurant.image = imageUrl;
+    await restaurant.save();
 
     res.status(200).json({
       success: true,
       message: "Restaurant updated successfully",
       restaurant,
     });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Upload image if provided
let imageUrl = restaurant.image;
if (req.file) {
const result = await uploadToCloudinary(req.file.buffer);
imageUrl = result.secure_url;
}
// Parse tags and slots
const parsedTags =
typeof tags === "string"
? tags.split(",").map((t: string) => t.trim())
: tags;
const parsedSlots =
typeof availableSlots === "string"
? availableSlots.split(",").map((s: string) => s.trim())
: availableSlots;
// Update fields
if (name) {
restaurant.name = name;
restaurant.slug = name
.toLowerCase()
.trim()
.replace(/\s+/g, "-");
}
if (description) restaurant.description = description;
if (cuisine) restaurant.cuisine = cuisine;
if (priceRange) restaurant.priceRange = priceRange;
if (location) restaurant.location = location;
if (address) restaurant.address = address;
if (chef) restaurant.chef = chef;
if (totalSeats) restaurant.totalSeats = totalSeats;
if (parsedTags) restaurant.tags = parsedTags;
if (parsedSlots) restaurant.availableSlots = parsedSlots;
if(tags){
restaurant.tags = typeof tags === "string" ? tags.split(",").map((t)=> t.trim()): tags;
}
if(availableSlots){
restaurant.availableSlots = typeof availableSlots === "string" ? availableSlots.split(",").map((s)=> s.trim()): availableSlots;
}
// Handle new image upload if any
// Handle new image upload if any
if (req.file) {
const result = await uploadToCloudinary(req.file.buffer);
imageUrl = result.secure_url;
}
const updated = await restaurant.save()
res.json(updated);
restaurant.image = imageUrl;
await restaurant.save();
res.status(200).json({
success: true,
message: "Restaurant updated successfully",
restaurant,
});
// Upload image if provided
let imageUrl = restaurant.image;
if (req.file) {
const result = await uploadToCloudinary(req.file.buffer);
imageUrl = result.secure_url;
}
// Parse tags and slots
const parsedTags =
typeof tags === "string"
? tags.split(",").map((t: string) => t.trim())
: tags;
const parsedSlots =
typeof availableSlots === "string"
? availableSlots.split(",").map((s: string) => s.trim())
: availableSlots;
// Update fields
if (name) {
restaurant.name = name;
restaurant.slug = name
.toLowerCase()
.trim()
.replace(/\s+/g, "-");
}
if (description) restaurant.description = description;
if (cuisine) restaurant.cuisine = cuisine;
if (priceRange) restaurant.priceRange = priceRange;
if (location) restaurant.location = location;
if (address) restaurant.address = address;
if (chef) restaurant.chef = chef;
if (totalSeats) restaurant.totalSeats = totalSeats;
if (parsedTags) restaurant.tags = parsedTags;
if (parsedSlots) restaurant.availableSlots = parsedSlots;
restaurant.image = imageUrl;
await restaurant.save();
res.status(200).json({
success: true,
message: "Restaurant updated successfully",
restaurant,
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/controllers/ownerController.ts` around lines 197 - 264, Remove the
duplicate update logic in the restaurant update handler: keep only one
Cloudinary upload block, one parsed tag/slot assignment path, one assignment of
imageUrl to restaurant.image before saving, and one restaurant.save followed by
the existing success response. Delete the intermediate res.json(updated), second
upload block, and subsequent duplicate save/response so the handler sends
exactly one response.

} catch (error: any) {
console.error(error);

res.status(500).json({
success: false,
message: error.message,
});
}
};
// ======================================================
// Get bookings for owner's restaurants
// GET /api/owner/bookings
// ======================================================
export const getOwnerBookings = async (
req: AuthRequest,
res: Response
): Promise<void> => {
try {
const restaurant = await Restaurant.findOne({
owner: req.user?.id,
});

if (!restaurant) {
res.status(404).json({
message: "Restaurant profile not found",
});
return;
}

const bookings = await Booking.find({
restaurant: restaurant.id,
})
.populate("user", "name email phone")
.sort({
bookingDate: -1,
timeSlot: -1,
});

res.status(200).json({
success: true,
bookings,
});
} catch (error: any) {
console.error(error);

res.status(500).json({
success: false,
message: error.message,
});
}
}

// Update status of a booking
// PUT / api / owner/booking/:id?status
export const updateBookingStatus = async (req: AuthRequest, res: Response ): Promise<void> => {
try {
const { status }= req.body;
if(!status|| !["confirmed", "cancelled", "completed"].includes(status)){
res.status(400).json({message: "Please enter a valid booking status "});
return
}
const booking = await Booking.findById(req.params.id)
if(!booking){
res.status(404).json({message: "Booking not found"});
return
}
// Verify booking bleongs to the owner's restaurants
const restaurant = await Restaurant.findById(booking.restaurant)
if(!restaurant|| restaurant.owner.toString() !== req.user?._id.toString()){
res.status(404).json({message: "Not authorized to manage this booking "});
return
}
booking.status = status ;
await booking.save();
res.json(booking);

}
catch (error: any) {
console.error(error);
res.status(400).json({message: error.message});


}
}
Comment on lines +319 to +348

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Inconsistent response shape and minor text issues in updateBookingStatus.

Every other handler in this file responds with { success: true/false, message, ... }, but here res.json(booking) on success and {message: error.message} on error omit success, breaking the consistent API contract clients likely rely on. Also, fix the typo "bleongs" → "belongs" and trailing spaces in the response messages ("Please enter a valid booking status ", "Not authorized to manage this booking ").

✏️ Suggested normalization
-    if(!status|| !["confirmed", "cancelled", "completed"].includes(status)){
-      res.status(400).json({message: "Please enter a valid booking status "});
+    if (!status || !["confirmed", "cancelled", "completed"].includes(status)) {
+      res.status(400).json({ success: false, message: "Please enter a valid booking status." });
       return
     }
     const booking = await Booking.findById(req.params.id)
     if(!booking){
-      res.status(404).json({message: "Booking not found"});
+      res.status(404).json({ success: false, message: "Booking not found" });
       return
     }
-    // Verify booking bleongs to the owner's restaurants 
+    // Verify booking belongs to the owner's restaurant
     const restaurant = await Restaurant.findById(booking.restaurant)
     if(!restaurant|| restaurant.owner.toString() !== req.user?._id.toString()){
-      res.status(404).json({message: "Not authorized to manage this booking "});
+      res.status(403).json({ success: false, message: "Not authorized to manage this booking." });
       return
     }
     booking.status = status ;
     await booking.save();
-    res.json(booking);
+    res.status(200).json({ success: true, booking });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const updateBookingStatus = async (req: AuthRequest, res: Response ): Promise<void> => {
try {
const { status }= req.body;
if(!status|| !["confirmed", "cancelled", "completed"].includes(status)){
res.status(400).json({message: "Please enter a valid booking status "});
return
}
const booking = await Booking.findById(req.params.id)
if(!booking){
res.status(404).json({message: "Booking not found"});
return
}
// Verify booking bleongs to the owner's restaurants
const restaurant = await Restaurant.findById(booking.restaurant)
if(!restaurant|| restaurant.owner.toString() !== req.user?._id.toString()){
res.status(404).json({message: "Not authorized to manage this booking "});
return
}
booking.status = status ;
await booking.save();
res.json(booking);
}
catch (error: any) {
console.error(error);
res.status(400).json({message: error.message});
}
}
export const updateBookingStatus = async (req: AuthRequest, res: Response ): Promise<void> => {
try {
const { status }= req.body;
if (!status || !["confirmed", "cancelled", "completed"].includes(status)) {
res.status(400).json({ success: false, message: "Please enter a valid booking status." });
return
}
const booking = await Booking.findById(req.params.id)
if(!booking){
res.status(404).json({ success: false, message: "Booking not found" });
return
}
// Verify booking belongs to the owner's restaurant
const restaurant = await Restaurant.findById(booking.restaurant)
if(!restaurant|| restaurant.owner.toString() !== req.user?._id.toString()){
res.status(403).json({ success: false, message: "Not authorized to manage this booking." });
return
}
booking.status = status ;
await booking.save();
res.status(200).json({ success: true, booking });
}
catch (error: any) {
console.error(error);
res.status(400).json({message: error.message});
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/controllers/ownerController.ts` around lines 319 - 348, Update
updateBookingStatus to use the file’s standard response shape: include success:
true with the booking on success, and success: false with the error message in
the catch response. Correct “bleongs” to “belongs” and remove trailing spaces
from the validation and authorization response messages.

Loading