瀏覽代碼

Bugfix: Implement getRecommendedProductsForCustomer action and related routes, including validation and error handling

BedirhanOZCAN 4 周之前
父節點
當前提交
7d379f1ad7

+ 89 - 0
src/actions/customer/getRecommendedProductsForCustomer/index.ts

@@ -0,0 +1,89 @@
+import mongoose from "mongoose";
+import {
+    RecommendedProduct
+} from "../../../models";
+import {
+    GetRecommendedForCustomerResult,
+    GetRecommendedForCustomerInput
+} from "./types";
+
+const getRecommendedProductsForCustomer = async (query: GetRecommendedForCustomerInput): Promise<GetRecommendedForCustomerResult> => {
+    try {
+        const {
+            menuID,
+            limit = "5"
+        } = query;
+
+        if (!mongoose.Types.ObjectId.isValid(menuID)) {
+            return {
+                message: "invalid-menu-id",
+                code: 400
+            };
+        }
+
+        const objectMenuID = new mongoose.Types.ObjectId(menuID);
+        const sampleSize = parseInt(limit, 10);
+
+        const recommendedProducts = await RecommendedProduct.aggregate([
+            {
+                $match: {
+                    menuID: objectMenuID,
+                    isActive: true
+                }
+            },
+            {
+                $sample: {
+                    size: sampleSize
+                }
+            },
+            {
+                $lookup: {
+                    from: "products",
+                    localField: "productID",
+                    foreignField: "_id",
+                    as: "productDetails"
+                }
+            },
+            {
+                $unwind: {
+                    path: "$productDetails",
+                    preserveNullAndEmptyArrays: false
+                }
+            },
+            {
+                $match: {
+                    "productDetails.isAvailable": true
+                }
+            }
+        ]);
+
+        const payload = recommendedProducts.map((rec) => {
+            return {
+                _id: rec._id.toString(),
+                productID: rec.productID.toString(),
+                title: rec.title,
+                description: rec.description,
+                productDetails: {
+                    price: rec.productDetails.price,
+                    photos: rec.productDetails.photos,
+                    originalTitle: rec.productDetails.title
+                }
+            };
+        });
+
+        return {
+            message: "customer-recommended-products-retrieved",
+            code: 200,
+            payload: payload
+        };
+
+    } catch (error) {
+        console.error("GetRecommendedProductsForCustomer error:", error);
+        return {
+            message: "internal-server-error",
+            code: 500
+        };
+    }
+};
+
+export default getRecommendedProductsForCustomer;

+ 22 - 0
src/actions/customer/getRecommendedProductsForCustomer/types.ts

@@ -0,0 +1,22 @@
+import {
+    IsNotEmpty,
+    IsString,
+    IsOptional,
+    IsNumberString
+} from "class-validator";
+
+export class GetRecommendedForCustomerInput {
+    @IsString({ message: "menuID-must-be-a-string" })
+    @IsNotEmpty({ message: "menuID-is-required" })
+    menuID!: string;
+
+    @IsNumberString({}, { message: "limit-must-be-a-number-string" })
+    @IsOptional()
+    limit?: string;
+}
+
+export type GetRecommendedForCustomerResult = {
+    message: string;
+    code: number;
+    payload?: any;
+};

+ 3 - 0
src/actions/customer/index.ts

@@ -0,0 +1,3 @@
+export {
+    default as getRecommendedProductsForCustomer
+} from "./getRecommendedProductsForCustomer";

+ 3 - 0
src/actions/customer/types/index.ts

@@ -0,0 +1,3 @@
+export {
+    GetRecommendedForCustomerInput
+} from "../getRecommendedProductsForCustomer/types";

+ 38 - 10
src/actions/menu/addRecommendedProduct/index.ts

@@ -1,7 +1,8 @@
 import mongoose from "mongoose";
 import {
     RecommendedProduct,
-    Product
+    Product,
+    Menu
 } from "../../../models";
 import {
     AddRecommendedProductResult,
@@ -15,20 +16,37 @@ const addRecommendedProduct = async (userID: string, recommendedProductLimit: nu
             ...rest
         } = input;
 
+        const objectUserID = new mongoose.Types.ObjectId(userID);
+        const objectProductID = new mongoose.Types.ObjectId(productID);
+
+        const activeMenu = await Menu.findOne({
+            userID: objectUserID,
+            isActive: true
+        });
+
+        if (!activeMenu) {
+            return {
+                message: "active-menu-not-found",
+                code: 404
+            };
+        }
+
+        const activeMenuID = activeMenu._id;
+
         const currentCount = await RecommendedProduct.countDocuments({
-            userID: userID
+            menuID: activeMenuID
         });
 
         if (currentCount >= recommendedProductLimit) {
             return {
-                message: "recommended-product-limit-reached",
+                message: "recommended-product-limit-reached-for-this-menu",
                 code: 403
             };
         }
 
         const existingProduct = await Product.findOne({
-            _id: new mongoose.Types.ObjectId(productID),
-            userID: userID
+            _id: objectProductID,
+            userID: objectUserID
         });
 
         if (!existingProduct) {
@@ -38,21 +56,31 @@ const addRecommendedProduct = async (userID: string, recommendedProductLimit: nu
             };
         }
 
+        const isProductInActiveMenu = activeMenu.products.some((p) => p.productID.toString() === productID);
+
+        if (!isProductInActiveMenu) {
+            return {
+                message: "product-is-not-in-active-menu",
+                code: 400
+            };
+        }
+
         const alreadyRecommended = await RecommendedProduct.findOne({
-            productID: new mongoose.Types.ObjectId(productID),
-            userID: userID
+            productID: objectProductID,
+            menuID: activeMenuID
         });
 
         if (alreadyRecommended) {
             return {
-                message: "product-is-already-recommended",
+                message: "product-is-already-recommended-in-this-menu",
                 code: 409
             };
         }
 
         const newRecommendedProduct = await RecommendedProduct.create({
-            productID: new mongoose.Types.ObjectId(productID),
-            userID: userID,
+            productID: objectProductID,
+            userID: objectUserID,
+            menuID: activeMenuID,
             ...rest
         });
 

+ 20 - 2
src/actions/menu/getRecommendedProducts/index.ts

@@ -1,6 +1,7 @@
 import mongoose from "mongoose";
 import {
-    RecommendedProduct
+    RecommendedProduct,
+    Menu
 } from "../../../models";
 import {
     GetRecommendedProductsResult,
@@ -14,8 +15,24 @@ const getRecommendedProducts = async (userID: string, query: GetRecommendedProdu
             search
         } = query;
 
+        const objectUserID = new mongoose.Types.ObjectId(userID);
+
+        const activeMenu = await Menu.findOne({
+            userID: objectUserID,
+            isActive: true
+        });
+
+        if (!activeMenu) {
+            return {
+                message: "no-active-menu-found",
+                code: 404,
+                payload: []
+            };
+        }
+
         const matchStage: any = {
-            userID: new mongoose.Types.ObjectId(userID)
+            userID: objectUserID,
+            menuID: activeMenu._id
         };
 
         if (isActive !== undefined) {
@@ -67,6 +84,7 @@ const getRecommendedProducts = async (userID: string, query: GetRecommendedProdu
         const payload = recommendedProducts.map((rec) => {
             return {
                 _id: rec._id.toString(),
+                menuID: rec.menuID.toString(),
                 productID: rec.productID.toString(),
                 title: rec.title,
                 description: rec.description,

+ 1 - 1
src/actions/menu/updateRecommendedProduct/index.ts

@@ -60,6 +60,7 @@ const updateRecommendedProduct = async (userID: string, input: UpdateRecommended
 
         const payload = {
             _id: formattedPayload._id.toString(),
+            menuID: formattedPayload.menuID.toString(),
             productID: formattedPayload.productID.toString(),
             title: formattedPayload.title,
             description: formattedPayload.description,
@@ -78,7 +79,6 @@ const updateRecommendedProduct = async (userID: string, input: UpdateRecommended
             code: 200,
             payload: payload
         };
-
     } catch (error) {
         console.error("UpdateRecommendedProduct action error:", error);
         return {

+ 25 - 0
src/controllers/customerController.ts

@@ -0,0 +1,25 @@
+import {
+    Response,
+    Request
+} from "express";
+import {
+    getRecommendedProductsForCustomer as _getRecommendedProductsForCustomer
+} from "../actions/customer";
+import {
+    GetRecommendedForCustomerInput
+} from "../actions/customer/getRecommendedProductsForCustomer/types";
+
+export const getRecommendedProductsForCustomer = async (req: Request, res: Response): Promise<void> => {
+    const query = req.query as unknown as GetRecommendedForCustomerInput;
+
+    const result = await _getRecommendedProductsForCustomer(query);
+
+    res.status(result.code)
+        .json({
+            message: result.message,
+            code: result.code,
+            ...(result.payload && {
+                payload: result.payload
+            })
+        });
+};

+ 5 - 1
src/models/RecommendedProduct.ts

@@ -4,8 +4,9 @@ import mongoose, {
 } from "mongoose";
 
 export interface IRecommendedProduct extends Document {
-    userID: mongoose.Types.ObjectId;
     productID: mongoose.Types.ObjectId;
+    userID: mongoose.Types.ObjectId;
+    menuID: mongoose.Types.ObjectId;
     description?: string;
     isActive: boolean;
     createdAt: Date;
@@ -19,6 +20,9 @@ const recommendedProductSchema = new Schema<IRecommendedProduct>(
         userID: {
             type: Schema.Types.ObjectId,
         },
+        menuID: {
+            type: Schema.Types.ObjectId,
+        },
         productID: {
             type: Schema.Types.ObjectId,
         },

+ 18 - 0
src/routes/customerRoutes.ts

@@ -0,0 +1,18 @@
+import {
+    Router
+} from "express";
+import {
+    getRecommendedProductsForCustomer
+} from "../controllers/customerController";
+import {
+    validateBody
+} from "../middlewares/validateBody";
+import {
+    GetRecommendedForCustomerInput
+} from "../actions/customer/types";
+
+const router = Router();
+
+router.get("/getRecommendedProducts", validateBody(GetRecommendedForCustomerInput), getRecommendedProductsForCustomer);
+
+export default router;

+ 3 - 1
src/routes/index.ts

@@ -3,13 +3,15 @@ import {
 } from "express";
 import authRoutes from "./authRoutes";
 import menuRoutes from "./menuRoutes";
+import customerRoutes from "./customerRoutes";
 import {
-    authMiddleware 
+    authMiddleware
 } from "../middlewares/authMiddleware";
 
 const router = Router();
 
 router.use("/auth", authRoutes);
 router.use("/menu", authMiddleware, menuRoutes);
+router.use("/customer", customerRoutes);
 
 export default router;