diff --git a/.env.example b/.env.example index a9774858..cdf1da8a 100644 --- a/.env.example +++ b/.env.example @@ -29,4 +29,5 @@ GOOGLE_AUTH_CALLBACK_URL= FLW_PUBLIC_KEY= FLW_SECRET_KEY= FLW_ENCRYPTION_KEY= -BASE_URL= \ No newline at end of file +BASE_URL= +PAYSTACK_SECRET_KEY=your_paystack_secret_key \ No newline at end of file diff --git a/package.json b/package.json index e9e61e43..f7b40c14 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "open": "^10.1.0", "passport": "^0.7.0", "passport-google-oauth2": "^0.2.0", + "paystack": "^2.0.1", "pdfkit": "^0.15.0", "pg": "^8.12.0", "pino": "^9.3.1", diff --git a/src/config/index.ts b/src/config/index.ts index 23c9c656..d94eb762 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,37 +1,38 @@ -import dotenv from "dotenv"; -dotenv.config(); - -const config = { - port: process.env.PORT ?? 8000, - "api-prefix": "api/v1", - DB_USER: process.env.DB_USER, - DB_HOST: process.env.DB_HOST, - DB_PASSWORD: process.env.DB_PASSWORD, - DB_PORT: process.env.DB_PORT, - DB_NAME: process.env.DB_NAME, - TOKEN_SECRET: process.env.AUTH_SECRET, - TOKEN_EXPIRY: process.env.AUTH_EXPIRY, - SMTP_USER: process.env.SMTP_USER, - SMTP_PASSWORD: process.env.SMTP_PASSWORD, - SMTP_HOST: process.env.SMTP_HOST, - SMTP_SERVICE: process.env.SMTP_SERVICE, - SMTP_PORT: process.env.SMTP_PORT, - REDIS_PASSWORD: process.env.REDIS_PASSWORD, - REDIS_HOST: process.env.REDIS_HOST, - REDIS_PORT: process.env.REDIS_PORT || 6379, - NODE_ENV: process.env.NODE_ENV, - TWILIO_SID: process.env.TWILIO_SID || "ACXXXXXXXXXXXXXXXX", - TWILIO_AUTH_TOKEN: process.env.TWILIO_AUTH_TOKEN || "twilo_auth_token", - TWILIO_PHONE_NUMBER: process.env.TWILIO_PHONE_NUMBER, - STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY, - GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID, - GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET, - GOOGLE_AUTH_CALLBACK_URL: process.env.GOOGLE_AUTH_CALLBACK_URL, - FLW_PUBLIC_KEY: process.env.FLW_PUBLIC_KEY, - FLW_SECRET_KEY: process.env.FLW_SECRET_KEY, - FLW_ENCRYPTION_KEY: process.env.FLW_ENCRYPTION_KEY, - LEMONSQUEEZY_SIGNING_KEY: process.env.LEMONSQUEEZY_SIGNING_KEY, - BASE_URL: process.env.BASE_URL, -}; - -export default config; +import dotenv from "dotenv"; +dotenv.config(); + +const config = { + port: process.env.PORT ?? 8000, + "api-prefix": "api/v1", + DB_USER: process.env.DB_USER, + DB_HOST: process.env.DB_HOST, + DB_PASSWORD: process.env.DB_PASSWORD, + DB_PORT: process.env.DB_PORT, + DB_NAME: process.env.DB_NAME, + TOKEN_SECRET: process.env.AUTH_SECRET, + TOKEN_EXPIRY: process.env.AUTH_EXPIRY, + SMTP_USER: process.env.SMTP_USER, + SMTP_PASSWORD: process.env.SMTP_PASSWORD, + SMTP_HOST: process.env.SMTP_HOST, + SMTP_SERVICE: process.env.SMTP_SERVICE, + SMTP_PORT: process.env.SMTP_PORT, + REDIS_PASSWORD: process.env.REDIS_PASSWORD, + REDIS_HOST: process.env.REDIS_HOST, + REDIS_PORT: process.env.REDIS_PORT || 6379, + NODE_ENV: process.env.NODE_ENV, + TWILIO_SID: process.env.TWILIO_SID || "ACXXXXXXXXXXXXXXXX", + TWILIO_AUTH_TOKEN: process.env.TWILIO_AUTH_TOKEN || "twilo_auth_token", + TWILIO_PHONE_NUMBER: process.env.TWILIO_PHONE_NUMBER, + STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY, + GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID, + GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET, + GOOGLE_AUTH_CALLBACK_URL: process.env.GOOGLE_AUTH_CALLBACK_URL, + FLW_PUBLIC_KEY: process.env.FLW_PUBLIC_KEY, + FLW_SECRET_KEY: process.env.FLW_SECRET_KEY, + FLW_ENCRYPTION_KEY: process.env.FLW_ENCRYPTION_KEY, + LEMONSQUEEZY_SIGNING_KEY: process.env.LEMONSQUEEZY_SIGNING_KEY, + BASE_URL: process.env.BASE_URL, + PAYSTACK_SECRET_KEY: process.env.PAYSTACK_SECRET_KEY, +}; + +export default config; diff --git a/src/controllers/BlogController.ts b/src/controllers/BlogController.ts index 32096007..70cf13b4 100644 --- a/src/controllers/BlogController.ts +++ b/src/controllers/BlogController.ts @@ -1,416 +1,600 @@ -import { Request, Response } from "express"; -import { BlogService } from "../services"; - -export class BlogController { - private blogService: BlogService; - - constructor() { - this.blogService = new BlogService(); - } - - /** - * @swagger - * /api/v1/blog: - * get: - * summary: Get a paginated list of blogs - * description: Retrieve a paginated list of blog posts - * tags: [Blog] - * parameters: - * - in: query - * name: page - * schema: - * type: integer - * default: 1 - * description: Page number for pagination - * - in: query - * name: limit - * schema: - * type: integer - * default: 10 - * description: Number of blog posts per page - * responses: - * 200: - * description: A paginated list of blog posts - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: success - * status_code: - * type: integer - * example: 200 - * data: - * type: array - * items: - * type: object - * properties: - * title: - * type: string - * example: My First Blog - * content: - * type: string - * example: This is the content of my first blog post. - * author: - * type: string - * example: John Doe - * published_at: - * type: string - * format: date-time - * example: 2023-07-21T19:58:00.000Z - * pagination: - * type: object - * properties: - * current_page: - * type: integer - * example: 1 - * per_page: - * type: integer - * example: 10 - * total_pages: - * type: integer - * example: 2 - * total_items: - * type: integer - * example: 15 - * 400: - * description: Invalid query parameters - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: bad request - * message: - * type: string - * example: Invalid query params passed - * status_code: - * type: integer - * example: 400 - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: error - * message: - * type: string - * example: Internal server error - * status_code: - * type: integer - * example: 500 - */ - async listBlogs(req: Request, res: Response): Promise { - try { - const page = parseInt(req.query.page as string) || 1; - const limit = parseInt(req.query.limit as string) || 10; - - if (page <= 0 || limit <= 0) { - res.status(400).json({ - status: "bad request", - message: "Invalid query params passed", - status_code: 400, - }); - return; - } - - const { blogs, totalItems } = await this.blogService.getPaginatedblogs( - page, - limit, - ); - - res.json({ - status: "success", - status_code: 200, - data: blogs.map((blog) => ({ - title: blog.title, - content: blog.content, - author: blog.author, - published_at: blog.published_at, - })), - pagination: { - current_page: page, - per_page: limit, - total_pages: Math.ceil(totalItems / limit), - total_items: totalItems, - }, - }); - } catch (error) { - res.status(500).json({ - status: "error", - message: "Internal server error", - status_code: 500, - }); - } - } - - /** - * @swagger - * /api/v1/blog/user: - * get: - * summary: Get a paginated list of blogs by user - * description: Retrieve a paginated list of blog posts created by the authenticated user - * tags: [Blog] - * parameters: - * - in: query - * name: page - * schema: - * type: integer - * default: 1 - * description: Page number for pagination - * - in: query - * name: limit - * schema: - * type: integer - * default: 10 - * description: Number of blog posts per page - * responses: - * 200: - * description: A paginated list of blog posts by the user - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: success - * status_code: - * type: integer - * example: 200 - * data: - * type: array - * items: - * type: object - * properties: - * title: - * type: string - * example: My First Blog - * content: - * type: string - * example: This is the content of my first blog post. - * author: - * type: string - * example: John Doe - * published_date: - * type: string - * format: date-time - * example: 2023-07-21T19:58:00.000Z - * pagination: - * type: object - * properties: - * current_page: - * type: integer - * example: 1 - * per_page: - * type: integer - * example: 10 - * total_pages: - * type: integer - * example: 2 - * total_items: - * type: integer - * example: 15 - * 400: - * description: Invalid query parameters - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: bad request - * message: - * type: string - * example: Invalid query params passed - * status_code: - * type: integer - * example: 400 - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: error - * message: - * type: string - * example: Internal server error - * status_code: - * type: integer - * example: 500 - */ - async listBlogsByUser(req: Request, res: Response): Promise { - try { - const user = req.user; - const page = parseInt(req.query.page as string) || 1; - const limit = parseInt(req.query.limit as string) || 10; - - if (!user || page <= 0 || limit <= 0) { - res.status(400).json({ - status: "bad request", - message: "Invalid query params passed", - status_code: 400, - }); - return; - } - - const { blogs, totalItems } = - await this.blogService.getPaginatedBlogsByUser(user.id, page, limit); - - res.json({ - status: "success", - status_code: 200, - data: blogs.map((blog) => ({ - title: blog.title, - content: blog.content, - author: blog.author.name, - published_date: blog.published_at, - })), - pagination: { - current_page: page, - per_page: limit, - total_pages: Math.ceil(totalItems / limit), - total_items: totalItems, - }, - }); - } catch (error) { - res.status(500).json({ - status: "error", - message: "Internal server error", - status_code: 500, - }); - } - } - - /** - * @swagger - * /api/v1/blog/{id}: - * delete: - * summary: Delete a blog post - * description: Delete a specific blog post by its ID - * tags: [Blog] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: string - * description: The ID of the blog post - * responses: - * 200: - * description: Blog post deleted successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * status_code: - * type: integer - * example: 200 - * message: - * type: string - * example: Blog post deleted successfully - * 404: - * description: Blog post not found - * content: - * application/json: - * schema: - * type: object - * properties: - * status_code: - * type: integer - * example: 404 - * error: - * type: string - * example: Blog post not found - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * type: object - * properties: - * status_code: - * type: integer - * example: 500 - * error: - * type: string - * example: Internal server error - * details: - * type: string - * example: Error message - */ - async deleteBlogPost(req: Request, res: Response): Promise { - try { - const { id } = req.params; - if (!id) { - res.status(401).json({ - status_code: 401, - error: "Unauthorized", - }); - } - - const deletedPost = await this.blogService.deleteBlogPost(id); - if (!deletedPost) { - res.status(404).json({ - status_code: 404, - error: "Blog post not found", - }); - } - - res.status(200).json({ - status_code: 200, - message: "Blog post deleted successfully", - }); - } catch (error) { - res.status(500).json({ - status_code: 500, - error: "Internal server error", - details: error.message, - }); - } - } - - async createBlogController(req: Request, res: Response) { - const { title, content, image_url, tags, categories } = req.body; - - try { - const newBlog = await this.blogService.createBlogPost( - title, - content, - req.user.id, - image_url, - tags, - categories, - ); - res.status(201).json({ - status: "success", - status_code: 201, - message: "Blog post created successfully", - data: { - blog: newBlog, - author: req.user.id, - }, - }); - } catch (error) { - res.status(500).json({ - status: "unsuccessful", - status_code: 500, - message: error.message, - }); - } - } -} +import { NextFunction, Request, Response } from "express"; +import { BlogService } from "../services"; +import { ResourceNotFound, ServerError, Forbidden } from "../middleware"; + +export class BlogController { + private blogService: BlogService; + + constructor() { + this.blogService = new BlogService(); + } + + /** + * @swagger + * /api/v1/blog: + * get: + * summary: Get a paginated list of blogs + * description: Retrieve a paginated list of blog posts + * tags: [Blog] + * parameters: + * - in: query + * name: page + * schema: + * type: integer + * default: 1 + * description: Page number for pagination + * - in: query + * name: limit + * schema: + * type: integer + * default: 10 + * description: Number of blog posts per page + * responses: + * 200: + * description: A paginated list of blog posts + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: success + * status_code: + * type: integer + * example: 200 + * data: + * type: array + * items: + * type: object + * properties: + * title: + * type: string + * example: My First Blog + * content: + * type: string + * example: This is the content of my first blog post. + * author: + * type: string + * example: John Doe + * published_at: + * type: string + * format: date-time + * example: 2023-07-21T19:58:00.000Z + * pagination: + * type: object + * properties: + * current_page: + * type: integer + * example: 1 + * per_page: + * type: integer + * example: 10 + * total_pages: + * type: integer + * example: 2 + * total_items: + * type: integer + * example: 15 + * 400: + * description: Invalid query parameters + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: bad request + * message: + * type: string + * example: Invalid query params passed + * status_code: + * type: integer + * example: 400 + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: error + * message: + * type: string + * example: Internal server error + * status_code: + * type: integer + * example: 500 + */ + async listBlogs(req: Request, res: Response): Promise { + try { + const page = parseInt(req.query.page as string) || 1; + const limit = parseInt(req.query.limit as string) || 10; + + if (page <= 0 || limit <= 0) { + res.status(400).json({ + status: "bad request", + message: "Invalid query params passed", + status_code: 400, + }); + return; + } + + const { blogs, totalItems } = await this.blogService.getPaginatedblogs( + page, + limit, + ); + + res.json({ + status: "success", + status_code: 200, + data: blogs.map((blog) => ({ + title: blog.title, + content: blog.content, + author: blog.author, + published_at: blog.published_at, + })), + pagination: { + current_page: page, + per_page: limit, + total_pages: Math.ceil(totalItems / limit), + total_items: totalItems, + }, + }); + } catch (error) { + res.status(500).json({ + status: "error", + message: "Internal server error", + status_code: 500, + }); + } + } + + /** + * @swagger + * /api/v1/blog/user: + * get: + * summary: Get a paginated list of blogs by user + * description: Retrieve a paginated list of blog posts created by the authenticated user + * tags: [Blog] + * parameters: + * - in: query + * name: page + * schema: + * type: integer + * default: 1 + * description: Page number for pagination + * - in: query + * name: limit + * schema: + * type: integer + * default: 10 + * description: Number of blog posts per page + * responses: + * 200: + * description: A paginated list of blog posts by the user + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: success + * status_code: + * type: integer + * example: 200 + * data: + * type: array + * items: + * type: object + * properties: + * title: + * type: string + * example: My First Blog + * content: + * type: string + * example: This is the content of my first blog post. + * author: + * type: string + * example: John Doe + * published_date: + * type: string + * format: date-time + * example: 2023-07-21T19:58:00.000Z + * pagination: + * type: object + * properties: + * current_page: + * type: integer + * example: 1 + * per_page: + * type: integer + * example: 10 + * total_pages: + * type: integer + * example: 2 + * total_items: + * type: integer + * example: 15 + * 400: + * description: Invalid query parameters + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: bad request + * message: + * type: string + * example: Invalid query params passed + * status_code: + * type: integer + * example: 400 + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: error + * message: + * type: string + * example: Internal server error + * status_code: + * type: integer + * example: 500 + */ + async listBlogsByUser(req: Request, res: Response): Promise { + try { + const user = req.user; + const page = parseInt(req.query.page as string) || 1; + const limit = parseInt(req.query.limit as string) || 10; + + if (!user || page <= 0 || limit <= 0) { + res.status(400).json({ + status: "bad request", + message: "Invalid query params passed", + status_code: 400, + }); + return; + } + + const { blogs, totalItems } = + await this.blogService.getPaginatedBlogsByUser(user.id, page, limit); + + res.json({ + status: "success", + status_code: 200, + data: blogs.map((blog) => ({ + title: blog.title, + content: blog.content, + author: blog.author.name, + published_date: blog.published_at, + })), + pagination: { + current_page: page, + per_page: limit, + total_pages: Math.ceil(totalItems / limit), + total_items: totalItems, + }, + }); + } catch (error) { + res.status(500).json({ + status: "error", + message: "Internal server error", + status_code: 500, + }); + } + } + + /** + * @swagger + * /api/v1/blog/{id}: + * delete: + * summary: Delete a blog post + * description: Delete a specific blog post by its ID + * tags: [Blog] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: The ID of the blog post + * responses: + * 200: + * description: Blog post deleted successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * status_code: + * type: integer + * example: 200 + * message: + * type: string + * example: Blog post deleted successfully + * 404: + * description: Blog post not found + * content: + * application/json: + * schema: + * type: object + * properties: + * status_code: + * type: integer + * example: 404 + * error: + * type: string + * example: Blog post not found + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * type: object + * properties: + * status_code: + * type: integer + * example: 500 + * error: + * type: string + * example: Internal server error + * details: + * type: string + * example: Error message + */ + async deleteBlogPost(req: Request, res: Response): Promise { + try { + const { id } = req.params; + if (!id) { + res.status(401).json({ + status_code: 401, + error: "Unauthorized", + }); + } + + const deletedPost = await this.blogService.deleteBlogPost(id); + if (!deletedPost) { + res.status(404).json({ + status_code: 404, + error: "Blog post not found", + }); + } + + res.status(200).json({ + status_code: 200, + message: "Blog post deleted successfully", + }); + } catch (error) { + res.status(500).json({ + status_code: 500, + error: "Internal server error", + details: error.message, + }); + } + } + + async createBlogController(req: Request, res: Response, next: NextFunction) { + const { title, content, image_url, tags, categories } = req.body; + + try { + const newBlog = await this.blogService.createBlogPost( + title, + content, + req.user.id, + image_url, + tags, + categories, + ); + res.status(201).json({ + status: "success", + status_code: 201, + message: "Blog post created successfully", + data: { + blog: newBlog, + author: req.user.id, + }, + }); + } catch (error) { + res.status(500).json({ + status: "unsuccessful", + status_code: 500, + message: error.message, + }); + } + } + /** + * @swagger + * /blog/edit/{id}: + * patch: + * summary: Edit a blog post + * description: Update the details of a blog post including title, content, image URL, tags, categories, and publish date. Only the author can update their blog post. + * tags: [Blog] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: The ID of the blog post to edit + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * title: + * type: string + * description: The title of the blog post + * content: + * type: string + * description: The content of the blog post + * image_url: + * type: string + * description: The URL of the blog post's image + * tags: + * type: string + * description: A comma-separated list of tags for the blog post + * categories: + * type: string + * description: A comma-separated list of categories for the blog post + * publish_date: + * type: string + * format: date-time + * description: The publish date of the blog post + * example: + * title: "Updated Blog Title" + * content: "This is the updated content of the blog post." + * image_url: "http://example.com/image.jpg" + * tags: "technology, AI" + * categories: "Tech News, Artificial Intelligence" + * publish_date: "2023-09-12T10:00:00Z" + * responses: + * 200: + * description: Blog post updated successfully. + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: success + * status_code: + * type: integer + * example: 200 + * message: + * type: string + * example: Blog post updated successfully. + * post: + * type: object + * properties: + * blog_id: + * type: string + * example: "12345" + * title: + * type: string + * example: "Updated Blog Title" + * content: + * type: string + * example: "This is the updated content of the blog post." + * tags: + * type: array + * items: + * type: string + * categories: + * type: array + * items: + * type: string + * image_urls: + * type: string + * example: "http://example.com/image.jpg" + * author: + * type: string + * example: "Author Name" + * updated_at: + * type: string + * format: date-time + * example: "2023-09-12T10:00:00Z" + * 400: + * description: Bad Request - Invalid input data. + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: unsuccessful + * status_code: + * type: integer + * example: 400 + * message: + * type: string + * example: Invalid request data. + * 403: + * description: Unauthorized - User is not allowed to update this blog post. + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: unsuccessful + * status_code: + * type: integer + * example: 403 + * message: + * type: string + * example: Unauthorized access. + * 404: + * description: Blog post not found. + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: unsuccessful + * status_code: + * type: integer + * example: 404 + * message: + * type: string + * example: Blog post not found. + * 500: + * description: An unexpected error occurred while processing the request. + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: unsuccessful + * status_code: + * type: integer + * example: 500 + * message: + * type: string + * example: An unexpected error occurred. + */ + + async updateBlog(req: Request, res: Response, next: NextFunction) { + try { + const userId = req.user.id; + const blogId = req.params.id; + + const updatedBlog = await this.blogService.updateBlog( + blogId, + req.body, + userId, + ); + res.status(200).json({ + status: "success", + status_code: 200, + message: "Blog post updated successfully.", + data: updatedBlog, + }); + } catch (error) { + if (error instanceof ResourceNotFound || error instanceof Forbidden) { + next(error); + } + next(new ServerError("Internal server error.")); + } + } +} diff --git a/src/controllers/FaqController.ts b/src/controllers/FaqController.ts index 22e43644..78873329 100644 --- a/src/controllers/FaqController.ts +++ b/src/controllers/FaqController.ts @@ -1,514 +1,516 @@ -import { NextFunction, Request, Response } from "express"; -import { FAQService } from "../services"; -import { UserRole } from "../enums/userRoles"; -import isSuperAdmin from "../utils/isSuperAdmin"; -import { Category } from "../models"; -import { HttpError } from "../middleware"; - -const faqService = new FAQService(); - -class FAQController { - /** - * @swagger - * tags: - * name: FAQ - * description: FAQ management - * - * /faqs: - * post: - * summary: Create a new FAQ - * tags: [FAQ] - * requestBody: - * description: Data required to create a new FAQ - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * question: - * type: string - * example: "What is organization?" - * answer: - * type: string - * example: "It's a group of people." - * category: - * type: string - * example: "general" - * responses: - * '201': - * description: FAQ created successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * status_code: - * type: integer - * example: 201 - * success: - * type: boolean - * example: true - * message: - * type: string - * example: "FAQ Created successfully" - * data: - * type: object - * properties: - * id: - * type: string - * example: "d3c9c6a1-8f1e-4e89-bb7a-087d8b6f68e5" - * question: - * type: string - * example: "What is organization?" - * answer: - * type: string - * example: "It's a group of people." - * category: - * type: string - * example: "general" - * createdBy: - * type: string - * example: "SUPER_ADMIN" - * '400': - * description: Bad Request if input data is invalid - * content: - * application/json: - * schema: - * type: object - * properties: - * status_code: - * type: integer - * example: 400 - * success: - * type: boolean - * example: false - * message: - * type: string - * example: "Invalid request data" - * '401': - * description: Unauthorized if user is not authenticated - * content: - * application/json: - * schema: - * type: object - * properties: - * status_code: - * type: integer - * example: 401 - * success: - * type: boolean - * example: false - * message: - * type: string - * example: "User not authenticated" - * '403': - * description: Forbidden if user is not a super admin - * content: - * application/json: - * schema: - * type: object - * properties: - * status_code: - * type: integer - * example: 403 - * success: - * type: boolean - * example: false - * message: - * type: string - * example: "User is not authorized to create FAQ" - * '500': - * description: Internal Server Error if an unexpected error occurs - * content: - * application/json: - * schema: - * type: object - * properties: - * status_code: - * type: integer - * example: 500 - * success: - * type: boolean - * example: false - * message: - * type: string - * example: "An unexpected error occurred" - */ - public async createFAQ(req: Request, res: Response, next: NextFunction) { - try { - const { question, answer, category } = req.body; - const userId = req.user?.id; - - if (!userId) { - return res.status(401).json({ - status_code: 401, - success: false, - message: "User not authenticated", - }); - } - - if (!question || !answer || !category) { - return res.status(400).json({ - status_code: 400, - success: false, - message: "Invalid request data", - }); - } - - const isAdmin = await isSuperAdmin(userId); - if (!isAdmin) { - return res.status(403).json({ - status_code: 403, - success: false, - message: "User is not authorized to create FAQ", - }); - } - - const faq = await faqService.createFaq({ - question, - answer, - category, - createdBy: UserRole.SUPER_ADMIN, - }); - - res.status(201).json({ - status_code: 201, - success: true, - message: "The FAQ has been successfully created.", - data: faq, - }); - } catch (error) { - res.status(500).json({ - status_code: 500, - success: false, - message: error.message || "An unexpected error occurred", - }); - } - } - - /** - * @swagger - * /faqs/{id}: - * put: - * summary: Update an FAQ - * description: Update an existing FAQ entry using the FAQ ID provided in the URL parameters and the update data in the request body. The request requires admin authorization. - * tags: [FAQ] - * parameters: - * - in: path - * name: faq_id - * required: true - * schema: - * type: string - * description: The ID of the FAQ entry - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * question: - * type: string - * description: The updated question text - * answer: - * type: string - * description: The updated answer text - * category: - * type: string - * description: The updated category - * example: - * question: "Updated question?" - * answer: "Updated answer." - * category: "General" - * responses: - * 200: - * description: The FAQ has been successfully updated. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * example: true - * message: - * type: string - * example: The FAQ has been successfully updated. - * data: - * type: object - * properties: - * id: - * type: string - * example: "123" - * question: - * type: string - * example: "Updated question?" - * answer: - * type: string - * example: "Updated answer." - * category: - * type: string - * example: "General" - * createdAt: - * type: string - * format: date-time - * example: "2023-01-01T00:00:00.000Z" - * updatedAt: - * type: string - * format: date-time - * example: "2023-01-02T00:00:00.000Z" - * status_code: - * type: integer - * example: 200 - * 400: - * description: Invalid request data or an error occurred while processing the request. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * example: false - * message: - * type: string - * example: Invalid request data - * status_code: - * type: integer - * example: 400 - * 403: - * description: Unauthorized access. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * example: false - * message: - * type: string - * example: Unauthorized access - * status_code: - * type: integer - * example: 403 - * 404: - * description: FAQ entry not found. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * example: false - * message: - * type: string - * example: FAQ entry with ID {faq_id} not found. - * status_code: - * type: integer - * example: 404 - */ - - public async updateFaq(req: Request, res: Response, next: NextFunction) { - try { - const faqId = req.params.id; - const { question, answer, category } = req.body; - - if (!question || !answer || !category) { - return res.status(400).json({ - status_code: 400, - success: false, - message: "Invalid request data", - }); - } - const isAdmin = await isSuperAdmin(req.user.id); - if (!isAdmin) { - return res.status(403).json({ - status_code: 403, - success: false, - message: "Unauthorized access", - }); - } - const faq = await faqService.updateFaq(req.body, faqId); - res.status(200).json({ - success: true, - message: "The FAQ has been successfully updated.", - data: faq, - status_code: 200, - }); - } catch (error) { - next(error); - } - } - - /** - * @swagger - * /faqs: - * get: - * summary: Retrieve all FAQs - * description: Retrieve a list of all FAQs with their respective questions, answers, and categories. - * tags: [FAQ] - * responses: - * 200: - * description: Successfully retrieved all FAQs. - * content: - * application/json: - * schema: - * type: array - * items: - * type: object - * properties: - * id: - * type: string - * description: The unique identifier for the FAQ. - * example: 12345 - * question: - * type: string - * description: The question part of the FAQ. - * example: What is the return policy? - * answer: - * type: string - * description: The answer part of the FAQ. - * example: You can return any item within 30 days. - * category: - * type: string - * description: The category of the FAQ. - * example: Returns - * 500: - * description: Internal server error. - */ - public async getFaq(req: Request, res: Response, next: NextFunction) { - try { - const faqs = await faqService.getAllFaqs(); - res.status(200).json({ - status_code: 200, - success: true, - message: "The FAQ has been retrieved successfully.", - data: faqs, - }); - } catch (error) { - next(error); - } - } - - /** - * @swagger - * /faqs/{faqId}: - * delete: - * summary: Delete an FAQ - * description: Deletes an existing FAQ entry by its ID. This endpoint requires the user to have super admin permissions. - * tags: [FAQ] - * parameters: - * - in: path - * name: faqId - * required: true - * schema: - * type: string - * description: The ID of the FAQ entry to delete - * responses: - * '200': - * description: The FAQ has been successfully deleted. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * example: true - * message: - * type: string - * example: The FAQ has been successfully deleted. - * status_code: - * type: integer - * example: 200 - * '400': - * description: Invalid request data. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * example: false - * message: - * type: string - * example: Invalid request data - * status_code: - * type: integer - * example: 400 - * '403': - * description: Unauthorized access. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * example: false - * message: - * type: string - * example: Unauthorized access - * status_code: - * type: integer - * example: 403 - * '404': - * description: FAQ entry not found. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * example: false - * message: - * type: string - * example: FAQ entry with ID {faqId} not found. - * status_code: - * type: integer - * example: 404 - * '500': - * description: Internal server error. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * example: false - * message: - * type: string - * example: Deletion failed - * status_code: - * type: integer - * example: 500 - */ - public async deleteFaq(req: Request, res: Response, next: NextFunction) { - try { - const { faqId } = req.params; - if (!faqId) { - throw new HttpError(422, "Validation failed: Valid ID required"); - } - const deletionSuccess = await faqService.deleteFaq(faqId); - - if (!deletionSuccess) { - throw new HttpError(404, "FAQ not found or could not be deleted"); - } - res.status(200).json({ - success: true, - message: "The FAQ has been successfully deleted.", - status_code: 200, - }); - } catch (err) { - next(err); - } - } -} - -export { FAQController }; +import { NextFunction, Request, Response } from "express"; +import { FAQService } from "../services"; +import { UserRole } from "../enums/userRoles"; +import isSuperAdmin from "../utils/isSuperAdmin"; +import { ServerError, BadRequest, HttpError } from "../middleware"; + +const faqService = new FAQService(); + +class FAQController { + /** + * @swagger + * tags: + * name: FAQ + * description: FAQ management + * + * /faqs: + * post: + * summary: Create a new FAQ + * tags: [FAQ] + * requestBody: + * description: Data required to create a new FAQ + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * question: + * type: string + * example: "What is organization?" + * answer: + * type: string + * example: "It's a group of people." + * category: + * type: string + * example: "general" + * responses: + * '201': + * description: FAQ created successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * status_code: + * type: integer + * example: 201 + * success: + * type: boolean + * example: true + * message: + * type: string + * example: "FAQ Created successfully" + * data: + * type: object + * properties: + * id: + * type: string + * example: "d3c9c6a1-8f1e-4e89-bb7a-087d8b6f68e5" + * question: + * type: string + * example: "What is organization?" + * answer: + * type: string + * example: "It's a group of people." + * category: + * type: string + * example: "general" + * createdBy: + * type: string + * example: "SUPER_ADMIN" + * '400': + * description: Bad Request if input data is invalid + * content: + * application/json: + * schema: + * type: object + * properties: + * status_code: + * type: integer + * example: 400 + * success: + * type: boolean + * example: false + * message: + * type: string + * example: "Invalid request data" + * '401': + * description: Unauthorized if user is not authenticated + * content: + * application/json: + * schema: + * type: object + * properties: + * status_code: + * type: integer + * example: 401 + * success: + * type: boolean + * example: false + * message: + * type: string + * example: "User not authenticated" + * '403': + * description: Forbidden if user is not a super admin + * content: + * application/json: + * schema: + * type: object + * properties: + * status_code: + * type: integer + * example: 403 + * success: + * type: boolean + * example: false + * message: + * type: string + * example: "User is not authorized to create FAQ" + * '500': + * description: Internal Server Error if an unexpected error occurs + * content: + * application/json: + * schema: + * type: object + * properties: + * status_code: + * type: integer + * example: 500 + * success: + * type: boolean + * example: false + * message: + * type: string + * example: "An unexpected error occurred" + */ + public async createFAQ(req: Request, res: Response, next: NextFunction) { + try { + const { question, answer, category } = req.body; + const userId = req.user?.id; + + if (!userId) { + return res.status(401).json({ + status_code: 401, + success: false, + message: "User not authenticated", + }); + } + + if (!question || !answer || !category) { + return res.status(400).json({ + status_code: 400, + success: false, + message: "Invalid request data", + }); + } + + const isAdmin = await isSuperAdmin(userId); + if (!isAdmin) { + return res.status(403).json({ + status_code: 403, + success: false, + message: "User is not authorized to create FAQ", + }); + } + + const faq = await faqService.createFaq({ + question, + answer, + category, + createdBy: UserRole.SUPER_ADMIN, + }); + + res.status(201).json({ + status_code: 201, + success: true, + message: "The FAQ has been successfully created.", + data: faq, + }); + } catch (error) { + res.status(500).json({ + status_code: 500, + success: false, + message: error.message || "An unexpected error occurred", + }); + } + } + + /** + * @swagger + * /faqs/{id}: + * put: + * summary: Update an FAQ + * description: Update an existing FAQ entry using the FAQ ID provided in the URL parameters and the update data in the request body. The request requires admin authorization. + * tags: [FAQ] + * parameters: + * - in: path + * name: faq_id + * required: true + * schema: + * type: string + * description: The ID of the FAQ entry + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * question: + * type: string + * description: The updated question text + * answer: + * type: string + * description: The updated answer text + * category: + * type: string + * description: The updated category + * example: + * question: "Updated question?" + * answer: "Updated answer." + * category: "General" + * responses: + * 200: + * description: The FAQ has been successfully updated. + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: The FAQ has been successfully updated. + * data: + * type: object + * properties: + * id: + * type: string + * example: "123" + * question: + * type: string + * example: "Updated question?" + * answer: + * type: string + * example: "Updated answer." + * category: + * type: string + * example: "General" + * createdAt: + * type: string + * format: date-time + * example: "2023-01-01T00:00:00.000Z" + * updatedAt: + * type: string + * format: date-time + * example: "2023-01-02T00:00:00.000Z" + * status_code: + * type: integer + * example: 200 + * 400: + * description: Invalid request data or an error occurred while processing the request. + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Invalid request data + * status_code: + * type: integer + * example: 400 + * 403: + * description: Unauthorized access. + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Unauthorized access + * status_code: + * type: integer + * example: 403 + * 404: + * description: FAQ entry not found. + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: FAQ entry with ID {faq_id} not found. + * status_code: + * type: integer + * example: 404 + */ + + public async updateFaq(req: Request, res: Response, next: NextFunction) { + try { + const faqId = req.params.id; + const { question, answer, category } = req.body; + + if (!question || !answer || !category) { + return res.status(400).json({ + status_code: 400, + success: false, + message: "Invalid request data", + }); + } + const isAdmin = await isSuperAdmin(req.user.id); + if (!isAdmin) { + return res.status(403).json({ + status_code: 403, + success: false, + message: "Unauthorized access", + }); + } + const faq = await faqService.updateFaq(req.body, faqId); + res.status(200).json({ + success: true, + message: "The FAQ has been successfully updated.", + data: faq, + status_code: 200, + }); + } catch (error) { + if (error instanceof BadRequest) { + next(error); + } + next(new ServerError("Internal server error.")); + } + } + + /** + * @swagger + * /faqs: + * get: + * summary: Retrieve all FAQs + * description: Retrieve a list of all FAQs with their respective questions, answers, and categories. + * tags: [FAQ] + * responses: + * 200: + * description: Successfully retrieved all FAQs. + * content: + * application/json: + * schema: + * type: array + * items: + * type: object + * properties: + * id: + * type: string + * description: The unique identifier for the FAQ. + * example: 12345 + * question: + * type: string + * description: The question part of the FAQ. + * example: What is the return policy? + * answer: + * type: string + * description: The answer part of the FAQ. + * example: You can return any item within 30 days. + * category: + * type: string + * description: The category of the FAQ. + * example: Returns + * 500: + * description: Internal server error. + */ + public async getFaq(req: Request, res: Response, next: NextFunction) { + try { + const faqs = await faqService.getAllFaqs(); + res.status(200).json({ + status_code: 200, + success: true, + message: "The FAQ has been retrieved successfully.", + data: faqs, + }); + } catch (error) { + next(error); + } + } + + /** + * @swagger + * /faqs/{faqId}: + * delete: + * summary: Delete an FAQ + * description: Deletes an existing FAQ entry by its ID. This endpoint requires the user to have super admin permissions. + * tags: [FAQ] + * parameters: + * - in: path + * name: faqId + * required: true + * schema: + * type: string + * description: The ID of the FAQ entry to delete + * responses: + * '200': + * description: The FAQ has been successfully deleted. + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: The FAQ has been successfully deleted. + * status_code: + * type: integer + * example: 200 + * '400': + * description: Invalid request data. + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Invalid request data + * status_code: + * type: integer + * example: 400 + * '403': + * description: Unauthorized access. + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Unauthorized access + * status_code: + * type: integer + * example: 403 + * '404': + * description: FAQ entry not found. + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: FAQ entry with ID {faqId} not found. + * status_code: + * type: integer + * example: 404 + * '500': + * description: Internal server error. + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Deletion failed + * status_code: + * type: integer + * example: 500 + */ + public async deleteFaq(req: Request, res: Response, next: NextFunction) { + try { + const { faqId } = req.params; + if (!faqId) { + throw new HttpError(422, "Validation failed: Valid ID required"); + } + const deletionSuccess = await faqService.deleteFaq(faqId); + + if (!deletionSuccess) { + throw new HttpError(404, "FAQ not found or could not be deleted"); + } + res.status(200).json({ + success: true, + message: "The FAQ has been successfully deleted.", + status_code: 200, + }); + } catch (err) { + next(err); + } + } +} + +export { FAQController }; diff --git a/src/controllers/ProductController.ts b/src/controllers/ProductController.ts index 652a56b3..37a3dfd1 100644 --- a/src/controllers/ProductController.ts +++ b/src/controllers/ProductController.ts @@ -1,639 +1,639 @@ -import { NextFunction, Request, Response } from "express"; -import { ProductService } from "../services/product.services"; -import { BadRequest } from "../middleware"; - -class ProductController { - private productService: ProductService; - - constructor() { - this.productService = new ProductService(); - } - - /** - * @openapi - * tags: - * - name: Product API - * description: Product API related routes - */ - - /** - * @openapi - * /api/v1/organisation/{:id}/product: - * post: - * tags: - * - Product API - * summary: Create a new product - * description: Create a new product for organisations. - * parameters: - * - name: org_id - * in: path - * required: true - * description: ID of the organisation - * schema: - * type: string - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * name: - * type: string - * description: - * type: string - * category: - * type: string - * price: - * type: number - * quantity: - * type: number - * image: - * type: string - * is_deleted: - * type: boolean - * required: - * - name - * - price - * - quantity - * responses: - * 201: - * description: Product created successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: "success" - * message: - * type: string - * example: "Product created successfully" - * data: - * type: object - * properties: - * id: - * type: string - * name: - * type: string - * description: - * type: string - * price: - * type: number - * status: - * type: string - * is_deleted: - * type: boolean - * quantity: - * type: number - * created_at: - * type: string - * format: date-time - * updated_at: - * type: string - * format: date-time - * 400: - * description: Invalid input or missing organisation ID - * 409: - * description: Server error - */ - public createProduct = async (req: Request, res: Response) => { - const id = req.params.org_id; - const product = req.body; - const newProduct = await this.productService.createProduct(id, product); - res - .status(201) - .json({ message: "Product created successfully", data: newProduct }); - }; - - /** - * @openapi - * /api/v1/organisation/{:id}/product/search: - * get: - * tags: - * - Product API - * summary: Get All products. - * description: Get All products within an organisation. - * parameters: - * - name: org_id - * in: path - * required: true - * description: ID of the organisation - * schema: - * type: string - * - name: name - * in: query - * required: false - * description: Name of the product - * schema: - * type: string - * - name: category - * in: query - * required: false - * description: Category of the product - * schema: - * type: string - * - name: minPrice - * in: query - * required: false - * description: Minimum price of the product - * schema: - * type: number - * - name: maxPrice - * in: query - * required: false - * description: Maximum price of the product - * schema: - * type: number - * - name: page - * in: query - * required: false - * description: Page number for pagination - * schema: - * type: number - * default: 1 - * - name: limit - * in: query - * required: false - * description: Number of results per page - * schema: - * type: number - * default: 10 - * responses: - * 200: - * description: Product search successful - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * example: "Product search successful" - * data: - * type: object - * properties: - * pagination: - * type: object - * properties: - * total: - * type: number - * page: - * type: number - * limit: - * type: number - * totalPages: - * type: number - * products: - * type: array - * items: - * type: object - * properties: - * id: - * type: string - * name: - * type: string - * description: - * type: string - * price: - * type: number - * category: - * type: string - * status: - * type: string - * quantity: - * type: number - * created_at: - * type: string - * format: date-time - * updated_at: - * type: string - * format: date-time - * 400: - * description: Invalid input or missing organisation ID - * 404: - * description: No products found - */ - - public getProduct = async ( - req: Request, - res: Response, - next: NextFunction, - ) => { - try { - const orgId = req.params.org_id; - const { - name, - category, - minPrice, - maxPrice, - page = 1, - limit = 10, - } = req.query as any; - const searchCriteria = { - name, - category, - minPrice: Number(minPrice), - maxPrice: Number(maxPrice), - }; - - const products = await this.productService.getProducts( - orgId, - searchCriteria, - Number(page), - Number(limit), - ); - return res - .status(200) - .json({ message: "Product search successful", data: products }); - } catch (error) { - next(error); - } - }; - - /** - * @openapi - * /api/v1/products/{product_id}: - * delete: - * summary: Delete a product by its ID - * tags: [Product] - * parameters: - * - in: path - * name: product_id - * required: true - * schema: - * type: string - * description: The ID of the product to delete - * responses: - * 200: - * description: Product deleted successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * example: Product deleted successfully - * status_code: - * type: integer - * example: 200 - * 400: - * description: Bad request due to invalid product ID - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: Bad Request - * message: - * type: string - * example: Invalid Product Id - * status_code: - * type: integer - * example: 400 - * 404: - * description: Product not found - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: Not Found - * message: - * type: string - * example: Product not found - * status_code: - * type: integer - * example: 404 - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: An unexpected error occurred - * message: - * type: string - * example: Internal server error - * status_code: - * type: integer - * example: 500 - */ - - public deleteProduct = async (req: Request, res: Response) => { - const { org_id, product_id } = req.params; - await this.productService.deleteProduct(org_id, product_id); - res.status(200).json({ message: "Product deleted successfully" }); - }; - - /** - * @swagger - * /api/v1/{org_id}/products/{product_id}: - * put: - * summary: Update a product - * description: Update details of a product within an organization. - * tags: - * - Products - * parameters: - * - in: path - * name: org_id - * required: true - * schema: - * type: string - * description: The ID of the organization - * - in: path - * name: product_id - * required: true - * schema: - * type: string - * description: The ID of the product - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * description: - * type: string - * example: "product 1 updated price" - * price: - * type: number - * example: 30 - * name: - * type: string - * category: - * type: string - * quantity: - * type: integer - * image: - * type: string - * size: - * type: string - * stock_status: - * type: string - * responses: - * 200: - * description: Product updated successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: "success" - * status_code: - * type: integer - * example: 200 - * message: - * type: string - * example: "product update successful" - * data: - * type: object - * properties: - * id: - * type: string - * example: "b14e4406-80f6-4029-9681-715798741c8f" - * name: - * type: string - * example: "Product 1" - * description: - * type: string - * example: "product 1 updated price" - * price: - * type: number - * example: 30 - * quantity: - * type: integer - * example: 1 - * category: - * type: string - * example: "test product" - * image: - * type: string - * example: "image url" - * updated_at: - * type: string - * format: date-time - * example: "2024-08-08T09:06:36.210Z" - * created_at: - * type: string - * format: date-time - * example: "2024-08-08T07:12:33.936Z" - * size: - * type: string - * example: "Standard" - * stock_status: - * type: string - * example: "low on stock" - * 400: - * description: Bad request - * content: - * application/json: - * schema: - * type: object - * properties: - * status_code: - * type: integer - * example: 400 - * message: - * type: string - * example: "user not a member of organization" - * 403: - * description: Forbidden - * content: - * application/json: - * schema: - * type: object - * properties: - * status_code: - * type: integer - * example: 403 - * message: - * type: string - * example: "Forbidden: User not an admin or super admin" - * 404: - * description: Resource not found - * content: - * application/json: - * schema: - * type: object - * properties: - * status_code: - * type: integer - * example: 404 - * message: - * type: string - * example: "Product with id b14e4406-80f6-4029-9681-715798741c8f not found" - * 422: - * description: Unprocessable Entity - * content: - * application/json: - * schema: - * type: object - * properties: - * status_code: - * type: integer - * example: 422 - * message: - * type: string - * example: "Product ID not provided" - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * type: object - * properties: - * status_code: - * type: integer - * example: 500 - * message: - * type: string - * example: "Internal server error" - */ - public updateProduct = async (req: Request, res: Response) => { - const { org_id, product_id } = req.params; - const updatedProduct = await this.productService.updateProduct( - org_id, - product_id, - req.body, - ); - res.status(200).json({ - status_code: 200, - message: "product update successful", - data: updatedProduct, - }); - } - -/** - * @openapi - * /api/v1/organizations/{org_id}/products/{product_id}: - * get: - * summary: get a product by its ID - * tags: [Product] - * parameters: - * - in: path - * name: product_id - * required: true - * schema: - * type: string - * description: The ID of the product to get - * responses: - * 200: - * description: Product retrieved successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * example: Product retrieved successfully - * data: - * type: object - * properties: - * id: - * type: string - * name: - * type: string - * description: - * type: string - * price: - * type: number - * quantity: - * type: number - * category: - * type: string - * image: - * type: string - * updated_at: - * type: string - * format: date-time - * created_at: - * type: string - * format: date-time - * size: - * type: string - * stock_status: - * type: string - * status_code: - * type: integer - * example: 200 - * 400: - * description: Bad request due to invalid product ID - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: Bad Request - * message: - * type: string - * example: Invalid Product Id - * status_code: - * type: integer - * example: 400 - * 404: - * description: Product not found - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: Not Found - * message: - * type: string - * example: Product not found - * status_code: - * type: integer - * example: 404 - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: An unexpected error occurred - * message: - * type: string - * example: Internal server error - * status_code: - * type: integer - * example: 500 - */ -public getSingleProduct = async (req: Request, res: Response) => { - const { org_id, product_id } = req.params; - if (product_id && org_id) { - const product = await this.productService.getProduct(org_id, product_id); - if (product) { - res.status(200).json({ - status_code: 200, - message: "Product retrieved successfully", - data: product, - }); - } - } else { - return new BadRequest("Invalid Product ID"); - } -}; -} - -export { ProductController }; +import { NextFunction, Request, Response } from "express"; +import { ProductService } from "../services/product.services"; +import { BadRequest } from "../middleware"; + +class ProductController { + private productService: ProductService; + + constructor() { + this.productService = new ProductService(); + } + + /** + * @openapi + * tags: + * - name: Product API + * description: Product API related routes + */ + + /** + * @openapi + * /api/v1/organisation/{:id}/product: + * post: + * tags: + * - Product API + * summary: Create a new product + * description: Create a new product for organisations. + * parameters: + * - name: org_id + * in: path + * required: true + * description: ID of the organisation + * schema: + * type: string + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * name: + * type: string + * description: + * type: string + * category: + * type: string + * price: + * type: number + * quantity: + * type: number + * image: + * type: string + * is_deleted: + * type: boolean + * required: + * - name + * - price + * - quantity + * responses: + * 201: + * description: Product created successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: "success" + * message: + * type: string + * example: "Product created successfully" + * data: + * type: object + * properties: + * id: + * type: string + * name: + * type: string + * description: + * type: string + * price: + * type: number + * status: + * type: string + * is_deleted: + * type: boolean + * quantity: + * type: number + * created_at: + * type: string + * format: date-time + * updated_at: + * type: string + * format: date-time + * 400: + * description: Invalid input or missing organisation ID + * 409: + * description: Server error + */ + public createProduct = async (req: Request, res: Response) => { + const id = req.params.org_id; + const product = req.body; + const newProduct = await this.productService.createProduct(id, product); + res + .status(201) + .json({ message: "Product created successfully", data: newProduct }); + }; + + /** + * @openapi + * /api/v1/organisation/{:id}/product/search: + * get: + * tags: + * - Product API + * summary: Get All products. + * description: Get All products within an organisation. + * parameters: + * - name: org_id + * in: path + * required: true + * description: ID of the organisation + * schema: + * type: string + * - name: name + * in: query + * required: false + * description: Name of the product + * schema: + * type: string + * - name: category + * in: query + * required: false + * description: Category of the product + * schema: + * type: string + * - name: minPrice + * in: query + * required: false + * description: Minimum price of the product + * schema: + * type: number + * - name: maxPrice + * in: query + * required: false + * description: Maximum price of the product + * schema: + * type: number + * - name: page + * in: query + * required: false + * description: Page number for pagination + * schema: + * type: number + * default: 1 + * - name: limit + * in: query + * required: false + * description: Number of results per page + * schema: + * type: number + * default: 10 + * responses: + * 200: + * description: Product search successful + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "Product search successful" + * data: + * type: object + * properties: + * pagination: + * type: object + * properties: + * total: + * type: number + * page: + * type: number + * limit: + * type: number + * totalPages: + * type: number + * products: + * type: array + * items: + * type: object + * properties: + * id: + * type: string + * name: + * type: string + * description: + * type: string + * price: + * type: number + * category: + * type: string + * status: + * type: string + * quantity: + * type: number + * created_at: + * type: string + * format: date-time + * updated_at: + * type: string + * format: date-time + * 400: + * description: Invalid input or missing organisation ID + * 404: + * description: No products found + */ + + public getProduct = async ( + req: Request, + res: Response, + next: NextFunction, + ) => { + try { + const orgId = req.params.org_id; + const { + name, + category, + minPrice, + maxPrice, + page = 1, + limit = 10, + } = req.query as any; + const searchCriteria = { + name, + category, + minPrice: Number(minPrice), + maxPrice: Number(maxPrice), + }; + + const products = await this.productService.getProducts( + orgId, + searchCriteria, + Number(page), + Number(limit), + ); + return res + .status(200) + .json({ message: "Product search successful", data: products }); + } catch (error) { + next(error); + } + }; + + /** + * @openapi + * /api/v1/products/{product_id}: + * delete: + * summary: Delete a product by its ID + * tags: [Product] + * parameters: + * - in: path + * name: product_id + * required: true + * schema: + * type: string + * description: The ID of the product to delete + * responses: + * 200: + * description: Product deleted successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Product deleted successfully + * status_code: + * type: integer + * example: 200 + * 400: + * description: Bad request due to invalid product ID + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: Bad Request + * message: + * type: string + * example: Invalid Product Id + * status_code: + * type: integer + * example: 400 + * 404: + * description: Product not found + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: Not Found + * message: + * type: string + * example: Product not found + * status_code: + * type: integer + * example: 404 + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: An unexpected error occurred + * message: + * type: string + * example: Internal server error + * status_code: + * type: integer + * example: 500 + */ + + public deleteProduct = async (req: Request, res: Response) => { + const { org_id, product_id } = req.params; + await this.productService.deleteProduct(org_id, product_id); + res.status(200).json({ message: "Product deleted successfully" }); + }; + + /** + * @swagger + * /api/v1/{org_id}/products/{product_id}: + * put: + * summary: Update a product + * description: Update details of a product within an organization. + * tags: + * - Products + * parameters: + * - in: path + * name: org_id + * required: true + * schema: + * type: string + * description: The ID of the organization + * - in: path + * name: product_id + * required: true + * schema: + * type: string + * description: The ID of the product + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * description: + * type: string + * example: "product 1 updated price" + * price: + * type: number + * example: 30 + * name: + * type: string + * category: + * type: string + * quantity: + * type: integer + * image: + * type: string + * size: + * type: string + * stock_status: + * type: string + * responses: + * 200: + * description: Product updated successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: "success" + * status_code: + * type: integer + * example: 200 + * message: + * type: string + * example: "product update successful" + * data: + * type: object + * properties: + * id: + * type: string + * example: "b14e4406-80f6-4029-9681-715798741c8f" + * name: + * type: string + * example: "Product 1" + * description: + * type: string + * example: "product 1 updated price" + * price: + * type: number + * example: 30 + * quantity: + * type: integer + * example: 1 + * category: + * type: string + * example: "test product" + * image: + * type: string + * example: "image url" + * updated_at: + * type: string + * format: date-time + * example: "2024-08-08T09:06:36.210Z" + * created_at: + * type: string + * format: date-time + * example: "2024-08-08T07:12:33.936Z" + * size: + * type: string + * example: "Standard" + * stock_status: + * type: string + * example: "low on stock" + * 400: + * description: Bad request + * content: + * application/json: + * schema: + * type: object + * properties: + * status_code: + * type: integer + * example: 400 + * message: + * type: string + * example: "user not a member of organization" + * 403: + * description: Forbidden + * content: + * application/json: + * schema: + * type: object + * properties: + * status_code: + * type: integer + * example: 403 + * message: + * type: string + * example: "Forbidden: User not an admin or super admin" + * 404: + * description: Resource not found + * content: + * application/json: + * schema: + * type: object + * properties: + * status_code: + * type: integer + * example: 404 + * message: + * type: string + * example: "Product with id b14e4406-80f6-4029-9681-715798741c8f not found" + * 422: + * description: Unprocessable Entity + * content: + * application/json: + * schema: + * type: object + * properties: + * status_code: + * type: integer + * example: 422 + * message: + * type: string + * example: "Product ID not provided" + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * type: object + * properties: + * status_code: + * type: integer + * example: 500 + * message: + * type: string + * example: "Internal server error" + */ + public updateProduct = async (req: Request, res: Response) => { + const { org_id, product_id } = req.params; + const updatedProduct = await this.productService.updateProduct( + org_id, + product_id, + req.body, + ); + res.status(200).json({ + status_code: 200, + message: "product update successful", + data: updatedProduct, + }); + }; + + /** + * @openapi + * /api/v1/organizations/{org_id}/products/{product_id}: + * get: + * summary: get a product by its ID + * tags: [Product] + * parameters: + * - in: path + * name: product_id + * required: true + * schema: + * type: string + * description: The ID of the product to get + * responses: + * 200: + * description: Product retrieved successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Product retrieved successfully + * data: + * type: object + * properties: + * id: + * type: string + * name: + * type: string + * description: + * type: string + * price: + * type: number + * quantity: + * type: number + * category: + * type: string + * image: + * type: string + * updated_at: + * type: string + * format: date-time + * created_at: + * type: string + * format: date-time + * size: + * type: string + * stock_status: + * type: string + * status_code: + * type: integer + * example: 200 + * 400: + * description: Bad request due to invalid product ID + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: Bad Request + * message: + * type: string + * example: Invalid Product Id + * status_code: + * type: integer + * example: 400 + * 404: + * description: Product not found + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: Not Found + * message: + * type: string + * example: Product not found + * status_code: + * type: integer + * example: 404 + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: An unexpected error occurred + * message: + * type: string + * example: Internal server error + * status_code: + * type: integer + * example: 500 + */ + public getSingleProduct = async (req: Request, res: Response) => { + const { org_id, product_id } = req.params; + if (product_id && org_id) { + const product = await this.productService.getProduct(org_id, product_id); + if (product) { + res.status(200).json({ + status_code: 200, + message: "Product retrieved successfully", + data: product, + }); + } + } else { + return new BadRequest("Invalid Product ID"); + } + }; +} + +export { ProductController }; diff --git a/src/controllers/SqueezeController.ts b/src/controllers/SqueezeController.ts index 842a1ad0..306f4199 100644 --- a/src/controllers/SqueezeController.ts +++ b/src/controllers/SqueezeController.ts @@ -1,163 +1,238 @@ -import { Request, Response } from "express"; -import { SqueezeService } from "../services"; - -class SqueezeController { - private squeezeService: SqueezeService; - - constructor() { - this.squeezeService = new SqueezeService(); - } - - /** - * @openapi - * /api/v1/squeeze-pages: - * post: - * tags: - * - Squeeze - * summary: Create a new squeeze - * description: Create a new squeeze entry. - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * email: - * type: string - * first_name: - * type: string - * last_name: - * type: string - * phone: - * type: string - * location: - * type: string - * job_title: - * type: string - * company: - * type: string - * interests: - * type: array - * referral_source: - * type: string - * required: - * - email - * - first_name - * - last_name - * responses: - * 201: - * description: Squeeze created successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: "success" - * message: - * type: string - * example: "Squeeze created successfully" - * data: - * type: object - * 400: - * description: Bad request - * 409: - * description: Conflict - */ - public createSqueeze = async (req: Request, res: Response) => { - try { - const squeezeData = req.body; - const squeeze = await this.squeezeService.createSqueeze(squeezeData); // Use the instance method - res.status(201).json({ - status: "success", - message: "Squeeze record created successfully.", - data: squeeze, - }); - } catch (error) { - res.status(500).json({ - status: "error", - message: "An error occurred while creating the squeeze record.", - error: error.message, - }); - } - }; - /** - * @openapi - * /api/v1/squeeze/{id}: - * get: - * tags: - * - Squeeze - * summary: Get a squeeze record by ID - * description: Retrieve a single squeeze entry from the database by its ID. - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: string - * description: The ID of the squeeze record to retrieve - * responses: - * 200: - * description: Squeeze record found - * content: - * application/json: - * schema: - * type: object - * properties: - * id: - * type: string - * email: - * type: string - * first_name: - * type: string - * last_name: - * type: string - * phone: - * type: string - * location: - * type: string - * job_title: - * type: string - * company: - * type: string - * interests: - * type: array - * items: - * type: string - * referral_source: - * type: string - * 404: - * description: Squeeze record not found - * 500: - * description: Server error - */ - - public getSqueezeById = async (req: Request, res: Response) => { - try { - const { id } = req.params; - const squeeze = await this.squeezeService.getSqueezeById(id); - - if (!squeeze) { - return res.status(404).json({ - status: "error", - message: "Squeeze record not found.", - }); - } - - res.status(200).json({ - status: "success", - data: squeeze, - }); - } catch (error) { - res.status(500).json({ - status: "error", - message: "An error occurred while retrieving the squeeze record.", - error: error.message, - }); - } - }; -} - -export { SqueezeController }; +import { Request, Response } from "express"; +import { SqueezeService } from "../services"; + +class SqueezeController { + private squeezeService: SqueezeService; + + constructor() { + this.squeezeService = new SqueezeService(); + } + + /** + * @openapi + * /api/v1/squeezes: + * post: + * tags: + * - Squeeze + * summary: Create a new squeeze + * description: Create a new squeeze entry. + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * email: + * type: string + * first_name: + * type: string + * last_name: + * type: string + * phone: + * type: string + * location: + * type: string + * job_title: + * type: string + * company: + * type: string + * interests: + * type: array + * referral_source: + * type: string + * required: + * - email + * - first_name + * - last_name + * responses: + * 201: + * description: Squeeze created successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: "success" + * message: + * type: string + * example: "Squeeze created successfully" + * data: + * type: object + * 400: + * description: Bad request + * 409: + * description: Conflict + */ + public createSqueeze = async (req: Request, res: Response) => { + try { + const squeezeData = req.body; + const squeeze = await this.squeezeService.createSqueeze(squeezeData); // Use the instance method + res.status(201).json({ + status: "success", + message: "Squeeze record created successfully.", + data: squeeze, + }); + } catch (error) { + res.status(500).json({ + status: "error", + message: "An error occurred while creating the squeeze record.", + error: error.message, + }); + } + }; + /** + * @openapi + * /api/v1/squeeze/{id}: + * get: + * tags: + * - Squeeze + * summary: Get a squeeze record by ID + * description: Retrieve a single squeeze entry from the database by its ID. + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: The ID of the squeeze record to retrieve + * responses: + * 200: + * description: Squeeze record found + * content: + * application/json: + * schema: + * type: object + * properties: + * id: + * type: string + * email: + * type: string + * first_name: + * type: string + * last_name: + * type: string + * phone: + * type: string + * location: + * type: string + * job_title: + * type: string + * company: + * type: string + * interests: + * type: array + * items: + * type: string + * referral_source: + * type: string + * 404: + * description: Squeeze record not found + * 500: + * description: Server error + */ + + public getSqueezeById = async (req: Request, res: Response) => { + try { + const { id } = req.params; + const squeeze = await this.squeezeService.getSqueezeById(id); + + if (!squeeze) { + return res.status(404).json({ + status: "error", + message: "Squeeze record not found.", + }); + } + + res.status(200).json({ + status: "success", + data: squeeze, + }); + } catch (error) { + res.status(500).json({ + status: "error", + message: "An error occurred while retrieving the squeeze record.", + error: error.message, + }); + } + }; + + /** + * @openapi + * /api/v1/squeezes/{squeeze_id}: + * post: + * tags: + * - Squeeze + * summary: Update a squeeze page + * description: Update a squeeze entry. + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * email: + * type: string + * first_name: + * type: string + * last_name: + * type: string + * phone: + * type: string + * location: + * type: string + * job_title: + * type: string + * company: + * type: string + * interests: + * type: array + * referral_source: + * type: string + * responses: + * 200: + * description: Squeeze updated successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: "success" + * message: + * type: string + * example: "Squeeze updated successfully" + * data: + * type: object + * 400: + * description: Bad request + * 404: + * description: Squeeze service not found + * 500: + * description: Error occurred while updating the squeeze record. + */ + public updateSqueeze = async (req: Request, res: Response) => { + const { squeeze_id } = req.params; + const data = req.body; + const squeeze = await this.squeezeService.updateSqueeze(squeeze_id, data); + + if (squeeze) { + return res.status(200).json({ + status: "Success", + message: "Squeeze updated successfully", + data: squeeze, + }); + } else { + res.status(500).json({ + status: "error", + message: "Error occurred while updating the squeeze record.", + }); + } + }; +} + +export { SqueezeController }; diff --git a/src/controllers/UserController.ts b/src/controllers/UserController.ts index 71b33bd7..78004546 100644 --- a/src/controllers/UserController.ts +++ b/src/controllers/UserController.ts @@ -1,463 +1,3 @@ -<<<<<<< HEAD -// src/controllers/UserController.ts -import { isUUID } from "class-validator"; -import { NextFunction, Request, Response } from "express"; -import { validate } from "uuid"; -import { HttpError } from "../middleware"; -import { UserService } from "../services"; - -class UserController { - private userService: UserService; - - /** - * @swagger - * tags: - * name: User - * description: User related routes - */ - constructor() { - this.userService = new UserService(); - } - - /** - * @swagger - * /api/v1/users/me: - * get: - * tags: - * - User - * summary: Get User profile - * security: - * - bearerAuth: [] - * description: Api endpoint to retrieve the profile data of the currently authenticated user. This will allow users to access their own profile information. - * responses: - * 200: - * description: Fetched User profile Successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * status_code: - * type: integer - * example: 200 - * data: - * type: object - * properties: - * id: - * type: string - * example: 58b6 - * user_name: - * type: string - * example: yasuke - * email: - * type: string - * example: sam@gmail.com - * profile_picture: - * type: string - * example: https://avatar.com - * - * 401: - * description: Unauthorized access - * 404: - * description: Not found - * 500: - * description: Internal Server Error - * - */ - - static async getProfile(req: Request, res: Response, next: NextFunction) { - try { - const { id } = req.user; - - if (!id) { - return res.status(401).json({ - status_code: 401, - error: "Unauthorized! no ID provided", - }); - } - - if (!validate(id)) { - return res.status(400).json({ - status_code: 400, - error: "Unauthorized! Invalid User Id Format", - }); - } - - const user = await UserService.getUserById(id); - if (!user) { - return res.status(404).json({ - status_code: 404, - error: "User Not Found!", - }); - } - - if (user?.deletedAt || user?.is_deleted) { - return res.status(404).json({ - status_code: 404, - error: "User not found!", - }); - } - - res.status(200).json({ - status_code: 200, - message: "User profile details retrieved successfully", - data: { - id: user.id, - name: user.name, - email: user.email, - role: user.role, - profile_id: user.profile?.id, - first_name: user.profile?.first_name, - last_name: user.profile?.last_name, - phone_number: user.profile?.phone_number, - avatar_url: user.profile?.avatarUrl, - }, - }); - } catch (error) { - res.status(500).json({ - status_code: 500, - error: "Internal Server Error", - }); - } - } - - /** - * @swagger - * /api/v1/users: - * get: - * tags: - * - User - * summary: Get all users - * security: - * - bearerAuth: [] - * responses: - * 200: - * description: Get all users - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: success - * status_code: - * type: integer - * example: 200 - * message: - * type: string - * example: Users retrieved successfully - * pagination: - * type: object - * properties: - * totalItems: - * type: integer - * example: 100 - * totalPages: - * type: integer - * example: 10 - * currentPage: - * type: integer - * example: 1 - * data: - * type: array - * items: - * type: object - * properties: - * user_name: - * type: string - * example: Lewis - * email: - * type: string - * example: lewis@gmail.com - * 401: - * description: Unauthorized - * 500: - * description: Server Error - */ - - async getAllUsers(req: Request, res: Response) { - try { - const users = await this.userService.getAllUsers(); - res.json(users); - } catch (error) { - res.status(500).json({ message: error.message }); - } - } - - /** - * @swagger - * /api/v1/user/{id}: - * delete: - * tags: - * - User - * summary: Soft Delete a user - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: uuid - * description: The ID of the user - * responses: - * 202: - * description: User deleted successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: success - * status_code: - * type: integer - * example: 202 - * message: - * type: string - * example: User deleted successfully - * 400: - * description: Bad request - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: unsuccessful - * status_code: - * type: integer - * example: 400 - * message: - * type: string - * example: Valid id must be provided - * 404: - * description: Not found - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: unsuccessful - * status_code: - * type: integer - * example: 404 - * message: - * type: string - * example: User not found - * 500: - * description: Server Error - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: unsuccessful - * status_code: - * type: integer - * example: 500 - * message: - * type: string - * example: Failed to perform soft delete - */ - async deleteUser(req: Request, res: Response) { - const id = req.params.id; - - if (!id || !isUUID(id)) { - return res.status(400).json({ - status: "unsuccesful", - status_code: 400, - message: "Valid id must be provided", - }); - } - - try { - await this.userService.softDeleteUser(id); - - return res.status(202).json({ - status: "sucess", - message: "User deleted successfully", - status_code: 202, - }); - } catch (error) { - if (error instanceof HttpError) { - return res.status(error.status_code).json({ - message: error.message, - }); - } else { - return res.status(500).json({ - message: error.message || "Internal Server Error", - }); - } - } - } - - /** - * @swagger - * /api/v1/user/{id}: - * put: - * tags: - * - User - * summary: Update User Profile - * description: Update the profile of a user - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: string - * description: The ID of the user - * requestBody: - * required: true - * content: - * multipart/form-data: - * schema: - * type: object - * properties: - * avatarUrl: - * type: string - * format: binary - * description: The user's profile picture - * application/json: - * schema: - * type: object - * properties: - * first_name: - * type: string - * example: John - * last_name: - * type: string - * example: Doe - * phone_number: - * type: string - * example: 08012345678 - * responses: - * 200: - * description: User profile updated successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * id: - * type: string - * example: 315e0834-e96d-4ddc-9974-726e8c1e9cf9 - * name: - * type: string - * example: John Doe - * email: - * type: string - * example: johndoe@test.com - * google_id: - * type: string - * example: null - * isVerified: - * type: boolean - * example: true - * role: - * type: string - * example: user - * profile: - * type: object - * properties: - * id: - * type: string - * example: 315e0834-e96d-4ddc-9974-726e8c1e9cf9 - * first_name: - * type: string - * example: John - * last_name: - * type: string - * example: Doe - * phone_number: - * type: string - * example: 08012345678 - * avatarUrl: - * type: string - * example: https://avatar.com/avatar.png - * 400: - * description: Bad request - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: unsuccessful - * status_code: - * type: integer - * example: 400 - * message: - * type: string - * example: Valid id must be provided - * 404: - * description: Not found - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: unsuccessful - * status_code: - * type: integer - * example: 404 - * message: - * type: string - * example: User not found - * 500: - * description: Server Error - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: unsuccessful - * status_code: - * type: integer - * example: 500 - * message: - * type: string - * example: Internal Server Error - */ - - public updateUserProfile = async (req: Request, res: Response) => { - try { - const user = await this.userService.updateUserProfile( - req.params.user_id, - req.body, - req.file, - ); - res.status(200).json(user); - } catch (error) { - if (error instanceof HttpError) { - return res.status(error.status_code).json({ - message: error.message, - }); - } else { - return res.status(500).json({ - message: error.message || "Internal Server Error", - }); - } - } - }; -} - -export { UserController }; -======= // src/controllers/UserController.ts import { isUUID } from "class-validator"; import { NextFunction, Request, Response } from "express"; @@ -995,4 +535,3 @@ class UserController { } export { UserController }; ->>>>>>> dff20d3007b5e2fe48cfc08e4c31d9327b345571 diff --git a/src/controllers/blogCommentController.ts b/src/controllers/blogCommentController.ts index acaf2a8b..bbbabce7 100644 --- a/src/controllers/blogCommentController.ts +++ b/src/controllers/blogCommentController.ts @@ -1,389 +1,505 @@ -import { NextFunction, Request, Response } from "express"; -import { - editComment, - createComment, - getAllComments, -} from "../services/blogComment.services"; -import log from "../utils/logger"; -import { HttpError, ResourceNotFound } from "../middleware"; - -export class BlogCommentController { - /** - * @swagger - * /api/v1/blog/{blogId}/comment: - * post: - * summary: Create a comment on a blog post - * description: Add a new comment to a specific blog post - * tags: [BlogComment] - * parameters: - * - in: path - * name: blogId - * schema: - * type: string - * required: true - * description: ID of the blog post - * - in: body - * name: content - * schema: - * type: object - * properties: - * content: - * type: string - * example: This is a test comment. - * required: true - * description: Comment content - * responses: - * 201: - * description: Comment created successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: success - * status_code: - * type: integer - * example: 201 - * message: - * type: string - * example: Comment created successfully. - * data: - * type: object - * properties: - * content: - * type: string - * example: This is a test comment. - * createdAt: - * type: string - * format: date-time - * example: 2023-07-21T19:58:00.000Z - * 400: - * description: Invalid input - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: unsuccessful - * status_code: - * type: integer - * example: 400 - * message: - * type: string - * example: Comment content must be provided and cannot be empty. - * 404: - * description: Blog post not found - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: unsuccessful - * status_code: - * type: integer - * example: 404 - * message: - * type: string - * example: Blog not found - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: unsuccessful - * status_code: - * type: integer - * example: 500 - * message: - * type: string - * example: Failed to create comment. Please try again later. - */ - - async createComment(req: Request, res: Response) { - const blogId = req.params.blogId; - const { content } = req.body; - - try { - const comment = await createComment(blogId, content, req.user.id); - res.status(201).json({ - status: "success", - status_code: 201, - message: "Comment created successfully.", - data: comment, - }); - } catch (error: any) { - if (error.message === "Blog not found") { - res.status(404).json({ - status: "unsuccessful", - status_code: 404, - message: error.message, - }); - } else { - res.status(500).json({ - status: "unsuccessful", - status_code: 500, - message: "Failed to create comment. Please try again later.", - }); - } - } - } - - /** - * @swagger - * /api/v1/blog/{commentId}/edit-comment: - * patch: - * summary: Edit a comment - * description: Edit an existing comment by its ID - * tags: [BlogComment] - * parameters: - * - in: path - * name: commentId - * schema: - * type: integer - * required: true - * description: ID of the comment to be edited - * - in: body - * name: content - * schema: - * type: object - * properties: - * content: - * type: string - * example: Updated comment content. - * required: true - * description: New content for the comment - * responses: - * 201: - * description: Comment edited successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: successful - * status_code: - * type: integer - * example: 201 - * message: - * type: string - * example: Comment edited successfully. - * data: - * type: object - * properties: - * content: - * type: string - * example: Updated comment content. - * updatedAt: - * type: string - * format: date-time - * example: 2023-07-21T19:58:00.000Z - * 400: - * description: Invalid input - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: unsuccessful - * status_code: - * type: integer - * example: 400 - * message: - * type: string - * example: Comment content must be provided and cannot be empty. - * 403: - * description: Edit not allowed within 30 minutes - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: unsuccessful - * status_code: - * type: integer - * example: 403 - * message: - * type: string - * example: You cannot edit or create a comment within 30 minutes of its initial creation or last update. - * 404: - * description: Comment not found - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: unsuccessful - * status_code: - * type: integer - * example: 404 - * message: - * type: string - * example: The comment you are trying to edit does not exist - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: unsuccessful - * status_code: - * type: integer - * example: 500 - * message: - * type: string - * example: Failed to edit comment. Please try again later. - */ - - async editComment(req: Request, res: Response) { - try { - //get the commentId - const commentId: number = Number(req?.params?.commentId || 0); - - if (!commentId) - return res.status(400).json({ - status: "unsuccessful", - message: "Invalid comment ID", - status_code: 400, - }); - - //get the content - const { content } = req.body; - - if (content?.toString()?.trim() === "") - return res.status(400).json({ - status: "unsuccessful", - message: "Comment content must be provided and cannot be empty", - status_code: 400, - }); - - //call the function to edit the comment - const editedComment = await editComment(commentId, content as string); - - //return success - return res.status(201).json({ - status: "successful", - message: "Comment edited successfully", - data: editedComment, - status_code: 201, - }); - } catch (error: any) { - //check error message - if (error.message === "COMMENT_NOT_FOUND") { - return res.status(404).json({ - status: "unsuccessful", - message: "The comment you are trying to edit does not exist", - status_code: 404, - }); - } else if (error.message === "TIME_NOT_OK") { - return res.status(403).json({ - status: "unsuccessful", - message: - "You cannot edit or create a comment within 30 minutes of its initial creation or last update.", - status_code: 403, - }); - } else { - return res.status(500).json({ - status: "unsuccessful", - message: "Failed to edit comment. Please try again later.", - status_code: 500, - }); - } - } - } - - /** - * @swagger - * /api/v1/blog/{blogId}/comments: - * get: - * summary: Get all comments for a specific blog post - * description: Retrieves all comments associated with the specified blog post - * tags: [BlogComment] - * parameters: - * - in: path - * name: blogId - * required: true - * schema: - * type: string - * description: The ID of the blog post - * responses: - * 200: - * description: Comments retrieved successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: success - * status_code: - * type: integer - * example: 200 - * message: - * type: string - * example: Comments retrieved successfully. - * data: - * type: array - * items: - * type: object - * properties: - * id: - * type: integer - * example: 1 - * author: - * type: string - * example: Anonymous - * text: - * type: string - * example: Concrete Bedfordshire Streamlined withdrawal stable - * timestamp: - * type: string - * format: date-time - * example: 2024-08-08T08:59:13.633Z - * 404: - * description: Blog post not found - * 500: - * description: Internal server error - */ - - async getAllComments(req: Request, res: Response, next: NextFunction) { - const blogId = req.params.blogId; - - try { - const comments = await getAllComments(blogId); - res.status(200).json({ - status: "success", - status_code: 200, - message: "Comments retrieved successfully.", - data: comments, - }); - } catch (error) { - if (error instanceof ResourceNotFound) { - next(error); - } else { - next(new HttpError(500, "Internal server error")); - } - } - } -} +import { NextFunction, Request, Response } from "express"; +import { + editComment, + createComment, + getAllComments, + deleteComment, +} from "../services/blogComment.services"; +import log from "../utils/logger"; +import { HttpError, ResourceNotFound } from "../middleware"; + +export class BlogCommentController { + /** + * @swagger + * /api/v1/blog/{blogId}/comment: + * post: + * summary: Create a comment on a blog post + * description: Add a new comment to a specific blog post + * tags: [BlogComment] + * parameters: + * - in: path + * name: blogId + * schema: + * type: string + * required: true + * description: ID of the blog post + * - in: body + * name: content + * schema: + * type: object + * properties: + * content: + * type: string + * example: This is a test comment. + * required: true + * description: Comment content + * responses: + * 201: + * description: Comment created successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: success + * status_code: + * type: integer + * example: 201 + * message: + * type: string + * example: Comment created successfully. + * data: + * type: object + * properties: + * content: + * type: string + * example: This is a test comment. + * createdAt: + * type: string + * format: date-time + * example: 2023-07-21T19:58:00.000Z + * 400: + * description: Invalid input + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: unsuccessful + * status_code: + * type: integer + * example: 400 + * message: + * type: string + * example: Comment content must be provided and cannot be empty. + * 404: + * description: Blog post not found + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: unsuccessful + * status_code: + * type: integer + * example: 404 + * message: + * type: string + * example: Blog not found + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: unsuccessful + * status_code: + * type: integer + * example: 500 + * message: + * type: string + * example: Failed to create comment. Please try again later. + */ + + async createComment(req: Request, res: Response) { + const blogId = req.params.blogId; + const { content } = req.body; + + try { + const comment = await createComment(blogId, content, req.user.id); + res.status(201).json({ + status: "success", + status_code: 201, + message: "Comment created successfully.", + data: comment, + }); + } catch (error: any) { + if (error.message === "Blog not found") { + res.status(404).json({ + status: "unsuccessful", + status_code: 404, + message: error.message, + }); + } else { + res.status(500).json({ + status: "unsuccessful", + status_code: 500, + message: "Failed to create comment. Please try again later.", + }); + } + } + } + + /** + * @swagger + * /api/v1/blog/{commentId}/edit-comment: + * patch: + * summary: Edit a comment + * description: Edit an existing comment by its ID + * tags: [BlogComment] + * parameters: + * - in: path + * name: commentId + * schema: + * type: integer + * required: true + * description: ID of the comment to be edited + * - in: body + * name: content + * schema: + * type: object + * properties: + * content: + * type: string + * example: Updated comment content. + * required: true + * description: New content for the comment + * responses: + * 201: + * description: Comment edited successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: successful + * status_code: + * type: integer + * example: 201 + * message: + * type: string + * example: Comment edited successfully. + * data: + * type: object + * properties: + * content: + * type: string + * example: Updated comment content. + * updatedAt: + * type: string + * format: date-time + * example: 2023-07-21T19:58:00.000Z + * 400: + * description: Invalid input + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: unsuccessful + * status_code: + * type: integer + * example: 400 + * message: + * type: string + * example: Comment content must be provided and cannot be empty. + * 403: + * description: Edit not allowed within 30 minutes + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: unsuccessful + * status_code: + * type: integer + * example: 403 + * message: + * type: string + * example: You cannot edit or create a comment within 30 minutes of its initial creation or last update. + * 404: + * description: Comment not found + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: unsuccessful + * status_code: + * type: integer + * example: 404 + * message: + * type: string + * example: The comment you are trying to edit does not exist + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: unsuccessful + * status_code: + * type: integer + * example: 500 + * message: + * type: string + * example: Failed to edit comment. Please try again later. + */ + + async editComment(req: Request, res: Response) { + try { + //get the commentId + const commentId: number = Number(req?.params?.commentId || 0); + + if (!commentId) + return res.status(400).json({ + status: "unsuccessful", + message: "Invalid comment ID", + status_code: 400, + }); + + //get the content + const { content } = req.body; + + if (content?.toString()?.trim() === "") + return res.status(400).json({ + status: "unsuccessful", + message: "Comment content must be provided and cannot be empty", + status_code: 400, + }); + + //call the function to edit the comment + const editedComment = await editComment(commentId, content as string); + + //return success + return res.status(201).json({ + status: "successful", + message: "Comment edited successfully", + data: editedComment, + status_code: 201, + }); + } catch (error: any) { + //check error message + if (error.message === "COMMENT_NOT_FOUND") { + return res.status(404).json({ + status: "unsuccessful", + message: "The comment you are trying to edit does not exist", + status_code: 404, + }); + } else if (error.message === "TIME_NOT_OK") { + return res.status(403).json({ + status: "unsuccessful", + message: + "You cannot edit or create a comment within 30 minutes of its initial creation or last update.", + status_code: 403, + }); + } else { + return res.status(500).json({ + status: "unsuccessful", + message: "Failed to edit comment. Please try again later.", + status_code: 500, + }); + } + } + } + + /** + * @swagger + * /api/v1/blog/{blogId}/comments: + * get: + * summary: Get all comments for a specific blog post + * description: Retrieves all comments associated with the specified blog post + * tags: [BlogComment] + * parameters: + * - in: path + * name: blogId + * required: true + * schema: + * type: string + * description: The ID of the blog post + * responses: + * 200: + * description: Comments retrieved successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: success + * status_code: + * type: integer + * example: 200 + * message: + * type: string + * example: Comments retrieved successfully. + * data: + * type: array + * items: + * type: object + * properties: + * id: + * type: integer + * example: 1 + * author: + * type: string + * example: Anonymous + * text: + * type: string + * example: Concrete Bedfordshire Streamlined withdrawal stable + * timestamp: + * type: string + * format: date-time + * example: 2024-08-08T08:59:13.633Z + * 404: + * description: Blog post not found + * 500: + * description: Internal server error + */ + + async getAllComments(req: Request, res: Response, next: NextFunction) { + const blogId = req.params.blogId; + + try { + const comments = await getAllComments(blogId); + res.status(200).json({ + status: "success", + status_code: 200, + message: "Comments retrieved successfully.", + data: comments, + }); + } catch (error) { + if (error instanceof ResourceNotFound) { + next(error); + } else { + next(new HttpError(500, "Internal server error")); + } + } + } + + /** + * @swagger + * /blog/{commentId}: + * delete: + * summary: Delete a specific comment + * tags: [Comments] + * parameters: + * - in: path + * name: commentId + * required: true + * description: The ID of the comment to be deleted + * schema: + * type: string + * example: "comment-12345" + * responses: + * 200: + * description: Comment deleted successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: "success" + * status_code: + * type: integer + * example: 200 + * message: + * type: string + * example: "Comment deleted successfully" + * 400: + * description: Invalid comment ID + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: "unsuccessful" + * message: + * type: string + * example: "Invalid comment ID" + * status_code: + * type: integer + * example: 400 + * 404: + * description: Comment not found + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * example: "Comment not found" + * status_code: + * type: integer + * example: 404 + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * example: "Internal server error" + * status_code: + * type: integer + * example: 500 + */ + + async deleteComment(req: Request, res: Response, next: NextFunction) { + try { + const commentId = req.params?.commentId || null; + + if (!commentId) { + return res.status(400).json({ + status: "unsuccessful", + message: "Invalid comment ID", + status_code: 400, + }); + } + + const hasDeletedComment = await deleteComment(commentId, req.user.id); + + return res.status(200).json({ + status: "success", + status_code: 200, + message: "Comment deleted successfully", + }); + } catch (error) { + if (error instanceof ResourceNotFound) { + next(error); + } else if (error.message === "COMMENT_NOT_FOUND") { + return res.status(404).json({ + status: "unsuccessful", + message: "The comment you are trying to delete does not exist", + status_code: 404, + }); + } else if (error.message === "UNAUTHORIZED_ACTION") { + return res.status(404).json({ + status: "unsuccessful", + message: "Sorry, but you are not the author of this comment", + status_code: 404, + }); + } else { + next(new HttpError(500, "Internal server error")); + } + } + } +} diff --git a/src/controllers/index.ts b/src/controllers/index.ts index ee3e5322..b76b730f 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -1,23 +1,24 @@ -// silence is golden -export * from "./AuthController"; -export * from "./UserController"; -export * from "./ProductController"; -export * from "./HelpController"; -export * from "./roleController"; -export * from "./AdminController"; -export * from "./NotificationSettingsController"; -export * from "./BlogController"; -export * from "./exportController"; -export * from "./BlogController"; -export * from "./PaymentLemonSqueezyController"; -export * from "./PaymentController"; -export * from "./BlogController"; -export * from "./PaymentLemonSqueezyController"; -export * from "./contactController"; -export * from "./FaqController"; -export * from "./OrgController"; -export * from "./runTestController"; -export * from "./billingController"; -export * from "./SqueezeController"; -export * from "./NotificationController"; -export * from "./billingplanController"; +// silence is golden +export * from "./AuthController"; +export * from "./UserController"; +export * from "./ProductController"; +export * from "./HelpController"; +export * from "./roleController"; +export * from "./AdminController"; +export * from "./NotificationSettingsController"; +export * from "./BlogController"; +export * from "./exportController"; +export * from "./BlogController"; +export * from "./PaymentLemonSqueezyController"; +export * from "./PaymentController"; +export * from "./BlogController"; +export * from "./PaymentLemonSqueezyController"; +export * from "./contactController"; +export * from "./FaqController"; +export * from "./OrgController"; +export * from "./runTestController"; +export * from "./paymentPaystackController"; +export * from "./billingController"; +export * from "./SqueezeController"; +export * from "./NotificationController"; +export * from "./billingplanController"; diff --git a/src/controllers/jobController.ts b/src/controllers/jobController.ts index 60b4bd29..73e5e999 100644 --- a/src/controllers/jobController.ts +++ b/src/controllers/jobController.ts @@ -1,133 +1,140 @@ -import { Request, Response } from "express"; -import { JobService } from "../services/job.service"; -import { HttpError } from "../middleware"; -import AppDataSource from "../data-source"; - -export class JobController { - private jobService: JobService; - - constructor() { - this.jobService = new JobService(); - } - - async createJob(req: Request, res: Response) { - try { - const job = await this.jobService.create(req); - res.json({ - message: "Job listing created successfully", - status_code: 201, - data: job, - }); - } catch (error) { - res.status(500).json({ message: error.message, status_code: 400 }); - } - } - - /** - * @swagger - * /jobs/{jobId}: - * delete: - * summary: Delete a job by its ID - * description: Deletes a specific job listing by its unique ID. - * tags: - * - Jobs - * parameters: - * - in: path - * name: jobId - * required: true - * description: The ID of the job to delete. - * schema: - * type: string - * responses: - * 200: - * description: Job listing deleted successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * example: "Job listing deleted successfully" - * status_code: - * type: integer - * example: 200 - * data: - * type: object - * description: The deleted job object - * properties: - * id: - * type: string - * example: "abc123" - * title: - * type: string - * example: "Software Engineer" - * description: - * type: string - * example: "Develop and maintain software applications." - * // Add other job properties as needed - * 404: - * description: Job not found - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * example: "Job not found" - * status_code: - * type: integer - * example: 404 - * 422: - * description: Validation failed: Valid job ID required - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * example: "Validation failed: Valid job ID required" - * status_code: - * type: integer - * example: 422 - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * example: "Internal server error" - * status_code: - * type: integer - * example: 500 - */ - - async deleteJob(req: Request, res: Response) { - try { - const jobId = req.params.jobId; - if (!jobId) { - throw new HttpError(422, "Validation failed: Valid job ID required"); - } - - const deletedJob = await this.jobService.delete(jobId); - res.status(200).json({ - message: "Job listing deleted successfully", - status_code: 200, - data: deletedJob, - }); - } catch (error) { - if (error instanceof HttpError) { - res.status(error.status_code).json({ message: error.message }); - } else { - res - .status(500) - .json({ message: "Internal server error", status_code: 500 }); - } - } - } -} +import { Request, Response } from "express"; +import { JobService } from "../services/job.service"; +import { HttpError } from "../middleware"; +import AppDataSource from "../data-source"; + +export class JobController { + private jobService: JobService; + + constructor() { + this.jobService = new JobService(); + } + + async createJob(req: Request, res: Response) { + try { + const job = await this.jobService.create(req); + res.json({ + message: "Job listing created successfully", + status_code: 201, + data: job, + }); + } catch (error) { + res.status(500).json({ message: error.message, status_code: 400 }); + } + } + + async getAllJobs(req: Request, res: Response) { + try { + const billing = await this.jobService.getAllJobs(req); + res.status(200).json({ message: "Jobs retrieved successfully", billing }); + } catch (error) { + res.status(500).json({ message: error.message }); + } + } + /** + * @swagger + * /jobs/{jobId}: + * delete: + * summary: Delete a job by its ID + * description: Deletes a specific job listing by its unique ID. + * tags: + * - Jobs + * parameters: + * - in: path + * name: jobId + * required: true + * description: The ID of the job to delete. + * schema: + * type: string + * responses: + * 200: + * description: Job listing deleted successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "Job listing deleted successfully" + * status_code: + * type: integer + * example: 200 + * data: + * type: object + * description: The deleted job object + * properties: + * id: + * type: string + * example: "abc123" + * title: + * type: string + * example: "Software Engineer" + * description: + * type: string + * example: "Develop and maintain software applications." + * 404: + * description: Job not found + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "Job not found" + * status_code: + * type: integer + * example: 404 + * 422: + * description: Validation failed. Valid job ID required + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "Validation failed. Valid job ID required" + * status_code: + * type: integer + * example: 422 + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "Internal server error" + * status_code: + * type: integer + * example: 500 + */ + + async deleteJob(req: Request, res: Response) { + try { + const jobId = req.params.jobId; + if (!jobId) { + throw new HttpError(422, "Validation failed: Valid job ID required"); + } + + const deletedJob = await this.jobService.delete(jobId); + res.status(200).json({ + message: "Job listing deleted successfully", + status_code: 200, + data: deletedJob, + }); + } catch (error) { + if (error instanceof HttpError) { + res.status(error.status_code).json({ message: error.message }); + } else { + res + .status(500) + .json({ message: "Internal server error", status_code: 500 }); + } + } + } +} diff --git a/src/controllers/paymentPaystackController.ts b/src/controllers/paymentPaystackController.ts new file mode 100644 index 00000000..dcab05d4 --- /dev/null +++ b/src/controllers/paymentPaystackController.ts @@ -0,0 +1,92 @@ +import { Request, Response } from "express"; +import { initializePayment, verifyPayment } from "../services"; +import log from "../utils/logger"; + +/** + * @swagger + * api/v1/payments/paystack/initiate: + * post: + * summary: Initiate a payment using Paystack + * tags: [Payments] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * organization_id: + * type: string + * plan_id: + * type: string + * full_name: + * type: string + * billing_option: + * type: string + * enum: [monthly, yearly] + * redirect_url: + * type: string + * example: http://boilerplate.com/setting + * responses: + * 200: + * description: Payment initiated successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * redirect: + * type: string + * example: https://paystack.com/redirect-url + * 400: + * description: Billing plan or organization not found + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: integer + * example: 400 + * message: + * type: string + * example: Billing plan or organization not found + * 500: + * description: Error initiating payment + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * example: Error initiating payment + */ +export const initializePaymentPaystack = async ( + req: Request, + res: Response, +) => { + try { + const response = await initializePayment(req.body); + res.json(response); + } catch (error) { + console.log(error); + log.error("Error initiating payment:", error); + res.status(500).json({ error: "Error initiating payment" }); + } +}; + +/** + * Verifies a payment using Paystack + * @param req - Express request object + * @param res - Express response object + */ +export const verifyPaymentPaystack = async (req: Request, res: Response) => { + try { + const response = await verifyPayment(req.params.reference); + res.json(response); + } catch (error) { + log.error("Error verifying payment:", error); + res.status(500).json({ error: "Error verifying payment" }); + } +}; diff --git a/src/index.ts b/src/index.ts index 8646f748..7e013adc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,114 +1,116 @@ -import cors from "cors"; -import dotenv from "dotenv"; -import express, { Express, Request, Response } from "express"; -import "reflect-metadata"; -import swaggerUi from "swagger-ui-express"; -import config from "./config"; -import passport from "./config/google.passport.config"; -import AppDataSource from "./data-source"; -import { errorHandler, routeNotFound } from "./middleware"; -import { - adminRouter, - authRoute, - billingPlanRouter, - blogRouter, - contactRouter, - exportRouter, - faqRouter, - helpRouter, - jobRouter, - newsLetterSubscriptionRoute, - paymentFlutterwaveRouter, - paymentRouter, - paymentStripeRouter, - productRouter, - billingRouter, - runTestRouter, - sendEmailRoute, - testimonialRoute, - userRouter, - squeezeRoute, - notificationsettingsRouter, - notificationRouter, -} from "./routes"; -import { orgRouter } from "./routes/organisation"; -import { smsRouter } from "./routes/sms"; -import swaggerSpec from "./swaggerConfig"; -import { Limiter } from "./utils"; -import log from "./utils/logger"; -import ServerAdapter from "./views/bull-board"; -import { roleRouter } from "./routes/roles"; -dotenv.config(); - -const port = config.port; -const server: Express = express(); -server.options("*", cors()); -server.use( - cors({ - origin: "*", - methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"], - allowedHeaders: [ - "Origin", - "X-Requested-With", - "Content-Type", - "Authorization", - ], - }), -); - -server.use(Limiter); -server.use(express.json()); -server.use(express.urlencoded({ extended: true })); -server.use(passport.initialize()); - -server.get("/", (req: Request, res: Response) => { - res.send({ message: "I am the express API responding for team panther" }); -}); -server.get("/api/v1", (req: Request, res: Response) => { - res.json({ message: "I am the express API responding for team Panther" }); -}); - -server.get("/api/v1/probe", (req: Request, res: Response) => { - res.send("I am the express api responding for team panther"); -}); -server.use("/run-tests", runTestRouter); -server.use("/api/v1", faqRouter); -server.use("/api/v1", authRoute); -server.use("/api/v1", userRouter); -server.use("/api/v1/queues", ServerAdapter.getRouter()); -server.use("/api/v1", adminRouter); -server.use("/api/v1", sendEmailRoute); -server.use("/api/v1", helpRouter); -server.use("/api/v1", productRouter); -server.use("/api/v1", paymentFlutterwaveRouter); -server.use("/api/v1", paymentStripeRouter); -server.use("/api/v1", smsRouter); -server.use("/api/v1", notificationsettingsRouter); -server.use("/api/v1", notificationRouter); -server.use("/api/v1", paymentRouter); -server.use("/api/v1", billingRouter); -server.use("/api/v1", orgRouter); -server.use("/api/v1", exportRouter); -server.use("/api/v1", testimonialRoute); -server.use("/api/v1", blogRouter); -server.use("/api/v1", contactRouter); -server.use("/api/v1", jobRouter); -server.use("/api/v1", roleRouter); -server.use("/api/v1", billingPlanRouter); -server.use("/api/v1", newsLetterSubscriptionRoute); -server.use("/api/v1", squeezeRoute); - -server.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec)); - -server.use(routeNotFound); -server.use(errorHandler); - -AppDataSource.initialize() - .then(async () => { - server.listen(port, () => { - log.info(`Server is listening on port ${port}`); - }); - }) - .catch((error) => console.error(error)); - -export default server; +import cors from "cors"; +import dotenv from "dotenv"; +import express, { Express, Request, Response } from "express"; +import "reflect-metadata"; +import swaggerUi from "swagger-ui-express"; +import config from "./config"; +import passport from "./config/google.passport.config"; +import AppDataSource from "./data-source"; +import { errorHandler, routeNotFound } from "./middleware"; +import { + adminRouter, + authRoute, + billingPlanRouter, + blogRouter, + contactRouter, + exportRouter, + faqRouter, + helpRouter, + jobRouter, + newsLetterSubscriptionRoute, + paymentFlutterwaveRouter, + paymentRouter, + paymentStripeRouter, + productRouter, + billingRouter, + runTestRouter, + sendEmailRoute, + testimonialRoute, + userRouter, + paymentPaystackRouter, + squeezeRoute, + notificationsettingsRouter, + notificationRouter, +} from "./routes"; +import { orgRouter } from "./routes/organisation"; +import { smsRouter } from "./routes/sms"; +import swaggerSpec from "./swaggerConfig"; +import { Limiter } from "./utils"; +import log from "./utils/logger"; +import ServerAdapter from "./views/bull-board"; +import { roleRouter } from "./routes/roles"; +dotenv.config(); + +const port = config.port; +const server: Express = express(); +server.options("*", cors()); +server.use( + cors({ + origin: "*", + methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"], + allowedHeaders: [ + "Origin", + "X-Requested-With", + "Content-Type", + "Authorization", + ], + }), +); + +server.use(Limiter); +server.use(express.json()); +server.use(express.urlencoded({ extended: true })); +server.use(passport.initialize()); + +server.get("/", (req: Request, res: Response) => { + res.send({ message: "I am the express API responding for team panther" }); +}); +server.get("/api/v1", (req: Request, res: Response) => { + res.json({ message: "I am the express API responding for team Panther" }); +}); + +server.get("/api/v1/probe", (req: Request, res: Response) => { + res.send("I am the express api responding for team panther"); +}); +server.use("/run-tests", runTestRouter); +server.use("/api/v1", faqRouter); +server.use("/api/v1", authRoute); +server.use("/api/v1", userRouter); +server.use("/api/v1/queues", ServerAdapter.getRouter()); +server.use("/api/v1", adminRouter); +server.use("/api/v1", sendEmailRoute); +server.use("/api/v1", helpRouter); +server.use("/api/v1", productRouter); +server.use("/api/v1", paymentFlutterwaveRouter); +server.use("/api/v1", paymentStripeRouter); +server.use("/api/v1", smsRouter); +server.use("/api/v1", notificationsettingsRouter); +server.use("/api/v1", notificationRouter); +server.use("/api/v1", paymentRouter); +server.use("/api/v1", billingRouter); +server.use("/api/v1", orgRouter); +server.use("/api/v1", exportRouter); +server.use("/api/v1", testimonialRoute); +server.use("/api/v1", blogRouter); +server.use("/api/v1", contactRouter); +server.use("/api/v1", jobRouter); +server.use("/api/v1", roleRouter); +server.use("/api/v1", paymentPaystackRouter); +server.use("/api/v1", billingPlanRouter); +server.use("/api/v1", newsLetterSubscriptionRoute); +server.use("/api/v1", squeezeRoute); + +server.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec)); + +server.use(routeNotFound); +server.use(errorHandler); + +AppDataSource.initialize() + .then(async () => { + server.listen(port, () => { + log.info(`Server is listening on port ${port}`); + }); + }) + .catch((error) => console.error(error)); + +export default server; diff --git a/src/models/billing-plan.ts b/src/models/billing-plan.ts index 03ac0d24..7c078a90 100644 --- a/src/models/billing-plan.ts +++ b/src/models/billing-plan.ts @@ -1,44 +1,44 @@ -import { - Entity, - PrimaryGeneratedColumn, - Column, - ManyToOne, - OneToMany, -} from "typeorm"; -import { Organization } from "./organization"; -import { Payment } from "./payment"; - -@Entity() -export class BillingPlan { - @PrimaryGeneratedColumn("uuid") - id: string; - - @Column("uuid") - organizationId: string; - - @Column() - name: string; - - @Column("decimal", { precision: 10, scale: 2 }) - price: number; - - @Column() - currency: string; - - @Column() - duration: string; - - @Column({ nullable: true }) - description: string; - - @Column("simple-array") - features: string[]; - - @ManyToOne(() => Organization, (organization) => organization.billingPlans, { - onDelete: "CASCADE", - }) - organization: Organization; - - @OneToMany(() => Payment, (payment) => payment.billingPlan) - payments: Payment[]; -} +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + OneToMany, +} from "typeorm"; +import { Organization } from "./organization"; +import { Payment } from "./payment"; + +@Entity() +export class BillingPlan { + @PrimaryGeneratedColumn("uuid") + id: string; + + @Column("uuid", { nullable: true }) + organizationId: string; + + @Column() + name: string; + + @Column("decimal", { precision: 10, scale: 2 }) + price: number; + + @Column() + currency: string; + + @Column() + duration: string; + + @Column({ nullable: true }) + description: string; + + @Column("simple-array") + features: string[]; + + @ManyToOne(() => Organization, (organization) => organization.billingPlans, { + onDelete: "CASCADE", + }) + organization: Organization; + + @OneToMany(() => Payment, (payment) => payment.billingPlan) + payments: Payment[]; +} diff --git a/src/models/category.ts b/src/models/category.ts index 138e200a..b9600d74 100644 --- a/src/models/category.ts +++ b/src/models/category.ts @@ -1,14 +1,14 @@ -import { Entity, PrimaryGeneratedColumn, Column, ManyToMany } from "typeorm"; -import { Blog } from "./blog"; - -@Entity() -export class Category { - @PrimaryGeneratedColumn() - id: number; - - @Column() - name: string; - - @ManyToMany(() => Blog, (blog) => blog.categories) - blogs: Blog[]; -} +import { Entity, PrimaryGeneratedColumn, Column, ManyToMany } from "typeorm"; +import { Blog } from "./blog"; + +@Entity() +export class Category { + @PrimaryGeneratedColumn("uuid") + id: number; + + @Column() + name: string; + + @ManyToMany(() => Blog, (blog) => blog.categories) + blogs: Blog[]; +} diff --git a/src/models/index.ts b/src/models/index.ts index b67de043..c7c7f50c 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -1,34 +1,36 @@ -export * from "./blog"; -export * from "./category"; - -export * from "./comment"; -export * from "./contact"; -export * from "./emailQueue"; - -export * from "./contact"; -export * from "./invitation"; -export * from "./log"; -export * from "./payment"; - -export * from "./faq"; -export * from "./helpcentertopic"; -export * from "./invitation"; -export * from "./job"; -export * from "./like"; -export * from "./log"; -export * from "./notificationsettings"; -export * from "./organization"; -export * from "./organization-member"; -export * from "./organization-role.entity"; - -export * from "./payment"; -export * from "./permissions.entity"; -export * from "./product"; -export * from "./profile"; -export * from "./sms"; -export * from "./tag"; -export * from "./user"; -export * from "./user-organisation"; - -export * from "./squeeze"; -export * from "./notification"; +export * from "./blog"; +export * from "./category"; + +export * from "./comment"; +export * from "./contact"; +export * from "./emailQueue"; + +export * from "./contact"; +export * from "./invitation"; +export * from "./log"; +export * from "./payment"; + +export * from "./faq"; +// export * from "./orgInviteToken"; +export * from "./billing-plan"; +export * from "./helpcentertopic"; +export * from "./invitation"; +export * from "./job"; +export * from "./like"; +export * from "./log"; +export * from "./notificationsettings"; +export * from "./organization"; +export * from "./organization-member"; +export * from "./organization-role.entity"; + +export * from "./payment"; +export * from "./permissions.entity"; +export * from "./product"; +export * from "./profile"; +export * from "./sms"; +export * from "./tag"; +export * from "./user"; +export * from "./user-organisation"; + +export * from "./squeeze"; +export * from "./notification"; diff --git a/src/models/tag.ts b/src/models/tag.ts index 2720a095..cb9842d9 100644 --- a/src/models/tag.ts +++ b/src/models/tag.ts @@ -1,14 +1,14 @@ -import { Entity, PrimaryGeneratedColumn, Column, ManyToMany } from "typeorm"; -import { Blog } from "./blog"; - -@Entity() -export class Tag { - @PrimaryGeneratedColumn() - id: number; - - @Column() - name: string; - - @ManyToMany(() => Blog, (blog) => blog.tags) - blogs: Blog[]; -} +import { Entity, PrimaryGeneratedColumn, Column, ManyToMany } from "typeorm"; +import { Blog } from "./blog"; + +@Entity() +export class Tag { + @PrimaryGeneratedColumn("uuid") + id: number; + + @Column() + name: string; + + @ManyToMany(() => Blog, (blog) => blog.tags) + blogs: Blog[]; +} diff --git a/src/routes/blog.ts b/src/routes/blog.ts index 74961746..12db04ce 100644 --- a/src/routes/blog.ts +++ b/src/routes/blog.ts @@ -1,55 +1,63 @@ -import { Router } from "express"; -import { BlogCommentController } from "../controllers/blogCommentController"; -import { BlogController } from "../controllers/BlogController"; -// import { createBlogController } from "../controllers/createBlogController" -import { updateBlogController } from "../controllers/updateBlogController"; -import { authMiddleware } from "../middleware"; -import { requestBodyValidator } from "../middleware/request-validation"; -import { createBlogSchema } from "../utils/request-body-validator"; - -const blogRouter = Router(); -const blogController = new BlogController(); -const blogCommentController = new BlogCommentController(); - -blogRouter.get("/blog/", blogController.listBlogs.bind(blogController)); -blogRouter.post( - "/blogs", - requestBodyValidator(createBlogSchema), - authMiddleware, - blogController.createBlogController.bind(blogController), -); - -blogRouter.get( - "/blog/user", - authMiddleware, - blogController.listBlogsByUser.bind(blogController), -); -blogRouter.put("/:id", authMiddleware, updateBlogController); - -blogRouter.delete( - "/blog/:id", - authMiddleware, - blogController.deleteBlogPost.bind(blogController), -); - -//endpoint to create a comment on a blog post -blogRouter.post( - "/blog/:postId/comment", - authMiddleware, - blogCommentController.createComment.bind(blogCommentController), -); - -//endpoint to edit a comment on a blog post -blogRouter.patch( - "/blog/:commentId/edit-comment", - authMiddleware, - blogCommentController.editComment.bind(blogCommentController), -); - -blogRouter.get( - "/blog/:blogId/comments", - authMiddleware, - blogCommentController.getAllComments.bind(blogCommentController), -); - -export { blogRouter }; +import { Router } from "express"; +import { BlogCommentController } from "../controllers/blogCommentController"; +import { BlogController } from "../controllers/BlogController"; +import { authMiddleware, checkPermissions } from "../middleware"; +import { requestBodyValidator } from "../middleware/request-validation"; +import { createBlogSchema } from "../utils/request-body-validator"; +import { UserRole } from "../enums/userRoles"; + +const blogRouter = Router(); +const blogController = new BlogController(); +const blogCommentController = new BlogCommentController(); + +blogRouter.get("/blog/", blogController.listBlogs.bind(blogController)); +blogRouter.post( + "/blogs", + requestBodyValidator(createBlogSchema), + authMiddleware, + blogController.createBlogController.bind(blogController), +); + +blogRouter.get( + "/blog/user", + authMiddleware, + blogController.listBlogsByUser.bind(blogController), +); +blogRouter.patch( + "/blog/edit/:id", + requestBodyValidator(createBlogSchema), + authMiddleware, + blogController.updateBlog.bind(blogController), +); + +blogRouter.delete( + "/blog/:id", + authMiddleware, + blogController.deleteBlogPost.bind(blogController), +); + +blogRouter.post( + "/blog/:postId/comment", + authMiddleware, + blogCommentController.createComment.bind(blogCommentController), +); + +blogRouter.patch( + "/blog/:commentId/edit-comment", + authMiddleware, + blogCommentController.editComment.bind(blogCommentController), +); + +blogRouter.delete( + "/blog/:commentId", + authMiddleware, + blogCommentController.deleteComment.bind(blogCommentController), +); + +blogRouter.get( + "/blog/:blogId/comments", + authMiddleware, + blogCommentController.getAllComments.bind(blogCommentController), +); + +export { blogRouter }; diff --git a/src/routes/index.ts b/src/routes/index.ts index 2ee7ad57..95ee8377 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,24 +1,25 @@ -export * from "./auth"; -export * from "./admin"; -export * from "./blog"; -export * from "./contact"; -export * from "./export"; -export * from "./help-center"; -export * from "./job"; -export * from "./organisation"; -export * from "./notificationsettings"; -export * from "./paymentStripe"; -export * from "./paymentLemonSqueezy"; -export * from "./payment"; -export * from "./product"; -export * from "./sendEmail.route"; -export * from "./sms"; -export * from "./testimonial"; -export * from "./user"; -export * from "./faq"; -export * from "./run-test"; -export * from "./billing-plans"; -export * from "./squeeze"; -export * from "./newsLetterSubscription"; -export * from "./notification"; -export * from "./billingplan"; +export * from "./auth"; +export * from "./admin"; +export * from "./blog"; +export * from "./contact"; +export * from "./export"; +export * from "./help-center"; +export * from "./job"; +export * from "./organisation"; +export * from "./notificationsettings"; +export * from "./paymentStripe"; +export * from "./paymentLemonSqueezy"; +export * from "./payment"; +export * from "./product"; +export * from "./sendEmail.route"; +export * from "./sms"; +export * from "./testimonial"; +export * from "./user"; +export * from "./faq"; +export * from "./run-test"; +export * from "./paymentPaystack"; +export * from "./billing-plans"; +export * from "./squeeze"; +export * from "./newsLetterSubscription"; +export * from "./notification"; +export * from "./billingplan"; diff --git a/src/routes/job.ts b/src/routes/job.ts index 946e0522..13a6b859 100644 --- a/src/routes/job.ts +++ b/src/routes/job.ts @@ -1,20 +1,22 @@ -import { Router } from "express"; -import { JobController } from "../controllers/jobController"; -import { authMiddleware } from "../middleware"; - -const jobRouter = Router(); - -const jobController = new JobController(); - -jobRouter.post( - "/jobs", - authMiddleware, - jobController.createJob.bind(jobController), -); -jobRouter.delete( - "/jobs/:jobId", - authMiddleware, - jobController.deleteJob.bind(jobController), -); - -export { jobRouter }; +import { Router } from "express"; +import { JobController } from "../controllers/jobController"; +import { authMiddleware } from "../middleware"; + +const jobRouter = Router(); + +const jobController = new JobController(); + +jobRouter.post( + "/jobs", + authMiddleware, + jobController.createJob.bind(jobController), +); +jobRouter.delete( + "/jobs/:jobId", + authMiddleware, + jobController.deleteJob.bind(jobController), +); + +jobRouter.get("/jobs", jobController.getAllJobs.bind(jobController)); + +export { jobRouter }; diff --git a/src/routes/paymentPaystack.ts b/src/routes/paymentPaystack.ts new file mode 100644 index 00000000..23164718 --- /dev/null +++ b/src/routes/paymentPaystack.ts @@ -0,0 +1,22 @@ +import { Router } from "express"; +import { + initializePaymentPaystack, + verifyPaymentPaystack, +} from "../controllers"; +import { authMiddleware } from "../middleware"; + +const paymentPaystackRouter = Router(); + +paymentPaystackRouter.post( + "/payments/paystack/initiate", + authMiddleware, + initializePaymentPaystack, +); + +paymentPaystackRouter.get( + "/payments/paystack/verify/:reference", + authMiddleware, + verifyPaymentPaystack, +); + +export { paymentPaystackRouter }; diff --git a/src/routes/squeeze.ts b/src/routes/squeeze.ts index 6953c035..ea5cfc7d 100644 --- a/src/routes/squeeze.ts +++ b/src/routes/squeeze.ts @@ -1,20 +1,26 @@ -import { Router } from "express"; -import { SqueezeController } from "../controllers"; -import { authMiddleware } from "../middleware"; - -const squeezeRoute = Router(); -const squeezecontroller = new SqueezeController(); - -squeezeRoute.post( - "/squeeze-pages", - authMiddleware, - squeezecontroller.createSqueeze.bind(squeezecontroller), -); - -squeezeRoute.get( - "/squeeze/:id", - authMiddleware, - squeezecontroller.getSqueezeById.bind(squeezecontroller), -); - -export { squeezeRoute }; +import { Router } from "express"; +import { SqueezeController } from "../controllers"; +import { authMiddleware } from "../middleware"; + +const squeezeRoute = Router(); +const squeezecontroller = new SqueezeController(); + +squeezeRoute.post( + "/squeezes", + authMiddleware, + squeezecontroller.createSqueeze.bind(squeezecontroller), +); + +squeezeRoute.get( + "/squeeze/:id", + authMiddleware, + squeezecontroller.getSqueezeById.bind(squeezecontroller), +); + +squeezeRoute.put( + "/squeezes/:squeeze_id", + authMiddleware, + squeezecontroller.updateSqueeze.bind(squeezecontroller), +); + +export { squeezeRoute }; diff --git a/src/routes/user.ts b/src/routes/user.ts index 50f007af..b84b0e9b 100644 --- a/src/routes/user.ts +++ b/src/routes/user.ts @@ -1,32 +1,3 @@ -<<<<<<< HEAD -import { Router } from "express"; -import { UserController } from "../controllers"; -import { authMiddleware } from "../middleware"; -import { multerConfig } from "../config/multer"; - -const upload = multerConfig.single("avatarUrl"); - -const userRouter = Router(); -const userController = new UserController(); -userRouter.get("/users/", userController.getAllUsers.bind(UserController)); - -userRouter.delete( - "/users/:id", - authMiddleware, - userController.deleteUser.bind(userController), -); - -userRouter.get("/users/me", authMiddleware, UserController.getProfile); - -userRouter.patch( - "/users/:user_id", - authMiddleware, - upload, - userController.updateUserProfile, -); - -export { userRouter }; -======= import { Router } from "express"; import { UserController } from "../controllers"; import { authMiddleware } from "../middleware"; @@ -64,4 +35,3 @@ userRouter.put( ); export { userRouter }; ->>>>>>> dff20d3007b5e2fe48cfc08e4c31d9327b345571 diff --git a/src/services/blog.services.ts b/src/services/blog.services.ts index 06040ffc..7f56fbac 100644 --- a/src/services/blog.services.ts +++ b/src/services/blog.services.ts @@ -1,120 +1,187 @@ -import { Repository } from "typeorm"; -import AppDataSource from "../data-source"; -import { Category, Tag, User } from "../models"; -import { Blog } from "../models/blog"; - -export class BlogService { - getAllComments(mockBlogId: string) { - throw new Error("Method not implemented."); - } - private blogRepository: Repository; - private categoryRepository: Repository; - private tagRepository: Repository; - private userRepository: Repository; - - constructor() { - this.blogRepository = AppDataSource.getRepository(Blog); - this.categoryRepository = AppDataSource.getRepository(Category); - this.tagRepository = AppDataSource.getRepository(Tag); - this.userRepository = AppDataSource.getRepository(User); - } - - async getPaginatedblogs( - page: number, - limit: number, - ): Promise<{ blogs: Blog[]; totalItems: number }> { - const [blogs, totalItems] = await this.blogRepository.findAndCount({ - skip: (page - 1) * limit, - take: limit, - }); - - return { blogs, totalItems }; - } - - async getPaginatedBlogsByUser( - userId: string, - page: number, - limit: number, - ): Promise<{ blogs: Blog[]; totalItems: number }> { - const [blogs, totalItems] = await this.blogRepository.findAndCount({ - where: { author: { id: userId } }, - skip: (page - 1) * limit, - take: limit, - relations: ["author"], - }); - - return { blogs, totalItems }; - } - - async deleteBlogPost(id: string): Promise { - try { - const result = await this.blogRepository.delete(id); - return result.affected !== 0; - } catch (error) { - throw new Error("Error deleting blog post"); - } - } - - async createBlogPost( - title: string, - content: string, - authorId: string, - image_url?: string, - tags?: string, - categories?: string, - ) { - try { - let tagEntities; - let categoryEntities; - const newBlog = new Blog(); - newBlog.title = title; - newBlog.content = content; - newBlog.image_url = image_url; - newBlog.published_at = new Date(); - const author = await this.userRepository.findOne({ - where: { id: authorId }, - }); - newBlog.author = author; - - const tagsContent = tags.split(","); - const categoriesContent = categories.split(","); - - if (tagsContent && tagsContent.length > 0) { - tagEntities = await Promise.all( - tagsContent.map(async (tagName: string) => { - let tag = await this.tagRepository.findOne({ - where: { name: tagName }, - }); - if (!tag) { - tag = this.tagRepository.create({ name: tagName }); - await this.tagRepository.save(tag); - } - return tag; - }), - ); - } - - if (categoriesContent && categoriesContent.length > 0) { - categoryEntities = await Promise.all( - categoriesContent.map(async (categoryName) => { - let category = await this.categoryRepository.findOne({ - where: { name: categoryName }, - }); - if (!category) { - category = this.categoryRepository.create({ name: categoryName }); - await this.categoryRepository.save(category); - } - - return category; - }), - ); - } - - newBlog.tags = tagEntities; - newBlog.categories = categoryEntities; - return await this.blogRepository.save(newBlog); - } catch (error) { - throw error; - } - } -} +import { Repository } from "typeorm"; +import AppDataSource from "../data-source"; +import { Category, Tag, User } from "../models"; +import { Blog } from "../models/blog"; +import { ResourceNotFound, Forbidden } from "../middleware"; + +export class BlogService { + getAllComments(mockBlogId: string) { + throw new Error("Method not implemented."); + } + private blogRepository: Repository; + private categoryRepository: Repository; + private tagRepository: Repository; + private userRepository: Repository; + + constructor() { + this.blogRepository = AppDataSource.getRepository(Blog); + this.categoryRepository = AppDataSource.getRepository(Category); + this.tagRepository = AppDataSource.getRepository(Tag); + this.userRepository = AppDataSource.getRepository(User); + } + + async getPaginatedblogs( + page: number, + limit: number, + ): Promise<{ blogs: Blog[]; totalItems: number }> { + const [blogs, totalItems] = await this.blogRepository.findAndCount({ + skip: (page - 1) * limit, + take: limit, + }); + + return { blogs, totalItems }; + } + + async getPaginatedBlogsByUser( + userId: string, + page: number, + limit: number, + ): Promise<{ blogs: Blog[]; totalItems: number }> { + const [blogs, totalItems] = await this.blogRepository.findAndCount({ + where: { author: { id: userId } }, + skip: (page - 1) * limit, + take: limit, + relations: ["author"], + }); + + return { blogs, totalItems }; + } + + async deleteBlogPost(id: string): Promise { + try { + const result = await this.blogRepository.delete(id); + return result.affected !== 0; + } catch (error) { + throw new Error("Error deleting blog post"); + } + } + + async createBlogPost( + title: string, + content: string, + authorId: string, + image_url?: string, + tags?: string, + categories?: string, + ) { + try { + let tagEntities; + let categoryEntities; + const newBlog = new Blog(); + newBlog.title = title; + newBlog.content = content; + newBlog.image_url = image_url; + newBlog.published_at = new Date(); + const author = await this.userRepository.findOne({ + where: { id: authorId }, + }); + newBlog.author = author; + + const tagsContent = tags.split(","); + const categoriesContent = categories.split(","); + + if (tagsContent && tagsContent.length > 0) { + tagEntities = await Promise.all( + tagsContent.map(async (tagName: string) => { + let tag = await this.tagRepository.findOne({ + where: { name: tagName }, + }); + if (!tag) { + tag = this.tagRepository.create({ name: tagName }); + await this.tagRepository.save(tag); + } + return tag; + }), + ); + } + + if (categoriesContent && categoriesContent.length > 0) { + categoryEntities = await Promise.all( + categoriesContent.map(async (categoryName) => { + let category = await this.categoryRepository.findOne({ + where: { name: categoryName }, + }); + if (!category) { + category = this.categoryRepository.create({ name: categoryName }); + await this.categoryRepository.save(category); + } + + return category; + }), + ); + } + + newBlog.tags = tagEntities; + newBlog.categories = categoryEntities; + return await this.blogRepository.save(newBlog); + } catch (error) { + throw error; + } + } + async updateBlog(blogId: string, payload: any, userId: string) { + const blog = await this.blogRepository.findOne({ + where: { id: blogId }, + relations: ["author", "tags", "categories"], + }); + + if (!blog) { + throw new ResourceNotFound("Blog post not found"); + } + if (blog.author.id !== userId) { + throw new Forbidden("You are not authorized to edit this blog post"); + } + const user = await this.userRepository.findOne({ where: { id: userId } }); + + blog.title = payload.title; + blog.content = payload.content; + blog.image_url = payload.image_url; + blog.author = user; + blog.published_at = payload.publish_date; + if (payload.tags) { + const tagsContent = payload.tags.split(","); + const tagEntities = await Promise.all( + tagsContent.map(async (tagName: string) => { + let tag = await this.tagRepository.findOne({ + where: { name: tagName }, + }); + if (!tag) { + tag = this.tagRepository.create({ name: tagName }); + await this.tagRepository.save(tag); + } + return tag; + }), + ); + blog.tags = tagEntities; + } + + if (payload.categories) { + const categoriesContent = payload.categories.split(","); + const categoryEntities = await Promise.all( + categoriesContent.map(async (categoryName: string) => { + let category = await this.categoryRepository.findOne({ + where: { name: categoryName }, + }); + if (!category) { + category = this.categoryRepository.create({ name: categoryName }); + await this.categoryRepository.save(category); + } + return category; + }), + ); + blog.categories = categoryEntities; + } + + const updatedBlog = await this.blogRepository.save(blog); + + return { + blog_id: updatedBlog.id, + title: updatedBlog.title, + content: updatedBlog.content, + tags: updatedBlog.tags, + categories: updatedBlog.categories, + image_urls: updatedBlog.image_url, + author: updatedBlog.author.name, + updated_at: updatedBlog.updated_at, + }; + } +} diff --git a/src/services/blogComment.services.ts b/src/services/blogComment.services.ts index bf3debc4..ec970492 100644 --- a/src/services/blogComment.services.ts +++ b/src/services/blogComment.services.ts @@ -1,108 +1,133 @@ -import AppDataSource, { initializeDataSource } from "../data-source"; -import { Comment } from "../models/comment"; -import { Blog } from "../models/blog"; -import log from "../utils/logger"; -import { ResourceNotFound } from "../middleware"; -import { User } from "../models"; - -let commentRepository; -let blogRepository; - -async function initializeRepositories() { - await initializeDataSource(); - commentRepository = AppDataSource.getRepository(Comment); - blogRepository = AppDataSource.getRepository(Blog); -} - -export const createComment = async ( - blogId: string, - content: string, - userId: string, -) => { - await initializeRepositories(); - const blog = await blogRepository.findOneBy({ id: blogId }); - const user = await AppDataSource.getRepository(User).findOneBy({ - id: userId, - }); - - if (!blog) throw new Error("Blog not found"); - if (!user) throw new Error("User not found"); - - const newComment = new Comment(); - newComment.content = content; - newComment.blog = blog; - newComment.author = user; - - return commentRepository.save(newComment); -}; - -function hasThirtyMinutesElapsed( - createdAt: Date, - updatedAt: Date | null, -): boolean { - const currentTime = new Date(); - - // Create Date objects for createdAt and updatedAt - const createdAtDate = new Date(createdAt); - const updatedAtDate = updatedAt ? new Date(updatedAt) : null; - - // Calculate the time 30 minutes after createdAt and updatedAt - const createdAtPlus30Min = new Date(createdAtDate.getTime() + 30 * 60 * 1000); - const updatedAtPlus30Min = updatedAtDate - ? new Date(updatedAtDate.getTime() + 30 * 60 * 1000) - : null; - - // Check if 30 minutes have elapsed since either createdAt or updatedAt - if (updatedAtPlus30Min && currentTime < updatedAtPlus30Min) { - return false; - } else if (currentTime < createdAtPlus30Min) { - return false; - } - - // 30 minutes have elapsed since either createdAt or updatedAt - return true; -} - -export const editComment = async (commentId: number, content: string) => { - await initializeRepositories(); - //retrieve the comment - const comment = await commentRepository.findOneBy({ id: commentId }); - - //check if comment exists - if (!comment) throw new Error("COMMENT_NOT_FOUND"); - - //destructure the comment data - const { created_at, updated_at } = comment; - - //check if created_at or updated_at is more than 30 mins - const has30minsElapsed = hasThirtyMinutesElapsed(created_at, updated_at); - - //conditional check to determine if time for update is okay - if (!has30minsElapsed) { - throw new Error("TIME_NOT_OK"); - } - - //update the comment data - const updateResult = await commentRepository.update(commentId, { content }); - - return updateResult; -}; - -export const getAllComments = async (blogId: string) => { - await initializeRepositories(); - const blog = await blogRepository.findOne({ - where: { id: blogId }, - relations: ["comments", "comments.author"], - }); - - if (!blog) { - throw new ResourceNotFound("Blog post not found"); - } - - return blog.comments.map((comment) => ({ - id: comment.id, - author: comment.author ? comment.author.name : "Anonymous", - text: comment.content, - timestamp: comment.created_at.toISOString(), - })); -}; +import AppDataSource, { initializeDataSource } from "../data-source"; +import { Comment } from "../models/comment"; +import { Blog } from "../models/blog"; +import log from "../utils/logger"; +import { ResourceNotFound } from "../middleware"; +import { User } from "../models"; + +let commentRepository; +let blogRepository; + +async function initializeRepositories() { + await initializeDataSource(); + commentRepository = AppDataSource.getRepository(Comment); + blogRepository = AppDataSource.getRepository(Blog); +} + +export const createComment = async ( + blogId: string, + content: string, + userId: string, +) => { + await initializeRepositories(); + const blog = await blogRepository.findOneBy({ id: blogId }); + const user = await AppDataSource.getRepository(User).findOneBy({ + id: userId, + }); + + if (!blog) throw new Error("Blog not found"); + if (!user) throw new Error("User not found"); + + const newComment = new Comment(); + newComment.content = content; + newComment.blog = blog; + newComment.author = user; + + return commentRepository.save(newComment); +}; + +function hasThirtyMinutesElapsed( + createdAt: Date, + updatedAt: Date | null, +): boolean { + const currentTime = new Date(); + + // Create Date objects for createdAt and updatedAt + const createdAtDate = new Date(createdAt); + const updatedAtDate = updatedAt ? new Date(updatedAt) : null; + + // Calculate the time 30 minutes after createdAt and updatedAt + const createdAtPlus30Min = new Date(createdAtDate.getTime() + 30 * 60 * 1000); + const updatedAtPlus30Min = updatedAtDate + ? new Date(updatedAtDate.getTime() + 30 * 60 * 1000) + : null; + + // Check if 30 minutes have elapsed since either createdAt or updatedAt + if (updatedAtPlus30Min && currentTime < updatedAtPlus30Min) { + return false; + } else if (currentTime < createdAtPlus30Min) { + return false; + } + + // 30 minutes have elapsed since either createdAt or updatedAt + return true; +} + +export const editComment = async (commentId: number, content: string) => { + await initializeRepositories(); + //retrieve the comment + const comment = await commentRepository.findOneBy({ id: commentId }); + + //check if comment exists + if (!comment) throw new Error("COMMENT_NOT_FOUND"); + + //destructure the comment data + const { created_at, updated_at } = comment; + + //check if created_at or updated_at is more than 30 mins + const has30minsElapsed = hasThirtyMinutesElapsed(created_at, updated_at); + + //conditional check to determine if time for update is okay + if (!has30minsElapsed) { + throw new Error("TIME_NOT_OK"); + } + + //update the comment data + const updateResult = await commentRepository.update(commentId, { content }); + + return updateResult; +}; + +export const getAllComments = async (blogId: string) => { + await initializeRepositories(); + const blog = await blogRepository.findOne({ + where: { id: blogId }, + relations: ["comments", "comments.author"], + }); + + if (!blog) { + throw new ResourceNotFound("Blog post not found"); + } + + return blog.comments.map((comment) => ({ + id: comment.id, + author: comment.author ? comment.author.name : "Anonymous", + text: comment.content, + timestamp: comment.created_at.toISOString(), + })); +}; + +export const deleteComment = async (commentId: string, userId: string) => { + await initializeRepositories(); + + const comment = await commentRepository.findOne({ + where: { id: commentId }, + relations: ["author"], + }); + + if (!comment) { + throw new Error("COMMENT_NOT_FOUND"); + } + + const { author } = comment; + + if (author.id !== userId) { + throw new Error("UNAUTHORIZED_ACTION"); + } + + await commentRepository.delete(commentId); + + return { + message: "Comment deleted successfully", + }; +}; diff --git a/src/services/index.ts b/src/services/index.ts index 4cc21b4f..5e885d16 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1,16 +1,17 @@ -export * from "./auth.services"; -export * from "./user.services"; -export * from "./help.services"; -export * from "./product.services"; -export * from "./blog.services"; -export * from "./admin.services"; -export * from "./export.services"; -export * from "./sendEmail.services"; -export * from "./payment/flutter.service"; -export * from "./contactService"; -export * from "./faq.services"; -export * from "./org.services"; -export * from "./billing-plans.services"; -export * from "./squeezeService"; -export * from "./blogComment.services"; -export * from "./notification.services"; +export * from "./auth.services"; +export * from "./user.services"; +export * from "./help.services"; +export * from "./product.services"; +export * from "./blog.services"; +export * from "./admin.services"; +export * from "./export.services"; +export * from "./sendEmail.services"; +// export * from "./payment/flutter.service"; +export * from "./contactService"; +export * from "./faq.services"; +export * from "./org.services"; +export * from "./payment/paystack.service"; +export * from "./billing-plans.services"; +export * from "./squeezeService"; +export * from "./blogComment.services"; +export * from "./notification.services"; diff --git a/src/services/job.service.ts b/src/services/job.service.ts index 43936df4..be0395ef 100644 --- a/src/services/job.service.ts +++ b/src/services/job.service.ts @@ -1,52 +1,61 @@ -import { NextFunction, Request, Response } from "express"; -import { Job } from "../models"; -import AppDataSource from "../data-source"; -import { Repository } from "typeorm"; -import { HttpError } from "../middleware"; - -export class JobService { - private jobRepository: Repository; - - constructor() { - this.jobRepository = AppDataSource.getRepository(Job); - } - - public async create(req: Request): Promise { - const { title, description, location, salary, job_type, company_name } = - req.body; - const user_id = (req as Record).user.id; - - const jobEntity = Job.create({ - user_id, - title, - description, - location, - salary, - job_type, - company_name, - }); - const job = await Job.save(jobEntity); - return job; - } - - public async delete(jobId: string): Promise { - try { - const existingJob = await this.jobRepository.findOne({ - where: { id: jobId }, - }); - if (!existingJob) { - throw new HttpError(404, "Job not found"); - } - - await this.jobRepository.delete({ id: jobId }); - - return existingJob; - } catch (err) { - if (err instanceof HttpError) { - throw err; - } else { - throw new HttpError(500, err.message || "Error deleting job"); - } - } - } -} +import { NextFunction, Request, Response } from "express"; +import { Job } from "../models"; +import AppDataSource from "../data-source"; +import { Repository } from "typeorm"; +import { HttpError } from "../middleware"; + +export class JobService { + private jobRepository: Repository; + + constructor() { + this.jobRepository = AppDataSource.getRepository(Job); + } + + public async create(req: Request): Promise { + const { title, description, location, salary, job_type, company_name } = + req.body; + const user_id = (req as Record).user.id; + + const jobEntity = Job.create({ + user_id, + title, + description, + location, + salary, + job_type, + company_name, + }); + const job = await Job.save(jobEntity); + return job; + } + + public async getAllJobs(req: Request): Promise { + try { + return await Job.find(); + } catch (error) { + console.error("Failed to fetch jobs", error); + throw new Error("Could not fetch jobs "); + } + } + + public async delete(jobId: string): Promise { + try { + const existingJob = await this.jobRepository.findOne({ + where: { id: jobId }, + }); + if (!existingJob) { + throw new HttpError(404, "Job not found"); + } + + await this.jobRepository.delete({ id: jobId }); + + return existingJob; + } catch (err) { + if (err instanceof HttpError) { + throw err; + } else { + throw new HttpError(500, err.message || "Error deleting job"); + } + } + } +} diff --git a/src/services/payment/paystack.service.ts b/src/services/payment/paystack.service.ts new file mode 100644 index 00000000..eb2b0653 --- /dev/null +++ b/src/services/payment/paystack.service.ts @@ -0,0 +1,91 @@ +import Paystack from "paystack"; +import { v4 as uuidv4 } from "uuid"; +import config from "../../config"; +import { Payment, Organization, BillingPlan } from "../../models"; +import AppDataSource from "../../data-source"; + +const paystack = new Paystack(config.PAYSTACK_SECRET_KEY); + +export const initializePayment = async (customerDetails: { + organization_id?: string; + plan_id?: string; + full_name?: string; + billing_option?: "monthly" | "yearly"; + redirect_url?: string; +}): Promise => { + try { + const tx_ref = `pst-${uuidv4()}-${Date.now()}`; + // Fetch billing plan and organization details + const billingPlanRepository = AppDataSource.getRepository(BillingPlan); + const organizationRepository = AppDataSource.getRepository(Organization); + + const billingPlan = await billingPlanRepository.findOneBy({ + id: customerDetails.plan_id, + }); + const organization = await organizationRepository.findOneBy({ + id: customerDetails.organization_id, + }); + + if (!billingPlan || !organization) { + throw new Error("Billing plan or organization not found"); + } + + const payload = { + email: organization?.email || "hng@gmail.com", + amount: (billingPlan?.price || 1000) * 100, // Paystack expects amount in kobo + currency: "NGN", + }; + + const response = await paystack.transaction.initialize(payload); + + await saveTransactionToDatabase({ + ...customerDetails, + description: `Payment of ${billingPlan.price || 1000} ${billingPlan.currency || "NGN"} via Paystack`, + metadata: { tx_ref, paystack_response: response }, + paymentServiceId: response.data.reference, + currency: billingPlan.currency || "NGN", + amount: billingPlan.price || 1000, + status: "pending", + provider: "paystack", + }); + + return { + status: 200, + message: "Payment initiated successfully", + data: { + payment_url: response.data.authorization_url, + }, + }; + } catch (error) { + throw error; + } +}; + +export const verifyPayment = async (reference: string): Promise => { + try { + const response = await paystack.transaction.verify(reference); + + const paymentStatus = + response.data.status === "success" ? "completed" : "failed"; + await updatePaymentStatus(reference, paymentStatus); + + return response; + } catch (error) { + throw error; + } +}; + +const saveTransactionToDatabase = async (transactionData: any) => { + const paymentRepository = AppDataSource.getRepository(Payment); + const payment = paymentRepository.create(transactionData); + await paymentRepository.save(payment); +}; + +const updatePaymentStatus = async (reference: string, status: string) => { + const paymentRepository = AppDataSource.getRepository(Payment); + const payment = await paymentRepository.findOneBy({ id: reference }); + if (payment) { + payment.status = status as "pending" | "completed" | "failed"; + await paymentRepository.save(payment); + } +}; diff --git a/src/services/squeezeService.ts b/src/services/squeezeService.ts index 9b7404b0..2f7dea69 100644 --- a/src/services/squeezeService.ts +++ b/src/services/squeezeService.ts @@ -1,52 +1,79 @@ -import { Squeeze } from "../models"; -import AppDataSource from "../data-source"; -import { Conflict, BadRequest } from "../middleware"; -import { squeezeSchema } from "../schema/squeezeSchema"; -import { Repository } from "typeorm"; - -class SqueezeService { - private squeezeRepository: Repository; - - constructor() { - this.squeezeRepository = AppDataSource.getRepository(Squeeze); - } - - public async createSqueeze(data: Partial): Promise { - const validation = squeezeSchema.safeParse(data); - if (!validation.success) { - throw new Conflict( - "Validation failed: " + - validation.error.errors.map((e) => e.message).join(", "), - ); - } - - try { - const existingSqueeze = await this.squeezeRepository.findOne({ - where: { email: data.email }, - }); - - if (existingSqueeze) { - throw new Conflict( - "A squeeze has already been generated using this email", - ); - } - - const squeeze = this.squeezeRepository.create(data); - const savedSqueeze = await this.squeezeRepository.save(squeeze); - return savedSqueeze; - } catch (error) { - throw new BadRequest("Failed to create squeeze: " + error.message); - } - } - - public async getSqueezeById(id: string): Promise { - try { - const squeeze = await this.squeezeRepository.findOne({ where: { id } }); - return squeeze; - } catch (error) { - throw new BadRequest("Failed to retrieve squeeze: " + error.message); - } - } -} - -export { SqueezeService }; +import { Squeeze } from "../models"; +import AppDataSource from "../data-source"; +import { + Conflict, + BadRequest, + ResourceNotFound, + ServerError, +} from "../middleware"; +import { squeezeSchema } from "../schema/squeezeSchema"; +import { Repository } from "typeorm"; + +class SqueezeService { + private squeezeRepository: Repository; + + constructor() { + this.squeezeRepository = AppDataSource.getRepository(Squeeze); + } + + public async createSqueeze(data: Partial): Promise { + const validation = squeezeSchema.safeParse(data); + if (!validation.success) { + throw new Conflict( + "Validation failed: " + + validation.error.errors.map((e) => e.message).join(", "), + ); + } + + try { + const existingSqueeze = await this.squeezeRepository.findOne({ + where: { email: data.email }, + }); + + if (existingSqueeze) { + throw new Conflict( + "A squeeze has already been generated using this email", + ); + } + + const squeeze = this.squeezeRepository.create(data); + const savedSqueeze = await this.squeezeRepository.save(squeeze); + return savedSqueeze; + } catch (error) { + throw new BadRequest("Failed to create squeeze: " + error.message); + } + } + + public async getSqueezeById(id: string): Promise { + try { + const squeeze = await this.squeezeRepository.findOne({ where: { id } }); + return squeeze; + } catch (error) { + throw new BadRequest("Failed to retrieve squeeze: " + error.message); + } + } + + public async updateSqueeze(id: string, data: Squeeze): Promise { + const validation = squeezeSchema.safeParse(data); + if (!validation.success) { + throw new Conflict( + "Validation failed: " + + validation.error.errors.map((e) => e.message).join(", "), + ); + } + try { + const findSqueeze = await this.squeezeRepository.findOne({ + where: { id }, + }); + if (!findSqueeze) { + throw new ResourceNotFound("Squeeze not found"); + } + const newSqueeze = this.squeezeRepository.merge(findSqueeze, data); + return await this.squeezeRepository.save(newSqueeze); + } catch (error) { + throw new ServerError(error.message); + } + } +} + +export { SqueezeService }; diff --git a/src/services/user.services.ts b/src/services/user.services.ts index 0677a904..c149a941 100644 --- a/src/services/user.services.ts +++ b/src/services/user.services.ts @@ -1,140 +1,3 @@ -<<<<<<< HEAD -// src/services/UserService.ts -import { Repository, UpdateResult } from "typeorm"; -import { cloudinary } from "../config/multer"; -import AppDataSource from "../data-source"; -import { HttpError } from "../middleware"; -import { Profile } from "../models/profile"; -import { User } from "../models/user"; - -interface IUserProfileUpdate { - first_name: string; - last_name: string; - phone_number: string; - avatarUrl: string; -} - -export class UserService { - private userRepository: Repository; - - constructor() { - this.userRepository = AppDataSource.getRepository(User); - } - - static async getUserById(id: string): Promise { - const userRepository = AppDataSource.getRepository(User); - const user = userRepository.findOne({ - where: { id }, - relations: ["profile"], - withDeleted: true, - }); - return user; - } - - public async getAllUsers(): Promise { - const users = await User.find({ - relations: ["profile", "products", "organizations"], - }); - return users; - } - - public async softDeleteUser(id: string): Promise { - const user = await this.userRepository.findOne({ - where: { id }, - }); - - if (!user) { - throw new HttpError(404, "User Not Found"); - } - - user.is_deleted = true; - await this.userRepository.save(user); - const deletedUser = await this.userRepository.softDelete({ id }); - return deletedUser; - } - - public updateUserProfile = async ( - id: string, - payload: IUserProfileUpdate, - file?: Express.Multer.File, - ): Promise => { - try { - const userRepository = AppDataSource.getRepository(User); - const profileRepository = AppDataSource.getRepository(Profile); - - const user = await userRepository.findOne({ - where: { id }, - relations: ["profile"], - }); - - if (!user) { - throw new Error("User not found"); - } - - const profile: Partial = { - // first_name: payload.first_name, - // last_name: payload.last_name, - // phone_number: payload.phone_number, - avatarUrl: file ? file.path : undefined, - ...payload, - }; - - const userProfile = await profileRepository.findOne({ - where: { id: user.profile.id }, - }); - - if (!userProfile) { - throw new Error("Profile not found"); - } - - if (file) { - // delete old image from cloudinary - const oldImage = userProfile.avatarUrl; - if (oldImage) { - const publicId = oldImage.split("/").pop()?.split(".")[0]; - await cloudinary.uploader.destroy(publicId); - } - - // upload new image to cloudinary - const { path } = file; - const result = await cloudinary.uploader.upload(path); - userProfile.avatarUrl = result.secure_url; - } - - await profileRepository.update(userProfile.id, profile); - - user.profile = userProfile; - - await userRepository.update(user.id, user); - - if (profile.first_name || profile.last_name) { - const updatedProfile = await profileRepository.findOne({ - where: { id: user.profile.id }, - }); - if (updatedProfile) { - user.name = `${updatedProfile.first_name} ${updatedProfile.last_name}`; - } - // user.name = `${payload.first_name} ${payload.last_name}`; - await userRepository.update(user.id, user); - } - - const updatedUser = await userRepository.findOne({ - where: { id }, - relations: ["profile"], - }); - - // remove password from response - if (updatedUser) { - delete updatedUser.password; - } - - return updatedUser; - } catch (error) { - throw new Error(error.message); - } - }; -} -======= import { Repository, UpdateResult } from "typeorm"; import AppDataSource from "../data-source"; import { BadRequest, HttpError, ResourceNotFound } from "../middleware"; @@ -343,4 +206,3 @@ export class UserService { return user; } } ->>>>>>> dff20d3007b5e2fe48cfc08e4c31d9327b345571 diff --git a/src/test/blog.spec.ts b/src/test/blog.spec.ts index 8e5bec8d..a06a6b7d 100644 --- a/src/test/blog.spec.ts +++ b/src/test/blog.spec.ts @@ -1,174 +1,377 @@ -import { afterEach, beforeEach, describe, expect, it } from "@jest/globals"; -import { DeleteResult, Repository } from "typeorm"; -import AppDataSource from "../data-source"; -import { Category, Tag, User } from "../models"; -import { Blog } from "../models/blog"; -import { BlogService } from "../services"; - -jest.mock("../data-source", () => ({ - __esModule: true, - default: { - getRepository: jest.fn(), - initialize: jest.fn(), - isInitialized: false, - }, -})); - -jest.mock("../models"); - -describe("BlogService", () => { - let blogService: BlogService; - let blogRepositoryMock: jest.Mocked>; - let tagRepositoryMock: jest.Mocked>; - let categoryRepositoryMock: jest.Mocked>; - let userRepositoryMock: jest.Mocked>; - - beforeEach(() => { - blogRepositoryMock = { - delete: jest.fn(), - save: jest.fn(), - // Add other methods if needed - } as any; // Casting to any to match the mocked repository - tagRepositoryMock = { - findOne: jest.fn(), - create: jest.fn(), - save: jest.fn(), - } as any; - categoryRepositoryMock = { - findOne: jest.fn(), - create: jest.fn(), - save: jest.fn(), - } as any; - userRepositoryMock = { - findOne: jest.fn(), - } as any; - (AppDataSource.getRepository as jest.Mock).mockImplementation((entity) => { - if (entity === Blog) return blogRepositoryMock; - if (entity === User) return userRepositoryMock; - if (entity === Tag) return tagRepositoryMock; - if (entity === Category) return categoryRepositoryMock; - }); - - // Initialize the BlogService after setting up the mock - blogService = new BlogService(); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - describe("deleteBlogPost", () => { - it("should successfully delete a blog post", async () => { - const id = "some-id"; - const deleteResult: DeleteResult = { - affected: 1, - raw: [], // Provide an empty array or appropriate mock value - }; - - blogRepositoryMock.delete.mockResolvedValue(deleteResult); - - const result = await blogService.deleteBlogPost(id); - - expect(result).toBe(true); - expect(blogRepositoryMock.delete).toHaveBeenCalledWith(id); - }); - - it("should return false when the blog post does not exist", async () => { - const id = "non-existing-id"; - const deleteResult: DeleteResult = { - affected: 0, - raw: [], // Provide an empty array or appropriate mock value - }; - - blogRepositoryMock.delete.mockResolvedValue(deleteResult); - - const result = await blogService.deleteBlogPost(id); - - expect(result).toBe(false); - expect(blogRepositoryMock.delete).toHaveBeenCalledWith(id); - }); - - it("should throw an error if there is an issue with deletion", async () => { - const id = "some-id"; - const error = new Error("Deletion failed"); - - blogRepositoryMock.delete.mockRejectedValue(error); - - await expect(blogService.deleteBlogPost(id)).rejects.toThrow( - "Error deleting blog post", - ); - expect(blogRepositoryMock.delete).toHaveBeenCalledWith(id); - }); - }); - - describe("createBlogPost", () => { - it("should create a new blogpost with tags and categories", async () => { - const payload = { - title: "Test Blog", - content: "This is a test blog", - authorId: "user-1", - image_url: "http://example.com", - tags: "tag-1,tag-2", - categories: "category-1,category-2", - }; - - const mockUser = { id: payload.authorId } as User; - const mockTag = { name: "tag-1" } as Tag; - const mockTag2 = { name: "tag-2" } as Tag; - const mockCategory = { name: "category-1" } as Category; - const mockCategory2 = { name: "category-2" } as Category; - - userRepositoryMock.findOne.mockResolvedValue(mockUser); - tagRepositoryMock.findOne.mockResolvedValue(null); - tagRepositoryMock.create.mockImplementation((tagData) => tagData as Tag); - tagRepositoryMock.save.mockImplementation((tag) => - Promise.resolve({ - id: 1, - blogs: tag.blogs as any, - name: tag.name as any, - }), - ); - categoryRepositoryMock.findOne.mockResolvedValue(null); - categoryRepositoryMock.create.mockImplementation( - (categoryData) => categoryData as Category, - ); - - categoryRepositoryMock.save.mockImplementation((category) => - Promise.resolve(category as any), - ); - - const expectedBlog = { - id: "some-id", - title: payload.title, - content: payload.content, - image_url: payload.image_url, - author: mockUser, - tags: [mockTag, mockTag2], - categories: [mockCategory, mockCategory2], - comments: [], - created_at: expect.any(Date), - updated_at: expect.any(Date), - published_at: expect.any(Date), - } as unknown as Blog; - - blogRepositoryMock.save.mockResolvedValue(expectedBlog); - - const response = await blogService.createBlogPost( - payload.title, - payload.content, - payload.authorId, - payload.image_url, - payload.tags, - payload.categories, - ); - - expect(response).toEqual(expectedBlog); - expect(tagRepositoryMock.findOne).toHaveBeenCalledTimes(2); - expect(tagRepositoryMock.create).toHaveBeenCalledTimes(2); - expect(tagRepositoryMock.save).toHaveBeenCalledTimes(2); - expect(categoryRepositoryMock.findOne).toHaveBeenCalledTimes(2); - expect(categoryRepositoryMock.create).toHaveBeenCalledTimes(2); - expect(categoryRepositoryMock.save).toHaveBeenCalledTimes(2); - }); - }); -}); +import { afterEach, beforeEach, describe, expect, it } from "@jest/globals"; +import { DeleteResult, Repository } from "typeorm"; +import AppDataSource from "../data-source"; +import { Category, Tag, User } from "../models"; +import { Blog } from "../models/blog"; +import { BlogService } from "../services"; +import { Forbidden, ResourceNotFound } from "../middleware"; + +jest.mock("../data-source", () => ({ + __esModule: true, + default: { + getRepository: jest.fn(), + initialize: jest.fn(), + isInitialized: false, + }, +})); + +jest.mock("../models"); + +describe("BlogService", () => { + let blogService: BlogService; + let blogRepositoryMock: jest.Mocked>; + let tagRepositoryMock: jest.Mocked>; + let categoryRepositoryMock: jest.Mocked>; + let userRepositoryMock: jest.Mocked>; + + beforeEach(() => { + blogRepositoryMock = { + delete: jest.fn(), + save: jest.fn(), + findOne: jest.fn(), + } as any; + tagRepositoryMock = { + findOne: jest.fn(), + create: jest.fn(), + save: jest.fn(), + } as any; + categoryRepositoryMock = { + findOne: jest.fn(), + create: jest.fn(), + save: jest.fn(), + } as any; + userRepositoryMock = { + findOne: jest.fn(), + } as any; + (AppDataSource.getRepository as jest.Mock).mockImplementation((entity) => { + if (entity === Blog) return blogRepositoryMock; + if (entity === User) return userRepositoryMock; + if (entity === Tag) return tagRepositoryMock; + if (entity === Category) return categoryRepositoryMock; + }); + + blogService = new BlogService(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe("deleteBlogPost", () => { + it("should successfully delete a blog post", async () => { + const id = "some-id"; + const deleteResult: DeleteResult = { + affected: 1, + raw: [], + }; + + blogRepositoryMock.delete.mockResolvedValue(deleteResult); + + const result = await blogService.deleteBlogPost(id); + + expect(result).toBe(true); + expect(blogRepositoryMock.delete).toHaveBeenCalledWith(id); + }); + + it("should return false when the blog post does not exist", async () => { + const id = "non-existing-id"; + const deleteResult: DeleteResult = { + affected: 0, + raw: [], + }; + + blogRepositoryMock.delete.mockResolvedValue(deleteResult); + + const result = await blogService.deleteBlogPost(id); + + expect(result).toBe(false); + expect(blogRepositoryMock.delete).toHaveBeenCalledWith(id); + }); + + it("should throw an error if there is an issue with deletion", async () => { + const id = "some-id"; + const error = new Error("Deletion failed"); + + blogRepositoryMock.delete.mockRejectedValue(error); + + await expect(blogService.deleteBlogPost(id)).rejects.toThrow( + "Error deleting blog post", + ); + expect(blogRepositoryMock.delete).toHaveBeenCalledWith(id); + }); + }); + + describe("createBlogPost", () => { + it("should create a new blogpost with tags and categories", async () => { + const payload = { + title: "Test Blog", + content: "This is a test blog", + authorId: "user-1", + image_url: "http://example.com", + tags: "tag-1,tag-2", + categories: "category-1,category-2", + }; + + const mockUser = { id: payload.authorId } as User; + const mockTag = { name: "tag-1" } as Tag; + const mockTag2 = { name: "tag-2" } as Tag; + const mockCategory = { name: "category-1" } as Category; + const mockCategory2 = { name: "category-2" } as Category; + + userRepositoryMock.findOne.mockResolvedValue(mockUser); + tagRepositoryMock.findOne.mockResolvedValue(null); + tagRepositoryMock.create.mockImplementation((tagData) => tagData as Tag); + tagRepositoryMock.save.mockImplementation((tag) => + Promise.resolve({ + id: 1, + blogs: tag.blogs as any, + name: tag.name as any, + }), + ); + categoryRepositoryMock.findOne.mockResolvedValue(null); + categoryRepositoryMock.create.mockImplementation( + (categoryData) => categoryData as Category, + ); + + categoryRepositoryMock.save.mockImplementation((category) => + Promise.resolve(category as any), + ); + + const expectedBlog = { + id: "some-id", + title: payload.title, + content: payload.content, + image_url: payload.image_url, + author: mockUser, + tags: [mockTag, mockTag2], + categories: [mockCategory, mockCategory2], + comments: [], + created_at: expect.any(Date), + updated_at: expect.any(Date), + published_at: expect.any(Date), + } as unknown as Blog; + + blogRepositoryMock.save.mockResolvedValue(expectedBlog); + + const response = await blogService.createBlogPost( + payload.title, + payload.content, + payload.authorId, + payload.image_url, + payload.tags, + payload.categories, + ); + + expect(response).toEqual(expectedBlog); + expect(tagRepositoryMock.findOne).toHaveBeenCalledTimes(2); + expect(tagRepositoryMock.create).toHaveBeenCalledTimes(2); + expect(tagRepositoryMock.save).toHaveBeenCalledTimes(2); + expect(categoryRepositoryMock.findOne).toHaveBeenCalledTimes(2); + expect(categoryRepositoryMock.create).toHaveBeenCalledTimes(2); + expect(categoryRepositoryMock.save).toHaveBeenCalledTimes(2); + }); + }); + + describe("updateBlog", () => { + it("should update a blog post with new data, tags, and categories", async () => { + const blogId = "blog-123"; + const userId = "user-456"; + const payload = { + title: "Updated Blog Title", + content: "Updated Blog Content", + image_url: "updated-image.jpg", + tags: "tag1,tag2", + categories: "category1,category2", + }; + + const mockUser = { id: userId, name: "User Name" } as User; + const existingBlog = { + id: blogId, + title: "Old Title", + content: "Old Content", + image_url: "old-image.jpg", + author: { id: "user-456", name: "User Name" }, + tags: [], + categories: [], + comments: [], + likes: 0, + } as unknown as Blog; + + const tag1 = { id: "tag-1", name: "tag1" } as unknown as Tag; + const tag2 = { id: "tag-2", name: "tag2" } as unknown as Tag; + const category1 = { + id: "category-1", + name: "category1", + } as unknown as Category; + const category2 = { + id: "category-2", + name: "category2", + } as unknown as Category; + + blogRepositoryMock.findOne.mockResolvedValue(existingBlog); + userRepositoryMock.findOne.mockResolvedValue(mockUser); + tagRepositoryMock.findOne + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(null); + categoryRepositoryMock.findOne + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(null); + tagRepositoryMock.create + .mockReturnValueOnce(tag1) + .mockReturnValueOnce(tag2); + categoryRepositoryMock.create + .mockReturnValueOnce(category1) + .mockReturnValueOnce(category2); + tagRepositoryMock.save + .mockResolvedValueOnce(tag1) + .mockResolvedValueOnce(tag2); + categoryRepositoryMock.save + .mockResolvedValueOnce(category1) + .mockResolvedValueOnce(category2); + blogRepositoryMock.save.mockResolvedValue({ + ...existingBlog, + ...payload, + tags: [tag1, tag2], + categories: [category1, category2], + }); + + const result = await blogService.updateBlog(blogId, payload, userId); + + expect(blogRepositoryMock.findOne).toHaveBeenCalledWith({ + where: { id: blogId }, + relations: ["author", "tags", "categories"], + }); + expect(userRepositoryMock.findOne).toHaveBeenCalledWith({ + where: { id: userId }, + }); + expect(tagRepositoryMock.findOne).toHaveBeenCalledTimes(2); + expect(categoryRepositoryMock.findOne).toHaveBeenCalledTimes(2); + expect(blogRepositoryMock.save).toHaveBeenCalledWith( + expect.objectContaining({ + ...existingBlog, + ...payload, + tags: [tag1, tag2], + categories: [category1, category2], + author: mockUser, + }), + ); + + expect(result).toEqual({ + blog_id: blogId, + title: payload.title, + content: payload.content, + tags: [tag1, tag2], + categories: [category1, category2], + image_urls: payload.image_url, + author: mockUser.name, + }); + }); + + it("should throw Forbidden if the user is not authorized to update the blog post", async () => { + const blogId = "blog-123"; + const userId = "user-789"; + const payload = { + title: "Updated Blog Title", + content: "Updated Blog Content", + image_url: "updated-image.jpg", + tags: "tag1,tag2", + categories: "category1,category2", + }; + + const existingBlog = { + id: blogId, + title: "Old Title", + content: "Old Content", + image_url: "old-image.jpg", + author: { id: "user-456" }, + tags: [], + categories: [], + comments: [], + likes: 0, + } as unknown as Blog; + + blogRepositoryMock.findOne.mockResolvedValue(existingBlog); + + await expect( + blogService.updateBlog(blogId, payload, userId), + ).rejects.toThrow(Forbidden); + }); + + it("should throw ResourceNotFound if the blog post does not exist", async () => { + const blogId = "non-existent-blog"; + const userId = "user-456"; + const payload = {}; + + blogRepositoryMock.findOne.mockResolvedValue(null); + + await expect( + blogService.updateBlog(blogId, payload, userId), + ).rejects.toThrow(ResourceNotFound); + }); + + it("should create new tags and categories if they do not exist", async () => { + const blogId = "blog-123"; + const userId = "user-456"; + const payload = { + title: "Updated Blog Title", + content: "Updated Blog Content", + image_url: "updated-image.jpg", + tags: "newTag1,newTag2", + categories: "newCategory1,newCategory2", + }; + + const mockUser = { id: userId, name: "User Name" } as User; + const existingBlog = { + id: blogId, + tags: [], + categories: [], + author: { id: userId }, + } as unknown as Blog; + const newTag1 = { id: "new-tag-1", name: "newTag1" } as unknown as Tag; + const newTag2 = { id: "new-tag-2", name: "newTag2" } as unknown as Tag; + const newCategory1 = { + id: "new-category-1", + name: "newCategory1", + } as unknown as Category; + const newCategory2 = { + id: "new-category-2", + name: "newCategory2", + } as unknown as Category; + + blogRepositoryMock.findOne.mockResolvedValue(existingBlog); + userRepositoryMock.findOne.mockResolvedValue(mockUser); + tagRepositoryMock.findOne.mockResolvedValue(null); + tagRepositoryMock.create + .mockReturnValueOnce(newTag1) + .mockReturnValueOnce(newTag2); + categoryRepositoryMock.findOne.mockResolvedValue(null); + categoryRepositoryMock.create + .mockReturnValueOnce(newCategory1) + .mockReturnValueOnce(newCategory2); + tagRepositoryMock.save + .mockResolvedValueOnce(newTag1) + .mockResolvedValueOnce(newTag2); + categoryRepositoryMock.save + .mockResolvedValueOnce(newCategory1) + .mockResolvedValueOnce(newCategory2); + blogRepositoryMock.save.mockResolvedValue({ + ...existingBlog, + ...payload, + tags: [newTag1, newTag2], + categories: [newCategory1, newCategory2], + author: mockUser, + }); + + const result = await blogService.updateBlog(blogId, payload, userId); + + expect(tagRepositoryMock.save).toHaveBeenCalledWith(newTag1); + expect(tagRepositoryMock.save).toHaveBeenCalledWith(newTag2); + expect(categoryRepositoryMock.save).toHaveBeenCalledWith(newCategory1); + expect(categoryRepositoryMock.save).toHaveBeenCalledWith(newCategory2); + expect(result.tags).toEqual(expect.arrayContaining([newTag1, newTag2])); + expect(result.categories).toEqual( + expect.arrayContaining([newCategory1, newCategory2]), + ); + }); + }); +}); diff --git a/src/test/jobs.spec.ts b/src/test/jobs.spec.ts new file mode 100644 index 00000000..e33491d0 --- /dev/null +++ b/src/test/jobs.spec.ts @@ -0,0 +1,66 @@ +import { Request, Response } from "express"; +import { JobController } from "../controllers/jobController"; +import { JobService } from "../services/job.service"; + +// Mock the JobService module +jest.mock("../services/job.service"); + +describe("JobController", () => { + let jobController: JobController; + let jobService: JobService; + let req: Partial; + let res: Partial; + + beforeEach(() => { + jobService = new JobService(); + jobController = new JobController(); + jobController["jobService"] = jobService; // Inject the mocked service + req = {}; + res = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + }; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("getAllJobs", () => { + it("should return an array of jobs with a 200 status code", async () => { + // Arrange + const mockJobs = [ + { id: 1, title: "Software Developer" }, + { id: 2, title: "Data Scientist" }, + ]; + (jobService.getAllJobs as jest.Mock).mockResolvedValue(mockJobs); + + // Act + await jobController.getAllJobs(req as Request, res as Response); + + // Assert + expect(jobService.getAllJobs).toHaveBeenCalledTimes(1); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith({ + message: "Jobs retrieved successfully", + billing: mockJobs, + }); + }); + + it("should return a 500 status code with an error message if jobService throws an error", async () => { + // Arrange + const errorMessage = "Failed to fetch jobs"; + (jobService.getAllJobs as jest.Mock).mockRejectedValue( + new Error(errorMessage), + ); + + // Act + await jobController.getAllJobs(req as Request, res as Response); + + // Assert + expect(jobService.getAllJobs).toHaveBeenCalledTimes(1); + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ message: errorMessage }); + }); + }); +}); diff --git a/src/test/squeeze.spect.ts b/src/test/squeeze.spect.ts index 3dc3bb41..88ab7480 100644 --- a/src/test/squeeze.spect.ts +++ b/src/test/squeeze.spect.ts @@ -1,81 +1,85 @@ -import express from "express"; -import request from "supertest"; -import { Router } from "express"; - -const mockSqueezeService = { - getSqueezeById: jest.fn(), -}; - -const app = express(); -app.use(express.json()); - -const squeezeRouter = Router(); -squeezeRouter.get("/squeeze/:id", async (req, res) => { - try { - const { id } = req.params; - const squeeze = await mockSqueezeService.getSqueezeById(id); - - if (!squeeze) { - return res.status(404).json({ - status: "error", - message: "Squeeze record not found.", - }); - } - - res.status(200).json({ - status: "success", - data: squeeze, - }); - } catch (error) { - res.status(500).json({ - status: "error", - message: "An error occurred while retrieving the squeeze record.", - error: error.message, - }); - } -}); - -app.use("/api/v1", squeezeRouter); - -describe("GET /api/v1/squeeze/:id", () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it("should return 200 and the squeeze data if the record is found", async () => { - const mockSqueeze = { - id: "1", - name: "Test Squeeze", - }; - - mockSqueezeService.getSqueezeById.mockResolvedValue(mockSqueeze); - - const response = await request(app).get("/api/v1/squeeze/1").expect(200); - - expect(response.body.status).toBe("success"); - expect(response.body.data).toEqual(mockSqueeze); - }); - - it("should return 404 if the squeeze record is not found", async () => { - mockSqueezeService.getSqueezeById.mockResolvedValue(null); - - const response = await request(app).get("/api/v1/squeeze/999").expect(404); - - expect(response.body.status).toBe("error"); - expect(response.body.message).toBe("Squeeze record not found."); - }); - - it("should return 500 if an error occurs while retrieving the squeeze", async () => { - mockSqueezeService.getSqueezeById.mockRejectedValue( - new Error("Database error"), - ); - - const response = await request(app).get("/api/v1/squeeze/1").expect(500); - - expect(response.body.status).toBe("error"); - expect(response.body.message).toBe( - "An error occurred while retrieving the squeeze record.", - ); - expect(response.body.error).toBe("Database error"); - }); -}); +//@ts-nocheck +import express from "express"; +import request from "supertest"; +import { Router } from "express"; +import { SqueezeService } from "../services"; +import { Squeeze } from "../models"; +import { ResourceNotFound } from "../middleware"; + +const mockSqueezeService = { + getSqueezeById: jest.fn(), +}; + +const app = express(); +app.use(express.json()); + +const squeezeRouter = Router(); +squeezeRouter.get("/squeeze/:id", async (req, res) => { + try { + const { id } = req.params; + const squeeze = await mockSqueezeService.getSqueezeById(id); + + if (!squeeze) { + return res.status(404).json({ + status: "error", + message: "Squeeze record not found.", + }); + } + + res.status(200).json({ + status: "success", + data: squeeze, + }); + } catch (error) { + res.status(500).json({ + status: "error", + message: "An error occurred while retrieving the squeeze record.", + error: error.message, + }); + } +}); + +app.use("/api/v1", squeezeRouter); + +describe("GET /api/v1/squeeze/:id", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("should return 200 and the squeeze data if the record is found", async () => { + const mockSqueeze = { + id: "1", + name: "Test Squeeze", + }; + + mockSqueezeService.getSqueezeById.mockResolvedValue(mockSqueeze); + + const response = await request(app).get("/api/v1/squeeze/1").expect(200); + + expect(response.body.status).toBe("success"); + expect(response.body.data).toEqual(mockSqueeze); + }); + + it("should return 404 if the squeeze record is not found", async () => { + mockSqueezeService.getSqueezeById.mockResolvedValue(null); + + const response = await request(app).get("/api/v1/squeeze/999").expect(404); + + expect(response.body.status).toBe("error"); + expect(response.body.message).toBe("Squeeze record not found."); + }); + + it("should return 500 if an error occurs while retrieving the squeeze", async () => { + mockSqueezeService.getSqueezeById.mockRejectedValue( + new Error("Database error"), + ); + + const response = await request(app).get("/api/v1/squeeze/1").expect(500); + + expect(response.body.status).toBe("error"); + expect(response.body.message).toBe( + "An error occurred while retrieving the squeeze record.", + ); + expect(response.body.error).toBe("Database error"); + }); +}); diff --git a/src/utils/request-body-validator.ts b/src/utils/request-body-validator.ts index 9869c17e..da038cf2 100644 --- a/src/utils/request-body-validator.ts +++ b/src/utils/request-body-validator.ts @@ -1,15 +1,16 @@ -import { z } from "zod"; - -const emailSchema = z.object({ - email: z.string().email(), -}); - -const createBlogSchema = z.object({ - title: z.string(), - content: z.string(), - image_url: z.string(), - tags: z.string().optional(), - categories: z.string().optional(), -}); - -export { createBlogSchema, emailSchema }; +import { z } from "zod"; + +const emailSchema = z.object({ + email: z.string().email(), +}); + +const createBlogSchema = z.object({ + title: z.string(), + content: z.string(), + image_url: z.string(), + tags: z.string().optional(), + categories: z.string().optional(), + publish_date: z.string().optional(), +}); + +export { createBlogSchema, emailSchema }; diff --git a/yarn.lock b/yarn.lock index 22dd3b1c..4872ce1b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1381,7 +1381,7 @@ array-ify@^1.0.0: resolved "https://registry.yarnpkg.com/array-ify/-/array-ify-1.0.0.tgz#9e528762b4a9066ad163a6962a364418e9626ece" integrity sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng== -asap@^2.0.0: +asap@^2.0.0, asap@~2.0.3: version "2.0.6" resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== @@ -4824,6 +4824,14 @@ pause@0.0.1: resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d" integrity sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg== +paystack@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/paystack/-/paystack-2.0.1.tgz#14a854be4a9e29ecaeaea39f6ee34bedb18c3879" + integrity sha512-reVONV7ZUMln/iWeM60n0BbogF3/zFWmUrqbKYVNzEAv+p9TcWDCHfNZ2mBGXzIXhyTsNXWwf4wNcXe28btAHw== + dependencies: + promise "^7.1.1" + request "^2.79.0" + pdfkit@^0.15.0: version "0.15.0" resolved "https://registry.yarnpkg.com/pdfkit/-/pdfkit-0.15.0.tgz#7152f1bfa500c37d25b5f8cd4850db09a8108941" @@ -5034,6 +5042,13 @@ process@^0.11.10: resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== +promise@^7.1.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" + integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg== + dependencies: + asap "~2.0.3" + prompts@^2.0.1: version "2.4.2" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" @@ -5211,7 +5226,7 @@ regexp.prototype.flags@^1.5.1: es-errors "^1.3.0" set-function-name "^2.0.1" -request@~2.88.2: +request@^2.79.0, request@~2.88.2: version "2.88.2" resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==