فهرست منبع

Feature: Implement menu management actions including add, update, delete, and retrieve menus

emrecevik106 1 ماه پیش
والد
کامیت
bc08b1d35c

+ 84 - 76
src/actions/auth/login/index.ts

@@ -9,94 +9,102 @@ import {
 import redis from "../../../config/redis";
 
 const login = async (input: LoginInput): Promise<LoginResult> => {
-    const {
-        password,
-        mail
-    } = input;
+    try{
+        const {
+            password,
+            mail
+        } = input;
 
-    const user = await User.findOne({
-        mail
-    });
+        const user = await User.findOne({
+            mail
+        });
 
-    if (!user) {
-        return {
-            message: "user-not-found",
-            code: 404,
-        };
-    }
-
-    if (user.password !== password) {
-        return {
-            message: "wrong-password",
-            code: 401,
-        };
-    }
-
-    if (!user.isPhoneVerified) {
-        return {
-            message: "please-verify-your-phone-first",
-            code: 403,
-            payload: {
-                userID: user._id.toString()
-            }
-        };
-    }
-
-    if (!user.isApproved) {
-        return {
-            message: "your-account-is-currently-under-review-we-will-get-back-to-you",
-            code: 200,
-        };
-    }
+        if (!user) {
+            return {
+                message: "user-not-found",
+                code: 404,
+            };
+        }
 
-    const accessToken = jwt.sign(
-        {
-            companyName: user.companyName,
-            fullName: user.fullName,
-            userID: user._id,
-            mail: user.mail
-        },
-        process.env.JWT_SECRET as string,
-        {
-            expiresIn: "4h"
+        if (user.password !== password) {
+            return {
+                message: "wrong-password",
+                code: 401,
+            };
         }
-    );
 
-    await redis.setex(user._id.toString(), 14400, accessToken);
+        if (!user.isPhoneVerified) {
+            return {
+                message: "please-verify-your-phone-first",
+                code: 403,
+                payload: {
+                    userID: user._id.toString()
+                }
+            };
+        }
 
-    const refreshToken = jwt.sign(
-        {
-            companyName: user.companyName,
-            fullName: user.fullName,
-            userID: user._id,
-            mail: user.mail
-        },
-        process.env.JWT_SECRET as string,
-        {
-            expiresIn: "30d"
+        if (!user.isApproved) {
+            return {
+                message: "your-account-is-currently-under-review-we-will-get-back-to-you",
+                code: 200,
+            };
         }
-    );
 
-    user.refreshToken = refreshToken;
-    await user.save();
+        const accessToken = jwt.sign(
+            {
+                companyName: user.companyName,
+                fullName: user.fullName,
+                userID: user._id,
+                mail: user.mail
+            },
+            process.env.JWT_SECRET as string,
+            {
+                expiresIn: "4h"
+            }
+        );
 
-    return {
-        message: "login-successful",
-        code: 200,
-        payload: {
-            refreshToken,
-            accessToken,
-            user: {
-                phoneNumber: user.phoneNumber,
+        await redis.setex(user._id.toString(), 14400, accessToken);
+
+        const refreshToken = jwt.sign(
+            {
                 companyName: user.companyName,
-                userID: user._id.toString(),
-                firstName: user.firstName,
-                lastName: user.lastName,
                 fullName: user.fullName,
-                mail: user.mail,
+                userID: user._id,
+                mail: user.mail
+            },
+            process.env.JWT_SECRET as string,
+            {
+                expiresIn: "30d"
+            }
+        );
+
+        user.refreshToken = refreshToken;
+        await user.save();
+
+        return {
+            message: "login-successful",
+            code: 200,
+            payload: {
+                refreshToken,
+                accessToken,
+                user: {
+                    phoneNumber: user.phoneNumber,
+                    companyName: user.companyName,
+                    userID: user._id.toString(),
+                    firstName: user.firstName,
+                    lastName: user.lastName,
+                    fullName: user.fullName,
+                    mail: user.mail,
+                },
             },
-        },
-    };
+        };
+    }
+    catch (error) {
+        return {
+            message: "internal-server-error",
+            code: 500,
+        };
+    }
 };
 
 export default login;

+ 6 - 6
src/actions/auth/refreshToken/index.ts

@@ -42,7 +42,7 @@ const refreshToken = async (input: RefreshTokenInput): Promise<RefreshTokenResul
             };
         }
 
-        const newAccessToken = jwt.sign(
+        const accessToken = jwt.sign(
             {
                 companyName: decoded.companyName,
                 fullName: decoded.fullName,
@@ -55,7 +55,7 @@ const refreshToken = async (input: RefreshTokenInput): Promise<RefreshTokenResul
             }
         );
 
-        const newRefreshToken = jwt.sign(
+        const refreshToken = jwt.sign(
             {
                 companyName: decoded.companyName,
                 fullName: decoded.fullName,
@@ -68,17 +68,17 @@ const refreshToken = async (input: RefreshTokenInput): Promise<RefreshTokenResul
             }
         );
 
-        await redis.setex(userID, 14400, newAccessToken);
+        await redis.setex(userID, 14400, accessToken);
 
-        user.refreshToken = newRefreshToken;
+        user.refreshToken = refreshToken;
         await user.save();
 
         return {
             code: 200,
             message: "token-refreshed",
             payload: {
-                refreshToken: newRefreshToken,
-                accessToken: newAccessToken
+                refreshToken: refreshToken,
+                accessToken: accessToken
             },
         };
     } catch (error) {

+ 17 - 0
src/actions/auth/register/index.ts

@@ -1,6 +1,8 @@
 import {
     Menu 
 } from "../../../models/Menu";
+import Plan from "../../../models/Plan";
+import Subscription from "../../../models/Subscription";
 import {
     User
 } from "../../../models/User";
@@ -86,6 +88,21 @@ const register = async (input: RegisterInput): Promise<RegisterResult> => {
             products: []
         });
 
+        const starterPlan = await Plan.findOne({
+            title: "Starter" 
+        });
+        
+        await Subscription.create({
+            userID: newUser._id.toString(),
+            planID: starterPlan._id,
+            title: starterPlan.title,
+            isActive: true,
+            startDate: new Date(),
+            endDate: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
+            status: "active",
+            price: starterPlan!.type.find((t: { type: string; price: number }) => t.type === "yearly")?.price ?? 0
+        });
+
         await smsSend({
             userID: newUser._id.toString()
         });

+ 10 - 0
src/actions/menu/addCategory/index.ts

@@ -13,6 +13,16 @@ const addCategory = async (userID: string, input: AddCategoryInput): Promise<Add
             index
         } = input;
 
+        /*  const categoryCount = await Category.countDocuments({
+            userID
+        });
+        if (categoryCount >= context.categoryLimit) {
+            return {
+                message: "category-limit-reached",
+                code: 403
+            };
+        } */
+
         const existing = await Category.findOne({
             userID,
             index

+ 51 - 0
src/actions/menu/addMenu/index.ts

@@ -0,0 +1,51 @@
+import {
+    Menu
+} from "../../../models/Menu";
+import {
+    AddMenuInput,
+    AddMenuResult
+} from "./types";
+
+const addMenu = async (userID: string, input: AddMenuInput): Promise<AddMenuResult> => {
+    try {
+        const {
+            description,
+            isViewPrice,
+            isActive,
+            title
+        } = input;
+
+        /*   const menuCount = await Menu.countDocuments({
+            userID
+        });
+        if (menuCount >= context.menuLimit) {
+            return {
+                message: "menu-limit-reached",
+                code: 403
+            };
+        }  */
+
+        const newMenu = await Menu.create({
+            isViewPrice,
+            description,
+            products: [],
+            isActive,
+            userID,
+            title
+        });
+
+        return {
+            message: "menu-created-successfully",
+            code: 201,
+            payload: newMenu
+        };
+    } catch (error) {
+        console.error("AddMenu error:", error);
+        return {
+            message: "internal-server-error",
+            code: 500
+        };
+    }
+};
+
+export default addMenu;

+ 30 - 0
src/actions/menu/addMenu/types.ts

@@ -0,0 +1,30 @@
+import {
+    IsBoolean,
+    IsNotEmpty,
+    IsOptional,
+    IsString
+} from "class-validator";
+
+export class AddMenuInput {
+    @IsString()
+    @IsNotEmpty({ message: "title-is-required" })
+    title!: string;
+
+    @IsString()
+    @IsOptional()
+    description?: string;
+
+    @IsBoolean()
+    @IsOptional()
+    isViewPrice?: boolean;
+
+    @IsBoolean()
+    @IsOptional()
+    isActive?: boolean;
+}
+
+export type AddMenuResult = {
+    message: string;
+    code: number;
+    payload?: any;
+};

+ 10 - 0
src/actions/menu/addProduct/index.ts

@@ -20,6 +20,16 @@ const addProduct = async (userID: string, input: AddProductInput): Promise<AddPr
             ? categoryIDs
             : categoryIDs ? [categoryIDs] : [];
 
+        /*   const productCount = await Product.countDocuments({
+            userID
+        });
+        if (productCount >= context.productLimit) {
+            return {
+                message: "product-limit-reached",
+                code: 403
+            };
+        } */
+
         const newProduct = await Product.create({
             categoryIDs: normalizedCategoryIDs,
             ...rest,

+ 69 - 0
src/actions/menu/deleteMenu/index.ts

@@ -0,0 +1,69 @@
+import {
+    Menu
+} from "../../../models/Menu";
+import {
+    DeleteMenuInput,
+    DeleteMenuResult
+} from "./types";
+
+const deleteMenu = async (userID: string, input: DeleteMenuInput): Promise<DeleteMenuResult> => {
+    try {
+        const {
+            menuIDs
+        } = input;
+
+        const menu = await Menu.findOne({
+            _id: {
+                $in: menuIDs 
+            },
+            userID
+        });
+
+        if (!menu) {
+            return {
+                message: "menu-not-found",
+                code: 404
+            };
+        }
+
+        const activeMenuCount = await Menu.countDocuments({ // kullanıcının toplam aktif menü sayısı
+            userID,
+            isActive: true
+        });
+
+        const deletingActiveCount = await Menu.countDocuments({ //silinmek istenen menüler arasında kaç tane aktif var
+            _id: {
+                $in: menuIDs 
+            },
+            userID,
+            isActive: true
+        });
+
+        if (activeMenuCount - deletingActiveCount < 1) {
+            return {
+                message: "cannot-delete-last-active-menu",
+                code: 400
+            };
+        }
+
+        await Menu.deleteMany({
+            _id: {
+                $in: menuIDs 
+            },
+            userID
+        });
+
+        return {
+            message: "menu-deleted-successfully",
+            code: 200
+        };
+    } catch (error) {
+        console.error("DeleteMenu error:", error);
+        return {
+            message: "internal-server-error",
+            code: 500
+        };
+    }
+};
+
+export default deleteMenu;

+ 15 - 0
src/actions/menu/deleteMenu/types.ts

@@ -0,0 +1,15 @@
+import {
+    IsNotEmpty,
+    IsArray
+} from "class-validator";
+
+export class DeleteMenuInput {
+    @IsArray()
+    @IsNotEmpty({ message: "menuIDs-is-required" })
+    menuIDs!: string[];
+}
+
+export type DeleteMenuResult = {
+    message: string;
+    code: number;
+};

+ 1 - 25
src/actions/menu/getCategories/index.ts

@@ -1,38 +1,14 @@
 import {
     Category 
 } from "../../../models/Category";
-import {
-    Menu 
-} from "../../../models/Menu";
 import {
     GetCategoriesResult 
 } from "./types";
 
 const getCategories = async (userID: string): Promise<GetCategoriesResult> => {
     try {
-        const activeMenu = await Menu.findOne({
-            userID,
-            isActive: true
-        });
-
-        if (!activeMenu) {
-            return {
-                message: "no-active-menu",
-                code: 404
-            };
-        }
-        
-        const categoryIDs = [
-            ...new Set(
-                activeMenu.products.flatMap((p: any) => p.categoryIDs.map((id: any) => id.toString()))
-            )
-        ];
-
         const categories = await Category.find({
-            userID,
-            _id: {
-                $in: categoryIDs 
-            }
+            userID
         }).sort({
             index: 1 
         });

+ 60 - 0
src/actions/menu/getMenus/index.ts

@@ -0,0 +1,60 @@
+import {
+    Menu
+} from "../../../models/Menu";
+import {
+    GetMenusInput,
+    GetMenusResult
+} from "./types";
+
+const getMenus = async (userID: string, query: GetMenusInput): Promise<GetMenusResult> => {
+    try {
+        const {
+            isActive,
+            search
+        } = query;
+
+        const matchStage: any = {
+            userID 
+        };
+
+        if (isActive !== undefined) {
+            matchStage.isActive = isActive === "true";
+        }
+
+        if (search) {
+            matchStage.$or = [
+                {
+                    title: {
+                        $regex: search,
+                        $options: "i"
+                    }
+                },
+                {
+                    description: {
+                        $regex: search,
+                        $options: "i"
+                    }
+                }
+            ];
+        }
+
+        const menus = await Menu.find(matchStage)
+            .sort({
+                createdAt: -1
+            });
+
+        return {
+            message: "menus-retrieved-successfully",
+            code: 200,
+            payload: menus
+        };
+    } catch (error) {
+        console.error("GetMenus error:", error);
+        return {
+            message: "internal-server-error",
+            code: 500
+        };
+    }
+};
+
+export default getMenus;

+ 20 - 0
src/actions/menu/getMenus/types.ts

@@ -0,0 +1,20 @@
+import {
+    IsBoolean,
+    IsOptional,
+    IsString
+} from "class-validator";
+
+export class GetMenusInput {
+    @IsString()
+    @IsOptional()
+    search?: string;
+
+    @IsOptional()
+    isActive?: string;
+}
+
+export type GetMenusResult = {
+    message: string;
+    code: number;
+    payload?: any;
+};

+ 13 - 0
src/actions/menu/index.ts

@@ -23,3 +23,16 @@ export {
 export {
     default as deleteProduct
 } from "./deleteProduct";
+
+export {
+    default as addMenu
+} from "./addMenu";
+export {
+    default as getMenus
+} from "./getMenus";
+export {
+    default as deleteMenu
+} from "./deleteMenu";
+export {
+    default as updateMenu
+} from "./updateMenu";

+ 10 - 0
src/actions/menu/types/index.ts

@@ -17,3 +17,13 @@ export {
 export {
     DeleteProductInput
 } from "../deleteProduct/types";
+
+export {
+    AddMenuInput
+} from "../addMenu/types";
+export {
+    UpdateMenuInput
+} from "../updateMenu/types";
+export {
+    DeleteMenuInput
+} from "../deleteMenu/types";

+ 50 - 0
src/actions/menu/updateMenu/index.ts

@@ -0,0 +1,50 @@
+import {
+    Menu
+} from "../../../models/Menu";
+import {
+    UpdateMenuInput,
+    UpdateMenuResult
+} from "./types";
+
+const updateMenu = async (userID: string, input: UpdateMenuInput): Promise<UpdateMenuResult> => {
+    try {
+        const {
+            menuID,
+            ...updateFields
+        } = input;
+
+        const updatedMenu = await Menu.findOneAndUpdate(
+            {
+                _id: menuID,
+                userID
+            },
+            {
+                $set: updateFields
+            },
+            {
+                new: true
+            }
+        );
+
+        if (!updatedMenu) {
+            return {
+                message: "menu-not-found",
+                code: 404
+            };
+        }
+
+        return {
+            message: "menu-updated-successfully",
+            code: 200,
+            payload: updatedMenu
+        };
+    } catch (error) {
+        console.error("UpdateMenu error:", error);
+        return {
+            message: "internal-server-error",
+            code: 500
+        };
+    }
+};
+
+export default updateMenu;

+ 35 - 0
src/actions/menu/updateMenu/types.ts

@@ -0,0 +1,35 @@
+import {
+    IsBoolean,
+    IsNotEmpty,
+    IsOptional,
+    IsString,
+    Validate
+} from "class-validator";
+
+export class UpdateMenuInput {
+    @IsString()
+    @IsNotEmpty({ message: "menuID-is-required" })
+    menuID!: string;
+
+    @IsString()
+    @IsOptional()
+    title?: string;
+
+    @IsString()
+    @IsOptional()
+    description?: string;
+
+    @IsBoolean()
+    @IsOptional()
+    isViewPrice?: boolean;
+
+    @IsBoolean()
+    @IsOptional()
+    isActive?: boolean;
+}
+
+export type UpdateMenuResult = {
+    message: string;
+    code: number;
+    payload?: any;
+};

+ 61 - 3
src/controllers/menuController.ts

@@ -12,7 +12,11 @@ import {
     getCategories as _getCategories,
     getProducts as _getProducts,
     addCategory as _addCategory,
-    addProduct as _addProduct
+    addProduct as _addProduct,
+    updateMenu as _updateMenu,
+    deleteMenu as _deleteMenu,
+    getMenus as _getMenus,
+    addMenu as _addMenu
 } from "../actions/menu";
 
 //#region Category Controllers
@@ -22,7 +26,10 @@ export const addCategory = async (req: AuthRequest, res: Response): Promise<void
     res.status(result.code)
         .json({
             message: result.message,
-            code: result.code
+            code: result.code,
+            ...(result.payload && {
+                payload: result.payload
+            })
         });
 };
 
@@ -115,4 +122,55 @@ export const deleteProduct = async (req: AuthRequest, res: Response): Promise<vo
             })
         });
 };
-// #endregion
+// #endregion
+
+//#region Menu Controllers
+export const addMenu = async (req: AuthRequest, res: Response): Promise<void> => {
+    const result = await _addMenu(req.context!.userID, req.body);
+
+    res.status(result.code)
+        .json({
+            message: result.message,
+            code: result.code,
+            ...(result.payload && {
+                payload: result.payload
+            })
+        });
+};
+
+export const getMenus = async (req: AuthRequest, res: Response): Promise<void> => {
+    const result = await _getMenus(req.context!.userID, req.query);
+
+    res.status(result.code)
+        .json({
+            message: result.message,
+            code: result.code,
+            ...(result.payload && {
+                payload: result.payload
+            })
+        });
+};
+
+export const deleteMenu = async (req: AuthRequest, res: Response): Promise<void> => {
+    const result = await _deleteMenu(req.context!.userID, req.body);
+
+    res.status(result.code)
+        .json({
+            message: result.message,
+            code: result.code
+        });
+};
+
+export const updateMenu = async (req: AuthRequest, res: Response): Promise<void> => {
+    const result = await _updateMenu(req.context!.userID, req.body);
+
+    res.status(result.code)
+        .json({
+            message: result.message,
+            code: result.code,
+            ...(result.payload && {
+                payload: result.payload
+            })
+        });
+};
+//#endregion

+ 7 - 5
src/models/Menu.ts

@@ -8,12 +8,12 @@ export interface IMenu extends Document {
     title: string;
     description?: string;
     isActive?: boolean;
+    isViewPrice: boolean;
     products: Array<{
         categoryIDs: mongoose.Types.ObjectId[];
         productID: mongoose.Types.ObjectId;
         isViewPrice: boolean;
     }>;
-    isViewPrice: boolean;
     createdAt: Date;
     updatedAt: Date;
 }
@@ -34,6 +34,10 @@ const menuSchema = new Schema<IMenu>(
             type: Boolean,
             default: true
         },
+        isViewPrice: {
+            type: Boolean,
+            default: true
+        },
         products: [
             {
                 categoryIDs: {
@@ -46,12 +50,10 @@ const menuSchema = new Schema<IMenu>(
                 },
                 isViewPrice: {
                     type: Boolean,
+                    default: true
                 }
             }
-        ],
-        isViewPrice: {
-            type: Boolean,
-        }
+        ]
     },
     {
         timestamps: true

+ 14 - 2
src/routes/menuRoutes.ts

@@ -9,7 +9,11 @@ import {
     updateProduct,
     addCategory,
     getProducts,
-    addProduct
+    addProduct,
+    deleteMenu,
+    updateMenu,
+    addMenu,
+    getMenus
 } from "../controllers/menuController";
 import {
     UpdateCategoryInput,
@@ -17,7 +21,10 @@ import {
     UpdateProductInput,
     DeleteProductInput,
     AddCategoryInput,
-    AddProductInput
+    AddProductInput,
+    UpdateMenuInput,
+    DeleteMenuInput,
+    AddMenuInput
 } from "../actions/menu/types";
 import {
     authMiddleware
@@ -38,4 +45,9 @@ router.put("/updateProduct", authMiddleware, validateBody(UpdateProductInput), u
 router.post("/addProduct", authMiddleware, validateBody(AddProductInput), addProduct);
 router.get("/getProducts", authMiddleware, getProducts);
 
+router.delete("/deleteMenu", authMiddleware, validateBody(DeleteMenuInput), deleteMenu);
+router.put("/updateMenu", authMiddleware, validateBody(UpdateMenuInput), updateMenu);
+router.post("/addMenu", authMiddleware, validateBody(AddMenuInput), addMenu);
+router.get("/getMenus", authMiddleware, getMenus);
+
 export default router;