From e65836ebd186de29ee3207f1a12b4cd63a8acedb Mon Sep 17 00:00:00 2001 From: mukunzidd Date: Tue, 16 Apr 2024 17:00:32 +0200 Subject: [PATCH 01/51] Initial commit --- README.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..ea3fab6 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# khnights-ecomm-backend \ No newline at end of file From 71b3b47e7dd15323d456a9ea327031556af1f2f0 Mon Sep 17 00:00:00 2001 From: mukunzidd Date: Tue, 16 Apr 2024 17:01:38 +0200 Subject: [PATCH 02/51] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ea3fab6..b291bad 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# khnights-ecomm-backend \ No newline at end of file +# Backend API From 68d57f1d751b37ac6b4e4ec14b88df54eb5fce22 Mon Sep 17 00:00:00 2001 From: elijah Date: Mon, 22 Apr 2024 19:15:08 +0200 Subject: [PATCH 03/51] project setup --- .gitignore | 4 ++ package.json | 31 +++++++++++ src/configs/index.ts | 1 + src/controllers/index.ts | 1 + src/index.ts | 19 +++++++ src/middlewares/index.ts | 1 + src/routes/index.ts | 10 ++++ src/services/index.ts | 1 + src/utils/index.ts | 1 + src/utils/response.utils.ts | 31 +++++++++++ tsconfig.json | 101 ++++++++++++++++++++++++++++++++++++ 11 files changed, 201 insertions(+) create mode 100644 .gitignore create mode 100644 package.json create mode 100644 src/configs/index.ts create mode 100644 src/controllers/index.ts create mode 100644 src/index.ts create mode 100644 src/middlewares/index.ts create mode 100644 src/routes/index.ts create mode 100644 src/services/index.ts create mode 100644 src/utils/index.ts create mode 100644 src/utils/response.utils.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a104b9a --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules +.vscode +.env +package-lock.json \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..ddbe706 --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "knights-ecomm-be", + "version": "1.0.0", + "description": "E-commerce backend", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "dev": "nodemon src/index.ts", + "build": "tsc -p .", + "start": "node dist/index.js" + }, + "keywords": [], + "author": " Scrum master", + "license": "ISC", + "dependencies": { + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.19.2", + "jsend": "^1.1.0", + "nodemon": "^3.1.0", + "ts-node": "^10.9.2", + "typescript": "^5.4.5", + "typescript-jsend": "^0.1.1" + }, + "devDependencies": { + "@types/body-parser": "^1.19.5", + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/jsend": "^1.0.32" + } +} diff --git a/src/configs/index.ts b/src/configs/index.ts new file mode 100644 index 0000000..ceca67a --- /dev/null +++ b/src/configs/index.ts @@ -0,0 +1 @@ +// export all configs \ No newline at end of file diff --git a/src/controllers/index.ts b/src/controllers/index.ts new file mode 100644 index 0000000..97a75e3 --- /dev/null +++ b/src/controllers/index.ts @@ -0,0 +1 @@ +// export all controllers \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..046a8dc --- /dev/null +++ b/src/index.ts @@ -0,0 +1,19 @@ +import express, { Request, Response } from "express"; +import cors from "cors"; +import dotenv from "dotenv"; +import router from "./routes"; +dotenv.config(); + +const app = express(); +const port = process.env.PORT as string +app.use(express.json()); + +app.use(cors()); + +app.get('/', (req: Request, res: Response) => { + res.send('Knights Ecommerce API'); +}); +app.use(router) +app.listen(port, () => { + console.log(`[server]: Server is running at http://localhost:${port}`); +}) \ No newline at end of file diff --git a/src/middlewares/index.ts b/src/middlewares/index.ts new file mode 100644 index 0000000..147fee8 --- /dev/null +++ b/src/middlewares/index.ts @@ -0,0 +1 @@ +// export all middlewares \ No newline at end of file diff --git a/src/routes/index.ts b/src/routes/index.ts new file mode 100644 index 0000000..aef2710 --- /dev/null +++ b/src/routes/index.ts @@ -0,0 +1,10 @@ +import { Request, Response, Router } from "express"; +import { responseSuccess } from "../utils/response.utils"; +const router = Router(); +router.get("/status", (req: Request, res: Response) => { + return responseSuccess(res, 202, "This is a testing route that returns: 201"); +}) +// All routes should be imported here and get export after specifying first route +// example router.use("/stock". stockRoutes) =>:: First import stockRoutes and use it here, This shows how the route export will be handled + +export default router; \ No newline at end of file diff --git a/src/services/index.ts b/src/services/index.ts new file mode 100644 index 0000000..7242a4e --- /dev/null +++ b/src/services/index.ts @@ -0,0 +1 @@ +// export all Services \ No newline at end of file diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..6a3daee --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1 @@ +// export all utils \ No newline at end of file diff --git a/src/utils/response.utils.ts b/src/utils/response.utils.ts new file mode 100644 index 0000000..47be81f --- /dev/null +++ b/src/utils/response.utils.ts @@ -0,0 +1,31 @@ +import { Response } from 'express'; +import jsend from 'jsend'; + +interface ApiResponse { + code: number; + resp_msg: string; + data?: any; +} + +export const responseSuccess = (res: Response, status_code: number, message: string, data?: any): Response => { + return res.status(200).json(jsend.success({ + code: status_code, + message, + data, + })); +}; + +export const responseError = (res: Response, status_code: number, message: string, data?: any): Response => { + return res.status(400).json(jsend.error({ + code: status_code, + message, + data, + })); +}; + +export const responseServerError = (res: Response, error: string): Response => { + return res.status(500).json(jsend.error({ + code: 999, + message: `There is a problem with the server!: ${error}`, + })); +}; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..dbd8deb --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,101 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + /* Language and Environment */ + "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + /* Modules */ + "module": "commonjs", /* Specify what module code is generated. */ + "rootDir": "./src", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + /* JavaScript Support */ + "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./dist", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} \ No newline at end of file From e1d34c1030c690fa2ae8c47cb5698527741dcb0f Mon Sep 17 00:00:00 2001 From: Mc-Knight Date: Wed, 24 Apr 2024 14:48:28 +0200 Subject: [PATCH 04/51] chore: swagger API documentation --- package.json | 4 ++++ src/configs/swagger.ts | 30 ++++++++++++++++++++++++++++++ src/docs/index.yml | 23 +++++++++++++++++++++++ src/index.ts | 19 ++++++++++++------- src/routes/index.ts | 8 ++++---- src/startups/docs.ts | 7 +++++++ src/startups/getSwaggerServer.ts | 13 +++++++++++++ 7 files changed, 93 insertions(+), 11 deletions(-) create mode 100644 src/configs/swagger.ts create mode 100644 src/docs/index.yml create mode 100644 src/startups/docs.ts create mode 100644 src/startups/getSwaggerServer.ts diff --git a/package.json b/package.json index ddbe706..674b4a0 100644 --- a/package.json +++ b/package.json @@ -13,11 +13,15 @@ "author": " Scrum master", "license": "ISC", "dependencies": { + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.6", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.19.2", "jsend": "^1.1.0", "nodemon": "^3.1.0", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.0", "ts-node": "^10.9.2", "typescript": "^5.4.5", "typescript-jsend": "^0.1.1" diff --git a/src/configs/swagger.ts b/src/configs/swagger.ts new file mode 100644 index 0000000..89816be --- /dev/null +++ b/src/configs/swagger.ts @@ -0,0 +1,30 @@ +import swaggerJSDoc from "swagger-jsdoc"; +import { getSwaggerServer } from "../startups/getSwaggerServer"; + +const swaggerServer = getSwaggerServer(); + +const options: swaggerJSDoc.Options = { + definition: { + openapi: "3.0.1", + info: { + title: "Knights E-commerce API Documentation", + version: "1.0.0", + description: "knights E-commerce - Backend API", + }, + servers: [{ url: swaggerServer }], + components: { + securitySchemes: { + bearerAuth: { + type: "http", + scheme: "bearer", + bearerFormat: "JWT", + }, + }, + }, + }, + apis: ["./src/docs/*.ts", "./src/docs/*.yml"], +}; + +const swaggerSpec = swaggerJSDoc(options); + +export default swaggerSpec; diff --git a/src/docs/index.yml b/src/docs/index.yml new file mode 100644 index 0000000..fff85d2 --- /dev/null +++ b/src/docs/index.yml @@ -0,0 +1,23 @@ +/: + get: + tags: + - index + summary: "initial endpoint" + description: "This is initial endpoint" + responses: + "200": + description: "success response" + "500": + description: "server error" + +/status: + get: + tags: + - testing + summary: "status endpoint" + description: "This is status endpoint" + responses: + "200": + description: "success response" + "500": + description: "server error" diff --git a/src/index.ts b/src/index.ts index 046a8dc..b2d79b6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,18 +2,23 @@ import express, { Request, Response } from "express"; import cors from "cors"; import dotenv from "dotenv"; import router from "./routes"; +import { addDocumentation } from "./startups/docs"; + dotenv.config(); const app = express(); -const port = process.env.PORT as string +const port = process.env.PORT as string; app.use(express.json()); -app.use(cors()); +app.use(cors({ origin: "*" })); -app.get('/', (req: Request, res: Response) => { - res.send('Knights Ecommerce API'); +addDocumentation(app); +app.get("/api/v1", (req: Request, res: Response) => { + res.send("Knights Ecommerce API"); }); -app.use(router) + +app.use(router); + app.listen(port, () => { - console.log(`[server]: Server is running at http://localhost:${port}`); -}) \ No newline at end of file + console.log(`[server]: Server is running at http://localhost:${port}/api/v1`); +}); diff --git a/src/routes/index.ts b/src/routes/index.ts index aef2710..091ddbe 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,10 +1,10 @@ import { Request, Response, Router } from "express"; import { responseSuccess } from "../utils/response.utils"; const router = Router(); -router.get("/status", (req: Request, res: Response) => { - return responseSuccess(res, 202, "This is a testing route that returns: 201"); -}) +router.get("/api/v1/status", (req: Request, res: Response) => { + return responseSuccess(res, 202, "This is a testing route that returns: 201"); +}); // All routes should be imported here and get export after specifying first route // example router.use("/stock". stockRoutes) =>:: First import stockRoutes and use it here, This shows how the route export will be handled -export default router; \ No newline at end of file +export default router; diff --git a/src/startups/docs.ts b/src/startups/docs.ts new file mode 100644 index 0000000..355cfc1 --- /dev/null +++ b/src/startups/docs.ts @@ -0,0 +1,7 @@ +import { type Express } from "express"; +import swaggerUI from "swagger-ui-express"; +import swagger from "../configs/swagger"; + +export const addDocumentation = (app: Express): void => { + app.use("/api/v1/docs", swaggerUI.serve, swaggerUI.setup(swagger)); +}; diff --git a/src/startups/getSwaggerServer.ts b/src/startups/getSwaggerServer.ts new file mode 100644 index 0000000..52aa30a --- /dev/null +++ b/src/startups/getSwaggerServer.ts @@ -0,0 +1,13 @@ +import dotenv from "dotenv"; + +dotenv.config(); + +function getSwaggerServer(): string { + if (process.env.SWAGGER_SERVER !== undefined) { + return process.env.SWAGGER_SERVER; + } + + return "http://localhost:7000/api/v1"; +} + +export { getSwaggerServer }; From db28e5c67c64dc740bf59ae84f23ab261b1fc0b5 Mon Sep 17 00:00:00 2001 From: maxCastro1 Date: Wed, 24 Apr 2024 16:20:39 +0200 Subject: [PATCH 05/51] created a new branch, modifie gitignore to not push coverage folder , and added a line to stop the server when done with testing --- .gitignore | 3 ++- jest.config.ts | 19 +++++++++++++++++++ package.json | 12 +++++++++--- src/__test__/route.test.ts | 14 ++++++++++++++ src/index.ts | 4 ++-- 5 files changed, 46 insertions(+), 6 deletions(-) create mode 100644 jest.config.ts create mode 100644 src/__test__/route.test.ts diff --git a/.gitignore b/.gitignore index a104b9a..2b9bb31 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules .vscode .env -package-lock.json \ No newline at end of file +package-lock.json +coverage/ \ No newline at end of file diff --git a/jest.config.ts b/jest.config.ts new file mode 100644 index 0000000..04538ec --- /dev/null +++ b/jest.config.ts @@ -0,0 +1,19 @@ +/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ +export default { + preset: "ts-jest", + testEnvironment: "node", + testMatch: ["**/**/*.test.ts"], + verbose: true, + forceExit: true, + clearMocks: true, + resetMocks: true, + restoreMocks: true, + collectCoverageFrom: [ + "src/**/*.{ts,tsx}", // Include all JavaScript/JSX files in the src directory + ], + coveragePathIgnorePatterns: [ + "/node_modules/", // Exclude the node_modules directory + "/__tests__/", // Exclude the tests directory + ], + }; + \ No newline at end of file diff --git a/package.json b/package.json index ddbe706..d926574 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "E-commerce backend", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", + "test": "jest --coverage --detectOpenHandles --verbose --runInBand", "dev": "nodemon src/index.ts", "build": "tsc -p .", "start": "node dist/index.js" @@ -18,14 +18,20 @@ "express": "^4.19.2", "jsend": "^1.1.0", "nodemon": "^3.1.0", + "superagent": "^9.0.1", "ts-node": "^10.9.2", - "typescript": "^5.4.5", "typescript-jsend": "^0.1.1" }, "devDependencies": { "@types/body-parser": "^1.19.5", "@types/cors": "^2.8.17", "@types/express": "^4.17.21", - "@types/jsend": "^1.0.32" + "@types/jest": "^29.5.12", + "@types/jsend": "^1.0.32", + "@types/supertest": "^6.0.2", + "jest": "^29.7.0", + "supertest": "^6.3.4", + "ts-jest": "^29.1.2", + "typescript": "^5.4.5" } } diff --git a/src/__test__/route.test.ts b/src/__test__/route.test.ts new file mode 100644 index 0000000..af92a44 --- /dev/null +++ b/src/__test__/route.test.ts @@ -0,0 +1,14 @@ +import request from 'supertest'; +import {app, server} from '../index'; // update this with the path to your app file + +describe('GET /', () => { + afterAll(done => { + server.close(done); + }); + + it('responds with "Knights Ecommerce API"', done => { + request(app) + .get('/') + .expect(200, 'Knights Ecommerce API', done); + }); +}); diff --git a/src/index.ts b/src/index.ts index 046a8dc..b33ebab 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,7 @@ import dotenv from "dotenv"; import router from "./routes"; dotenv.config(); -const app = express(); +export const app = express(); const port = process.env.PORT as string app.use(express.json()); @@ -14,6 +14,6 @@ app.get('/', (req: Request, res: Response) => { res.send('Knights Ecommerce API'); }); app.use(router) -app.listen(port, () => { +export const server = app.listen(port, () => { console.log(`[server]: Server is running at http://localhost:${port}`); }) \ No newline at end of file From 9f44f7b0fb27e3691dbca6d66de245cdbab294b2 Mon Sep 17 00:00:00 2001 From: Iadivin Date: Wed, 24 Apr 2024 20:28:11 +0200 Subject: [PATCH 06/51] ch: Updated README file --- README.md | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b291bad..48532f7 100644 --- a/README.md +++ b/README.md @@ -1 +1,60 @@ -# Backend API +# E-commerse Backend API +## Description +This repository contains an E-commerce APIs, serving as backend for an E-commerce frontend application, It powers the functionalities for the frontend, such as storing, retrieving, deleting data and much more. +## Documentation + +List of endpoints exposed by the service + +## Setup +### Technologies used +- Languages: + - TypeScript +- Package manager: + - npm +- Stack to use: + - Node.js + - Express.js + - PostgresSQL +- Testing: + - Jest + - Supertest +- API Documentation + - Swagger Documentation + +### Getting Started + +- Clone this project on your local machine + ``` + git clone https://github.com/atlp-rwanda/knights-ecomm-be.git + ``` +- Navigate to project directory + ``` + cd knights-ecomm-be + ``` +- Install dependencies + ``` + npm install + ``` + +### Run The Service +- Run the application + ``` + nmp run dev + ``` + +## Testing + +- Run tests + ``` + npm test + ``` + +## Authors +- [Maxime Mizero](https://github.com/maxCastro1) +- [Elie Kuradusenge](https://github.com/elijahladdie) +- [Byishimo Teto Jose](https://github.com/MC-Knight) +- [Iragena Aime Divin](https://github.com/aimedivin) +- [Gloria Niyonkuru Sinseswa](https://github.com/GSinseswa721) +- [Grace Uwicyeza](https://github.com/UwicyezaG) +- [Jean Paul Elisa Ndevu](https://github.com/Ndevu12) +- [Gisa Mugisha Caleb Pacifique](https://github.com/Calebgisa72) \ No newline at end of file From 3ff45ef03ca946acf9e8e67af4cc9cfb0391cdea Mon Sep 17 00:00:00 2001 From: Iadivin Date: Thu, 25 Apr 2024 10:41:11 +0200 Subject: [PATCH 07/51] fix: change 'nmp run dev' to 'npm run dev' --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 48532f7..a5e9bd7 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ List of endpoints exposed by the service ### Run The Service - Run the application ``` - nmp run dev + npm run dev ``` ## Testing @@ -52,7 +52,7 @@ List of endpoints exposed by the service ## Authors - [Maxime Mizero](https://github.com/maxCastro1) - [Elie Kuradusenge](https://github.com/elijahladdie) -- [Byishimo Teto Jose](https://github.com/MC-Knight) +- [Byishimo Teto Joseph](https://github.com/MC-Knight) - [Iragena Aime Divin](https://github.com/aimedivin) - [Gloria Niyonkuru Sinseswa](https://github.com/GSinseswa721) - [Grace Uwicyeza](https://github.com/UwicyezaG) From 969e724b5c48f80db5f411fef4a0fd327d553f68 Mon Sep 17 00:00:00 2001 From: Mc-Knight Date: Wed, 24 Apr 2024 14:48:28 +0200 Subject: [PATCH 08/51] chore: swagger API documentation --- package.json | 4 ++++ src/configs/swagger.ts | 30 ++++++++++++++++++++++++++++++ src/docs/index.yml | 23 +++++++++++++++++++++++ src/index.ts | 13 ++++++++----- src/routes/index.ts | 8 ++++---- src/startups/docs.ts | 7 +++++++ src/startups/getSwaggerServer.ts | 13 +++++++++++++ 7 files changed, 89 insertions(+), 9 deletions(-) create mode 100644 src/configs/swagger.ts create mode 100644 src/docs/index.yml create mode 100644 src/startups/docs.ts create mode 100644 src/startups/getSwaggerServer.ts diff --git a/package.json b/package.json index d926574..580e10b 100644 --- a/package.json +++ b/package.json @@ -13,11 +13,15 @@ "author": " Scrum master", "license": "ISC", "dependencies": { + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.6", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.19.2", "jsend": "^1.1.0", "nodemon": "^3.1.0", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.0", "superagent": "^9.0.1", "ts-node": "^10.9.2", "typescript-jsend": "^0.1.1" diff --git a/src/configs/swagger.ts b/src/configs/swagger.ts new file mode 100644 index 0000000..89816be --- /dev/null +++ b/src/configs/swagger.ts @@ -0,0 +1,30 @@ +import swaggerJSDoc from "swagger-jsdoc"; +import { getSwaggerServer } from "../startups/getSwaggerServer"; + +const swaggerServer = getSwaggerServer(); + +const options: swaggerJSDoc.Options = { + definition: { + openapi: "3.0.1", + info: { + title: "Knights E-commerce API Documentation", + version: "1.0.0", + description: "knights E-commerce - Backend API", + }, + servers: [{ url: swaggerServer }], + components: { + securitySchemes: { + bearerAuth: { + type: "http", + scheme: "bearer", + bearerFormat: "JWT", + }, + }, + }, + }, + apis: ["./src/docs/*.ts", "./src/docs/*.yml"], +}; + +const swaggerSpec = swaggerJSDoc(options); + +export default swaggerSpec; diff --git a/src/docs/index.yml b/src/docs/index.yml new file mode 100644 index 0000000..fff85d2 --- /dev/null +++ b/src/docs/index.yml @@ -0,0 +1,23 @@ +/: + get: + tags: + - index + summary: "initial endpoint" + description: "This is initial endpoint" + responses: + "200": + description: "success response" + "500": + description: "server error" + +/status: + get: + tags: + - testing + summary: "status endpoint" + description: "This is status endpoint" + responses: + "200": + description: "success response" + "500": + description: "server error" diff --git a/src/index.ts b/src/index.ts index b33ebab..66fbc0d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,18 +2,21 @@ import express, { Request, Response } from "express"; import cors from "cors"; import dotenv from "dotenv"; import router from "./routes"; +import { addDocumentation } from "./startups/docs"; + dotenv.config(); export const app = express(); -const port = process.env.PORT as string +const port = process.env.PORT as string; app.use(express.json()); -app.use(cors()); +app.use(cors({ origin: "*" })); -app.get('/', (req: Request, res: Response) => { - res.send('Knights Ecommerce API'); +addDocumentation(app); +app.get("/api/v1", (req: Request, res: Response) => { + res.send("Knights Ecommerce API"); }); app.use(router) export const server = app.listen(port, () => { console.log(`[server]: Server is running at http://localhost:${port}`); -}) \ No newline at end of file +}) diff --git a/src/routes/index.ts b/src/routes/index.ts index aef2710..091ddbe 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,10 +1,10 @@ import { Request, Response, Router } from "express"; import { responseSuccess } from "../utils/response.utils"; const router = Router(); -router.get("/status", (req: Request, res: Response) => { - return responseSuccess(res, 202, "This is a testing route that returns: 201"); -}) +router.get("/api/v1/status", (req: Request, res: Response) => { + return responseSuccess(res, 202, "This is a testing route that returns: 201"); +}); // All routes should be imported here and get export after specifying first route // example router.use("/stock". stockRoutes) =>:: First import stockRoutes and use it here, This shows how the route export will be handled -export default router; \ No newline at end of file +export default router; diff --git a/src/startups/docs.ts b/src/startups/docs.ts new file mode 100644 index 0000000..355cfc1 --- /dev/null +++ b/src/startups/docs.ts @@ -0,0 +1,7 @@ +import { type Express } from "express"; +import swaggerUI from "swagger-ui-express"; +import swagger from "../configs/swagger"; + +export const addDocumentation = (app: Express): void => { + app.use("/api/v1/docs", swaggerUI.serve, swaggerUI.setup(swagger)); +}; diff --git a/src/startups/getSwaggerServer.ts b/src/startups/getSwaggerServer.ts new file mode 100644 index 0000000..52aa30a --- /dev/null +++ b/src/startups/getSwaggerServer.ts @@ -0,0 +1,13 @@ +import dotenv from "dotenv"; + +dotenv.config(); + +function getSwaggerServer(): string { + if (process.env.SWAGGER_SERVER !== undefined) { + return process.env.SWAGGER_SERVER; + } + + return "http://localhost:7000/api/v1"; +} + +export { getSwaggerServer }; From 084fc148699cb51f43399ee54e21c6e576bf5358 Mon Sep 17 00:00:00 2001 From: maxCastro1 Date: Fri, 26 Apr 2024 12:27:08 +0200 Subject: [PATCH 09/51] deleting duplicates in package.json --- package.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/package.json b/package.json index bd6dc1c..580e10b 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,6 @@ "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.0", "superagent": "^9.0.1", - "swagger-jsdoc": "^6.2.8", - "swagger-ui-express": "^5.0.0", "ts-node": "^10.9.2", "typescript-jsend": "^0.1.1" }, From e97eca05c00bb6a4ebc9f26cfcb4cf48b9aae048 Mon Sep 17 00:00:00 2001 From: "Gisa M. Caleb Pacifique" Date: Thu, 25 Apr 2024 06:41:53 +0200 Subject: [PATCH 10/51] Added a global error handler middleware --- src/index.ts | 8 ++++++++ src/middlewares/errorHandler.ts | 30 ++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 src/middlewares/errorHandler.ts diff --git a/src/index.ts b/src/index.ts index 66fbc0d..ac9c007 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ import dotenv from "dotenv"; import router from "./routes"; import { addDocumentation } from "./startups/docs"; +import {CustomError,errorHandler} from "./middlewares/errorHandler"; dotenv.config(); export const app = express(); @@ -12,11 +13,18 @@ app.use(express.json()); app.use(cors({ origin: "*" })); +app.all('*', (req: Request,res: Response,next) =>{ + const error = new CustomError(`Can't find ${req.originalUrl} on the server!`,404); + error.status = 'fail'; + next(error); +}) + addDocumentation(app); app.get("/api/v1", (req: Request, res: Response) => { res.send("Knights Ecommerce API"); }); app.use(router) +app.use(errorHandler); export const server = app.listen(port, () => { console.log(`[server]: Server is running at http://localhost:${port}`); }) diff --git a/src/middlewares/errorHandler.ts b/src/middlewares/errorHandler.ts new file mode 100644 index 0000000..e597542 --- /dev/null +++ b/src/middlewares/errorHandler.ts @@ -0,0 +1,30 @@ +import { Request, Response, NextFunction } from 'express'; + +class CustomError extends Error { + statusCode: number; + status: string; + + constructor(message: string, statusCode: number) { + super(message); + this.statusCode = statusCode; + this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error'; + Error.captureStackTrace(this, this.constructor); + } +} + +const errorHandler = ( + err: CustomError, + req: Request, + res: Response, + next: NextFunction +) => { + err.statusCode = err.statusCode || 500; + err.status = err.status || 'error'; + res.status(err.statusCode).json({ + status: err.statusCode, + message: err.message + }); + console.error(err.stack); +}; + +export { CustomError, errorHandler }; From 6190a172d8e61105237dae3812be7b2bd58bc729 Mon Sep 17 00:00:00 2001 From: GSinseswa721 Date: Wed, 24 Apr 2024 06:24:16 -0700 Subject: [PATCH 11/51] linking and formatting --- .eslintrc | 13 +++++++ .prettierrc | 6 ++++ eslint.config.mjs | 9 +++++ package.json | 19 ++++++---- src/configs/index.ts | 2 +- src/controllers/index.ts | 2 +- src/middlewares/index.ts | 2 +- src/services/index.ts | 2 +- src/utils/index.ts | 2 +- src/utils/response.utils.ts | 69 +++++++++++++++++++++++-------------- tsconfig.json | 18 +++++----- 11 files changed, 98 insertions(+), 46 deletions(-) create mode 100644 .eslintrc create mode 100644 .prettierrc create mode 100644 eslint.config.mjs diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..512673c --- /dev/null +++ b/.eslintrc @@ -0,0 +1,13 @@ +// // .eslintrc +// { +// "extends": ["eslint:recommended", "plugin:prettier/recommended"] +// } +{ + "devDependencies": { + "eslint": "^9.1.1", + "@eslint/js": "^2.1.0", + "typescript-eslint": "^7.7.1" + } +} + + \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..475ccf1 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "semi": false, + "tabWidth": 2, + "trailingComma": "none" +} diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..2ac6930 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,9 @@ +import globals from 'globals' +import pluginJs from '@eslint/js' +import tseslint from 'typescript-eslint' + +export default [ + { languageOptions: { globals: globals.browser } }, + pluginJs.configs.recommended, + ...tseslint.configs.recommended +] diff --git a/package.json b/package.json index 580e10b..019a1cd 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,10 @@ "test": "jest --coverage --detectOpenHandles --verbose --runInBand", "dev": "nodemon src/index.ts", "build": "tsc -p .", - "start": "node dist/index.js" + "start": "node dist/index.js", + "lint": "eslint .", + "lint:fix": "eslint --fix .", + "format": "prettier --write ." }, "keywords": [], "author": " Scrum master", @@ -27,15 +30,17 @@ "typescript-jsend": "^0.1.1" }, "devDependencies": { + "@eslint/js": "^9.1.1", "@types/body-parser": "^1.19.5", "@types/cors": "^2.8.17", "@types/express": "^4.17.21", - "@types/jest": "^29.5.12", "@types/jsend": "^1.0.32", - "@types/supertest": "^6.0.2", - "jest": "^29.7.0", - "supertest": "^6.3.4", - "ts-jest": "^29.1.2", - "typescript": "^5.4.5" + "@typescript-eslint/eslint-plugin": "^7.7.1", + "@typescript-eslint/parser": "^7.7.1", + "eslint": "^9.1.1", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", + "prettier": "^3.2.5", + "typescript-eslint": "^7.7.1" } } diff --git a/src/configs/index.ts b/src/configs/index.ts index ceca67a..4a6e728 100644 --- a/src/configs/index.ts +++ b/src/configs/index.ts @@ -1 +1 @@ -// export all configs \ No newline at end of file +// export all configs diff --git a/src/controllers/index.ts b/src/controllers/index.ts index 97a75e3..f4880c6 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -1 +1 @@ -// export all controllers \ No newline at end of file +// export all controllers diff --git a/src/middlewares/index.ts b/src/middlewares/index.ts index 147fee8..3cac451 100644 --- a/src/middlewares/index.ts +++ b/src/middlewares/index.ts @@ -1 +1 @@ -// export all middlewares \ No newline at end of file +// export all middlewares diff --git a/src/services/index.ts b/src/services/index.ts index 7242a4e..73cc501 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1 +1 @@ -// export all Services \ No newline at end of file +// export all Services diff --git a/src/utils/index.ts b/src/utils/index.ts index 6a3daee..490375a 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1 +1 @@ -// export all utils \ No newline at end of file +// export all utils diff --git a/src/utils/response.utils.ts b/src/utils/response.utils.ts index 47be81f..64fe34b 100644 --- a/src/utils/response.utils.ts +++ b/src/utils/response.utils.ts @@ -1,31 +1,50 @@ -import { Response } from 'express'; -import jsend from 'jsend'; +import { Response } from 'express' +import jsend from 'jsend' interface ApiResponse { - code: number; - resp_msg: string; - data?: any; + code: number + resp_msg: string + data?: any } -export const responseSuccess = (res: Response, status_code: number, message: string, data?: any): Response => { - return res.status(200).json(jsend.success({ - code: status_code, - message, - data, - })); -}; +export const responseSuccess = ( + res: Response, + status_code: number, + message: string, + data?: any +): Response => { + return res.status(200).json( + jsend.success({ + code: status_code, + message, + data + }) + ) +} -export const responseError = (res: Response, status_code: number, message: string, data?: any): Response => { - return res.status(400).json(jsend.error({ - code: status_code, - message, - data, - })); -}; +export const responseError = ( + res: Response, + status_code: number, + message: string, + data?: any +): Response => { + return res.status(400).json( + jsend.error({ + code: status_code, + message, + data + }) + ) +} -export const responseServerError = (res: Response, error: string): Response => { - return res.status(500).json(jsend.error({ - code: 999, - message: `There is a problem with the server!: ${error}`, - })); -}; \ No newline at end of file +export const responseServerError = ( + res: Response, + error: string +): Response => { + return res.status(500).json( + jsend.error({ + code: 999, + message: `There is a problem with the server!: ${error}` + }) + ) +} diff --git a/tsconfig.json b/tsconfig.json index dbd8deb..5adb021 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,7 @@ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ @@ -22,8 +22,8 @@ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ /* Modules */ - "module": "commonjs", /* Specify what module code is generated. */ - "rootDir": "./src", /* Specify the root folder within your source files. */ + "module": "commonjs" /* Specify what module code is generated. */, + "rootDir": "./src" /* Specify the root folder within your source files. */, // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ @@ -40,7 +40,7 @@ // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ /* JavaScript Support */ - "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + "allowJs": true /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */, // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ /* Emit */ @@ -50,7 +50,7 @@ // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - "outDir": "./dist", /* Specify an output folder for all emitted files. */ + "outDir": "./dist" /* Specify an output folder for all emitted files. */, // "removeComments": true, /* Disable emitting comments. */ // "noEmit": true, /* Disable emitting files from a compilation. */ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ @@ -71,11 +71,11 @@ // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, /* Type Checking */ - "strict": true, /* Enable all strict type-checking options. */ + "strict": true /* Enable all strict type-checking options. */, // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ @@ -98,4 +98,4 @@ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ } -} \ No newline at end of file +} From 6af2158aeb3dde708b3b52f02bc32815e577aaf0 Mon Sep 17 00:00:00 2001 From: GSinseswa721 Date: Thu, 25 Apr 2024 02:28:07 -0700 Subject: [PATCH 12/51] fixing eslint and tsconfiguration --- .eslintrc | 19 ++++++++++--------- .eslintrc.js | 10 ++++++++++ eslint.config.mjs | 9 --------- eslint.config.ts | 10 ++++++++++ package.json | 4 +++- src/controllers/index.ts | 4 ++++ tsconfig.json | 6 ++++-- 7 files changed, 41 insertions(+), 21 deletions(-) create mode 100644 .eslintrc.js delete mode 100644 eslint.config.mjs create mode 100644 eslint.config.ts diff --git a/.eslintrc b/.eslintrc index 512673c..65ba60e 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,13 +1,14 @@ // // .eslintrc -// { -// "extends": ["eslint:recommended", "plugin:prettier/recommended"] -// } -{ - "devDependencies": { - "eslint": "^9.1.1", - "@eslint/js": "^2.1.0", - "typescript-eslint": "^7.7.1" + { + "extends": ["eslint:recommended", "plugin:prettier/recommended"] } -} + +// { +// "devDependencies": { +// "eslint": "^9.1.1", +// "@eslint/js": "^2.1.0", +// "typescript-eslint": "^7.7.1" +// } +// } \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..b26b0b4 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,10 @@ +// // .eslintrc.js +// module.exports = { +// }; +module.exports = { + env: { + node: true, // Specify 'node' environment + es6: true, // Specify ECMAScript version 6 + }, + // Other ESLint configurations... + }; \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs deleted file mode 100644 index 2ac6930..0000000 --- a/eslint.config.mjs +++ /dev/null @@ -1,9 +0,0 @@ -import globals from 'globals' -import pluginJs from '@eslint/js' -import tseslint from 'typescript-eslint' - -export default [ - { languageOptions: { globals: globals.browser } }, - pluginJs.configs.recommended, - ...tseslint.configs.recommended -] diff --git a/eslint.config.ts b/eslint.config.ts new file mode 100644 index 0000000..67c6a34 --- /dev/null +++ b/eslint.config.ts @@ -0,0 +1,10 @@ +import globals from "globals"; +import pluginJs from "@eslint/js"; +import tseslint from "typescript-eslint"; + + +export default [ + {languageOptions: { globals: globals.browser }}, + pluginJs.configs.recommended, + ...tseslint.configs.recommended, +]; \ No newline at end of file diff --git a/package.json b/package.json index 019a1cd..6602088 100644 --- a/package.json +++ b/package.json @@ -33,11 +33,13 @@ "@eslint/js": "^9.1.1", "@types/body-parser": "^1.19.5", "@types/cors": "^2.8.17", + "@types/eslint": "^8.56.10", + "@types/eslint__js": "^8.42.3", "@types/express": "^4.17.21", "@types/jsend": "^1.0.32", "@typescript-eslint/eslint-plugin": "^7.7.1", "@typescript-eslint/parser": "^7.7.1", - "eslint": "^9.1.1", + "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", "prettier": "^3.2.5", diff --git a/src/controllers/index.ts b/src/controllers/index.ts index f4880c6..d9f4022 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -1 +1,5 @@ // export all controllers +function myFunction () { + console.log('Hello'); + } + myFunction(); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 5adb021..465158b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -29,7 +29,7 @@ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ - // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + "types": ["node"], /* Specify type package names to be included without being referenced in a source file. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ @@ -97,5 +97,7 @@ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ - } + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules"], } From 1a5992fe9bdbe4c9e5431d4e4890cbea42cc6e9b Mon Sep 17 00:00:00 2001 From: GSinseswa721 Date: Thu, 25 Apr 2024 03:12:49 -0700 Subject: [PATCH 13/51] fixing-eslint config --- .eslintrc | 21 +++++++++++---------- .eslintrc.js | 10 ---------- .eslintrc.ts | 2 ++ eslint.config.mjs | 9 +++++++++ eslint.config.ts | 34 +++++++++++++++++++++++++--------- package.json | 3 ++- src/controllers/index.ts | 8 ++++---- tsconfig.json | 6 ++++-- 8 files changed, 57 insertions(+), 36 deletions(-) delete mode 100644 .eslintrc.js create mode 100644 .eslintrc.ts create mode 100644 eslint.config.mjs diff --git a/.eslintrc b/.eslintrc index 65ba60e..5e321d5 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,14 +1,15 @@ // // .eslintrc - { - "extends": ["eslint:recommended", "plugin:prettier/recommended"] - } + // { + // "extends": ["eslint:recommended", "plugin:prettier/recommended"] + // } -// { -// "devDependencies": { -// "eslint": "^9.1.1", -// "@eslint/js": "^2.1.0", -// "typescript-eslint": "^7.7.1" -// } -// } +{ + "devDependencies": { + "eslint": "^9.1.1", + "@eslint/js": "^2.1.0", + "typescript-eslint": "^7.7.1" + } +} + \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index b26b0b4..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,10 +0,0 @@ -// // .eslintrc.js -// module.exports = { -// }; -module.exports = { - env: { - node: true, // Specify 'node' environment - es6: true, // Specify ECMAScript version 6 - }, - // Other ESLint configurations... - }; \ No newline at end of file diff --git a/.eslintrc.ts b/.eslintrc.ts new file mode 100644 index 0000000..8efc941 --- /dev/null +++ b/.eslintrc.ts @@ -0,0 +1,2 @@ +// .eslintrc.js +module.exports = {} diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..2ac6930 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,9 @@ +import globals from 'globals' +import pluginJs from '@eslint/js' +import tseslint from 'typescript-eslint' + +export default [ + { languageOptions: { globals: globals.browser } }, + pluginJs.configs.recommended, + ...tseslint.configs.recommended +] diff --git a/eslint.config.ts b/eslint.config.ts index 67c6a34..6be3fac 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -1,10 +1,26 @@ -import globals from "globals"; -import pluginJs from "@eslint/js"; -import tseslint from "typescript-eslint"; +// import globals from 'globals' +// import pluginJs from '@eslint/js' +// import tseslint from 'typescript-eslint' - -export default [ - {languageOptions: { globals: globals.browser }}, - pluginJs.configs.recommended, - ...tseslint.configs.recommended, -]; \ No newline at end of file +// export default [ +// { languageOptions: { globals: globals.browser } }, +// pluginJs.configs.recommended, +// ...tseslint.configs.recommended +// ] +module.exports = { + root: true, // Set root to true to stop ESLint from searching for parent configuration files + env: { + node: true // Set environment to Node.js + }, + parser: '@typescript-eslint/parser', // Specify the TypeScript parser + parserOptions: { + ecmaVersion: 2016, // Set ECMAScript version to 2021 (or appropriate version) + sourceType: 'module' // Set source type to module + }, + extends: ['eslint:recommended'], // Use recommended ESLint rules + rules: { + // Add custom rules here + // For example: + // 'no-console': 'off', // Disable 'no-console' rule + } +} diff --git a/package.json b/package.json index 6602088..e8dcc1e 100644 --- a/package.json +++ b/package.json @@ -39,9 +39,10 @@ "@types/jsend": "^1.0.32", "@typescript-eslint/eslint-plugin": "^7.7.1", "@typescript-eslint/parser": "^7.7.1", - "eslint": "^8.56.0", + "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", + "globals": "^15.0.0", "prettier": "^3.2.5", "typescript-eslint": "^7.7.1" } diff --git a/src/controllers/index.ts b/src/controllers/index.ts index d9f4022..eb5c0a8 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -1,5 +1,5 @@ // export all controllers -function myFunction () { - console.log('Hello'); - } - myFunction(); \ No newline at end of file +function myFunction() { + console.log('Hello') +} +myFunction() diff --git a/tsconfig.json b/tsconfig.json index 465158b..e88ebd7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -29,7 +29,9 @@ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ - "types": ["node"], /* Specify type package names to be included without being referenced in a source file. */ + "types": [ + "node" + ] /* Specify type package names to be included without being referenced in a source file. */, // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ @@ -99,5 +101,5 @@ "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, "include": ["src/**/*.ts"], - "exclude": ["node_modules"], + "exclude": ["node_modules"] } From de570073a6ff0a784061a6f774ca09384941a9bb Mon Sep 17 00:00:00 2001 From: GSinseswa721 Date: Fri, 26 Apr 2024 01:18:41 -0700 Subject: [PATCH 14/51] fixing prettier --- .eslintrc | 15 --------------- .eslintrc.js | 9 +++++++++ .eslintrc.ts | 2 -- .prettierrc | 21 ++++++++++++++++++--- eslint.config.mjs | 9 --------- eslint.config.ts | 26 -------------------------- package.json | 26 +++++++++++++++++++++++--- src/controllers/index.ts | 4 ++-- src/utils/response.utils.ts | 33 +++++++++++++++------------------ tsconfig.json | 4 +--- 10 files changed, 68 insertions(+), 81 deletions(-) delete mode 100644 .eslintrc create mode 100644 .eslintrc.js delete mode 100644 .eslintrc.ts delete mode 100644 eslint.config.mjs delete mode 100644 eslint.config.ts diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 5e321d5..0000000 --- a/.eslintrc +++ /dev/null @@ -1,15 +0,0 @@ -// // .eslintrc - // { - // "extends": ["eslint:recommended", "plugin:prettier/recommended"] - // } - -{ - "devDependencies": { - "eslint": "^9.1.1", - "@eslint/js": "^2.1.0", - "typescript-eslint": "^7.7.1" - } -} - - - \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..42f954f --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,9 @@ +module.exports = { + root: true, + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint'], + extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], + rules: { + '@typescript-eslint/no-explicit-any': 'off', + }, +}; diff --git a/.eslintrc.ts b/.eslintrc.ts deleted file mode 100644 index 8efc941..0000000 --- a/.eslintrc.ts +++ /dev/null @@ -1,2 +0,0 @@ -// .eslintrc.js -module.exports = {} diff --git a/.prettierrc b/.prettierrc index 475ccf1..157d2eb 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,6 +1,21 @@ { - "singleQuote": true, - "semi": false, + "trailingComma": "es5", "tabWidth": 2, - "trailingComma": "none" + "semi": true, + "singleQuote": true, + "printWidth": 120, + "bracketSpacing": true, + "arrowParens": "avoid", + "proseWrap": "always", + "jsxSingleQuote": true, + "quoteProps": "consistent", + "endOfLine": "auto", + "overrides": [ + { + "files": "*.js", + "options": { + "printWidth": 80 + } + } + ] } diff --git a/eslint.config.mjs b/eslint.config.mjs deleted file mode 100644 index 2ac6930..0000000 --- a/eslint.config.mjs +++ /dev/null @@ -1,9 +0,0 @@ -import globals from 'globals' -import pluginJs from '@eslint/js' -import tseslint from 'typescript-eslint' - -export default [ - { languageOptions: { globals: globals.browser } }, - pluginJs.configs.recommended, - ...tseslint.configs.recommended -] diff --git a/eslint.config.ts b/eslint.config.ts deleted file mode 100644 index 6be3fac..0000000 --- a/eslint.config.ts +++ /dev/null @@ -1,26 +0,0 @@ -// import globals from 'globals' -// import pluginJs from '@eslint/js' -// import tseslint from 'typescript-eslint' - -// export default [ -// { languageOptions: { globals: globals.browser } }, -// pluginJs.configs.recommended, -// ...tseslint.configs.recommended -// ] -module.exports = { - root: true, // Set root to true to stop ESLint from searching for parent configuration files - env: { - node: true // Set environment to Node.js - }, - parser: '@typescript-eslint/parser', // Specify the TypeScript parser - parserOptions: { - ecmaVersion: 2016, // Set ECMAScript version to 2021 (or appropriate version) - sourceType: 'module' // Set source type to module - }, - extends: ['eslint:recommended'], // Use recommended ESLint rules - rules: { - // Add custom rules here - // For example: - // 'no-console': 'off', // Disable 'no-console' rule - } -} diff --git a/package.json b/package.json index e8dcc1e..74f0a1e 100644 --- a/package.json +++ b/package.json @@ -7,13 +7,13 @@ "test": "jest --coverage --detectOpenHandles --verbose --runInBand", "dev": "nodemon src/index.ts", "build": "tsc -p .", - "start": "node dist/index.js", + "start": "node dist/index.js", "lint": "eslint .", "lint:fix": "eslint --fix .", "format": "prettier --write ." }, "keywords": [], - "author": " Scrum master", + "author": "Scrum master", "license": "ISC", "dependencies": { "@types/swagger-jsdoc": "^6.0.4", @@ -41,9 +41,29 @@ "@typescript-eslint/parser": "^7.7.1", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", + "eslint-plugin-custom-plugin": "^1.0.0", + "eslint-plugin-eslint-comments": "^3.2.0", "eslint-plugin-prettier": "^5.1.3", - "globals": "^15.0.0", "prettier": "^3.2.5", "typescript-eslint": "^7.7.1" + }, + "eslintConfig": { + "root": true, + "env": { + "node": true, + "es6": true + }, + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended" + ], + "parserOptions": { + "ecmaVersion": 2021, + "sourceType": "module" + }, + "rules": { + "@typescript-eslint/no-unused-vars": "error", + "@typescript-eslint/explicit-module-boundary-types": "off" + } } } diff --git a/src/controllers/index.ts b/src/controllers/index.ts index eb5c0a8..9459dbd 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -1,5 +1,5 @@ // export all controllers function myFunction() { - console.log('Hello') + console.log('Hello'); } -myFunction() +myFunction(); diff --git a/src/utils/response.utils.ts b/src/utils/response.utils.ts index 64fe34b..0f4b680 100644 --- a/src/utils/response.utils.ts +++ b/src/utils/response.utils.ts @@ -1,10 +1,10 @@ -import { Response } from 'express' -import jsend from 'jsend' +import { Response } from 'express'; +import jsend from 'jsend'; interface ApiResponse { - code: number - resp_msg: string - data?: any + code: number; + resp_msg: string; + data?: any; } export const responseSuccess = ( @@ -17,10 +17,10 @@ export const responseSuccess = ( jsend.success({ code: status_code, message, - data + data, }) - ) -} + ); +}; export const responseError = ( res: Response, @@ -32,19 +32,16 @@ export const responseError = ( jsend.error({ code: status_code, message, - data + data, }) - ) -} + ); +}; -export const responseServerError = ( - res: Response, - error: string -): Response => { +export const responseServerError = (res: Response, error: string): Response => { return res.status(500).json( jsend.error({ code: 999, - message: `There is a problem with the server!: ${error}` + message: `There is a problem with the server!: ${error}`, }) - ) -} + ); +}; diff --git a/tsconfig.json b/tsconfig.json index e88ebd7..bfaf5b0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -29,9 +29,7 @@ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ - "types": [ - "node" - ] /* Specify type package names to be included without being referenced in a source file. */, + "types": ["node"] /* Specify type package names to be included without being referenced in a source file. */, // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ From 312e0e7fb19cdc26c1face282787f49cef665533 Mon Sep 17 00:00:00 2001 From: GSinseswa721 Date: Fri, 26 Apr 2024 05:01:08 -0700 Subject: [PATCH 15/51] logger --- .eslintrc.js | 26 +++++++++++++- .gitignore | 4 ++- README.md | 28 +++++++++++---- jest.config.ts | 3 +- package.json | 18 ++++++++-- src/configs/swagger.ts | 20 +++++------ src/controllers/index.ts | 2 +- src/docs/index.yml | 23 ------------ src/index.ts | 16 ++++++--- src/middlewares/errorHandler.ts | 6 ++-- src/routes/index.ts | 8 ++--- src/startups/docs.ts | 8 ++--- src/startups/getSwaggerServer.ts | 6 ++-- src/utils/logger.ts | 60 ++++++++++++++++++++++++++++++++ 14 files changed, 164 insertions(+), 64 deletions(-) delete mode 100644 src/docs/index.yml create mode 100644 src/utils/logger.ts diff --git a/.eslintrc.js b/.eslintrc.js index 42f954f..47d4e4f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,3 +1,4 @@ +// eslint-disable-next-line no-undef module.exports = { root: true, parser: '@typescript-eslint/parser', @@ -5,5 +6,28 @@ module.exports = { extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], rules: { '@typescript-eslint/no-explicit-any': 'off', + "@typescript-eslint/no-unused-vars": [ + "warn", + { + "args": "all", + "argsIgnorePattern": "^_", + "caughtErrors": "all", + "caughtErrorsIgnorePattern": "^_", + "destructuredArrayIgnorePattern": "^_", + "varsIgnorePattern": "^_", + "ignoreRestSiblings": true + } + ], + 'no-undef': 'warn', + 'semi': ['warn', 'always'], + 'no-multi-spaces': 'warn', + 'no-trailing-spaces': 'warn', + 'space-before-function-paren': ['warn', 'always'], + 'func-style': ['warn', 'declaration', { 'allowArrowFunctions': true }], + 'camelcase': 'warn', + '@typescript-eslint/explicit-function-return-type': ['warn', { allowExpressions: true }], + '@typescript-eslint/explicit-member-accessibility': ['warn', { accessibility: 'explicit' }], + 'no-unused-vars': 'warn', + 'no-extra-semi': 'warn', }, -}; +}; \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2b9bb31..79afa5d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ node_modules .vscode .env package-lock.json -coverage/ \ No newline at end of file +coverage/ +dist +/src/logs \ No newline at end of file diff --git a/README.md b/README.md index a5e9bd7..7e2ae64 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,26 @@ # E-commerse Backend API + ## Description -This repository contains an E-commerce APIs, serving as backend for an E-commerce frontend application, It powers the functionalities for the frontend, such as storing, retrieving, deleting data and much more. + +This repository contains an E-commerce APIs, serving as backend for an E-commerce frontend application, It powers the +functionalities for the frontend, such as storing, retrieving, deleting data and much more. + ## Documentation List of endpoints exposed by the service ## Setup +- to use loggers in program use below functions +```bash +logger.error('This is an error message'); +logger.warn('This is a warning message'); +logger.info('This is an informational message'); +logger.debug('This is a debug message'); + +``` + ### Technologies used + - Languages: - TypeScript - Package manager: @@ -28,15 +42,16 @@ List of endpoints exposed by the service git clone https://github.com/atlp-rwanda/knights-ecomm-be.git ``` - Navigate to project directory - ``` - cd knights-ecomm-be - ``` -- Install dependencies + ``` + cd knights-ecomm-be + ``` +- Install dependencies ``` npm install ``` ### Run The Service + - Run the application ``` npm run dev @@ -50,6 +65,7 @@ List of endpoints exposed by the service ``` ## Authors + - [Maxime Mizero](https://github.com/maxCastro1) - [Elie Kuradusenge](https://github.com/elijahladdie) - [Byishimo Teto Joseph](https://github.com/MC-Knight) @@ -57,4 +73,4 @@ List of endpoints exposed by the service - [Gloria Niyonkuru Sinseswa](https://github.com/GSinseswa721) - [Grace Uwicyeza](https://github.com/UwicyezaG) - [Jean Paul Elisa Ndevu](https://github.com/Ndevu12) -- [Gisa Mugisha Caleb Pacifique](https://github.com/Calebgisa72) \ No newline at end of file +- [Gisa Mugisha Caleb Pacifique](https://github.com/Calebgisa72) diff --git a/jest.config.ts b/jest.config.ts index 04538ec..3d8dfdc 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -13,7 +13,6 @@ export default { ], coveragePathIgnorePatterns: [ "/node_modules/", // Exclude the node_modules directory - "/__tests__/", // Exclude the tests directory + "/__tests__/", // Exclude the tests directory ], }; - \ No newline at end of file diff --git a/package.json b/package.json index 74f0a1e..70d4bec 100644 --- a/package.json +++ b/package.json @@ -16,27 +16,37 @@ "author": "Scrum master", "license": "ISC", "dependencies": { + "@types/express-winston": "^4.0.0", "@types/swagger-jsdoc": "^6.0.4", "@types/swagger-ui-express": "^4.1.6", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.19.2", + "express-winston": "^4.2.0", "jsend": "^1.1.0", + "morgan": "^1.10.0", "nodemon": "^3.1.0", + "source-map-support": "^0.5.21", + "superagent": "^9.0.1", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.0", - "superagent": "^9.0.1", + "ts-log-debug": "^5.5.3", "ts-node": "^10.9.2", - "typescript-jsend": "^0.1.1" + "typescript-jsend": "^0.1.1", + "winston": "^3.13.0" }, "devDependencies": { "@eslint/js": "^9.1.1", "@types/body-parser": "^1.19.5", "@types/cors": "^2.8.17", + "@types/dotenv": "^8.2.0", "@types/eslint": "^8.56.10", "@types/eslint__js": "^8.42.3", "@types/express": "^4.17.21", + "@types/jest": "^29.5.12", "@types/jsend": "^1.0.32", + "@types/morgan": "^1.9.9", + "@types/winston": "^2.4.4", "@typescript-eslint/eslint-plugin": "^7.7.1", "@typescript-eslint/parser": "^7.7.1", "eslint": "^8.57.0", @@ -44,7 +54,11 @@ "eslint-plugin-custom-plugin": "^1.0.0", "eslint-plugin-eslint-comments": "^3.2.0", "eslint-plugin-prettier": "^5.1.3", + "jest": "^29.7.0", "prettier": "^3.2.5", + "supertest": "^7.0.0", + "ts-jest": "^29.1.2", + "typescript": "^5.4.5", "typescript-eslint": "^7.7.1" }, "eslintConfig": { diff --git a/src/configs/swagger.ts b/src/configs/swagger.ts index 89816be..239d43a 100644 --- a/src/configs/swagger.ts +++ b/src/configs/swagger.ts @@ -1,28 +1,28 @@ -import swaggerJSDoc from "swagger-jsdoc"; -import { getSwaggerServer } from "../startups/getSwaggerServer"; +import swaggerJSDoc from 'swagger-jsdoc'; +import { getSwaggerServer } from '../startups/getSwaggerServer'; const swaggerServer = getSwaggerServer(); const options: swaggerJSDoc.Options = { definition: { - openapi: "3.0.1", + openapi: '3.0.1', info: { - title: "Knights E-commerce API Documentation", - version: "1.0.0", - description: "knights E-commerce - Backend API", + title: 'Knights E-commerce API Documentation', + version: '1.0.0', + description: 'knights E-commerce - Backend API', }, servers: [{ url: swaggerServer }], components: { securitySchemes: { bearerAuth: { - type: "http", - scheme: "bearer", - bearerFormat: "JWT", + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', }, }, }, }, - apis: ["./src/docs/*.ts", "./src/docs/*.yml"], + apis: ['./src/docs/*.ts', './src/docs/*.yml'], }; const swaggerSpec = swaggerJSDoc(options); diff --git a/src/controllers/index.ts b/src/controllers/index.ts index 9459dbd..8c85221 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -1,5 +1,5 @@ // export all controllers -function myFunction() { +function myFunction () { console.log('Hello'); } myFunction(); diff --git a/src/docs/index.yml b/src/docs/index.yml deleted file mode 100644 index fff85d2..0000000 --- a/src/docs/index.yml +++ /dev/null @@ -1,23 +0,0 @@ -/: - get: - tags: - - index - summary: "initial endpoint" - description: "This is initial endpoint" - responses: - "200": - description: "success response" - "500": - description: "server error" - -/status: - get: - tags: - - testing - summary: "status endpoint" - description: "This is status endpoint" - responses: - "200": - description: "success response" - "500": - description: "server error" diff --git a/src/index.ts b/src/index.ts index ac9c007..c090dcb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ import router from "./routes"; import { addDocumentation } from "./startups/docs"; import {CustomError,errorHandler} from "./middlewares/errorHandler"; +import morgan from 'morgan'; dotenv.config(); export const app = express(); @@ -17,14 +18,21 @@ app.all('*', (req: Request,res: Response,next) =>{ const error = new CustomError(`Can't find ${req.originalUrl} on the server!`,404); error.status = 'fail'; next(error); -}) +}); addDocumentation(app); app.get("/api/v1", (req: Request, res: Response) => { res.send("Knights Ecommerce API"); }); -app.use(router) +app.use(router); app.use(errorHandler); -export const server = app.listen(port, () => { + +//morgan +const morganFormat = ':method :url :status :response-time ms - :res[content-length]'; +app.use(morgan(morganFormat)); + + + +app.listen(port, () => { console.log(`[server]: Server is running at http://localhost:${port}`); -}) +}); diff --git a/src/middlewares/errorHandler.ts b/src/middlewares/errorHandler.ts index e597542..74cca91 100644 --- a/src/middlewares/errorHandler.ts +++ b/src/middlewares/errorHandler.ts @@ -1,10 +1,10 @@ -import { Request, Response, NextFunction } from 'express'; +import { Request, Response } from 'express'; class CustomError extends Error { statusCode: number; status: string; - constructor(message: string, statusCode: number) { + constructor (message: string, statusCode: number) { super(message); this.statusCode = statusCode; this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error'; @@ -16,7 +16,7 @@ const errorHandler = ( err: CustomError, req: Request, res: Response, - next: NextFunction + ) => { err.statusCode = err.statusCode || 500; err.status = err.status || 'error'; diff --git a/src/routes/index.ts b/src/routes/index.ts index 091ddbe..8ae7a19 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,8 +1,8 @@ -import { Request, Response, Router } from "express"; -import { responseSuccess } from "../utils/response.utils"; +import { Request, Response, Router } from 'express'; +import { responseSuccess } from '../utils/response.utils'; const router = Router(); -router.get("/api/v1/status", (req: Request, res: Response) => { - return responseSuccess(res, 202, "This is a testing route that returns: 201"); +router.get('/api/v1/status', (req: Request, res: Response) => { + return responseSuccess(res, 202, 'This is a testing route that returns: 201'); }); // All routes should be imported here and get export after specifying first route // example router.use("/stock". stockRoutes) =>:: First import stockRoutes and use it here, This shows how the route export will be handled diff --git a/src/startups/docs.ts b/src/startups/docs.ts index 355cfc1..cf248b8 100644 --- a/src/startups/docs.ts +++ b/src/startups/docs.ts @@ -1,7 +1,7 @@ -import { type Express } from "express"; -import swaggerUI from "swagger-ui-express"; -import swagger from "../configs/swagger"; +import { type Express } from 'express'; +import swaggerUI from 'swagger-ui-express'; +import swagger from '../configs/swagger'; export const addDocumentation = (app: Express): void => { - app.use("/api/v1/docs", swaggerUI.serve, swaggerUI.setup(swagger)); + app.use('/api/v1/docs', swaggerUI.serve, swaggerUI.setup(swagger)); }; diff --git a/src/startups/getSwaggerServer.ts b/src/startups/getSwaggerServer.ts index 52aa30a..87a1f51 100644 --- a/src/startups/getSwaggerServer.ts +++ b/src/startups/getSwaggerServer.ts @@ -1,13 +1,13 @@ -import dotenv from "dotenv"; +import dotenv from 'dotenv'; dotenv.config(); -function getSwaggerServer(): string { +function getSwaggerServer (): string { if (process.env.SWAGGER_SERVER !== undefined) { return process.env.SWAGGER_SERVER; } - return "http://localhost:7000/api/v1"; + return 'http://localhost:7000/api/v1'; } export { getSwaggerServer }; diff --git a/src/utils/logger.ts b/src/utils/logger.ts new file mode 100644 index 0000000..e537ca6 --- /dev/null +++ b/src/utils/logger.ts @@ -0,0 +1,60 @@ +import { createLogger, format, transports } from 'winston'; +// import winston, { createLogger, transports, format } from 'winston'; + +// Define custom logging levels and colors +const logLevels = { + levels: { + error: 0, + warn: 1, + info: 2, + http: 3, + debug: 4, + }, + colors: { + error: 'red', + warn: 'yellow', + info: 'green', + http: 'magenta', + debug: 'cyan', + }, +}; + +// Configure Winston logger +const logger = createLogger({ + level: 'info', + levels: logLevels.levels, + format: format.combine( + format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + format.errors({ stack: true }), + format.splat(), + format.json() + ), + transports: [ + new transports.Console({ + format: format.combine( + format.colorize(), // Enable colorization + format.printf(({ level, message, timestamp }) => { + const color = logLevels.colors[level as keyof typeof logLevels.colors] || 'white'; + return `\x1b[${color}m${timestamp} [${level}]: ${message}\x1b[0m`; // Apply color to log message + }) + ), + }), + ], +}); + +// Add colors to the logger instance +const { combine, timestamp, printf, colorize } = format; +logger.add( + new transports.Console({ + format: combine( + colorize(), + timestamp(), + printf(({ level, message, timestamp }) => { + const color = logLevels.colors[level as keyof typeof logLevels.colors] || 'white'; + return `\x1b[${color}m${timestamp} [${level}]: ${message}\x1b[0m`; // Apply color to log message + }) + ), + }) +); + +export default logger; From 422d64f86e7690df80edc9406613b6545831a22e Mon Sep 17 00:00:00 2001 From: elijah Date: Sun, 28 Apr 2024 00:16:28 +0200 Subject: [PATCH 16/51] fixing eslint configuration and testing packages missing --- .eslintrc.js | 4 ++-- package.json | 20 +------------------- src/index.ts | 11 +++-------- src/routes/index.ts | 4 ++++ tsconfig.json | 2 +- 5 files changed, 11 insertions(+), 30 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 47d4e4f..885e908 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -18,7 +18,7 @@ module.exports = { "ignoreRestSiblings": true } ], - 'no-undef': 'warn', + 'no-undef': 'off', 'semi': ['warn', 'always'], 'no-multi-spaces': 'warn', 'no-trailing-spaces': 'warn', @@ -26,7 +26,7 @@ module.exports = { 'func-style': ['warn', 'declaration', { 'allowArrowFunctions': true }], 'camelcase': 'warn', '@typescript-eslint/explicit-function-return-type': ['warn', { allowExpressions: true }], - '@typescript-eslint/explicit-member-accessibility': ['warn', { accessibility: 'explicit' }], + '@typescript-eslint/explicit-member-accessibility': ['off', { accessibility: 'explicit' }], 'no-unused-vars': 'warn', 'no-extra-semi': 'warn', }, diff --git a/package.json b/package.json index 70d4bec..969c161 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "@types/jest": "^29.5.12", "@types/jsend": "^1.0.32", "@types/morgan": "^1.9.9", + "@types/supertest": "^6.0.2", "@types/winston": "^2.4.4", "@typescript-eslint/eslint-plugin": "^7.7.1", "@typescript-eslint/parser": "^7.7.1", @@ -60,24 +61,5 @@ "ts-jest": "^29.1.2", "typescript": "^5.4.5", "typescript-eslint": "^7.7.1" - }, - "eslintConfig": { - "root": true, - "env": { - "node": true, - "es6": true - }, - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended" - ], - "parserOptions": { - "ecmaVersion": 2021, - "sourceType": "module" - }, - "rules": { - "@typescript-eslint/no-unused-vars": "error", - "@typescript-eslint/explicit-module-boundary-types": "off" - } } } diff --git a/src/index.ts b/src/index.ts index c090dcb..f51225b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,18 +13,13 @@ const port = process.env.PORT as string; app.use(express.json()); app.use(cors({ origin: "*" })); - +app.use(router); +addDocumentation(app); app.all('*', (req: Request,res: Response,next) =>{ const error = new CustomError(`Can't find ${req.originalUrl} on the server!`,404); error.status = 'fail'; next(error); }); - -addDocumentation(app); -app.get("/api/v1", (req: Request, res: Response) => { - res.send("Knights Ecommerce API"); -}); -app.use(router); app.use(errorHandler); //morgan @@ -33,6 +28,6 @@ app.use(morgan(morganFormat)); -app.listen(port, () => { +export const server = app.listen(port, () => { console.log(`[server]: Server is running at http://localhost:${port}`); }); diff --git a/src/routes/index.ts b/src/routes/index.ts index 8ae7a19..78cb97d 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,9 +1,13 @@ import { Request, Response, Router } from 'express'; import { responseSuccess } from '../utils/response.utils'; const router = Router(); +router.get("/", (req: Request, res: Response) => { + res.send("Knights Ecommerce API"); +}); router.get('/api/v1/status', (req: Request, res: Response) => { return responseSuccess(res, 202, 'This is a testing route that returns: 201'); }); + // All routes should be imported here and get export after specifying first route // example router.use("/stock". stockRoutes) =>:: First import stockRoutes and use it here, This shows how the route export will be handled diff --git a/tsconfig.json b/tsconfig.json index bfaf5b0..7d6a779 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -29,7 +29,7 @@ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ - "types": ["node"] /* Specify type package names to be included without being referenced in a source file. */, + "types": ["node", "jest"] /* Specify type package names to be included without being referenced in a source file. */, // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ From f28b9c1ff044d414a526276ffdf5ed751a9272bc Mon Sep 17 00:00:00 2001 From: Jean Paul Elisa NIYOKWIZERWA Date: Thu, 25 Apr 2024 12:06:10 +0200 Subject: [PATCH 17/51] chore: database integration chore: database integration chore: swagger API documentation update update update update chore: database set up chore: database set up fix: resolve conflict in chore-db_set_up and develop branches ch: Updated README file fix: change 'nmp run dev' to 'npm run dev' created a new branch, modifie gitignore to not push coverage folder , and added a line to stop the server when done with testing chore: swagger API documentation deleting duplicates in package.json Added a global error handler middleware fix: resolving conflict fix 6148 bytes .eslintrc.js | 33 +++++++++++++ .gitignore | 5 +- .prettierrc | 21 +++++++++ README.md | 77 ++++++++++++++++++++++++++++++- jest.config.js | 20 ++++++++ package.json | 50 ++++++++++++++++++-- src/.DS_Store | Bin 0 -> 6148 bytes src/Onstart/dbConnection.ts | 14 ++++++ src/__test__/route.test.ts | 14 ++++++ src/configs/db_config.js | 38 +++++++++++++++ src/configs/db_config.ts | 39 ++++++++++++++++ src/configs/index.ts | 2 +- src/configs/swagger.js | 28 +++++++++++ src/configs/swagger.ts | 30 ++++++++++++ src/controllers/index.ts | 6 ++- src/index.js | 32 +++++++++++++ src/index.ts | 42 +++++++++++------ src/middlewares/errorHandler.js | 40 ++++++++++++++++ src/middlewares/errorHandler.ts | 30 ++++++++++++ src/middlewares/index.ts | 2 +- src/routes/index.js | 14 ++++++ src/routes/index.ts | 16 ++++--- src/services/index.ts | 2 +- src/startups/dbConnection.js | 58 +++++++++++++++++++++++ src/startups/docs.js | 9 ++++ src/startups/docs.ts | 7 +++ src/startups/getSwaggerServer.js | 12 +++++ src/startups/getSwaggerServer.ts | 13 ++++++ src/utils/index.ts | 2 +- src/utils/logger.ts | 60 ++++++++++++++++++++++++ src/utils/response.utils.js | 27 +++++++++++ src/utils/response.utils.ts | 56 ++++++++++++++-------- tsconfig.json | 24 +++++----- 34 files changed, 761 insertions(+), 62 deletions(-) create mode 100644 .DS_Store create mode 100644 .eslintrc.js create mode 100644 .prettierrc create mode 100644 jest.config.js create mode 100644 src/.DS_Store create mode 100644 src/Onstart/dbConnection.ts create mode 100644 src/__test__/route.test.ts create mode 100644 src/configs/db_config.js create mode 100644 src/configs/db_config.ts create mode 100644 src/configs/swagger.js create mode 100644 src/configs/swagger.ts create mode 100644 src/index.js create mode 100644 src/middlewares/errorHandler.js create mode 100644 src/middlewares/errorHandler.ts create mode 100644 src/routes/index.js create mode 100644 src/startups/dbConnection.js create mode 100644 src/startups/docs.js create mode 100644 src/startups/docs.ts create mode 100644 src/startups/getSwaggerServer.js create mode 100644 src/startups/getSwaggerServer.ts create mode 100644 src/utils/logger.ts create mode 100644 src/utils/response.utils.js diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..8d8b5223a8d468913133f64cb9e014d556216012 GIT binary patch literal 6148 zcmeHK&x_MQ6n@iYZR%R6u)~eZfYpZX20#%2Oe}=P zd8`%~`KioVMvbf?GJ1yJ-LTud*Pn~j16l#Cz~86G${KIO*&0dfoB4 zgJWjRzVqqLm*ZP$zx&g_o3mfc`uhioZ$qYvRJZ^EaB!gmP3Vc%5Pa-~`#x{0x#+`0 z^OU~}BieFaa{b2DqhH1*^Lbr3`Sw=kvvf^8ewyF!#3%Dz+5bpuxd?q|K^qRS?qZ(T zFk3GAWq1Z5wu70>-ytiVdHM-UcHA(7RVtrZCTpym%b7WI)qHFp@R1$0qG4ET4IWbO9_PL@ZMU5JUT4%S zuHEKA)bfJPKoC5)gCUO|c!A4DH9id7P;hPOF!N@fbfp#e HqYC^0$dUq} literal 0 HcmV?d00001 diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..885e908 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,33 @@ +// eslint-disable-next-line no-undef +module.exports = { + root: true, + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint'], + extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], + rules: { + '@typescript-eslint/no-explicit-any': 'off', + "@typescript-eslint/no-unused-vars": [ + "warn", + { + "args": "all", + "argsIgnorePattern": "^_", + "caughtErrors": "all", + "caughtErrorsIgnorePattern": "^_", + "destructuredArrayIgnorePattern": "^_", + "varsIgnorePattern": "^_", + "ignoreRestSiblings": true + } + ], + 'no-undef': 'off', + 'semi': ['warn', 'always'], + 'no-multi-spaces': 'warn', + 'no-trailing-spaces': 'warn', + 'space-before-function-paren': ['warn', 'always'], + 'func-style': ['warn', 'declaration', { 'allowArrowFunctions': true }], + 'camelcase': 'warn', + '@typescript-eslint/explicit-function-return-type': ['warn', { allowExpressions: true }], + '@typescript-eslint/explicit-member-accessibility': ['off', { accessibility: 'explicit' }], + 'no-unused-vars': 'warn', + 'no-extra-semi': 'warn', + }, +}; \ No newline at end of file diff --git a/.gitignore b/.gitignore index a104b9a..79afa5d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ node_modules .vscode .env -package-lock.json \ No newline at end of file +package-lock.json +coverage/ +dist +/src/logs \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..157d2eb --- /dev/null +++ b/.prettierrc @@ -0,0 +1,21 @@ +{ + "trailingComma": "es5", + "tabWidth": 2, + "semi": true, + "singleQuote": true, + "printWidth": 120, + "bracketSpacing": true, + "arrowParens": "avoid", + "proseWrap": "always", + "jsxSingleQuote": true, + "quoteProps": "consistent", + "endOfLine": "auto", + "overrides": [ + { + "files": "*.js", + "options": { + "printWidth": 80 + } + } + ] +} diff --git a/README.md b/README.md index b291bad..7e2ae64 100644 --- a/README.md +++ b/README.md @@ -1 +1,76 @@ -# Backend API +# E-commerse Backend API + +## Description + +This repository contains an E-commerce APIs, serving as backend for an E-commerce frontend application, It powers the +functionalities for the frontend, such as storing, retrieving, deleting data and much more. + +## Documentation + +List of endpoints exposed by the service + +## Setup +- to use loggers in program use below functions +```bash +logger.error('This is an error message'); +logger.warn('This is a warning message'); +logger.info('This is an informational message'); +logger.debug('This is a debug message'); + +``` + +### Technologies used + +- Languages: + - TypeScript +- Package manager: + - npm +- Stack to use: + - Node.js + - Express.js + - PostgresSQL +- Testing: + - Jest + - Supertest +- API Documentation + - Swagger Documentation + +### Getting Started + +- Clone this project on your local machine + ``` + git clone https://github.com/atlp-rwanda/knights-ecomm-be.git + ``` +- Navigate to project directory + ``` + cd knights-ecomm-be + ``` +- Install dependencies + ``` + npm install + ``` + +### Run The Service + +- Run the application + ``` + npm run dev + ``` + +## Testing + +- Run tests + ``` + npm test + ``` + +## Authors + +- [Maxime Mizero](https://github.com/maxCastro1) +- [Elie Kuradusenge](https://github.com/elijahladdie) +- [Byishimo Teto Joseph](https://github.com/MC-Knight) +- [Iragena Aime Divin](https://github.com/aimedivin) +- [Gloria Niyonkuru Sinseswa](https://github.com/GSinseswa721) +- [Grace Uwicyeza](https://github.com/UwicyezaG) +- [Jean Paul Elisa Ndevu](https://github.com/Ndevu12) +- [Gisa Mugisha Caleb Pacifique](https://github.com/Calebgisa72) diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..2eab859 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,20 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ +exports.default = { + preset: "ts-jest", + testEnvironment: "node", + testMatch: ["**/**/*.test.ts"], + verbose: true, + forceExit: true, + clearMocks: true, + resetMocks: true, + restoreMocks: true, + collectCoverageFrom: [ + "src/**/*.{ts,tsx}", // Include all JavaScript/JSX files in the src directory + ], + coveragePathIgnorePatterns: [ + "/node_modules/", // Exclude the node_modules directory + "/__tests__/", // Exclude the tests directory + ], +}; diff --git a/package.json b/package.json index ddbe706..87ca304 100644 --- a/package.json +++ b/package.json @@ -4,28 +4,68 @@ "description": "E-commerce backend", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", + "test": "jest --coverage --detectOpenHandles --verbose --runInBand", "dev": "nodemon src/index.ts", "build": "tsc -p .", - "start": "node dist/index.js" + "start": "node dist/index.js", + "lint": "eslint .", + "lint:fix": "eslint --fix .", + "format": "prettier --write ." }, "keywords": [], - "author": " Scrum master", + "author": "Scrum master", "license": "ISC", "dependencies": { + "@types/express-winston": "^4.0.0", + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.6", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.19.2", + "highlight.js": "^11.9.0", + "express-winston": "^4.2.0", "jsend": "^1.1.0", + "morgan": "^1.10.0", "nodemon": "^3.1.0", + "pg": "^8.11.5", + "reflect-metadata": "^0.2.2", + "source-map-support": "^0.5.21", + "superagent": "^9.0.1", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.0", + "ts-log-debug": "^5.5.3", "ts-node": "^10.9.2", + "typeorm": "^0.3.20", "typescript": "^5.4.5", - "typescript-jsend": "^0.1.1" + "typescript-jsend": "^0.1.1", + "winston": "^3.13.0" }, "devDependencies": { + "@eslint/js": "^9.1.1", "@types/body-parser": "^1.19.5", "@types/cors": "^2.8.17", + "@types/dotenv": "^8.2.0", + "@types/eslint": "^8.56.10", + "@types/eslint__js": "^8.42.3", "@types/express": "^4.17.21", - "@types/jsend": "^1.0.32" + "@types/jest": "^29.5.12", + "@types/jsend": "^1.0.32", + "@types/node": "^20.12.7", + "@types/morgan": "^1.9.9", + "@types/supertest": "^6.0.2", + "@types/winston": "^2.4.4", + "@typescript-eslint/eslint-plugin": "^7.7.1", + "@typescript-eslint/parser": "^7.7.1", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-custom-plugin": "^1.0.0", + "eslint-plugin-eslint-comments": "^3.2.0", + "eslint-plugin-prettier": "^5.1.3", + "jest": "^29.7.0", + "prettier": "^3.2.5", + "supertest": "^7.0.0", + "ts-jest": "^29.1.2", + "typescript": "^5.4.5", + "typescript-eslint": "^7.7.1" } } diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..181b83d6ac5e82818ee978408e78daf3b5f01b0c GIT binary patch literal 6148 zcmeHKOHRWu6dadAgR<$84N^`}i5rBfELd{^Xi}<5p^^Flmf3R$uE1$H56rWlqSh(f zszUEY&*L~Vb~29Q7=RmZ%Nt+_AYlsjE?FE1xh~m>p0*5#&iApv9P9h4E~Zt7Z4IaB zfXv+h1!^qujGg(br@SJs#VvF6RP{BuaZ3*Jxstm4q; zjzc`PFbB*5bKs;M;F>Lwqyeoo2h0I;pmjjL4;fQ1S8M|MPX`OX1t9j>?1OdrX%tPS zm@75`d4=JElo+H=w-_$S*`6scS8M_XIh<}joQ~{thvNL`>_78#xICbh=72e{@4#6& z9m@KD_xJvPUu0+IfI0B59B}=~!(_}y(zUhoaI)5B%txk#@S1?FgcZLOlPgQ{71IaX XGpP`B#U>zI81^IJZLq=|II07`e!6Po literal 0 HcmV?d00001 diff --git a/src/Onstart/dbConnection.ts b/src/Onstart/dbConnection.ts new file mode 100644 index 0000000..fc7843a --- /dev/null +++ b/src/Onstart/dbConnection.ts @@ -0,0 +1,14 @@ +import { OrmConfig } from "../configs/db_config"; + +const dbConnection = async () => { + await OrmConfig.initialize() + .then(() => { + console.log("[db]: Database connected successfully"); + }) + .catch((error) => { + console.log("[db]: Database connection failed"); + console.log(error); + }); +}; + +export { dbConnection }; diff --git a/src/__test__/route.test.ts b/src/__test__/route.test.ts new file mode 100644 index 0000000..af92a44 --- /dev/null +++ b/src/__test__/route.test.ts @@ -0,0 +1,14 @@ +import request from 'supertest'; +import {app, server} from '../index'; // update this with the path to your app file + +describe('GET /', () => { + afterAll(done => { + server.close(done); + }); + + it('responds with "Knights Ecommerce API"', done => { + request(app) + .get('/') + .expect(200, 'Knights Ecommerce API', done); + }); +}); diff --git a/src/configs/db_config.js b/src/configs/db_config.js new file mode 100644 index 0000000..1f61526 --- /dev/null +++ b/src/configs/db_config.js @@ -0,0 +1,38 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.OrmConfig = void 0; +var typeorm_1 = require("typeorm"); +var PORT = process.env.DEV_DB_PORT; +var DB_HOST = process.env.DEV_DB_HOST; +var DB_USER = process.env.DEV_DB_USER; +var DB_PASS = process.env.DEV_DB_PASS; +var DB_NAME = process.env.DEV_DB_NAME; +var DEV_DB_TYPE = process.env.DEV_DB_TYPE; +var port = PORT ? Number(PORT) : 5432; +var dbHost = DB_HOST ? DB_HOST : "localhost"; +var dbUser = DB_USER ? DB_USER : "test"; +var dbPass = DB_PASS ? DB_PASS : "test"; +var dbName = DB_NAME ? DB_NAME : "test"; +var dbType = DEV_DB_TYPE ? DEV_DB_TYPE : "postgres"; +var OrmConfig = new typeorm_1.DataSource({ + type: dbType, + host: dbHost, + port: port, + username: dbUser, + password: dbPass, + database: dbName, + synchronize: true, + logging: false, + // entities: [ + // "src/entity/**/*.ts" + // ], + // migrations: [ + // "src/migration/**/*.ts" + // ], + // extra: { + // ssl: { + // rejectUnauthorized: false, + // }, + // }, +}); +exports.OrmConfig = OrmConfig; diff --git a/src/configs/db_config.ts b/src/configs/db_config.ts new file mode 100644 index 0000000..2aa85b3 --- /dev/null +++ b/src/configs/db_config.ts @@ -0,0 +1,39 @@ +import { DataSource } from "typeorm"; + +const PORT = process.env.DEV_DB_PORT; +const DB_HOST = process.env.DEV_DB_HOST; +const DB_USER = process.env.DEV_DB_USER; +const DB_PASS = process.env.DEV_DB_PASS; +const DB_NAME = process.env.DEV_DB_NAME; +const DEV_DB_TYPE = process.env.DEV_DB_TYPE; + +const port = PORT ? Number(PORT) : 5432; +const dbHost = DB_HOST ? DB_HOST : "localhost"; +const dbUser = DB_USER ? DB_USER : "test"; +const dbPass = DB_PASS ? DB_PASS : "test"; +const dbName = DB_NAME ? DB_NAME : "test"; +const dbType = DEV_DB_TYPE ? DEV_DB_TYPE : "postgres"; + +const OrmConfig = new DataSource({ + type: dbType as any, + host: dbHost, + port: port, + username: dbUser, + password: dbPass, + database: dbName, + synchronize: true, + logging: false, + // entities: [ + // "src/entity/**/*.ts" + // ], + // migrations: [ + // "src/migration/**/*.ts" + // ], + // extra: { + // ssl: { + // rejectUnauthorized: false, + // }, + // }, +}); + +export { OrmConfig }; diff --git a/src/configs/index.ts b/src/configs/index.ts index ceca67a..4a6e728 100644 --- a/src/configs/index.ts +++ b/src/configs/index.ts @@ -1 +1 @@ -// export all configs \ No newline at end of file +// export all configs diff --git a/src/configs/swagger.js b/src/configs/swagger.js new file mode 100644 index 0000000..44a32d6 --- /dev/null +++ b/src/configs/swagger.js @@ -0,0 +1,28 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +var swagger_jsdoc_1 = require("swagger-jsdoc"); +var getSwaggerServer_1 = require("../startups/getSwaggerServer"); +var swaggerServer = (0, getSwaggerServer_1.getSwaggerServer)(); +var options = { + definition: { + openapi: '3.0.1', + info: { + title: 'Knights E-commerce API Documentation', + version: '1.0.0', + description: 'knights E-commerce - Backend API', + }, + servers: [{ url: swaggerServer }], + components: { + securitySchemes: { + bearerAuth: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + }, + }, + }, + }, + apis: ['./src/docs/*.ts', './src/docs/*.yml'], +}; +var swaggerSpec = (0, swagger_jsdoc_1.default)(options); +exports.default = swaggerSpec; diff --git a/src/configs/swagger.ts b/src/configs/swagger.ts new file mode 100644 index 0000000..239d43a --- /dev/null +++ b/src/configs/swagger.ts @@ -0,0 +1,30 @@ +import swaggerJSDoc from 'swagger-jsdoc'; +import { getSwaggerServer } from '../startups/getSwaggerServer'; + +const swaggerServer = getSwaggerServer(); + +const options: swaggerJSDoc.Options = { + definition: { + openapi: '3.0.1', + info: { + title: 'Knights E-commerce API Documentation', + version: '1.0.0', + description: 'knights E-commerce - Backend API', + }, + servers: [{ url: swaggerServer }], + components: { + securitySchemes: { + bearerAuth: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + }, + }, + }, + }, + apis: ['./src/docs/*.ts', './src/docs/*.yml'], +}; + +const swaggerSpec = swaggerJSDoc(options); + +export default swaggerSpec; diff --git a/src/controllers/index.ts b/src/controllers/index.ts index 97a75e3..8c85221 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -1 +1,5 @@ -// export all controllers \ No newline at end of file +// export all controllers +function myFunction () { + console.log('Hello'); +} +myFunction(); diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..4934b3d --- /dev/null +++ b/src/index.js @@ -0,0 +1,32 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.server = exports.app = void 0; +var express_1 = require("express"); +var cors_1 = require("cors"); +var dotenv_1 = require("dotenv"); +var routes_1 = require("./routes"); +var docs_1 = require("./startups/docs"); +var errorHandler_1 = require("./middlewares/errorHandler"); +var morgan_1 = require("morgan"); +var dbConnection_1 = require("./startups/dbConnection"); +dotenv_1.default.config(); +exports.app = (0, express_1.default)(); +var port = process.env.PORT; +exports.app.use(express_1.default.json()); +exports.app.use((0, cors_1.default)({ origin: '*' })); +exports.app.use(routes_1.default); +(0, docs_1.addDocumentation)(exports.app); +exports.app.all('*', function (req, res, next) { + var error = new errorHandler_1.CustomError("Can't find ".concat(req.originalUrl, " on the server!"), 404); + error.status = 'fail'; + next(error); +}); +exports.app.use(errorHandler_1.errorHandler); +// Start database connection +(0, dbConnection_1.dbConnection)(); +//morgan +var morganFormat = ':method :url :status :response-time ms - :res[content-length]'; +exports.app.use((0, morgan_1.default)(morganFormat)); +exports.server = exports.app.listen(port, function () { + console.log("[server]: Server is running at http://localhost:".concat(port)); +}); diff --git a/src/index.ts b/src/index.ts index 046a8dc..d118a8e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,19 +1,35 @@ -import express, { Request, Response } from "express"; -import cors from "cors"; -import dotenv from "dotenv"; -import router from "./routes"; +import express, { Request, Response } from 'express'; +import cors from 'cors'; +import dotenv from 'dotenv'; +import router from './routes'; +import { addDocumentation } from './startups/docs'; + +import { CustomError, errorHandler } from './middlewares/errorHandler'; +import morgan from 'morgan'; +import { dbConnection } from './startups/dbConnection'; dotenv.config(); -const app = express(); -const port = process.env.PORT as string +export const app = express(); +const port = process.env.PORT as string; app.use(express.json()); -app.use(cors()); +app.use(cors({ origin: '*' })); +app.use(router); +addDocumentation(app); +app.all('*', (req: Request, res: Response, next) => { + const error = new CustomError(`Can't find ${req.originalUrl} on the server!`, 404); + error.status = 'fail'; + next(error); +}); +app.use(errorHandler); + +// Start database connection +dbConnection(); + +//morgan +const morganFormat = ':method :url :status :response-time ms - :res[content-length]'; +app.use(morgan(morganFormat)); -app.get('/', (req: Request, res: Response) => { - res.send('Knights Ecommerce API'); +export const server = app.listen(port, () => { + console.log(`[server]: Server is running at http://localhost:${port}`); }); -app.use(router) -app.listen(port, () => { - console.log(`[server]: Server is running at http://localhost:${port}`); -}) \ No newline at end of file diff --git a/src/middlewares/errorHandler.js b/src/middlewares/errorHandler.js new file mode 100644 index 0000000..005cd14 --- /dev/null +++ b/src/middlewares/errorHandler.js @@ -0,0 +1,40 @@ +"use strict"; +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + if (typeof b !== "function" && b !== null) + throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.errorHandler = exports.CustomError = void 0; +var CustomError = /** @class */ (function (_super) { + __extends(CustomError, _super); + function CustomError(message, statusCode) { + var _this = _super.call(this, message) || this; + _this.statusCode = statusCode; + _this.status = "".concat(statusCode).startsWith('4') ? 'fail' : 'error'; + Error.captureStackTrace(_this, _this.constructor); + return _this; + } + return CustomError; +}(Error)); +exports.CustomError = CustomError; +var errorHandler = function (err, req, res) { + err.statusCode = err.statusCode || 500; + err.status = err.status || 'error'; + res.status(err.statusCode).json({ + status: err.statusCode, + message: err.message + }); + console.error(err.stack); +}; +exports.errorHandler = errorHandler; diff --git a/src/middlewares/errorHandler.ts b/src/middlewares/errorHandler.ts new file mode 100644 index 0000000..74cca91 --- /dev/null +++ b/src/middlewares/errorHandler.ts @@ -0,0 +1,30 @@ +import { Request, Response } from 'express'; + +class CustomError extends Error { + statusCode: number; + status: string; + + constructor (message: string, statusCode: number) { + super(message); + this.statusCode = statusCode; + this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error'; + Error.captureStackTrace(this, this.constructor); + } +} + +const errorHandler = ( + err: CustomError, + req: Request, + res: Response, + +) => { + err.statusCode = err.statusCode || 500; + err.status = err.status || 'error'; + res.status(err.statusCode).json({ + status: err.statusCode, + message: err.message + }); + console.error(err.stack); +}; + +export { CustomError, errorHandler }; diff --git a/src/middlewares/index.ts b/src/middlewares/index.ts index 147fee8..3cac451 100644 --- a/src/middlewares/index.ts +++ b/src/middlewares/index.ts @@ -1 +1 @@ -// export all middlewares \ No newline at end of file +// export all middlewares diff --git a/src/routes/index.js b/src/routes/index.js new file mode 100644 index 0000000..c97f953 --- /dev/null +++ b/src/routes/index.js @@ -0,0 +1,14 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +var express_1 = require("express"); +var response_utils_1 = require("../utils/response.utils"); +var router = (0, express_1.Router)(); +router.get("/", function (req, res) { + res.send("Knights Ecommerce API"); +}); +router.get('/api/v1/status', function (req, res) { + return (0, response_utils_1.responseSuccess)(res, 202, 'This is a testing route that returns: 201'); +}); +// All routes should be imported here and get export after specifying first route +// example router.use("/stock". stockRoutes) =>:: First import stockRoutes and use it here, This shows how the route export will be handled +exports.default = router; diff --git a/src/routes/index.ts b/src/routes/index.ts index aef2710..78cb97d 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,10 +1,14 @@ -import { Request, Response, Router } from "express"; -import { responseSuccess } from "../utils/response.utils"; +import { Request, Response, Router } from 'express'; +import { responseSuccess } from '../utils/response.utils'; const router = Router(); -router.get("/status", (req: Request, res: Response) => { - return responseSuccess(res, 202, "This is a testing route that returns: 201"); -}) +router.get("/", (req: Request, res: Response) => { + res.send("Knights Ecommerce API"); +}); +router.get('/api/v1/status', (req: Request, res: Response) => { + return responseSuccess(res, 202, 'This is a testing route that returns: 201'); +}); + // All routes should be imported here and get export after specifying first route // example router.use("/stock". stockRoutes) =>:: First import stockRoutes and use it here, This shows how the route export will be handled -export default router; \ No newline at end of file +export default router; diff --git a/src/services/index.ts b/src/services/index.ts index 7242a4e..73cc501 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1 +1 @@ -// export all Services \ No newline at end of file +// export all Services diff --git a/src/startups/dbConnection.js b/src/startups/dbConnection.js new file mode 100644 index 0000000..26e52af --- /dev/null +++ b/src/startups/dbConnection.js @@ -0,0 +1,58 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; + return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.dbConnection = void 0; +var db_config_1 = require("../configs/db_config"); +var dbConnection = function () { return __awaiter(void 0, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, db_config_1.OrmConfig.initialize() + .then(function () { + console.log("[db]: Database connected successfully"); + }) + .catch(function (error) { + console.log("[db]: Database connection failed"); + console.log(error); + })]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); +}); }; +exports.dbConnection = dbConnection; diff --git a/src/startups/docs.js b/src/startups/docs.js new file mode 100644 index 0000000..31219be --- /dev/null +++ b/src/startups/docs.js @@ -0,0 +1,9 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.addDocumentation = void 0; +var swagger_ui_express_1 = require("swagger-ui-express"); +var swagger_1 = require("../configs/swagger"); +var addDocumentation = function (app) { + app.use('/api/v1/docs', swagger_ui_express_1.default.serve, swagger_ui_express_1.default.setup(swagger_1.default)); +}; +exports.addDocumentation = addDocumentation; diff --git a/src/startups/docs.ts b/src/startups/docs.ts new file mode 100644 index 0000000..cf248b8 --- /dev/null +++ b/src/startups/docs.ts @@ -0,0 +1,7 @@ +import { type Express } from 'express'; +import swaggerUI from 'swagger-ui-express'; +import swagger from '../configs/swagger'; + +export const addDocumentation = (app: Express): void => { + app.use('/api/v1/docs', swaggerUI.serve, swaggerUI.setup(swagger)); +}; diff --git a/src/startups/getSwaggerServer.js b/src/startups/getSwaggerServer.js new file mode 100644 index 0000000..f6f9deb --- /dev/null +++ b/src/startups/getSwaggerServer.js @@ -0,0 +1,12 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getSwaggerServer = void 0; +var dotenv_1 = require("dotenv"); +dotenv_1.default.config(); +function getSwaggerServer() { + if (process.env.SWAGGER_SERVER !== undefined) { + return process.env.SWAGGER_SERVER; + } + return 'http://localhost:7000/api/v1'; +} +exports.getSwaggerServer = getSwaggerServer; diff --git a/src/startups/getSwaggerServer.ts b/src/startups/getSwaggerServer.ts new file mode 100644 index 0000000..87a1f51 --- /dev/null +++ b/src/startups/getSwaggerServer.ts @@ -0,0 +1,13 @@ +import dotenv from 'dotenv'; + +dotenv.config(); + +function getSwaggerServer (): string { + if (process.env.SWAGGER_SERVER !== undefined) { + return process.env.SWAGGER_SERVER; + } + + return 'http://localhost:7000/api/v1'; +} + +export { getSwaggerServer }; diff --git a/src/utils/index.ts b/src/utils/index.ts index 6a3daee..490375a 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1 +1 @@ -// export all utils \ No newline at end of file +// export all utils diff --git a/src/utils/logger.ts b/src/utils/logger.ts new file mode 100644 index 0000000..e537ca6 --- /dev/null +++ b/src/utils/logger.ts @@ -0,0 +1,60 @@ +import { createLogger, format, transports } from 'winston'; +// import winston, { createLogger, transports, format } from 'winston'; + +// Define custom logging levels and colors +const logLevels = { + levels: { + error: 0, + warn: 1, + info: 2, + http: 3, + debug: 4, + }, + colors: { + error: 'red', + warn: 'yellow', + info: 'green', + http: 'magenta', + debug: 'cyan', + }, +}; + +// Configure Winston logger +const logger = createLogger({ + level: 'info', + levels: logLevels.levels, + format: format.combine( + format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + format.errors({ stack: true }), + format.splat(), + format.json() + ), + transports: [ + new transports.Console({ + format: format.combine( + format.colorize(), // Enable colorization + format.printf(({ level, message, timestamp }) => { + const color = logLevels.colors[level as keyof typeof logLevels.colors] || 'white'; + return `\x1b[${color}m${timestamp} [${level}]: ${message}\x1b[0m`; // Apply color to log message + }) + ), + }), + ], +}); + +// Add colors to the logger instance +const { combine, timestamp, printf, colorize } = format; +logger.add( + new transports.Console({ + format: combine( + colorize(), + timestamp(), + printf(({ level, message, timestamp }) => { + const color = logLevels.colors[level as keyof typeof logLevels.colors] || 'white'; + return `\x1b[${color}m${timestamp} [${level}]: ${message}\x1b[0m`; // Apply color to log message + }) + ), + }) +); + +export default logger; diff --git a/src/utils/response.utils.js b/src/utils/response.utils.js new file mode 100644 index 0000000..9ed32f3 --- /dev/null +++ b/src/utils/response.utils.js @@ -0,0 +1,27 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.responseServerError = exports.responseError = exports.responseSuccess = void 0; +var jsend_1 = require("jsend"); +var responseSuccess = function (res, status_code, message, data) { + return res.status(200).json(jsend_1.default.success({ + code: status_code, + message: message, + data: data, + })); +}; +exports.responseSuccess = responseSuccess; +var responseError = function (res, status_code, message, data) { + return res.status(400).json(jsend_1.default.error({ + code: status_code, + message: message, + data: data, + })); +}; +exports.responseError = responseError; +var responseServerError = function (res, error) { + return res.status(500).json(jsend_1.default.error({ + code: 999, + message: "There is a problem with the server!: ".concat(error), + })); +}; +exports.responseServerError = responseServerError; diff --git a/src/utils/response.utils.ts b/src/utils/response.utils.ts index 47be81f..0f4b680 100644 --- a/src/utils/response.utils.ts +++ b/src/utils/response.utils.ts @@ -2,30 +2,46 @@ import { Response } from 'express'; import jsend from 'jsend'; interface ApiResponse { - code: number; - resp_msg: string; - data?: any; + code: number; + resp_msg: string; + data?: any; } -export const responseSuccess = (res: Response, status_code: number, message: string, data?: any): Response => { - return res.status(200).json(jsend.success({ - code: status_code, - message, - data, - })); +export const responseSuccess = ( + res: Response, + status_code: number, + message: string, + data?: any +): Response => { + return res.status(200).json( + jsend.success({ + code: status_code, + message, + data, + }) + ); }; -export const responseError = (res: Response, status_code: number, message: string, data?: any): Response => { - return res.status(400).json(jsend.error({ - code: status_code, - message, - data, - })); +export const responseError = ( + res: Response, + status_code: number, + message: string, + data?: any +): Response => { + return res.status(400).json( + jsend.error({ + code: status_code, + message, + data, + }) + ); }; export const responseServerError = (res: Response, error: string): Response => { - return res.status(500).json(jsend.error({ - code: 999, - message: `There is a problem with the server!: ${error}`, - })); -}; \ No newline at end of file + return res.status(500).json( + jsend.error({ + code: 999, + message: `There is a problem with the server!: ${error}`, + }) + ); +}; diff --git a/tsconfig.json b/tsconfig.json index dbd8deb..7d6a779 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,7 @@ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ @@ -22,14 +22,14 @@ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ /* Modules */ - "module": "commonjs", /* Specify what module code is generated. */ - "rootDir": "./src", /* Specify the root folder within your source files. */ + "module": "commonjs" /* Specify what module code is generated. */, + "rootDir": "./src" /* Specify the root folder within your source files. */, // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ - // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + "types": ["node", "jest"] /* Specify type package names to be included without being referenced in a source file. */, // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ @@ -40,7 +40,7 @@ // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ /* JavaScript Support */ - "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + "allowJs": true /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */, // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ /* Emit */ @@ -50,7 +50,7 @@ // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - "outDir": "./dist", /* Specify an output folder for all emitted files. */ + "outDir": "./dist" /* Specify an output folder for all emitted files. */, // "removeComments": true, /* Disable emitting comments. */ // "noEmit": true, /* Disable emitting files from a compilation. */ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ @@ -71,11 +71,11 @@ // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, /* Type Checking */ - "strict": true, /* Enable all strict type-checking options. */ + "strict": true /* Enable all strict type-checking options. */, // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ @@ -97,5 +97,7 @@ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ - } -} \ No newline at end of file + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules"] +} From 87b1b491c6582287653abf03ec0ab174321126c5 Mon Sep 17 00:00:00 2001 From: Icyeza Date: Sun, 28 Apr 2024 23:47:39 +0200 Subject: [PATCH 18/51] setup environment variable example --- .env.example | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..0a48e9f --- /dev/null +++ b/.env.example @@ -0,0 +1,13 @@ +PORT= ******************************** +APP_ENV= ******************************** +PDN_DB_NAME= ***************************** +DEV_DB_HOST= ******************************** +DEV_DB_PORT= ******************************** +DEV_DB_USER= ******************************** +DEV_DB_PASS= ***************************** +DEV_DB_TYPE= ******************************* + +PDN_DB_HOST= ******************************** +PDN_DB_PORT= ******************************** +PDN_DB_USER= ******************************** +PDN_DB_PASS= ******************************** \ No newline at end of file From 5b779095154e2f84d17e191c04842883b4494ada Mon Sep 17 00:00:00 2001 From: Iadivin Date: Sun, 28 Apr 2024 01:54:59 +0200 Subject: [PATCH 19/51] chore(README): adds project badges - Added badges for Build status, Coverage Status, and Version [Delivers #12, #11] --- .DS_Store | Bin 6148 -> 0 bytes .gitignore | 3 ++- README.md | 1 + src/.DS_Store | Bin 6148 -> 0 bytes 4 files changed, 3 insertions(+), 1 deletion(-) delete mode 100644 .DS_Store delete mode 100644 src/.DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 8d8b5223a8d468913133f64cb9e014d556216012..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK&x_MQ6n@iYZR%R6u)~eZfYpZX20#%2Oe}=P zd8`%~`KioVMvbf?GJ1yJ-LTud*Pn~j16l#Cz~86G${KIO*&0dfoB4 zgJWjRzVqqLm*ZP$zx&g_o3mfc`uhioZ$qYvRJZ^EaB!gmP3Vc%5Pa-~`#x{0x#+`0 z^OU~}BieFaa{b2DqhH1*^Lbr3`Sw=kvvf^8ewyF!#3%Dz+5bpuxd?q|K^qRS?qZ(T zFk3GAWq1Z5wu70>-ytiVdHM-UcHA(7RVtrZCTpym%b7WI)qHFp@R1$0qG4ET4IWbO9_PL@ZMU5JUT4%S zuHEKA)bfJPKoC5)gCUO|c!A4DH9id7P;hPOF!N@fbfp#e HqYC^0$dUq} diff --git a/.gitignore b/.gitignore index 79afa5d..1500c37 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ node_modules package-lock.json coverage/ dist -/src/logs \ No newline at end of file +/src/logs +.DS_Store \ No newline at end of file diff --git a/README.md b/README.md index 7e2ae64..96947a9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # E-commerse Backend API +[![knights-ecomm-be CI](https://github.com/atlp-rwanda/knights-ecomm-be/actions/workflows/ci.yml/badge.svg)](https://github.com/atlp-rwanda/knights-ecomm-be/actions/workflows/ci.yml)    [![Coverage Status](https://coveralls.io/repos/github/atlp-rwanda/knights-ecomm-be/badge.svg?branch=ch-ci-setup)](https://coveralls.io/github/atlp-rwanda/knights-ecomm-be?branch=ch-ci-setup)    [![Version](https://img.shields.io/badge/version-1.0.0-blue)](https://github.com/your-username/your-repo-name/releases/tag/v1.0.0) ## Description diff --git a/src/.DS_Store b/src/.DS_Store deleted file mode 100644 index 8a187faf661cfcad1bb1cda282b9847f2577b232..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKOHRWu5Pc2>g1YIF4N^`}i5rBfELisf(56(CLL&7C%j~%WSKu_92j1ADs&&d1 zMQA1(KgWLK&vR?X0NiL>+yVmtDP1tRVsgTyUc68kIeKBK6VZC5DBeZvr&nD1^P zejhK4DELVGuXth~^H$V*6m{Xpv}OA(ZnB8DDb{4Iuwy^76a24;e-3GP$??Q<6@@+y z6gpt;CabEfE9sm65HnN26fgxkqJZv5X}=@mv0<13ra*TTko_T}3+9SVK)-b`@mBz1 zkHy}Ymybq?$rN+NCLn8QE`$<8sM9Nk3*ju!l$R?u0Yf;PUOt@m?DU4>eD7>O^K`g8 zprxjODR8X7S$G}D`G5cW{r@<~-b?{g;9n`=dgIA>#3||7+M1l4wHf`9E+)Jt;68*I ie-x7|NAWe?8_P4v5Oc*QAWLZWA>d`O#1!~b1%3caJ#cjZ From 618cb5ce2d70d3aaba0873a85744ca8cb4632310 Mon Sep 17 00:00:00 2001 From: Iadivin Date: Fri, 26 Apr 2024 11:41:00 +0200 Subject: [PATCH 20/51] ch: Setup CI workflow for testing, coverage reporting, building and linting --- .eslintrc.js | 29 +++++++++++++----------- .github/workflows/ci.yml | 40 +++++++++++++++++++++++++++++++++ .gitignore | 2 +- README.md | 2 ++ jest.config.ts | 32 +++++++++++++------------- ormconfig.js | 24 ++++++++++++++++++++ src/__test__/route.test.ts | 24 ++++++++++++++------ src/configs/db_config.ts | 28 ----------------------- src/controllers/index.ts | 2 +- src/index.ts | 2 +- src/middlewares/errorHandler.ts | 37 +++++++++++++----------------- src/startups/dbConnection.ts | 17 ++++++-------- src/utils/response.utils.ts | 8 +++---- 13 files changed, 145 insertions(+), 102 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 ormconfig.js delete mode 100644 src/configs/db_config.ts diff --git a/.eslintrc.js b/.eslintrc.js index 885e908..7d487c3 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -3,31 +3,34 @@ module.exports = { root: true, parser: '@typescript-eslint/parser', plugins: ['@typescript-eslint'], - extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended' + ], rules: { '@typescript-eslint/no-explicit-any': 'off', - "@typescript-eslint/no-unused-vars": [ - "warn", + '@typescript-eslint/no-unused-vars': [ + 'warn', { - "args": "all", - "argsIgnorePattern": "^_", - "caughtErrors": "all", - "caughtErrorsIgnorePattern": "^_", - "destructuredArrayIgnorePattern": "^_", - "varsIgnorePattern": "^_", - "ignoreRestSiblings": true - } + args: 'all', + argsIgnorePattern: '^_', + caughtErrors: 'all', + caughtErrorsIgnorePattern: '^_', + destructuredArrayIgnorePattern: '^_', + varsIgnorePattern: '^_', + ignoreRestSiblings: true, + }, ], 'no-undef': 'off', 'semi': ['warn', 'always'], 'no-multi-spaces': 'warn', 'no-trailing-spaces': 'warn', 'space-before-function-paren': ['warn', 'always'], - 'func-style': ['warn', 'declaration', { 'allowArrowFunctions': true }], + 'func-style': ['warn', 'declaration', { allowArrowFunctions: true }], 'camelcase': 'warn', '@typescript-eslint/explicit-function-return-type': ['warn', { allowExpressions: true }], '@typescript-eslint/explicit-member-accessibility': ['off', { accessibility: 'explicit' }], 'no-unused-vars': 'warn', 'no-extra-semi': 'warn', }, -}; \ No newline at end of file +}; diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..99db3dd --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,40 @@ +name: knights-ecomm-be CI + +on: [push, pull_request] + +env: + DEV_DB_HOST: ${{secrets.DEV_DB_HOST}} + DEV_DB_PORT: ${{secrets.DEV_DB_PORT}} + DEV_DB_USER: ${{secrets.DEV_DB_USER}} + DEV_DB_PASS: ${{secrets.DEV_DB_PASS}} + DEV_DB_NAME: ${{secrets.DEV_DB_NAME}} + +jobs: + build-lint-test-coverage: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + + - name: Install dependencies + run: npm install + + - name: Run ESLint and Prettier + run: npm run lint + + - name: Build project + run: npm run build --if-present + + - name: Run tests + run: npm test + + - name: Upload coverage report to Coveralls + uses: coverallsapp/github-action@v2.2.3 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 1500c37..66612c2 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,4 @@ package-lock.json coverage/ dist /src/logs -.DS_Store \ No newline at end of file +.DS_Stor \ No newline at end of file diff --git a/README.md b/README.md index 96947a9..782f032 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,9 @@ functionalities for the frontend, such as storing, retrieving, deleting data and List of endpoints exposed by the service ## Setup + - to use loggers in program use below functions + ```bash logger.error('This is an error message'); logger.warn('This is a warning message'); diff --git a/jest.config.ts b/jest.config.ts index 3d8dfdc..296ffe3 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -1,18 +1,18 @@ /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ export default { - preset: "ts-jest", - testEnvironment: "node", - testMatch: ["**/**/*.test.ts"], - verbose: true, - forceExit: true, - clearMocks: true, - resetMocks: true, - restoreMocks: true, - collectCoverageFrom: [ - "src/**/*.{ts,tsx}", // Include all JavaScript/JSX files in the src directory - ], - coveragePathIgnorePatterns: [ - "/node_modules/", // Exclude the node_modules directory - "/__tests__/", // Exclude the tests directory - ], - }; + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/**/*.test.ts'], + verbose: true, + forceExit: true, + clearMocks: true, + resetMocks: true, + restoreMocks: true, + collectCoverageFrom: [ + 'src/**/*.{ts,tsx}', // Include all JavaScript/JSX files in the src directory + ], + coveragePathIgnorePatterns: [ + '/node_modules/', // Exclude the node_modules directory + '/__tests__/', // Exclude the tests directory + ], +}; diff --git a/ormconfig.js b/ormconfig.js new file mode 100644 index 0000000..a4d78a5 --- /dev/null +++ b/ormconfig.js @@ -0,0 +1,24 @@ +module.exports = { + "type": "postgres", + "host": `${process.env.DEV_DB_HOST}`, + "port": `${process.env.DEV_DB_PORT}`, + "username": `${process.env.DEV_DB_USER}`, + "password": `${process.env.DEV_DB_PASS}`, + "database": `${process.env.DEV_DB_NAME}`, + "synchronize": true, + "logging": false, + "entities": [ + "src/entities/**/*.ts" + ], + "migrations": [ + "src/migrations/**/*.ts" + ], + "subscribers": [ + "src/subscribers/**/*.ts" + ], + "cli": { + "entitiesDir": "src/entities", + "migrationsDir": "src/migrations", + "subscribersDir": "src/subscribers" + } +}; \ No newline at end of file diff --git a/src/__test__/route.test.ts b/src/__test__/route.test.ts index af92a44..401f5b8 100644 --- a/src/__test__/route.test.ts +++ b/src/__test__/route.test.ts @@ -1,14 +1,24 @@ import request from 'supertest'; -import {app, server} from '../index'; // update this with the path to your app file +import { app, server } from '../index'; // update this with the path to your app file + +import { createConnection, getConnection, getConnectionOptions } from 'typeorm'; + +beforeAll(async () => { + // Connect to the test database + const connectionOptions = await getConnectionOptions(); + await createConnection({ ...connectionOptions, name: 'testConnection' }); +}); +afterAll(async () => { + await getConnection('testConnection').close(); + server.close(); +}); describe('GET /', () => { - afterAll(done => { - server.close(done); - }); + // afterAll(done => { + // server.close(done); + // }); it('responds with "Knights Ecommerce API"', done => { - request(app) - .get('/') - .expect(200, 'Knights Ecommerce API', done); + request(app).get('/').expect(200, 'Knights Ecommerce API', done); }); }); diff --git a/src/configs/db_config.ts b/src/configs/db_config.ts deleted file mode 100644 index d2a19a4..0000000 --- a/src/configs/db_config.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { DataSource } from 'typeorm'; - -const PORT = process.env.DEV_DB_PORT; -const DB_HOST = process.env.DEV_DB_HOST; -const DB_USER = process.env.DEV_DB_USER; -const DB_PASS = process.env.DEV_DB_PASS; -const DB_NAME = process.env.DEV_DB_NAME; -const DEV_DB_TYPE = process.env.DEV_DB_TYPE; - -const port = PORT ? Number(PORT) : 5432; -const dbHost = DB_HOST ? DB_HOST : 'localhost'; -const dbUser = DB_USER ? DB_USER : 'test'; -const dbPass = DB_PASS ? DB_PASS : 'test'; -const dbName = DB_NAME ? DB_NAME : 'test'; -const dbType = DEV_DB_TYPE ? DEV_DB_TYPE : 'postgres'; - -const OrmConfig = new DataSource({ - type: dbType as any, - host: dbHost, - port: port, - username: dbUser, - password: dbPass, - database: dbName, - synchronize: true, - logging: false, -}); - -export { OrmConfig }; diff --git a/src/controllers/index.ts b/src/controllers/index.ts index 8c85221..4ba41c9 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -1,5 +1,5 @@ // export all controllers -function myFunction () { +function myFunction (): void { console.log('Hello'); } myFunction(); diff --git a/src/index.ts b/src/index.ts index d118a8e..36b592e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,7 +10,7 @@ import { dbConnection } from './startups/dbConnection'; dotenv.config(); export const app = express(); -const port = process.env.PORT as string; +const port = process.env.PORT || 8000; app.use(express.json()); app.use(cors({ origin: '*' })); diff --git a/src/middlewares/errorHandler.ts b/src/middlewares/errorHandler.ts index 74cca91..e4e967f 100644 --- a/src/middlewares/errorHandler.ts +++ b/src/middlewares/errorHandler.ts @@ -1,30 +1,25 @@ import { Request, Response } from 'express'; class CustomError extends Error { - statusCode: number; - status: string; + statusCode: number; + status: string; - constructor (message: string, statusCode: number) { - super(message); - this.statusCode = statusCode; - this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error'; - Error.captureStackTrace(this, this.constructor); - } + constructor (message: string, statusCode: number) { + super(message); + this.statusCode = statusCode; + this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error'; + Error.captureStackTrace(this, this.constructor); + } } -const errorHandler = ( - err: CustomError, - req: Request, - res: Response, - -) => { - err.statusCode = err.statusCode || 500; - err.status = err.status || 'error'; - res.status(err.statusCode).json({ - status: err.statusCode, - message: err.message - }); - console.error(err.stack); +const errorHandler = (err: CustomError, req: Request, res: Response) => { + err.statusCode = err.statusCode || 500; + err.status = err.status || 'error'; + res.status(err.statusCode).json({ + status: err.statusCode, + message: err.message, + }); + console.error(err.stack); }; export { CustomError, errorHandler }; diff --git a/src/startups/dbConnection.ts b/src/startups/dbConnection.ts index fc7843a..7fb387c 100644 --- a/src/startups/dbConnection.ts +++ b/src/startups/dbConnection.ts @@ -1,14 +1,11 @@ -import { OrmConfig } from "../configs/db_config"; +import { createConnection } from "typeorm"; const dbConnection = async () => { - await OrmConfig.initialize() - .then(() => { - console.log("[db]: Database connected successfully"); - }) - .catch((error) => { - console.log("[db]: Database connection failed"); - console.log(error); - }); + try { + const connection = await createConnection(); + console.log('Connected to the database'); + } catch (error) { + console.error('Error connecting to the database:', error); + } }; - export { dbConnection }; diff --git a/src/utils/response.utils.ts b/src/utils/response.utils.ts index 0f4b680..6f097a7 100644 --- a/src/utils/response.utils.ts +++ b/src/utils/response.utils.ts @@ -9,13 +9,13 @@ interface ApiResponse { export const responseSuccess = ( res: Response, - status_code: number, + statusCode: number, message: string, data?: any ): Response => { return res.status(200).json( jsend.success({ - code: status_code, + code: statusCode, message, data, }) @@ -24,13 +24,13 @@ export const responseSuccess = ( export const responseError = ( res: Response, - status_code: number, + statusCode: number, message: string, data?: any ): Response => { return res.status(400).json( jsend.error({ - code: status_code, + code: statusCode, message, data, }) From f590128548e767898b1c16f3a6aead2125714876 Mon Sep 17 00:00:00 2001 From: maxCastro1 Date: Wed, 1 May 2024 22:30:30 +0200 Subject: [PATCH 21/51] Implemented user registration feature with input validation, password hashing, and database integration --- .gitignore | 2 +- migrations/1714595134552-UserMigration.ts | 31 ++++++++++ package.json | 15 +++-- src/__test__/route.test.ts | 57 ++++++++++++++++--- src/controllers/authController.ts | 49 ++++++++++++++++ src/controllers/index.ts | 8 +-- src/entities/User.ts | 69 +++++++++++++++++++++++ src/index.ts | 4 +- src/middlewares/errorHandler.ts | 4 +- src/routes/UserRoutes.ts | 11 ++++ src/routes/index.ts | 11 ++-- tsconfig.json | 4 +- 12 files changed, 237 insertions(+), 28 deletions(-) create mode 100644 migrations/1714595134552-UserMigration.ts create mode 100644 src/controllers/authController.ts create mode 100644 src/entities/User.ts create mode 100644 src/routes/UserRoutes.ts diff --git a/.gitignore b/.gitignore index 66612c2..1500c37 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,4 @@ package-lock.json coverage/ dist /src/logs -.DS_Stor \ No newline at end of file +.DS_Store \ No newline at end of file diff --git a/migrations/1714595134552-UserMigration.ts b/migrations/1714595134552-UserMigration.ts new file mode 100644 index 0000000..05693f8 --- /dev/null +++ b/migrations/1714595134552-UserMigration.ts @@ -0,0 +1,31 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class CreateUserMigration1614495123940 implements MigrationInterface { + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE "user" ( + "id" uuid NOT NULL DEFAULT uuid_generate_v4(), + "firstName" character varying NOT NULL, + "lastName" character varying NOT NULL, + "email" character varying NOT NULL, + "password" character varying NOT NULL, + "gender" character varying NOT NULL, + "phoneNumber" character varying NOT NULL, + "photoUrl" character varying, + "verified" boolean NOT NULL, + "status" character varying NOT NULL CHECK (status IN ('active', 'suspended')), + "userType" character varying NOT NULL DEFAULT 'Buyer' CHECK (userType IN ('Admin', 'Buyer', 'Vendor')), + "createdAt" TIMESTAMP NOT NULL DEFAULT now(), + "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), + CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"), + CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id") + ) + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "user"`); + } + +} \ No newline at end of file diff --git a/package.json b/package.json index 87ca304..84a5b11 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,9 @@ "start": "node dist/index.js", "lint": "eslint .", "lint:fix": "eslint --fix .", - "format": "prettier --write ." + "format": "prettier --write .", + "typeorm": "typeorm-ts-node-commonjs", + "migration": " npm run typeorm migration:run -- -d ./ormconfig.js" }, "keywords": [], "author": "Scrum master", @@ -19,11 +21,13 @@ "@types/express-winston": "^4.0.0", "@types/swagger-jsdoc": "^6.0.4", "@types/swagger-ui-express": "^4.1.6", + "bcrypt": "^5.1.1", + "class-validator": "^0.14.1", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.19.2", - "highlight.js": "^11.9.0", "express-winston": "^4.2.0", + "highlight.js": "^11.9.0", "jsend": "^1.1.0", "morgan": "^1.10.0", "nodemon": "^3.1.0", @@ -42,6 +46,7 @@ }, "devDependencies": { "@eslint/js": "^9.1.1", + "@types/bcrypt": "^5.0.2", "@types/body-parser": "^1.19.5", "@types/cors": "^2.8.17", "@types/dotenv": "^8.2.0", @@ -50,8 +55,9 @@ "@types/express": "^4.17.21", "@types/jest": "^29.5.12", "@types/jsend": "^1.0.32", - "@types/node": "^20.12.7", "@types/morgan": "^1.9.9", + "@types/node": "^20.12.7", + "@types/reflect-metadata": "^0.1.0", "@types/supertest": "^6.0.2", "@types/winston": "^2.4.4", "@typescript-eslint/eslint-plugin": "^7.7.1", @@ -62,10 +68,11 @@ "eslint-plugin-eslint-comments": "^3.2.0", "eslint-plugin-prettier": "^5.1.3", "jest": "^29.7.0", + "jest-mock-extended": "^3.0.6", "prettier": "^3.2.5", "supertest": "^7.0.0", "ts-jest": "^29.1.2", "typescript": "^5.4.5", "typescript-eslint": "^7.7.1" } -} +} \ No newline at end of file diff --git a/src/__test__/route.test.ts b/src/__test__/route.test.ts index 401f5b8..6b296a3 100644 --- a/src/__test__/route.test.ts +++ b/src/__test__/route.test.ts @@ -1,24 +1,67 @@ import request from 'supertest'; import { app, server } from '../index'; // update this with the path to your app file - import { createConnection, getConnection, getConnectionOptions } from 'typeorm'; +import { User } from '../entities/User'; +import { getRepository, Repository } from 'typeorm'; beforeAll(async () => { // Connect to the test database const connectionOptions = await getConnectionOptions(); await createConnection({ ...connectionOptions, name: 'testConnection' }); }); + afterAll(async () => { await getConnection('testConnection').close(); server.close(); }); -describe('GET /', () => { - // afterAll(done => { - // server.close(done); - // }); - it('responds with "Knights Ecommerce API"', done => { - request(app).get('/').expect(200, 'Knights Ecommerce API', done); + +describe('GET /', () => { + it('This is a testing route that returns', done => { + request(app) + .get('/api/v1/status') + .expect(200) + .expect('Content-Type', /json/) + .expect({ + status: 'success', + data: { + code: 202, + message: 'This is a testing route that returns: 202' + } + }, done); }); }); +describe('POST /user/register', () => { + it('should register a new user and then delete it', async () => { + // Arrange + const newUser = { + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@example.com', + password: 'password', + gender: 'Male', + phoneNumber: '1234567890', + userType: 'Buyer', + status: 'active', + verified: true, + photoUrl: 'https://example.com/photo.jpg', + }; + + // Act + const res = await request(app) + .post('/user/register') + .send(newUser); + + // Assert + expect(res.status).toBe(201); + expect(res.body).toEqual({ message: 'User registered successfully' }); + + // Clean up: delete the test user + const userRepository = getRepository(User); + const user = await userRepository.findOne({ where: { email: newUser.email } }); + if (user) { + await userRepository.remove(user); + } + }); +}); \ No newline at end of file diff --git a/src/controllers/authController.ts b/src/controllers/authController.ts new file mode 100644 index 0000000..209a350 --- /dev/null +++ b/src/controllers/authController.ts @@ -0,0 +1,49 @@ +import { Request, Response } from 'express'; +import { User } from '../entities/User';; +import bcrypt from 'bcrypt'; +import { getRepository } from 'typeorm'; + + +class UserController { + static registerUser = async (req: Request, res: Response) => { + const { firstName, lastName, email, password, gender, phoneNumber, userType, status, verified, photoUrl } = req.body; + + // Validate user input + if (!(firstName && lastName && email && password && gender && phoneNumber && verified && photoUrl)) { + return res.status(400).json({ error: 'Please fill all the fields' }); + } + + const userRepository = getRepository(User); + + + // Check for existing user + const existingUser = await userRepository.findOneBy({ email }); + const existingUserNumber = await userRepository.findOneBy({ phoneNumber }); + + if (existingUser || existingUserNumber) { + return res.status(400).json({ error: 'Email or phone number already in use' }); + } + + const saltRounds = 10; + const hashedPassword = await bcrypt.hash(password, saltRounds); + + // Create user + const user = new User(); + user.firstName = firstName; + user.lastName = lastName; + user.email = email; + user.password = hashedPassword; + user.userType = userType; + user.gender = gender; + user.phoneNumber = phoneNumber; + user.photoUrl = photoUrl; + user.status = status ? status : 'active'; + user.verified = verified; + + // Save user + await userRepository.save(user); + + return res.status(201).json({ message: 'User registered successfully' }); + }; +} +export { UserController }; \ No newline at end of file diff --git a/src/controllers/index.ts b/src/controllers/index.ts index 4ba41c9..554dd4e 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -1,5 +1,3 @@ -// export all controllers -function myFunction (): void { - console.log('Hello'); -} -myFunction(); +import { UserController } from './authController'; + +export{UserController}; \ No newline at end of file diff --git a/src/entities/User.ts b/src/entities/User.ts new file mode 100644 index 0000000..2d0557f --- /dev/null +++ b/src/entities/User.ts @@ -0,0 +1,69 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + Unique, + CreateDateColumn, + UpdateDateColumn, + } from 'typeorm'; + import { IsEmail, IsNotEmpty, IsString, IsBoolean, IsIn } from 'class-validator'; + + @Entity() + @Unique(['email']) + export class User { + @PrimaryGeneratedColumn('uuid') + @IsNotEmpty() + id!: string; + + @Column() + @IsNotEmpty() + @IsString() + firstName!: string; + + @Column() + @IsNotEmpty() + @IsString() + lastName!: string; + + @Column() + @IsNotEmpty() + @IsEmail() + email!: string; + + @Column() + @IsNotEmpty() + password!: string; + + @Column() + @IsNotEmpty() + @IsString() + gender!: string; + + @Column() + @IsNotEmpty() + phoneNumber!: string; + + @Column({ nullable: true }) + photoUrl?: string; + + @Column() + @IsNotEmpty() + @IsBoolean() + verified!: boolean; + + @Column() + @IsNotEmpty() + @IsIn(['active', 'suspended']) + status!: 'active' | 'suspended'; + + @Column({ default: "Buyer" }) + @IsNotEmpty() + @IsIn(['Admin', 'Buyer', 'Vendor']) + userType!: 'Admin' | 'Buyer' | 'Vendor'; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; + } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 36b592e..55443f6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,8 @@ import cors from 'cors'; import dotenv from 'dotenv'; import router from './routes'; import { addDocumentation } from './startups/docs'; +import 'reflect-metadata'; + import { CustomError, errorHandler } from './middlewares/errorHandler'; import morgan from 'morgan'; @@ -32,4 +34,4 @@ app.use(morgan(morganFormat)); export const server = app.listen(port, () => { console.log(`[server]: Server is running at http://localhost:${port}`); -}); +}); \ No newline at end of file diff --git a/src/middlewares/errorHandler.ts b/src/middlewares/errorHandler.ts index e4e967f..8a24a54 100644 --- a/src/middlewares/errorHandler.ts +++ b/src/middlewares/errorHandler.ts @@ -1,4 +1,4 @@ -import { Request, Response } from 'express'; +import { NextFunction, Request, Response } from 'express'; class CustomError extends Error { statusCode: number; @@ -12,7 +12,7 @@ class CustomError extends Error { } } -const errorHandler = (err: CustomError, req: Request, res: Response) => { +const errorHandler = (err: CustomError, req: Request, res: Response,next: NextFunction) => { err.statusCode = err.statusCode || 500; err.status = err.status || 'error'; res.status(err.statusCode).json({ diff --git a/src/routes/UserRoutes.ts b/src/routes/UserRoutes.ts new file mode 100644 index 0000000..734a565 --- /dev/null +++ b/src/routes/UserRoutes.ts @@ -0,0 +1,11 @@ +import { Router } from 'express'; +import { UserController } from '../controllers/index'; + + +const { registerUser } = UserController; + +const router = Router(); + +router.post('/register', registerUser); + +export default router; \ No newline at end of file diff --git a/src/routes/index.ts b/src/routes/index.ts index 78cb97d..8d1a53a 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,14 +1,13 @@ import { Request, Response, Router } from 'express'; +import userRoutes from './UserRoutes'; import { responseSuccess } from '../utils/response.utils'; + const router = Router(); -router.get("/", (req: Request, res: Response) => { - res.send("Knights Ecommerce API"); -}); + router.get('/api/v1/status', (req: Request, res: Response) => { - return responseSuccess(res, 202, 'This is a testing route that returns: 201'); + return responseSuccess(res, 202, 'This is a testing route that returns: 202'); }); -// All routes should be imported here and get export after specifying first route -// example router.use("/stock". stockRoutes) =>:: First import stockRoutes and use it here, This shows how the route export will be handled +router.use('/user', userRoutes); export default router; diff --git a/tsconfig.json b/tsconfig.json index 7d6a779..6e7652b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,8 +12,8 @@ "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ - // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ - // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ From c7b1dc4f8519545b020b731a7f9e5d2b90f051f0 Mon Sep 17 00:00:00 2001 From: Iadivin Date: Fri, 3 May 2024 13:16:52 +0200 Subject: [PATCH 22/51] fix(user registration): resolve registration bug - ensure that a user provide neccessary inputs - restructure user entity - refactor other codes depending on user registration logic [Fixes #39] --- .env.example | 13 ++- .eslintrc.js | 15 ++- .github/workflows/ci.yml | 10 +- README.md | 7 +- migrations/1714595134552-UserMigration.ts | 18 ++- ormconfig.js | 63 +++++++---- package.json | 7 +- src/__test__/route.test.ts | 58 +++++----- src/controllers/authController.ts | 92 ++++++++------- src/controllers/index.ts | 2 +- src/entities/User.ts | 131 ++++++++++------------ src/index.ts | 4 +- src/middlewares/errorHandler.ts | 2 +- src/routes/UserRoutes.ts | 5 +- src/routes/index.ts | 2 +- src/startups/dbConnection.ts | 5 +- src/utils/response.utils.ts | 4 +- tsconfig.json | 9 +- 18 files changed, 241 insertions(+), 206 deletions(-) diff --git a/.env.example b/.env.example index 0a48e9f..3dff852 100644 --- a/.env.example +++ b/.env.example @@ -1,13 +1,20 @@ PORT= ******************************** APP_ENV= ******************************** -PDN_DB_NAME= ***************************** + +TEST_DB_HOST= ******************************** +TEST_DB_PORT= ******************************** +TEST_DB_USER= ******************************** +TEST_DB_PASS= ******************************** +TEST_DB_NAME= ******************************** + DEV_DB_HOST= ******************************** DEV_DB_PORT= ******************************** DEV_DB_USER= ******************************** DEV_DB_PASS= ***************************** -DEV_DB_TYPE= ******************************* +DEV_DB_NAME= ******************************* PDN_DB_HOST= ******************************** PDN_DB_PORT= ******************************** PDN_DB_USER= ******************************** -PDN_DB_PASS= ******************************** \ No newline at end of file +PDN_DB_PASS= ******************************** +PDN_DB_NAME= ***************************** \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js index 7d487c3..2607339 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -3,10 +3,7 @@ module.exports = { root: true, parser: '@typescript-eslint/parser', plugins: ['@typescript-eslint'], - extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended' - ], + extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], rules: { '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-unused-vars': [ @@ -28,8 +25,14 @@ module.exports = { 'space-before-function-paren': ['warn', 'always'], 'func-style': ['warn', 'declaration', { allowArrowFunctions: true }], 'camelcase': 'warn', - '@typescript-eslint/explicit-function-return-type': ['warn', { allowExpressions: true }], - '@typescript-eslint/explicit-member-accessibility': ['off', { accessibility: 'explicit' }], + '@typescript-eslint/explicit-function-return-type': [ + 'warn', + { allowExpressions: true }, + ], + '@typescript-eslint/explicit-member-accessibility': [ + 'off', + { accessibility: 'explicit' }, + ], 'no-unused-vars': 'warn', 'no-extra-semi': 'warn', }, diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 99db3dd..e3b23d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,11 +3,11 @@ name: knights-ecomm-be CI on: [push, pull_request] env: - DEV_DB_HOST: ${{secrets.DEV_DB_HOST}} - DEV_DB_PORT: ${{secrets.DEV_DB_PORT}} - DEV_DB_USER: ${{secrets.DEV_DB_USER}} - DEV_DB_PASS: ${{secrets.DEV_DB_PASS}} - DEV_DB_NAME: ${{secrets.DEV_DB_NAME}} + TEST_DB_HOST: ${{secrets.TEST_DB_HOST}} + TEST_DB_PORT: ${{secrets.TEST_DB_PORT}} + TEST_DB_USER: ${{secrets.TEST_DB_USER}} + TEST_DB_PASS: ${{secrets.TEST_DB_PASS}} + TEST_DB_NAME: ${{secrets.TEST_DB_NAME}} jobs: build-lint-test-coverage: diff --git a/README.md b/README.md index 782f032..79c9d64 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ # E-commerse Backend API -[![knights-ecomm-be CI](https://github.com/atlp-rwanda/knights-ecomm-be/actions/workflows/ci.yml/badge.svg)](https://github.com/atlp-rwanda/knights-ecomm-be/actions/workflows/ci.yml)    [![Coverage Status](https://coveralls.io/repos/github/atlp-rwanda/knights-ecomm-be/badge.svg?branch=ch-ci-setup)](https://coveralls.io/github/atlp-rwanda/knights-ecomm-be?branch=ch-ci-setup)    [![Version](https://img.shields.io/badge/version-1.0.0-blue)](https://github.com/your-username/your-repo-name/releases/tag/v1.0.0) + +[![knights-ecomm-be CI](https://github.com/atlp-rwanda/knights-ecomm-be/actions/workflows/ci.yml/badge.svg)](https://github.com/atlp-rwanda/knights-ecomm-be/actions/workflows/ci.yml) +   +[![Coverage Status](https://coveralls.io/repos/github/atlp-rwanda/knights-ecomm-be/badge.svg?branch=ch-ci-setup)](https://coveralls.io/github/atlp-rwanda/knights-ecomm-be?branch=ch-ci-setup) +   +[![Version](https://img.shields.io/badge/version-1.0.0-blue)](https://github.com/your-username/your-repo-name/releases/tag/v1.0.0) ## Description diff --git a/migrations/1714595134552-UserMigration.ts b/migrations/1714595134552-UserMigration.ts index 05693f8..7eb7953 100644 --- a/migrations/1714595134552-UserMigration.ts +++ b/migrations/1714595134552-UserMigration.ts @@ -1,9 +1,8 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; +import { MigrationInterface, QueryRunner } from 'typeorm'; export class CreateUserMigration1614495123940 implements MigrationInterface { - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` + public async up (queryRunner: QueryRunner): Promise { + await queryRunner.query(` CREATE TABLE "user" ( "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "firstName" character varying NOT NULL, @@ -22,10 +21,9 @@ export class CreateUserMigration1614495123940 implements MigrationInterface { CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id") ) `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE "user"`); - } + } -} \ No newline at end of file + public async down (queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "user"`); + } +} diff --git a/ormconfig.js b/ormconfig.js index a4d78a5..bc7acdf 100644 --- a/ormconfig.js +++ b/ormconfig.js @@ -1,24 +1,39 @@ -module.exports = { - "type": "postgres", - "host": `${process.env.DEV_DB_HOST}`, - "port": `${process.env.DEV_DB_PORT}`, - "username": `${process.env.DEV_DB_USER}`, - "password": `${process.env.DEV_DB_PASS}`, - "database": `${process.env.DEV_DB_NAME}`, - "synchronize": true, - "logging": false, - "entities": [ - "src/entities/**/*.ts" - ], - "migrations": [ - "src/migrations/**/*.ts" - ], - "subscribers": [ - "src/subscribers/**/*.ts" - ], - "cli": { - "entitiesDir": "src/entities", - "migrationsDir": "src/migrations", - "subscribersDir": "src/subscribers" - } -}; \ No newline at end of file +const devConfig = { + type: 'postgres', + host: process.env.DEV_DB_HOST, + port: process.env.DEV_DB_PORT, + username: process.env.DEV_DB_USER, + password: process.env.DEV_DB_PASS, + database: process.env.DEV_DB_NAME, + synchronize: true, + logging: false, + entities: ['src/entities/**/*.ts'], + migrations: ['src/migrations/**/*.ts'], + subscribers: ['src/subscribers/**/*.ts'], + cli: { + entitiesDir: 'src/entities', + migrationsDir: 'src/migrations', + subscribersDir: 'src/subscribers', + }, +}; + +const testConfig = { + type: 'postgres', + host: process.env.TEST_DB_HOST, + port: process.env.TEST_DB_PORT, + username: process.env.TEST_DB_USER, + password: process.env.TEST_DB_PASS, + database: process.env.TEST_DB_NAME, + synchronize: true, + logging: false, + entities: ['src/entities/**/*.ts'], + migrations: ['src/migrations/**/*.ts'], + subscribers: ['src/subscribers/**/*.ts'], + cli: { + entitiesDir: 'src/entities', + migrationsDir: 'src/migrations', + subscribersDir: 'src/subscribers', + }, +}; + +module.exports = process.env.NODE_ENV === 'test' ? testConfig : devConfig; diff --git a/package.json b/package.json index 84a5b11..df34cc1 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,8 @@ "description": "E-commerce backend", "main": "index.js", "scripts": { - "test": "jest --coverage --detectOpenHandles --verbose --runInBand", - "dev": "nodemon src/index.ts", + "test": "cross-env APP_ENV=test jest --coverage --detectOpenHandles --verbose --runInBand ", + "dev": "cross-env APP_ENV=dev nodemon src/index.ts", "build": "tsc -p .", "start": "node dist/index.js", "lint": "eslint .", @@ -24,6 +24,7 @@ "bcrypt": "^5.1.1", "class-validator": "^0.14.1", "cors": "^2.8.5", + "cross-env": "^7.0.3", "dotenv": "^16.4.5", "express": "^4.19.2", "express-winston": "^4.2.0", @@ -75,4 +76,4 @@ "typescript": "^5.4.5", "typescript-eslint": "^7.7.1" } -} \ No newline at end of file +} diff --git a/src/__test__/route.test.ts b/src/__test__/route.test.ts index 6b296a3..6505d02 100644 --- a/src/__test__/route.test.ts +++ b/src/__test__/route.test.ts @@ -1,21 +1,27 @@ import request from 'supertest'; -import { app, server } from '../index'; // update this with the path to your app file +import { app, server } from '../index'; import { createConnection, getConnection, getConnectionOptions } from 'typeorm'; import { User } from '../entities/User'; -import { getRepository, Repository } from 'typeorm'; beforeAll(async () => { // Connect to the test database const connectionOptions = await getConnectionOptions(); + await createConnection({ ...connectionOptions, name: 'testConnection' }); }); afterAll(async () => { - await getConnection('testConnection').close(); - server.close(); -}); + const connection = getConnection('testConnection'); + const userRepository = connection.getRepository(User); + + // Delete all records from the User + await userRepository.clear(); + // Close the connection to the test database + await connection.close(); + server.close(); +}); describe('GET /', () => { it('This is a testing route that returns', done => { @@ -23,17 +29,20 @@ describe('GET /', () => { .get('/api/v1/status') .expect(200) .expect('Content-Type', /json/) - .expect({ - status: 'success', - data: { - code: 202, - message: 'This is a testing route that returns: 202' - } - }, done); + .expect( + { + status: 'success', + data: { + code: 200, + message: 'This is a testing route.', + }, + }, + done + ); }); }); describe('POST /user/register', () => { - it('should register a new user and then delete it', async () => { + it('should register a new user', async () => { // Arrange const newUser = { firstName: 'John', @@ -43,25 +52,20 @@ describe('POST /user/register', () => { gender: 'Male', phoneNumber: '1234567890', userType: 'Buyer', - status: 'active', - verified: true, photoUrl: 'https://example.com/photo.jpg', }; // Act - const res = await request(app) - .post('/user/register') - .send(newUser); + const res = await request(app).post('/user/register').send(newUser); // Assert expect(res.status).toBe(201); - expect(res.body).toEqual({ message: 'User registered successfully' }); - - // Clean up: delete the test user - const userRepository = getRepository(User); - const user = await userRepository.findOne({ where: { email: newUser.email } }); - if (user) { - await userRepository.remove(user); - } + expect(res.body).toEqual({ + status: 'success', + data: { + code: 201, + message: 'User registered successfully', + }, + }); }); -}); \ No newline at end of file +}); diff --git a/src/controllers/authController.ts b/src/controllers/authController.ts index 209a350..6879bc0 100644 --- a/src/controllers/authController.ts +++ b/src/controllers/authController.ts @@ -1,49 +1,55 @@ import { Request, Response } from 'express'; -import { User } from '../entities/User';; +import { User } from '../entities/User'; import bcrypt from 'bcrypt'; import { getRepository } from 'typeorm'; - +import { responseError, responseServerError, responseSuccess } from '../utils/response.utils'; +import { validate } from 'class-validator'; class UserController { - static registerUser = async (req: Request, res: Response) => { - const { firstName, lastName, email, password, gender, phoneNumber, userType, status, verified, photoUrl } = req.body; - - // Validate user input - if (!(firstName && lastName && email && password && gender && phoneNumber && verified && photoUrl)) { - return res.status(400).json({ error: 'Please fill all the fields' }); - } - - const userRepository = getRepository(User); - - - // Check for existing user - const existingUser = await userRepository.findOneBy({ email }); - const existingUserNumber = await userRepository.findOneBy({ phoneNumber }); - - if (existingUser || existingUserNumber) { - return res.status(400).json({ error: 'Email or phone number already in use' }); - } - - const saltRounds = 10; - const hashedPassword = await bcrypt.hash(password, saltRounds); - - // Create user - const user = new User(); - user.firstName = firstName; - user.lastName = lastName; - user.email = email; - user.password = hashedPassword; - user.userType = userType; - user.gender = gender; - user.phoneNumber = phoneNumber; - user.photoUrl = photoUrl; - user.status = status ? status : 'active'; - user.verified = verified; - - // Save user - await userRepository.save(user); - - return res.status(201).json({ message: 'User registered successfully' }); - }; + static registerUser = async (req: Request, res: Response) => { + const { firstName, lastName, email, password, gender, phoneNumber, userType, photoUrl } = req.body; + + // Validate user input + if (!firstName || !lastName || !email || !password || !gender || !phoneNumber || !photoUrl) { + return responseError(res, 400, 'Please fill all the required fields'); + } + + const userRepository = getRepository(User); + + try { + // Check for existing user + const existingUser = await userRepository.findOneBy({ email }); + const existingUserNumber = await userRepository.findOneBy({ phoneNumber }); + + if (existingUser || existingUserNumber) { + return responseError(res, 409, 'Email or phone number already in use'); + } + + const saltRounds = 10; + const hashedPassword = await bcrypt.hash(password, saltRounds); + + // Create user + const user = new User(); + user.firstName = firstName; + user.lastName = lastName; + user.email = email; + user.password = hashedPassword; + user.userType = userType; + user.gender = gender; + user.phoneNumber = phoneNumber; + user.photoUrl = photoUrl; + + // Save user + await userRepository.save(user); + + return responseSuccess(res, 201, 'User registered successfully'); + } catch (error) { + if (error instanceof Error) { + return responseServerError(res, error.message); + } + + return responseServerError(res, 'Unknown error occurred'); + } + }; } -export { UserController }; \ No newline at end of file +export { UserController }; diff --git a/src/controllers/index.ts b/src/controllers/index.ts index 554dd4e..b581bf7 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -1,3 +1,3 @@ import { UserController } from './authController'; -export{UserController}; \ No newline at end of file +export { UserController }; diff --git a/src/entities/User.ts b/src/entities/User.ts index 2d0557f..f1be411 100644 --- a/src/entities/User.ts +++ b/src/entities/User.ts @@ -1,69 +1,62 @@ -import { - Entity, - PrimaryGeneratedColumn, - Column, - Unique, - CreateDateColumn, - UpdateDateColumn, - } from 'typeorm'; - import { IsEmail, IsNotEmpty, IsString, IsBoolean, IsIn } from 'class-validator'; - - @Entity() - @Unique(['email']) - export class User { - @PrimaryGeneratedColumn('uuid') - @IsNotEmpty() - id!: string; - - @Column() - @IsNotEmpty() - @IsString() - firstName!: string; - - @Column() - @IsNotEmpty() - @IsString() - lastName!: string; - - @Column() - @IsNotEmpty() - @IsEmail() - email!: string; - - @Column() - @IsNotEmpty() - password!: string; - - @Column() - @IsNotEmpty() - @IsString() - gender!: string; - - @Column() - @IsNotEmpty() - phoneNumber!: string; - - @Column({ nullable: true }) - photoUrl?: string; - - @Column() - @IsNotEmpty() - @IsBoolean() - verified!: boolean; - - @Column() - @IsNotEmpty() - @IsIn(['active', 'suspended']) - status!: 'active' | 'suspended'; - - @Column({ default: "Buyer" }) - @IsNotEmpty() - @IsIn(['Admin', 'Buyer', 'Vendor']) - userType!: 'Admin' | 'Buyer' | 'Vendor'; - - @CreateDateColumn() - createdAt!: Date; - - @UpdateDateColumn() - updatedAt!: Date; - } \ No newline at end of file +import { Entity, PrimaryGeneratedColumn, Column, Unique, CreateDateColumn, UpdateDateColumn } from 'typeorm'; +import { IsEmail, IsNotEmpty, IsString, IsBoolean, IsIn } from 'class-validator'; + +@Entity() +@Unique(['email']) +export class User { + @PrimaryGeneratedColumn('uuid') + @IsNotEmpty() + id!: string; + + @Column() + @IsNotEmpty() + @IsString() + firstName!: string; + + @Column() + @IsNotEmpty() + @IsString() + lastName!: string; + + @Column() + @IsNotEmpty() + @IsEmail() + email!: string; + + @Column() + @IsNotEmpty() + password!: string; + + @Column() + @IsNotEmpty() + @IsString() + gender!: string; + + @Column() + @IsNotEmpty() + phoneNumber!: string; + + @Column({ nullable: true }) + photoUrl?: string; + + @Column({ default: false }) + @IsNotEmpty() + @IsBoolean() + verified!: boolean; + + @Column({ default: 'active' }) + @IsNotEmpty() + @IsIn(['active', 'suspended']) + status!: 'active' | 'suspended'; + + @Column({ default: 'Buyer' }) + @IsNotEmpty() + @IsIn(['Admin', 'Buyer', 'Vendor']) + userType!: 'Admin' | 'Buyer' | 'Vendor'; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; +} diff --git a/src/index.ts b/src/index.ts index 55443f6..bb5ad3a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,6 @@ import router from './routes'; import { addDocumentation } from './startups/docs'; import 'reflect-metadata'; - import { CustomError, errorHandler } from './middlewares/errorHandler'; import morgan from 'morgan'; import { dbConnection } from './startups/dbConnection'; @@ -26,6 +25,7 @@ app.all('*', (req: Request, res: Response, next) => { app.use(errorHandler); // Start database connection + dbConnection(); //morgan @@ -34,4 +34,4 @@ app.use(morgan(morganFormat)); export const server = app.listen(port, () => { console.log(`[server]: Server is running at http://localhost:${port}`); -}); \ No newline at end of file +}); diff --git a/src/middlewares/errorHandler.ts b/src/middlewares/errorHandler.ts index 8a24a54..5e25feb 100644 --- a/src/middlewares/errorHandler.ts +++ b/src/middlewares/errorHandler.ts @@ -12,7 +12,7 @@ class CustomError extends Error { } } -const errorHandler = (err: CustomError, req: Request, res: Response,next: NextFunction) => { +const errorHandler = (err: CustomError, req: Request, res: Response, next: NextFunction) => { err.statusCode = err.statusCode || 500; err.status = err.status || 'error'; res.status(err.statusCode).json({ diff --git a/src/routes/UserRoutes.ts b/src/routes/UserRoutes.ts index 734a565..01ce115 100644 --- a/src/routes/UserRoutes.ts +++ b/src/routes/UserRoutes.ts @@ -1,11 +1,10 @@ -import { Router } from 'express'; +import { Router } from 'express'; import { UserController } from '../controllers/index'; - const { registerUser } = UserController; const router = Router(); router.post('/register', registerUser); -export default router; \ No newline at end of file +export default router; diff --git a/src/routes/index.ts b/src/routes/index.ts index 8d1a53a..1d95c3d 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -5,7 +5,7 @@ import { responseSuccess } from '../utils/response.utils'; const router = Router(); router.get('/api/v1/status', (req: Request, res: Response) => { - return responseSuccess(res, 202, 'This is a testing route that returns: 202'); + return responseSuccess(res, 200, 'This is a testing route.'); }); router.use('/user', userRoutes); diff --git a/src/startups/dbConnection.ts b/src/startups/dbConnection.ts index 7fb387c..44887d8 100644 --- a/src/startups/dbConnection.ts +++ b/src/startups/dbConnection.ts @@ -1,9 +1,10 @@ -import { createConnection } from "typeorm"; +import { createConnection } from 'typeorm'; const dbConnection = async () => { try { const connection = await createConnection(); - console.log('Connected to the database'); + console.log(`Connected to the ${process.env.APP_ENV} database`); + return connection; } catch (error) { console.error('Error connecting to the database:', error); } diff --git a/src/utils/response.utils.ts b/src/utils/response.utils.ts index 6f097a7..3be109b 100644 --- a/src/utils/response.utils.ts +++ b/src/utils/response.utils.ts @@ -13,7 +13,7 @@ export const responseSuccess = ( message: string, data?: any ): Response => { - return res.status(200).json( + return res.status(statusCode).json( jsend.success({ code: statusCode, message, @@ -28,7 +28,7 @@ export const responseError = ( message: string, data?: any ): Response => { - return res.status(400).json( + return res.status(statusCode).json( jsend.error({ code: statusCode, message, diff --git a/tsconfig.json b/tsconfig.json index 6e7652b..88326b4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,8 +12,8 @@ "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ - "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ - "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + "experimentalDecorators": true /* Enable experimental support for legacy experimental decorators. */, + "emitDecoratorMetadata": true /* Emit design-type metadata for decorated declarations in source files. */, // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ @@ -29,7 +29,10 @@ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ - "types": ["node", "jest"] /* Specify type package names to be included without being referenced in a source file. */, + "types": [ + "node", + "jest" + ] /* Specify type package names to be included without being referenced in a source file. */, // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ From 2b118c35fd6827368c8197a822912c2b5591ecc2 Mon Sep 17 00:00:00 2001 From: maxCastro1 Date: Wed, 1 May 2024 22:30:30 +0200 Subject: [PATCH 23/51] Implemented user registration feature with input validation, password hashing, and database integration fix(user registration): resolve registration bug - ensure that a user provide neccessary inputs - restructure user entity - refactor other codes depending on user registration logic [Fixes #39] rebasing from develop , adding verifie route and service and send email to the user rebasing from develop , adding verifie route and service and send email to the user --- .env.example | 13 +- .eslintrc.js | 15 ++- .github/workflows/ci.yml | 13 +- .gitignore | 2 +- README.md | 7 +- migrations/1714595134552-UserMigration.ts | 29 +++++ ormconfig.js | 63 ++++++---- package.json | 20 +++- src/__test__/route.test.ts | 111 ++++++++++++++++-- src/controllers/authController.ts | 15 +++ src/controllers/index.ts | 8 +- src/entities/User.ts | 62 ++++++++++ src/index.ts | 2 + src/middlewares/errorHandler.ts | 4 +- src/routes/UserRoutes.ts | 9 ++ src/routes/index.ts | 11 +- src/services/index.ts | 2 + .../userServices/userRegistrationService.ts | 74 ++++++++++++ .../userServices/userValidationService.ts | 28 +++++ src/startups/dbConnection.ts | 5 +- src/utils/response.utils.ts | 4 +- src/utils/sendMail.ts | 90 ++++++++++++++ tsconfig.json | 9 +- 23 files changed, 522 insertions(+), 74 deletions(-) create mode 100644 migrations/1714595134552-UserMigration.ts create mode 100644 src/controllers/authController.ts create mode 100644 src/entities/User.ts create mode 100644 src/routes/UserRoutes.ts create mode 100644 src/services/userServices/userRegistrationService.ts create mode 100644 src/services/userServices/userValidationService.ts create mode 100644 src/utils/sendMail.ts diff --git a/.env.example b/.env.example index 0a48e9f..3dff852 100644 --- a/.env.example +++ b/.env.example @@ -1,13 +1,20 @@ PORT= ******************************** APP_ENV= ******************************** -PDN_DB_NAME= ***************************** + +TEST_DB_HOST= ******************************** +TEST_DB_PORT= ******************************** +TEST_DB_USER= ******************************** +TEST_DB_PASS= ******************************** +TEST_DB_NAME= ******************************** + DEV_DB_HOST= ******************************** DEV_DB_PORT= ******************************** DEV_DB_USER= ******************************** DEV_DB_PASS= ***************************** -DEV_DB_TYPE= ******************************* +DEV_DB_NAME= ******************************* PDN_DB_HOST= ******************************** PDN_DB_PORT= ******************************** PDN_DB_USER= ******************************** -PDN_DB_PASS= ******************************** \ No newline at end of file +PDN_DB_PASS= ******************************** +PDN_DB_NAME= ***************************** \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js index 7d487c3..2607339 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -3,10 +3,7 @@ module.exports = { root: true, parser: '@typescript-eslint/parser', plugins: ['@typescript-eslint'], - extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended' - ], + extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], rules: { '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-unused-vars': [ @@ -28,8 +25,14 @@ module.exports = { 'space-before-function-paren': ['warn', 'always'], 'func-style': ['warn', 'declaration', { allowArrowFunctions: true }], 'camelcase': 'warn', - '@typescript-eslint/explicit-function-return-type': ['warn', { allowExpressions: true }], - '@typescript-eslint/explicit-member-accessibility': ['off', { accessibility: 'explicit' }], + '@typescript-eslint/explicit-function-return-type': [ + 'warn', + { allowExpressions: true }, + ], + '@typescript-eslint/explicit-member-accessibility': [ + 'off', + { accessibility: 'explicit' }, + ], 'no-unused-vars': 'warn', 'no-extra-semi': 'warn', }, diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 99db3dd..66f9ffa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,11 +3,14 @@ name: knights-ecomm-be CI on: [push, pull_request] env: - DEV_DB_HOST: ${{secrets.DEV_DB_HOST}} - DEV_DB_PORT: ${{secrets.DEV_DB_PORT}} - DEV_DB_USER: ${{secrets.DEV_DB_USER}} - DEV_DB_PASS: ${{secrets.DEV_DB_PASS}} - DEV_DB_NAME: ${{secrets.DEV_DB_NAME}} + TEST_DB_HOST: ${{secrets.TEST_DB_HOST}} + TEST_DB_PORT: ${{secrets.TEST_DB_PORT}} + TEST_DB_USER: ${{secrets.TEST_DB_USER}} + TEST_DB_PASS: ${{secrets.TEST_DB_PASS}} + TEST_DB_NAME: ${{secrets.TEST_DB_NAME}} + HOST: ${{secrets.HOST}} + AUTH_EMAIL: ${{secrets.AUTH_EMAIL}} + AUTH_PASSWORD: ${{secrets.AUTH_PASSWORD}} jobs: build-lint-test-coverage: diff --git a/.gitignore b/.gitignore index 66612c2..1500c37 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,4 @@ package-lock.json coverage/ dist /src/logs -.DS_Stor \ No newline at end of file +.DS_Store \ No newline at end of file diff --git a/README.md b/README.md index 782f032..58ad41f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ # E-commerse Backend API -[![knights-ecomm-be CI](https://github.com/atlp-rwanda/knights-ecomm-be/actions/workflows/ci.yml/badge.svg)](https://github.com/atlp-rwanda/knights-ecomm-be/actions/workflows/ci.yml)    [![Coverage Status](https://coveralls.io/repos/github/atlp-rwanda/knights-ecomm-be/badge.svg?branch=ch-ci-setup)](https://coveralls.io/github/atlp-rwanda/knights-ecomm-be?branch=ch-ci-setup)    [![Version](https://img.shields.io/badge/version-1.0.0-blue)](https://github.com/your-username/your-repo-name/releases/tag/v1.0.0) + +[![Coverage Status](https://coveralls.io/repos/github/atlp-rwanda/knights-ecomm-be/badge.svg?branch=develop)](https://coveralls.io/github/atlp-rwanda/knights-ecomm-be?branch=develop) +   +[![Coverage Status](https://coveralls.io/repos/github/atlp-rwanda/knights-ecomm-be/badge.svg?branch=ch-ci-setup)](https://coveralls.io/github/atlp-rwanda/knights-ecomm-be?branch=ch-ci-setup) +   +[![Version](https://img.shields.io/badge/version-1.0.0-blue)](https://github.com/your-username/your-repo-name/releases/tag/v1.0.0) ## Description diff --git a/migrations/1714595134552-UserMigration.ts b/migrations/1714595134552-UserMigration.ts new file mode 100644 index 0000000..7eb7953 --- /dev/null +++ b/migrations/1714595134552-UserMigration.ts @@ -0,0 +1,29 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CreateUserMigration1614495123940 implements MigrationInterface { + public async up (queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE "user" ( + "id" uuid NOT NULL DEFAULT uuid_generate_v4(), + "firstName" character varying NOT NULL, + "lastName" character varying NOT NULL, + "email" character varying NOT NULL, + "password" character varying NOT NULL, + "gender" character varying NOT NULL, + "phoneNumber" character varying NOT NULL, + "photoUrl" character varying, + "verified" boolean NOT NULL, + "status" character varying NOT NULL CHECK (status IN ('active', 'suspended')), + "userType" character varying NOT NULL DEFAULT 'Buyer' CHECK (userType IN ('Admin', 'Buyer', 'Vendor')), + "createdAt" TIMESTAMP NOT NULL DEFAULT now(), + "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), + CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"), + CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id") + ) + `); + } + + public async down (queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "user"`); + } +} diff --git a/ormconfig.js b/ormconfig.js index a4d78a5..bc7acdf 100644 --- a/ormconfig.js +++ b/ormconfig.js @@ -1,24 +1,39 @@ -module.exports = { - "type": "postgres", - "host": `${process.env.DEV_DB_HOST}`, - "port": `${process.env.DEV_DB_PORT}`, - "username": `${process.env.DEV_DB_USER}`, - "password": `${process.env.DEV_DB_PASS}`, - "database": `${process.env.DEV_DB_NAME}`, - "synchronize": true, - "logging": false, - "entities": [ - "src/entities/**/*.ts" - ], - "migrations": [ - "src/migrations/**/*.ts" - ], - "subscribers": [ - "src/subscribers/**/*.ts" - ], - "cli": { - "entitiesDir": "src/entities", - "migrationsDir": "src/migrations", - "subscribersDir": "src/subscribers" - } -}; \ No newline at end of file +const devConfig = { + type: 'postgres', + host: process.env.DEV_DB_HOST, + port: process.env.DEV_DB_PORT, + username: process.env.DEV_DB_USER, + password: process.env.DEV_DB_PASS, + database: process.env.DEV_DB_NAME, + synchronize: true, + logging: false, + entities: ['src/entities/**/*.ts'], + migrations: ['src/migrations/**/*.ts'], + subscribers: ['src/subscribers/**/*.ts'], + cli: { + entitiesDir: 'src/entities', + migrationsDir: 'src/migrations', + subscribersDir: 'src/subscribers', + }, +}; + +const testConfig = { + type: 'postgres', + host: process.env.TEST_DB_HOST, + port: process.env.TEST_DB_PORT, + username: process.env.TEST_DB_USER, + password: process.env.TEST_DB_PASS, + database: process.env.TEST_DB_NAME, + synchronize: true, + logging: false, + entities: ['src/entities/**/*.ts'], + migrations: ['src/migrations/**/*.ts'], + subscribers: ['src/subscribers/**/*.ts'], + cli: { + entitiesDir: 'src/entities', + migrationsDir: 'src/migrations', + subscribersDir: 'src/subscribers', + }, +}; + +module.exports = process.env.NODE_ENV === 'test' ? testConfig : devConfig; diff --git a/package.json b/package.json index 87ca304..f24cac8 100644 --- a/package.json +++ b/package.json @@ -4,13 +4,15 @@ "description": "E-commerce backend", "main": "index.js", "scripts": { - "test": "jest --coverage --detectOpenHandles --verbose --runInBand", - "dev": "nodemon src/index.ts", + "test": "cross-env APP_ENV=test jest --coverage --detectOpenHandles --verbose --runInBand ", + "dev": "cross-env APP_ENV=dev nodemon src/index.ts", "build": "tsc -p .", "start": "node dist/index.js", "lint": "eslint .", "lint:fix": "eslint --fix .", - "format": "prettier --write ." + "format": "prettier --write .", + "typeorm": "typeorm-ts-node-commonjs", + "migration": " npm run typeorm migration:run -- -d ./ormconfig.js" }, "keywords": [], "author": "Scrum master", @@ -19,13 +21,17 @@ "@types/express-winston": "^4.0.0", "@types/swagger-jsdoc": "^6.0.4", "@types/swagger-ui-express": "^4.1.6", + "bcrypt": "^5.1.1", + "class-validator": "^0.14.1", "cors": "^2.8.5", + "cross-env": "^7.0.3", "dotenv": "^16.4.5", "express": "^4.19.2", - "highlight.js": "^11.9.0", "express-winston": "^4.2.0", + "highlight.js": "^11.9.0", "jsend": "^1.1.0", "morgan": "^1.10.0", + "nodemailer": "^6.9.13", "nodemon": "^3.1.0", "pg": "^8.11.5", "reflect-metadata": "^0.2.2", @@ -42,6 +48,7 @@ }, "devDependencies": { "@eslint/js": "^9.1.1", + "@types/bcrypt": "^5.0.2", "@types/body-parser": "^1.19.5", "@types/cors": "^2.8.17", "@types/dotenv": "^8.2.0", @@ -50,8 +57,10 @@ "@types/express": "^4.17.21", "@types/jest": "^29.5.12", "@types/jsend": "^1.0.32", - "@types/node": "^20.12.7", "@types/morgan": "^1.9.9", + "@types/node": "^20.12.7", + "@types/nodemailer": "^6.4.15", + "@types/reflect-metadata": "^0.1.0", "@types/supertest": "^6.0.2", "@types/winston": "^2.4.4", "@typescript-eslint/eslint-plugin": "^7.7.1", @@ -62,6 +71,7 @@ "eslint-plugin-eslint-comments": "^3.2.0", "eslint-plugin-prettier": "^5.1.3", "jest": "^29.7.0", + "jest-mock-extended": "^3.0.6", "prettier": "^3.2.5", "supertest": "^7.0.0", "ts-jest": "^29.1.2", diff --git a/src/__test__/route.test.ts b/src/__test__/route.test.ts index 401f5b8..49cfe04 100644 --- a/src/__test__/route.test.ts +++ b/src/__test__/route.test.ts @@ -1,24 +1,117 @@ import request from 'supertest'; -import { app, server } from '../index'; // update this with the path to your app file - -import { createConnection, getConnection, getConnectionOptions } from 'typeorm'; +import { app, server } from '../index'; +import { createConnection, getConnection, getConnectionOptions, getRepository } from 'typeorm'; +import { User } from '../entities/User'; beforeAll(async () => { // Connect to the test database const connectionOptions = await getConnectionOptions(); + await createConnection({ ...connectionOptions, name: 'testConnection' }); }); + afterAll(async () => { - await getConnection('testConnection').close(); + const connection = getConnection('testConnection'); + const userRepository = connection.getRepository(User); + + // Delete all records from the User + await userRepository.clear(); + + // Close the connection to the test database + await connection.close(); + server.close(); }); describe('GET /', () => { - // afterAll(done => { - // server.close(done); - // }); + it('This is a testing route that returns', done => { + request(app) + .get('/api/v1/status') + .expect(200) + .expect('Content-Type', /json/) + .expect( + { + status: 'success', + data: { + code: 200, + message: 'This is a testing route.', + }, + }, + done + ); + }); +}); +describe('POST /user/register', () => { + it('should register a new user', async () => { + // Arrange + const newUser = { + firstName: 'John', + lastName: 'Doe', + email: 'johndoe06@example.com', + password: 'password', + gender: 'Male', + phoneNumber: '123678116', + userType: 'Buyer', + photoUrl: 'https://example.com/photo.jpg', + }; - it('responds with "Knights Ecommerce API"', done => { - request(app).get('/').expect(200, 'Knights Ecommerce API', done); + // Act + const res = await request(app).post('/user/register').send(newUser); + // Assert + expect(res.status).toBe(201); + expect(res.body).toEqual({ + status: 'success', + data: { + code: 201, + message: 'User registered successfully', + }, + }); + + // Clean up: delete the test user + const userRepository = getRepository(User); + const user = await userRepository.findOne({ where: { email: newUser.email } }); + if (user) { + await userRepository.remove(user); + } }); }); +describe('POST /user/verify/:id', () => { + it('should verify a user', async () => { + // Arrange + const newUser = { + firstName: 'John', + lastName: 'Doe', + email: 'john.doe1@example.com', + password: 'password', + gender: 'Male', + phoneNumber: '123456789', + userType: 'Buyer', + photoUrl: 'https://example.com/photo.jpg', + }; + + // Create a new user + const res = await request(app).post('/user/register').send(newUser); + + const userRepository = getRepository(User); + const user = await userRepository.findOne({ where: { email: newUser.email } }); + + if(user){ + const verifyRes = await request(app).get(`/user/verify/${user.id}`); + + // Assert + expect(verifyRes.status).toBe(200); + expect(verifyRes.text).toEqual('

User verified successfully

'); + + // Check that the user's verified field is now true + const verifiedUser = await userRepository.findOne({ where: { email: newUser.email } }); + if (verifiedUser){ + expect(verifiedUser.verified).toBe(true); + } + + } + + if (user) { + await userRepository.remove(user); + } + }); +}); \ No newline at end of file diff --git a/src/controllers/authController.ts b/src/controllers/authController.ts new file mode 100644 index 0000000..627db42 --- /dev/null +++ b/src/controllers/authController.ts @@ -0,0 +1,15 @@ +import { Request, Response } from 'express'; +import { User } from '../entities/User'; +import bcrypt from 'bcrypt'; +import { getRepository } from 'typeorm'; +import { responseError, responseServerError, responseSuccess } from '../utils/response.utils'; +import { validate } from 'class-validator'; +import { userVerificationService, userRegistrationService } from '../services'; + +export const userRegistration = async (req: Request, res: Response) => { + await userRegistrationService(req, res); +} +export const userVerification = async (req: Request, res: Response) => { + await userVerificationService(req, res); +} + diff --git a/src/controllers/index.ts b/src/controllers/index.ts index 4ba41c9..a03efc6 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -1,5 +1,3 @@ -// export all controllers -function myFunction (): void { - console.log('Hello'); -} -myFunction(); +import { userRegistration,userVerification } from './authController'; + +export { userRegistration,userVerification }; diff --git a/src/entities/User.ts b/src/entities/User.ts new file mode 100644 index 0000000..f1be411 --- /dev/null +++ b/src/entities/User.ts @@ -0,0 +1,62 @@ +import { Entity, PrimaryGeneratedColumn, Column, Unique, CreateDateColumn, UpdateDateColumn } from 'typeorm'; +import { IsEmail, IsNotEmpty, IsString, IsBoolean, IsIn } from 'class-validator'; + +@Entity() +@Unique(['email']) +export class User { + @PrimaryGeneratedColumn('uuid') + @IsNotEmpty() + id!: string; + + @Column() + @IsNotEmpty() + @IsString() + firstName!: string; + + @Column() + @IsNotEmpty() + @IsString() + lastName!: string; + + @Column() + @IsNotEmpty() + @IsEmail() + email!: string; + + @Column() + @IsNotEmpty() + password!: string; + + @Column() + @IsNotEmpty() + @IsString() + gender!: string; + + @Column() + @IsNotEmpty() + phoneNumber!: string; + + @Column({ nullable: true }) + photoUrl?: string; + + @Column({ default: false }) + @IsNotEmpty() + @IsBoolean() + verified!: boolean; + + @Column({ default: 'active' }) + @IsNotEmpty() + @IsIn(['active', 'suspended']) + status!: 'active' | 'suspended'; + + @Column({ default: 'Buyer' }) + @IsNotEmpty() + @IsIn(['Admin', 'Buyer', 'Vendor']) + userType!: 'Admin' | 'Buyer' | 'Vendor'; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; +} diff --git a/src/index.ts b/src/index.ts index 36b592e..bb5ad3a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ import cors from 'cors'; import dotenv from 'dotenv'; import router from './routes'; import { addDocumentation } from './startups/docs'; +import 'reflect-metadata'; import { CustomError, errorHandler } from './middlewares/errorHandler'; import morgan from 'morgan'; @@ -24,6 +25,7 @@ app.all('*', (req: Request, res: Response, next) => { app.use(errorHandler); // Start database connection + dbConnection(); //morgan diff --git a/src/middlewares/errorHandler.ts b/src/middlewares/errorHandler.ts index e4e967f..5e25feb 100644 --- a/src/middlewares/errorHandler.ts +++ b/src/middlewares/errorHandler.ts @@ -1,4 +1,4 @@ -import { Request, Response } from 'express'; +import { NextFunction, Request, Response } from 'express'; class CustomError extends Error { statusCode: number; @@ -12,7 +12,7 @@ class CustomError extends Error { } } -const errorHandler = (err: CustomError, req: Request, res: Response) => { +const errorHandler = (err: CustomError, req: Request, res: Response, next: NextFunction) => { err.statusCode = err.statusCode || 500; err.status = err.status || 'error'; res.status(err.statusCode).json({ diff --git a/src/routes/UserRoutes.ts b/src/routes/UserRoutes.ts new file mode 100644 index 0000000..7fcd3b5 --- /dev/null +++ b/src/routes/UserRoutes.ts @@ -0,0 +1,9 @@ +import { Router } from 'express'; +import { userRegistration, userVerification} from '../controllers/index'; + +const router = Router(); + +router.post('/register', userRegistration); +router.get('/verify/:id', userVerification); + +export default router; diff --git a/src/routes/index.ts b/src/routes/index.ts index 78cb97d..1d95c3d 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,14 +1,13 @@ import { Request, Response, Router } from 'express'; +import userRoutes from './UserRoutes'; import { responseSuccess } from '../utils/response.utils'; + const router = Router(); -router.get("/", (req: Request, res: Response) => { - res.send("Knights Ecommerce API"); -}); + router.get('/api/v1/status', (req: Request, res: Response) => { - return responseSuccess(res, 202, 'This is a testing route that returns: 201'); + return responseSuccess(res, 200, 'This is a testing route.'); }); -// All routes should be imported here and get export after specifying first route -// example router.use("/stock". stockRoutes) =>:: First import stockRoutes and use it here, This shows how the route export will be handled +router.use('/user', userRoutes); export default router; diff --git a/src/services/index.ts b/src/services/index.ts index 73cc501..271665e 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1 +1,3 @@ // export all Services +export * from './userServices/userRegistrationService'; +export * from './userServices/userValidationService'; \ No newline at end of file diff --git a/src/services/userServices/userRegistrationService.ts b/src/services/userServices/userRegistrationService.ts new file mode 100644 index 0000000..9b62908 --- /dev/null +++ b/src/services/userServices/userRegistrationService.ts @@ -0,0 +1,74 @@ + +import { Request, Response } from 'express'; +import { User } from '../../entities/User'; +import bcrypt from 'bcrypt'; +import { getRepository } from 'typeorm'; +import { responseError, responseServerError, responseSuccess } from '../../utils/response.utils'; +import sendMail from '../../utils/sendMail'; +import dotenv from 'dotenv'; +dotenv.config(); + +export const userRegistrationService = async (req: Request, res: Response) => { + const { firstName, lastName, email, password, gender, phoneNumber, userType, photoUrl } = req.body; + + // Validate user input + if (!firstName || !lastName || !email || !password || !gender || !phoneNumber || !photoUrl) { + return responseError(res, 400, 'Please fill all the required fields'); + } + + const userRepository = getRepository(User); + + try { + // Check for existing user + const existingUser = await userRepository.findOneBy({ email }); + const existingUserNumber = await userRepository.findOneBy({ phoneNumber }); + + if (existingUser || existingUserNumber) { + return responseError(res, 409, 'Email or phone number already in use'); + } + + const saltRounds = 10; + const hashedPassword = await bcrypt.hash(password, saltRounds); + + // Create user + const user = new User(); + user.firstName = firstName; + user.lastName = lastName; + user.email = email; + user.password = hashedPassword; + user.userType = userType; + user.gender = gender; + user.phoneNumber = phoneNumber; + user.photoUrl = photoUrl; + + // Save user + await userRepository.save(user); + if (process.env.AUTH_EMAIL && process.env.AUTH_PASSWORD) { + + const message = { + to: email, + from: process.env.AUTH_EMAIL, + subject: 'Welcome to the knights app', + text: `Welcome to the app, ${firstName} ${lastName}!`, + lastName: lastName, + firstName: firstName, + } + const link = `http://localhost:${process.env.PORT}/user/verify/${user.id}` + + sendMail(process.env.AUTH_EMAIL, process.env.AUTH_PASSWORD, message, link); + + + } else { + // return res.status(500).json({ error: 'Email or password for mail server not configured' }); + return responseError(res, 500 , 'Email or password for mail server not configured'); + } + + return responseSuccess(res, 201, 'User registered successfully'); + } catch (error) { + if (error instanceof Error) { + return responseServerError(res, error.message); + } + + return responseServerError(res, 'Unknown error occurred'); + } +}; \ No newline at end of file diff --git a/src/services/userServices/userValidationService.ts b/src/services/userServices/userValidationService.ts new file mode 100644 index 0000000..8d8fea6 --- /dev/null +++ b/src/services/userServices/userValidationService.ts @@ -0,0 +1,28 @@ +import { Request, Response } from 'express'; +import { User } from '../../entities/User'; +import { getRepository } from 'typeorm'; + + + +export const userVerificationService = async (req: Request, res: Response) => { + const { id } = req.params; + + // Validate user input + if (!id) { + return res.status(400).json({ error: 'Missing user ID' }); + } + + const userRepository = getRepository(User); + const user = await userRepository.findOneBy({id}); + + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + + user.verified = true; + + await userRepository.save(user); + + return res.status(200).send('

User verified successfully

'); + +} \ No newline at end of file diff --git a/src/startups/dbConnection.ts b/src/startups/dbConnection.ts index 7fb387c..44887d8 100644 --- a/src/startups/dbConnection.ts +++ b/src/startups/dbConnection.ts @@ -1,9 +1,10 @@ -import { createConnection } from "typeorm"; +import { createConnection } from 'typeorm'; const dbConnection = async () => { try { const connection = await createConnection(); - console.log('Connected to the database'); + console.log(`Connected to the ${process.env.APP_ENV} database`); + return connection; } catch (error) { console.error('Error connecting to the database:', error); } diff --git a/src/utils/response.utils.ts b/src/utils/response.utils.ts index 6f097a7..3be109b 100644 --- a/src/utils/response.utils.ts +++ b/src/utils/response.utils.ts @@ -13,7 +13,7 @@ export const responseSuccess = ( message: string, data?: any ): Response => { - return res.status(200).json( + return res.status(statusCode).json( jsend.success({ code: statusCode, message, @@ -28,7 +28,7 @@ export const responseError = ( message: string, data?: any ): Response => { - return res.status(400).json( + return res.status(statusCode).json( jsend.error({ code: statusCode, message, diff --git a/src/utils/sendMail.ts b/src/utils/sendMail.ts new file mode 100644 index 0000000..11117ab --- /dev/null +++ b/src/utils/sendMail.ts @@ -0,0 +1,90 @@ +import nodemailer from 'nodemailer'; + +const sendMail = async (userAuth: string, + passAuth: string, + message: {from: string,to:string, subject: string, text: string, firstName: string , lastName: string}, + link: string = '') => { + const transporter = nodemailer.createTransport({ + host: process.env.HOST, + port: 587, + secure: false, // true for 465, false for other ports + auth: { + user: userAuth, + pass: passAuth + }, + }); + + const { from, to, subject, text, firstName, lastName } = message; + + const mailOptions = { + from: from, + to: to, + subject: subject, + text: text, + firstName: firstName, + lastName: lastName, + html: ` + + + + + + +
+

+ Hello ${firstName} ${lastName}, +

+
+

${text}

+

+

${link && `click here to verifie your account`}

+

This message is from: Knights Andela

+
+ +
+ + + ` + + }; + + try { + const info = await transporter.sendMail(mailOptions); + console.log('Message sent: %s', info.messageId); + } catch (error) { + console.log('Error occurred while sending email', error); + } +}; + +export default sendMail; diff --git a/tsconfig.json b/tsconfig.json index 7d6a779..88326b4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,8 +12,8 @@ "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ - // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ - // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + "experimentalDecorators": true /* Enable experimental support for legacy experimental decorators. */, + "emitDecoratorMetadata": true /* Emit design-type metadata for decorated declarations in source files. */, // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ @@ -29,7 +29,10 @@ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ - "types": ["node", "jest"] /* Specify type package names to be included without being referenced in a source file. */, + "types": [ + "node", + "jest" + ] /* Specify type package names to be included without being referenced in a source file. */, // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ From ba87dcea4601fd489de37418b4059dabc8341a78 Mon Sep 17 00:00:00 2001 From: Mc-Knight Date: Sun, 5 May 2024 18:59:58 +0200 Subject: [PATCH 24/51] feat(user): add 2FA fields to User entity --- .env.example | 11 +- package.json | 4 + src/__test__/route.test.ts | 44 +- src/controllers/authController.ts | 29 +- src/docs/authDocs.yml | 127 ++ src/docs/swaggerDark.css | 1729 +++++++++++++++++ src/entities/User.ts | 10 + src/helper/emailTemplates.ts | 16 + src/index.ts | 2 +- src/routes/UserRoutes.ts | 6 +- src/services/index.ts | 4 +- src/services/user.services.ts | 0 .../userServices/userDisableTwoFactorAuth.ts | 29 + .../userServices/userEnableTwoFactorAuth.ts | 29 + src/services/userServices/userIsOTPValid.ts | 26 + src/services/userServices/userLoginService.ts | 5 + .../userServices/userRegistrationService.ts | 103 +- src/services/userServices/userSendOTPEmail.ts | 29 + .../userServices/userSendOTPMessage.ts | 26 + .../userStartTwoFactorAuthProcess.ts | 19 + src/services/userServices/userValidateOTP.ts | 24 + .../userServices/userValidationService.ts | 33 +- src/startups/docs.ts | 13 +- 23 files changed, 2223 insertions(+), 95 deletions(-) create mode 100644 src/docs/authDocs.yml create mode 100644 src/docs/swaggerDark.css create mode 100644 src/helper/emailTemplates.ts create mode 100644 src/services/user.services.ts create mode 100644 src/services/userServices/userDisableTwoFactorAuth.ts create mode 100644 src/services/userServices/userEnableTwoFactorAuth.ts create mode 100644 src/services/userServices/userIsOTPValid.ts create mode 100644 src/services/userServices/userLoginService.ts create mode 100644 src/services/userServices/userSendOTPEmail.ts create mode 100644 src/services/userServices/userSendOTPMessage.ts create mode 100644 src/services/userServices/userStartTwoFactorAuthProcess.ts create mode 100644 src/services/userServices/userValidateOTP.ts diff --git a/.env.example b/.env.example index 3dff852..469ba92 100644 --- a/.env.example +++ b/.env.example @@ -17,4 +17,13 @@ PDN_DB_HOST= ******************************** PDN_DB_PORT= ******************************** PDN_DB_USER= ******************************** PDN_DB_PASS= ******************************** -PDN_DB_NAME= ***************************** \ No newline at end of file +PDN_DB_NAME= ***************************** + + +APP_EMAIL= ******************************** +APP_PASSWORD= ******************************** +PINDO_API_KEY= ******************************** +PINDO_API_URL= ******************************** +PINDO_SENDER= ******************************** +JWT_SECRET= ******************************** +TWO_FA_MINS= ******************************** \ No newline at end of file diff --git a/package.json b/package.json index f24cac8..e303a19 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,11 @@ "license": "ISC", "dependencies": { "@types/express-winston": "^4.0.0", + "@types/jsonwebtoken": "^9.0.6", + "@types/nodemailer": "^6.4.14", "@types/swagger-jsdoc": "^6.0.4", "@types/swagger-ui-express": "^4.1.6", + "axios": "^1.6.8", "bcrypt": "^5.1.1", "class-validator": "^0.14.1", "cors": "^2.8.5", @@ -30,6 +33,7 @@ "express-winston": "^4.2.0", "highlight.js": "^11.9.0", "jsend": "^1.1.0", + "jsonwebtoken": "^9.0.2", "morgan": "^1.10.0", "nodemailer": "^6.9.13", "nodemon": "^3.1.0", diff --git a/src/__test__/route.test.ts b/src/__test__/route.test.ts index 49cfe04..a986f67 100644 --- a/src/__test__/route.test.ts +++ b/src/__test__/route.test.ts @@ -52,7 +52,6 @@ describe('POST /user/register', () => { gender: 'Male', phoneNumber: '123678116', userType: 'Buyer', - photoUrl: 'https://example.com/photo.jpg', }; // Act @@ -74,6 +73,38 @@ describe('POST /user/register', () => { await userRepository.remove(user); } }); + + it('should enable two-factor authentication', async () => { + const data = { + email: 'john.doe@example.com', + }; + + const res = await request(app).post('/user/enable-2fa').send(data); + + expect(res.status).toBe(200); + expect(res.body).toEqual({ + status: 'success', + data: { + message: 'Two Factor Authentication enabled successfully', + }, + }); + }); + + it('should disable two-factor authentication', async () => { + const data = { + email: 'john.doe@example.com', + }; + + const res = await request(app).post('/user/disable-2fa').send(data); + + expect(res.status).toBe(200); + expect(res.body).toEqual({ + status: 'success', + data: { + message: 'Two Factor Authentication disabled successfully', + }, + }); + }); }); describe('POST /user/verify/:id', () => { it('should verify a user', async () => { @@ -95,23 +126,22 @@ describe('POST /user/verify/:id', () => { const userRepository = getRepository(User); const user = await userRepository.findOne({ where: { email: newUser.email } }); - if(user){ + if (user) { const verifyRes = await request(app).get(`/user/verify/${user.id}`); // Assert expect(verifyRes.status).toBe(200); expect(verifyRes.text).toEqual('

User verified successfully

'); - + // Check that the user's verified field is now true const verifiedUser = await userRepository.findOne({ where: { email: newUser.email } }); - if (verifiedUser){ + if (verifiedUser) { expect(verifiedUser.verified).toBe(true); } - } - + if (user) { await userRepository.remove(user); } }); -}); \ No newline at end of file +}); diff --git a/src/controllers/authController.ts b/src/controllers/authController.ts index 627db42..db39950 100644 --- a/src/controllers/authController.ts +++ b/src/controllers/authController.ts @@ -1,15 +1,24 @@ import { Request, Response } from 'express'; -import { User } from '../entities/User'; -import bcrypt from 'bcrypt'; -import { getRepository } from 'typeorm'; -import { responseError, responseServerError, responseSuccess } from '../utils/response.utils'; -import { validate } from 'class-validator'; -import { userVerificationService, userRegistrationService } from '../services'; +import { otpTemplate } from '../helper/emailTemplates'; +import { + userVerificationService, + userRegistrationService, + userEnableTwoFactorAuth, + userDisableTwoFactorAuth, +} from '../services'; export const userRegistration = async (req: Request, res: Response) => { - await userRegistrationService(req, res); -} + await userRegistrationService(req, res); +}; + export const userVerification = async (req: Request, res: Response) => { - await userVerificationService(req, res); -} + await userVerificationService(req, res); +}; + +export const enable2FA = async (req: Request, res: Response) => { + await userEnableTwoFactorAuth(req, res); +}; +export const disable2FA = async (req: Request, res: Response) => { + await userDisableTwoFactorAuth(req, res); +}; diff --git a/src/docs/authDocs.yml b/src/docs/authDocs.yml new file mode 100644 index 0000000..def86e4 --- /dev/null +++ b/src/docs/authDocs.yml @@ -0,0 +1,127 @@ +/user/login: + post: + tags: + - Auth + summary: Login a user + description: Login a user with email and password + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + email: + type: string + format: email + password: + type: string + format: password + required: + - email + - password + responses: + '200': + description: user logged in successfully, complete 2fa process + '400': + description: provide an email and password, Invalid email or password, email not verified, account suspended + '401': + description: Unauthorized + '403': + description: Forbidden + '404': + description: user not found + '500': + description: Internal server error + +/users/enable-2fa: + post: + tags: + - Auth + summary: Enable 2fa + description: Enable 2fa for a user + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + type: string + format: email + required: + - email + responses: + '200': + description: 2fa enabled successfully + '401': + description: Unauthorized + '403': + description: Forbidden + '404': + description: user not found + '500': + description: Internal server error + +/users/disable-2fa: + post: + tags: + - Auth + summary: Disable 2fa + description: Disable 2fa for a user + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + type: string + format: email + required: + - email + responses: + '200': + description: 2fa disabled successfully + '401': + description: Unauthorized + '403': + description: Forbidden + '404': + description: user not found + '500': + description: Internal server error + +/users/verify-otp: + post: + tags: + - Auth + summary: Verify OTP + description: Verify OTP for 2fa + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + type: string + format: email + otp: + type: string + required: + - email + - otp + responses: + '200': + description: OTP verified successfully + '400': + description: Please provide an email and OTP code + '401': + description: Unauthorized + '403': + description: Forbidden + '404': + description: user not found + '500': + description: Internal server error diff --git a/src/docs/swaggerDark.css b/src/docs/swaggerDark.css new file mode 100644 index 0000000..0423113 --- /dev/null +++ b/src/docs/swaggerDark.css @@ -0,0 +1,1729 @@ +@media only screen and (prefers-color-scheme: dark) { + a { + color: #8c8cfa; + } + + ::-webkit-scrollbar-track-piece { + background-color: rgba(255, 255, 255, 0.2) !important; + } + + ::-webkit-scrollbar-track { + background-color: rgba(255, 255, 255, 0.3) !important; + } + + ::-webkit-scrollbar-thumb { + background-color: rgba(255, 255, 255, 0.5) !important; + } + + embed[type='application/pdf'] { + filter: invert(90%); + } + + html { + background: #1f1f1f !important; + box-sizing: border-box; + filter: contrast(100%) brightness(100%) saturate(100%); + overflow-y: scroll; + } + + body { + background: #1f1f1f; + background-color: #1f1f1f; + background-image: none !important; + } + + button, + input, + select, + textarea { + background-color: #1f1f1f; + color: #bfbfbf; + } + + font, + html { + color: #bfbfbf; + } + + .swagger-ui, + .swagger-ui section h3 { + color: #b5bac9; + } + + .swagger-ui a { + background-color: transparent; + } + + .swagger-ui mark { + background-color: #664b00; + color: #bfbfbf; + } + + .swagger-ui legend { + color: inherit; + } + + .swagger-ui .debug * { + outline: #e6da99 solid 1px; + } + + .swagger-ui .debug-white * { + outline: #fff solid 1px; + } + + .swagger-ui .debug-black * { + outline: #bfbfbf solid 1px; + } + + .swagger-ui .debug-grid { + background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MTRDOTY4N0U2N0VFMTFFNjg2MzZDQjkwNkQ4MjgwMEIiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MTRDOTY4N0Q2N0VFMTFFNjg2MzZDQjkwNkQ4MjgwMEIiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3NjcyQkQ3NjY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3NjcyQkQ3NzY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PsBS+GMAAAAjSURBVHjaYvz//z8DLsD4gcGXiYEAGBIKGBne//fFpwAgwAB98AaF2pjlUQAAAABJRU5ErkJggg==) + 0 0; + } + + .swagger-ui .debug-grid-16 { + background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6ODYyRjhERDU2N0YyMTFFNjg2MzZDQjkwNkQ4MjgwMEIiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6ODYyRjhERDQ2N0YyMTFFNjg2MzZDQjkwNkQ4MjgwMEIiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3NjcyQkQ3QTY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3NjcyQkQ3QjY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PvCS01IAAABMSURBVHjaYmR4/5+BFPBfAMFm/MBgx8RAGWCn1AAmSg34Q6kBDKMGMDCwICeMIemF/5QawEipAWwUhwEjMDvbAWlWkvVBwu8vQIABAEwBCph8U6c0AAAAAElFTkSuQmCC) + 0 0; + } + + .swagger-ui .debug-grid-8-solid { + background: url(data:image/jpeg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAAAAAD/4QMxaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjYtYzExMSA3OS4xNTgzMjUsIDIwMTUvMDkvMTAtMDE6MTA6MjAgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE1IChNYWNpbnRvc2gpIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOkIxMjI0OTczNjdCMzExRTZCMkJDRTI0MDgxMDAyMTcxIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOkIxMjI0OTc0NjdCMzExRTZCMkJDRTI0MDgxMDAyMTcxIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6QjEyMjQ5NzE2N0IzMTFFNkIyQkNFMjQwODEwMDIxNzEiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6QjEyMjQ5NzI2N0IzMTFFNkIyQkNFMjQwODEwMDIxNzEiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz7/7gAOQWRvYmUAZMAAAAAB/9sAhAAbGhopHSlBJiZBQi8vL0JHPz4+P0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHAR0pKTQmND8oKD9HPzU/R0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0f/wAARCAAIAAgDASIAAhEBAxEB/8QAWQABAQAAAAAAAAAAAAAAAAAAAAYBAQEAAAAAAAAAAAAAAAAAAAIEEAEBAAMBAAAAAAAAAAAAAAABADECA0ERAAEDBQAAAAAAAAAAAAAAAAARITFBUWESIv/aAAwDAQACEQMRAD8AoOnTV1QTD7JJshP3vSM3P//Z) + 0 0 #1c1c21; + } + + .swagger-ui .debug-grid-16-solid { + background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NzY3MkJEN0U2N0M1MTFFNkIyQkNFMjQwODEwMDIxNzEiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NzY3MkJEN0Y2N0M1MTFFNkIyQkNFMjQwODEwMDIxNzEiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3NjcyQkQ3QzY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3NjcyQkQ3RDY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pve6J3kAAAAzSURBVHjaYvz//z8D0UDsMwMjSRoYP5Gq4SPNbRjVMEQ1fCRDg+in/6+J1AJUxsgAEGAA31BAJMS0GYEAAAAASUVORK5CYII=) + 0 0 #1c1c21; + } + + .swagger-ui .b--black { + border-color: #000; + } + + .swagger-ui .b--near-black { + border-color: #121212; + } + + .swagger-ui .b--dark-gray { + border-color: #333; + } + + .swagger-ui .b--mid-gray { + border-color: #545454; + } + + .swagger-ui .b--gray { + border-color: #787878; + } + + .swagger-ui .b--silver { + border-color: #999; + } + + .swagger-ui .b--light-silver { + border-color: #6e6e6e; + } + + .swagger-ui .b--moon-gray { + border-color: #4d4d4d; + } + + .swagger-ui .b--light-gray { + border-color: #2b2b2b; + } + + .swagger-ui .b--near-white { + border-color: #242424; + } + + .swagger-ui .b--white { + border-color: #1c1c21; + } + + .swagger-ui .b--white-90 { + border-color: rgba(28, 28, 33, 0.9); + } + + .swagger-ui .b--white-80 { + border-color: rgba(28, 28, 33, 0.8); + } + + .swagger-ui .b--white-70 { + border-color: rgba(28, 28, 33, 0.7); + } + + .swagger-ui .b--white-60 { + border-color: rgba(28, 28, 33, 0.6); + } + + .swagger-ui .b--white-50 { + border-color: rgba(28, 28, 33, 0.5); + } + + .swagger-ui .b--white-40 { + border-color: rgba(28, 28, 33, 0.4); + } + + .swagger-ui .b--white-30 { + border-color: rgba(28, 28, 33, 0.3); + } + + .swagger-ui .b--white-20 { + border-color: rgba(28, 28, 33, 0.2); + } + + .swagger-ui .b--white-10 { + border-color: rgba(28, 28, 33, 0.1); + } + + .swagger-ui .b--white-05 { + border-color: rgba(28, 28, 33, 0.05); + } + + .swagger-ui .b--white-025 { + border-color: rgba(28, 28, 33, 0.024); + } + + .swagger-ui .b--white-0125 { + border-color: rgba(28, 28, 33, 0.01); + } + + .swagger-ui .b--black-90 { + border-color: rgba(0, 0, 0, 0.9); + } + + .swagger-ui .b--black-80 { + border-color: rgba(0, 0, 0, 0.8); + } + + .swagger-ui .b--black-70 { + border-color: rgba(0, 0, 0, 0.7); + } + + .swagger-ui .b--black-60 { + border-color: rgba(0, 0, 0, 0.6); + } + + .swagger-ui .b--black-50 { + border-color: rgba(0, 0, 0, 0.5); + } + + .swagger-ui .b--black-40 { + border-color: rgba(0, 0, 0, 0.4); + } + + .swagger-ui .b--black-30 { + border-color: rgba(0, 0, 0, 0.3); + } + + .swagger-ui .b--black-20 { + border-color: rgba(0, 0, 0, 0.2); + } + + .swagger-ui .b--black-10 { + border-color: rgba(0, 0, 0, 0.1); + } + + .swagger-ui .b--black-05 { + border-color: rgba(0, 0, 0, 0.05); + } + + .swagger-ui .b--black-025 { + border-color: rgba(0, 0, 0, 0.024); + } + + .swagger-ui .b--black-0125 { + border-color: rgba(0, 0, 0, 0.01); + } + + .swagger-ui .b--dark-red { + border-color: #bc2f36; + } + + .swagger-ui .b--red { + border-color: #c83932; + } + + .swagger-ui .b--light-red { + border-color: #ab3c2b; + } + + .swagger-ui .b--orange { + border-color: #cc6e33; + } + + .swagger-ui .b--purple { + border-color: #5e2ca5; + } + + .swagger-ui .b--light-purple { + border-color: #672caf; + } + + .swagger-ui .b--dark-pink { + border-color: #ab2b81; + } + + .swagger-ui .b--hot-pink { + border-color: #c03086; + } + + .swagger-ui .b--pink { + border-color: #8f2464; + } + + .swagger-ui .b--light-pink { + border-color: #721d4d; + } + + .swagger-ui .b--dark-green { + border-color: #1c6e50; + } + + .swagger-ui .b--green { + border-color: #279b70; + } + + .swagger-ui .b--light-green { + border-color: #228762; + } + + .swagger-ui .b--navy { + border-color: #0d1d35; + } + + .swagger-ui .b--dark-blue { + border-color: #20497e; + } + + .swagger-ui .b--blue { + border-color: #4380d0; + } + + .swagger-ui .b--light-blue { + border-color: #20517e; + } + + .swagger-ui .b--lightest-blue { + border-color: #143a52; + } + + .swagger-ui .b--washed-blue { + border-color: #0c312d; + } + + .swagger-ui .b--washed-green { + border-color: #0f3d2c; + } + + .swagger-ui .b--washed-red { + border-color: #411010; + } + + .swagger-ui .b--transparent { + border-color: transparent; + } + + .swagger-ui .b--gold, + .swagger-ui .b--light-yellow, + .swagger-ui .b--washed-yellow, + .swagger-ui .b--yellow { + border-color: #664b00; + } + + .swagger-ui .shadow-1 { + box-shadow: rgba(0, 0, 0, 0.2) 0 0 4px 2px; + } + + .swagger-ui .shadow-2 { + box-shadow: rgba(0, 0, 0, 0.2) 0 0 8px 2px; + } + + .swagger-ui .shadow-3 { + box-shadow: rgba(0, 0, 0, 0.2) 2px 2px 4px 2px; + } + + .swagger-ui .shadow-4 { + box-shadow: rgba(0, 0, 0, 0.2) 2px 2px 8px 0; + } + + .swagger-ui .shadow-5 { + box-shadow: rgba(0, 0, 0, 0.2) 4px 4px 8px 0; + } + + @media screen and (min-width: 30em) { + .swagger-ui .shadow-1-ns { + box-shadow: rgba(0, 0, 0, 0.2) 0 0 4px 2px; + } + + .swagger-ui .shadow-2-ns { + box-shadow: rgba(0, 0, 0, 0.2) 0 0 8px 2px; + } + + .swagger-ui .shadow-3-ns { + box-shadow: rgba(0, 0, 0, 0.2) 2px 2px 4px 2px; + } + + .swagger-ui .shadow-4-ns { + box-shadow: rgba(0, 0, 0, 0.2) 2px 2px 8px 0; + } + + .swagger-ui .shadow-5-ns { + box-shadow: rgba(0, 0, 0, 0.2) 4px 4px 8px 0; + } + } + + @media screen and (max-width: 60em) and (min-width: 30em) { + .swagger-ui .shadow-1-m { + box-shadow: rgba(0, 0, 0, 0.2) 0 0 4px 2px; + } + + .swagger-ui .shadow-2-m { + box-shadow: rgba(0, 0, 0, 0.2) 0 0 8px 2px; + } + + .swagger-ui .shadow-3-m { + box-shadow: rgba(0, 0, 0, 0.2) 2px 2px 4px 2px; + } + + .swagger-ui .shadow-4-m { + box-shadow: rgba(0, 0, 0, 0.2) 2px 2px 8px 0; + } + + .swagger-ui .shadow-5-m { + box-shadow: rgba(0, 0, 0, 0.2) 4px 4px 8px 0; + } + } + + @media screen and (min-width: 60em) { + .swagger-ui .shadow-1-l { + box-shadow: rgba(0, 0, 0, 0.2) 0 0 4px 2px; + } + + .swagger-ui .shadow-2-l { + box-shadow: rgba(0, 0, 0, 0.2) 0 0 8px 2px; + } + + .swagger-ui .shadow-3-l { + box-shadow: rgba(0, 0, 0, 0.2) 2px 2px 4px 2px; + } + + .swagger-ui .shadow-4-l { + box-shadow: rgba(0, 0, 0, 0.2) 2px 2px 8px 0; + } + + .swagger-ui .shadow-5-l { + box-shadow: rgba(0, 0, 0, 0.2) 4px 4px 8px 0; + } + } + + .swagger-ui .black-05 { + color: rgba(191, 191, 191, 0.05); + } + + .swagger-ui .bg-black-05 { + background-color: rgba(0, 0, 0, 0.05); + } + + .swagger-ui .black-90, + .swagger-ui .hover-black-90:focus, + .swagger-ui .hover-black-90:hover { + color: rgba(191, 191, 191, 0.9); + } + + .swagger-ui .black-80, + .swagger-ui .hover-black-80:focus, + .swagger-ui .hover-black-80:hover { + color: rgba(191, 191, 191, 0.8); + } + + .swagger-ui .black-70, + .swagger-ui .hover-black-70:focus, + .swagger-ui .hover-black-70:hover { + color: rgba(191, 191, 191, 0.7); + } + + .swagger-ui .black-60, + .swagger-ui .hover-black-60:focus, + .swagger-ui .hover-black-60:hover { + color: rgba(191, 191, 191, 0.6); + } + + .swagger-ui .black-50, + .swagger-ui .hover-black-50:focus, + .swagger-ui .hover-black-50:hover { + color: rgba(191, 191, 191, 0.5); + } + + .swagger-ui .black-40, + .swagger-ui .hover-black-40:focus, + .swagger-ui .hover-black-40:hover { + color: rgba(191, 191, 191, 0.4); + } + + .swagger-ui .black-30, + .swagger-ui .hover-black-30:focus, + .swagger-ui .hover-black-30:hover { + color: rgba(191, 191, 191, 0.3); + } + + .swagger-ui .black-20, + .swagger-ui .hover-black-20:focus, + .swagger-ui .hover-black-20:hover { + color: rgba(191, 191, 191, 0.2); + } + + .swagger-ui .black-10, + .swagger-ui .hover-black-10:focus, + .swagger-ui .hover-black-10:hover { + color: rgba(191, 191, 191, 0.1); + } + + .swagger-ui .hover-white-90:focus, + .swagger-ui .hover-white-90:hover, + .swagger-ui .white-90 { + color: rgba(255, 255, 255, 0.9); + } + + .swagger-ui .hover-white-80:focus, + .swagger-ui .hover-white-80:hover, + .swagger-ui .white-80 { + color: rgba(255, 255, 255, 0.8); + } + + .swagger-ui .hover-white-70:focus, + .swagger-ui .hover-white-70:hover, + .swagger-ui .white-70 { + color: rgba(255, 255, 255, 0.7); + } + + .swagger-ui .hover-white-60:focus, + .swagger-ui .hover-white-60:hover, + .swagger-ui .white-60 { + color: rgba(255, 255, 255, 0.6); + } + + .swagger-ui .hover-white-50:focus, + .swagger-ui .hover-white-50:hover, + .swagger-ui .white-50 { + color: rgba(255, 255, 255, 0.5); + } + + .swagger-ui .hover-white-40:focus, + .swagger-ui .hover-white-40:hover, + .swagger-ui .white-40 { + color: rgba(255, 255, 255, 0.4); + } + + .swagger-ui .hover-white-30:focus, + .swagger-ui .hover-white-30:hover, + .swagger-ui .white-30 { + color: rgba(255, 255, 255, 0.3); + } + + .swagger-ui .hover-white-20:focus, + .swagger-ui .hover-white-20:hover, + .swagger-ui .white-20 { + color: rgba(255, 255, 255, 0.2); + } + + .swagger-ui .hover-white-10:focus, + .swagger-ui .hover-white-10:hover, + .swagger-ui .white-10 { + color: rgba(255, 255, 255, 0.1); + } + + .swagger-ui .hover-moon-gray:focus, + .swagger-ui .hover-moon-gray:hover, + .swagger-ui .moon-gray { + color: #ccc; + } + + .swagger-ui .hover-light-gray:focus, + .swagger-ui .hover-light-gray:hover, + .swagger-ui .light-gray { + color: #ededed; + } + + .swagger-ui .hover-near-white:focus, + .swagger-ui .hover-near-white:hover, + .swagger-ui .near-white { + color: #f5f5f5; + } + + .swagger-ui .dark-red, + .swagger-ui .hover-dark-red:focus, + .swagger-ui .hover-dark-red:hover { + color: #e6999d; + } + + .swagger-ui .hover-red:focus, + .swagger-ui .hover-red:hover, + .swagger-ui .red { + color: #e69d99; + } + + .swagger-ui .hover-light-red:focus, + .swagger-ui .hover-light-red:hover, + .swagger-ui .light-red { + color: #e6a399; + } + + .swagger-ui .hover-orange:focus, + .swagger-ui .hover-orange:hover, + .swagger-ui .orange { + color: #e6b699; + } + + .swagger-ui .gold, + .swagger-ui .hover-gold:focus, + .swagger-ui .hover-gold:hover { + color: #e6d099; + } + + .swagger-ui .hover-yellow:focus, + .swagger-ui .hover-yellow:hover, + .swagger-ui .yellow { + color: #e6da99; + } + + .swagger-ui .hover-light-yellow:focus, + .swagger-ui .hover-light-yellow:hover, + .swagger-ui .light-yellow { + color: #ede6b6; + } + + .swagger-ui .hover-purple:focus, + .swagger-ui .hover-purple:hover, + .swagger-ui .purple { + color: #b99ae4; + } + + .swagger-ui .hover-light-purple:focus, + .swagger-ui .hover-light-purple:hover, + .swagger-ui .light-purple { + color: #bb99e6; + } + + .swagger-ui .dark-pink, + .swagger-ui .hover-dark-pink:focus, + .swagger-ui .hover-dark-pink:hover { + color: #e699cc; + } + + .swagger-ui .hot-pink, + .swagger-ui .hover-hot-pink:focus, + .swagger-ui .hover-hot-pink:hover, + .swagger-ui .hover-pink:focus, + .swagger-ui .hover-pink:hover, + .swagger-ui .pink { + color: #e699c7; + } + + .swagger-ui .hover-light-pink:focus, + .swagger-ui .hover-light-pink:hover, + .swagger-ui .light-pink { + color: #edb6d5; + } + + .swagger-ui .dark-green, + .swagger-ui .green, + .swagger-ui .hover-dark-green:focus, + .swagger-ui .hover-dark-green:hover, + .swagger-ui .hover-green:focus, + .swagger-ui .hover-green:hover { + color: #99e6c9; + } + + .swagger-ui .hover-light-green:focus, + .swagger-ui .hover-light-green:hover, + .swagger-ui .light-green { + color: #a1e8ce; + } + + .swagger-ui .hover-navy:focus, + .swagger-ui .hover-navy:hover, + .swagger-ui .navy { + color: #99b8e6; + } + + .swagger-ui .blue, + .swagger-ui .dark-blue, + .swagger-ui .hover-blue:focus, + .swagger-ui .hover-blue:hover, + .swagger-ui .hover-dark-blue:focus, + .swagger-ui .hover-dark-blue:hover { + color: #99bae6; + } + + .swagger-ui .hover-light-blue:focus, + .swagger-ui .hover-light-blue:hover, + .swagger-ui .light-blue { + color: #a9cbea; + } + + .swagger-ui .hover-lightest-blue:focus, + .swagger-ui .hover-lightest-blue:hover, + .swagger-ui .lightest-blue { + color: #d6e9f5; + } + + .swagger-ui .hover-washed-blue:focus, + .swagger-ui .hover-washed-blue:hover, + .swagger-ui .washed-blue { + color: #f7fdfc; + } + + .swagger-ui .hover-washed-green:focus, + .swagger-ui .hover-washed-green:hover, + .swagger-ui .washed-green { + color: #ebfaf4; + } + + .swagger-ui .hover-washed-yellow:focus, + .swagger-ui .hover-washed-yellow:hover, + .swagger-ui .washed-yellow { + color: #fbf9ef; + } + + .swagger-ui .hover-washed-red:focus, + .swagger-ui .hover-washed-red:hover, + .swagger-ui .washed-red { + color: #f9e7e7; + } + + .swagger-ui .color-inherit, + .swagger-ui .hover-inherit:focus, + .swagger-ui .hover-inherit:hover { + color: inherit; + } + + .swagger-ui .bg-black-90, + .swagger-ui .hover-bg-black-90:focus, + .swagger-ui .hover-bg-black-90:hover { + background-color: rgba(0, 0, 0, 0.9); + } + + .swagger-ui .bg-black-80, + .swagger-ui .hover-bg-black-80:focus, + .swagger-ui .hover-bg-black-80:hover { + background-color: rgba(0, 0, 0, 0.8); + } + + .swagger-ui .bg-black-70, + .swagger-ui .hover-bg-black-70:focus, + .swagger-ui .hover-bg-black-70:hover { + background-color: rgba(0, 0, 0, 0.7); + } + + .swagger-ui .bg-black-60, + .swagger-ui .hover-bg-black-60:focus, + .swagger-ui .hover-bg-black-60:hover { + background-color: rgba(0, 0, 0, 0.6); + } + + .swagger-ui .bg-black-50, + .swagger-ui .hover-bg-black-50:focus, + .swagger-ui .hover-bg-black-50:hover { + background-color: rgba(0, 0, 0, 0.5); + } + + .swagger-ui .bg-black-40, + .swagger-ui .hover-bg-black-40:focus, + .swagger-ui .hover-bg-black-40:hover { + background-color: rgba(0, 0, 0, 0.4); + } + + .swagger-ui .bg-black-30, + .swagger-ui .hover-bg-black-30:focus, + .swagger-ui .hover-bg-black-30:hover { + background-color: rgba(0, 0, 0, 0.3); + } + + .swagger-ui .bg-black-20, + .swagger-ui .hover-bg-black-20:focus, + .swagger-ui .hover-bg-black-20:hover { + background-color: rgba(0, 0, 0, 0.2); + } + + .swagger-ui .bg-white-90, + .swagger-ui .hover-bg-white-90:focus, + .swagger-ui .hover-bg-white-90:hover { + background-color: rgba(28, 28, 33, 0.9); + } + + .swagger-ui .bg-white-80, + .swagger-ui .hover-bg-white-80:focus, + .swagger-ui .hover-bg-white-80:hover { + background-color: rgba(28, 28, 33, 0.8); + } + + .swagger-ui .bg-white-70, + .swagger-ui .hover-bg-white-70:focus, + .swagger-ui .hover-bg-white-70:hover { + background-color: rgba(28, 28, 33, 0.7); + } + + .swagger-ui .bg-white-60, + .swagger-ui .hover-bg-white-60:focus, + .swagger-ui .hover-bg-white-60:hover { + background-color: rgba(28, 28, 33, 0.6); + } + + .swagger-ui .bg-white-50, + .swagger-ui .hover-bg-white-50:focus, + .swagger-ui .hover-bg-white-50:hover { + background-color: rgba(28, 28, 33, 0.5); + } + + .swagger-ui .bg-white-40, + .swagger-ui .hover-bg-white-40:focus, + .swagger-ui .hover-bg-white-40:hover { + background-color: rgba(28, 28, 33, 0.4); + } + + .swagger-ui .bg-white-30, + .swagger-ui .hover-bg-white-30:focus, + .swagger-ui .hover-bg-white-30:hover { + background-color: rgba(28, 28, 33, 0.3); + } + + .swagger-ui .bg-white-20, + .swagger-ui .hover-bg-white-20:focus, + .swagger-ui .hover-bg-white-20:hover { + background-color: rgba(28, 28, 33, 0.2); + } + + .swagger-ui .bg-black, + .swagger-ui .hover-bg-black:focus, + .swagger-ui .hover-bg-black:hover { + background-color: #000; + } + + .swagger-ui .bg-near-black, + .swagger-ui .hover-bg-near-black:focus, + .swagger-ui .hover-bg-near-black:hover { + background-color: #121212; + } + + .swagger-ui .bg-dark-gray, + .swagger-ui .hover-bg-dark-gray:focus, + .swagger-ui .hover-bg-dark-gray:hover { + background-color: #333; + } + + .swagger-ui .bg-mid-gray, + .swagger-ui .hover-bg-mid-gray:focus, + .swagger-ui .hover-bg-mid-gray:hover { + background-color: #545454; + } + + .swagger-ui .bg-gray, + .swagger-ui .hover-bg-gray:focus, + .swagger-ui .hover-bg-gray:hover { + background-color: #787878; + } + + .swagger-ui .bg-silver, + .swagger-ui .hover-bg-silver:focus, + .swagger-ui .hover-bg-silver:hover { + background-color: #999; + } + + .swagger-ui .bg-white, + .swagger-ui .hover-bg-white:focus, + .swagger-ui .hover-bg-white:hover { + background-color: #1c1c21; + } + + .swagger-ui .bg-transparent, + .swagger-ui .hover-bg-transparent:focus, + .swagger-ui .hover-bg-transparent:hover { + background-color: transparent; + } + + .swagger-ui .bg-dark-red, + .swagger-ui .hover-bg-dark-red:focus, + .swagger-ui .hover-bg-dark-red:hover { + background-color: #bc2f36; + } + + .swagger-ui .bg-red, + .swagger-ui .hover-bg-red:focus, + .swagger-ui .hover-bg-red:hover { + background-color: #c83932; + } + + .swagger-ui .bg-light-red, + .swagger-ui .hover-bg-light-red:focus, + .swagger-ui .hover-bg-light-red:hover { + background-color: #ab3c2b; + } + + .swagger-ui .bg-orange, + .swagger-ui .hover-bg-orange:focus, + .swagger-ui .hover-bg-orange:hover { + background-color: #cc6e33; + } + + .swagger-ui .bg-gold, + .swagger-ui .bg-light-yellow, + .swagger-ui .bg-washed-yellow, + .swagger-ui .bg-yellow, + .swagger-ui .hover-bg-gold:focus, + .swagger-ui .hover-bg-gold:hover, + .swagger-ui .hover-bg-light-yellow:focus, + .swagger-ui .hover-bg-light-yellow:hover, + .swagger-ui .hover-bg-washed-yellow:focus, + .swagger-ui .hover-bg-washed-yellow:hover, + .swagger-ui .hover-bg-yellow:focus, + .swagger-ui .hover-bg-yellow:hover { + background-color: #664b00; + } + + .swagger-ui .bg-purple, + .swagger-ui .hover-bg-purple:focus, + .swagger-ui .hover-bg-purple:hover { + background-color: #5e2ca5; + } + + .swagger-ui .bg-light-purple, + .swagger-ui .hover-bg-light-purple:focus, + .swagger-ui .hover-bg-light-purple:hover { + background-color: #672caf; + } + + .swagger-ui .bg-dark-pink, + .swagger-ui .hover-bg-dark-pink:focus, + .swagger-ui .hover-bg-dark-pink:hover { + background-color: #ab2b81; + } + + .swagger-ui .bg-hot-pink, + .swagger-ui .hover-bg-hot-pink:focus, + .swagger-ui .hover-bg-hot-pink:hover { + background-color: #c03086; + } + + .swagger-ui .bg-pink, + .swagger-ui .hover-bg-pink:focus, + .swagger-ui .hover-bg-pink:hover { + background-color: #8f2464; + } + + .swagger-ui .bg-light-pink, + .swagger-ui .hover-bg-light-pink:focus, + .swagger-ui .hover-bg-light-pink:hover { + background-color: #721d4d; + } + + .swagger-ui .bg-dark-green, + .swagger-ui .hover-bg-dark-green:focus, + .swagger-ui .hover-bg-dark-green:hover { + background-color: #1c6e50; + } + + .swagger-ui .bg-green, + .swagger-ui .hover-bg-green:focus, + .swagger-ui .hover-bg-green:hover { + background-color: #279b70; + } + + .swagger-ui .bg-light-green, + .swagger-ui .hover-bg-light-green:focus, + .swagger-ui .hover-bg-light-green:hover { + background-color: #228762; + } + + .swagger-ui .bg-navy, + .swagger-ui .hover-bg-navy:focus, + .swagger-ui .hover-bg-navy:hover { + background-color: #0d1d35; + } + + .swagger-ui .bg-dark-blue, + .swagger-ui .hover-bg-dark-blue:focus, + .swagger-ui .hover-bg-dark-blue:hover { + background-color: #20497e; + } + + .swagger-ui .bg-blue, + .swagger-ui .hover-bg-blue:focus, + .swagger-ui .hover-bg-blue:hover { + background-color: #4380d0; + } + + .swagger-ui .bg-light-blue, + .swagger-ui .hover-bg-light-blue:focus, + .swagger-ui .hover-bg-light-blue:hover { + background-color: #20517e; + } + + .swagger-ui .bg-lightest-blue, + .swagger-ui .hover-bg-lightest-blue:focus, + .swagger-ui .hover-bg-lightest-blue:hover { + background-color: #143a52; + } + + .swagger-ui .bg-washed-blue, + .swagger-ui .hover-bg-washed-blue:focus, + .swagger-ui .hover-bg-washed-blue:hover { + background-color: #0c312d; + } + + .swagger-ui .bg-washed-green, + .swagger-ui .hover-bg-washed-green:focus, + .swagger-ui .hover-bg-washed-green:hover { + background-color: #0f3d2c; + } + + .swagger-ui .bg-washed-red, + .swagger-ui .hover-bg-washed-red:focus, + .swagger-ui .hover-bg-washed-red:hover { + background-color: #411010; + } + + .swagger-ui .bg-inherit, + .swagger-ui .hover-bg-inherit:focus, + .swagger-ui .hover-bg-inherit:hover { + background-color: inherit; + } + + .swagger-ui .shadow-hover { + transition: all 0.5s cubic-bezier(0.165, 0.84, 0.44, 1) 0s; + } + + .swagger-ui .shadow-hover::after { + border-radius: inherit; + box-shadow: rgba(0, 0, 0, 0.2) 0 0 16px 2px; + content: ''; + height: 100%; + left: 0; + opacity: 0; + position: absolute; + top: 0; + transition: opacity 0.5s cubic-bezier(0.165, 0.84, 0.44, 1) 0s; + width: 100%; + z-index: -1; + } + + .swagger-ui .bg-animate, + .swagger-ui .bg-animate:focus, + .swagger-ui .bg-animate:hover { + transition: background-color 0.15s ease-in-out 0s; + } + + .swagger-ui .nested-links a { + color: #99bae6; + transition: color 0.15s ease-in 0s; + } + + .swagger-ui .nested-links a:focus, + .swagger-ui .nested-links a:hover { + color: #a9cbea; + transition: color 0.15s ease-in 0s; + } + + .swagger-ui .opblock-tag { + border-bottom: 1px solid rgba(58, 64, 80, 0.3); + color: #b5bac9; + transition: all 0.2s ease 0s; + } + + .swagger-ui .opblock-tag svg, + .swagger-ui section.models h4 svg { + transition: all 0.4s ease 0s; + } + + .swagger-ui .opblock { + border: 1px solid #000; + border-radius: 4px; + box-shadow: rgba(0, 0, 0, 0.19) 0 0 3px; + margin: 0 0 15px; + } + + .swagger-ui .opblock .tab-header .tab-item.active h4 span::after { + background: gray; + } + + .swagger-ui .opblock.is-open .opblock-summary { + border-bottom: 1px solid #000; + } + + .swagger-ui .opblock .opblock-section-header { + background: rgba(28, 28, 33, 0.8); + box-shadow: rgba(0, 0, 0, 0.1) 0 1px 2px; + } + + .swagger-ui .opblock .opblock-section-header > label > span { + padding: 0 10px 0 0; + } + + .swagger-ui .opblock .opblock-summary-method { + background: #000; + color: #fff; + text-shadow: rgba(0, 0, 0, 0.1) 0 1px 0; + } + + .swagger-ui .opblock.opblock-post { + background: rgba(72, 203, 144, 0.1); + border-color: #48cb90; + } + + .swagger-ui .opblock.opblock-post .opblock-summary-method, + .swagger-ui .opblock.opblock-post .tab-header .tab-item.active h4 span::after { + background: #48cb90; + } + + .swagger-ui .opblock.opblock-post .opblock-summary { + border-color: #48cb90; + } + + .swagger-ui .opblock.opblock-put { + background: rgba(213, 157, 88, 0.1); + border-color: #d59d58; + } + + .swagger-ui .opblock.opblock-put .opblock-summary-method, + .swagger-ui .opblock.opblock-put .tab-header .tab-item.active h4 span::after { + background: #d59d58; + } + + .swagger-ui .opblock.opblock-put .opblock-summary { + border-color: #d59d58; + } + + .swagger-ui .opblock.opblock-delete { + background: rgba(200, 50, 50, 0.1); + border-color: #c83232; + } + + .swagger-ui .opblock.opblock-delete .opblock-summary-method, + .swagger-ui .opblock.opblock-delete .tab-header .tab-item.active h4 span::after { + background: #c83232; + } + + .swagger-ui .opblock.opblock-delete .opblock-summary { + border-color: #c83232; + } + + .swagger-ui .opblock.opblock-get { + background: rgba(42, 105, 167, 0.1); + border-color: #2a69a7; + } + + .swagger-ui .opblock.opblock-get .opblock-summary-method, + .swagger-ui .opblock.opblock-get .tab-header .tab-item.active h4 span::after { + background: #2a69a7; + } + + .swagger-ui .opblock.opblock-get .opblock-summary { + border-color: #2a69a7; + } + + .swagger-ui .opblock.opblock-patch { + background: rgba(92, 214, 188, 0.1); + border-color: #5cd6bc; + } + + .swagger-ui .opblock.opblock-patch .opblock-summary-method, + .swagger-ui .opblock.opblock-patch .tab-header .tab-item.active h4 span::after { + background: #5cd6bc; + } + + .swagger-ui .opblock.opblock-patch .opblock-summary { + border-color: #5cd6bc; + } + + .swagger-ui .opblock.opblock-head { + background: rgba(140, 63, 207, 0.1); + border-color: #8c3fcf; + } + + .swagger-ui .opblock.opblock-head .opblock-summary-method, + .swagger-ui .opblock.opblock-head .tab-header .tab-item.active h4 span::after { + background: #8c3fcf; + } + + .swagger-ui .opblock.opblock-head .opblock-summary { + border-color: #8c3fcf; + } + + .swagger-ui .opblock.opblock-options { + background: rgba(36, 89, 143, 0.1); + border-color: #24598f; + } + + .swagger-ui .opblock.opblock-options .opblock-summary-method, + .swagger-ui .opblock.opblock-options .tab-header .tab-item.active h4 span::after { + background: #24598f; + } + + .swagger-ui .opblock.opblock-options .opblock-summary { + border-color: #24598f; + } + + .swagger-ui .opblock.opblock-deprecated { + background: rgba(46, 46, 46, 0.1); + border-color: #2e2e2e; + opacity: 0.6; + } + + .swagger-ui .opblock.opblock-deprecated .opblock-summary-method, + .swagger-ui .opblock.opblock-deprecated .tab-header .tab-item.active h4 span::after { + background: #2e2e2e; + } + + .swagger-ui .opblock.opblock-deprecated .opblock-summary { + border-color: #2e2e2e; + } + + .swagger-ui .filter .operation-filter-input { + border: 2px solid #2b3446; + } + + .swagger-ui .tab li:first-of-type::after { + background: rgba(0, 0, 0, 0.2); + } + + .swagger-ui .download-contents { + background: #7c8192; + color: #fff; + } + + .swagger-ui .scheme-container { + background: #1c1c21; + box-shadow: rgba(0, 0, 0, 0.15) 0 1px 2px 0; + } + + .swagger-ui .loading-container .loading::before { + animation: + 1s linear 0s infinite normal none running rotation, + 0.5s ease 0s 1 normal none running opacity; + border-color: rgba(0, 0, 0, 0.6) rgba(84, 84, 84, 0.1) rgba(84, 84, 84, 0.1); + } + + .swagger-ui .response-control-media-type--accept-controller select { + border-color: #196619; + } + + .swagger-ui .response-control-media-type__accept-message { + color: #99e699; + } + + .swagger-ui .version-pragma__message code { + background-color: #3b3b3b; + } + + .swagger-ui .btn { + background: 0 0; + border: 2px solid gray; + box-shadow: rgba(0, 0, 0, 0.1) 0 1px 2px; + color: #b5bac9; + } + + .swagger-ui .btn:hover { + box-shadow: rgba(0, 0, 0, 0.3) 0 0 5px; + } + + .swagger-ui .btn.authorize, + .swagger-ui .btn.cancel { + background-color: transparent; + border-color: #a72a2a; + color: #e69999; + } + + .swagger-ui .btn.cancel:hover { + background-color: #a72a2a; + color: #fff; + } + + .swagger-ui .btn.authorize { + border-color: #48cb90; + color: #9ce3c3; + } + + .swagger-ui .btn.authorize svg { + fill: #9ce3c3; + } + + .btn.authorize.unlocked:hover { + background-color: #48cb90; + color: #fff; + } + + .btn.authorize.unlocked:hover svg { + fill: #fbfbfb; + } + + .swagger-ui .btn.execute { + background-color: #5892d5; + border-color: #5892d5; + color: #fff; + } + + .swagger-ui .copy-to-clipboard { + background: #7c8192; + } + + .swagger-ui .copy-to-clipboard button { + background: url('data:image/svg+xml;charset=utf-8,') + 50% center no-repeat; + } + + .swagger-ui select { + background: url('data:image/svg+xml;charset=utf-8,') + right 10px center/20px no-repeat #212121; + background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM6c29kaXBvZGk9Imh0dHA6Ly9zb2RpcG9kaS5zb3VyY2Vmb3JnZS5uZXQvRFREL3NvZGlwb2RpLTAuZHRkIgogICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgaW5rc2NhcGU6dmVyc2lvbj0iMS4wICg0MDM1YTRmYjQ5LCAyMDIwLTA1LTAxKSIKICAgc29kaXBvZGk6ZG9jbmFtZT0iZG93bmxvYWQuc3ZnIgogICBpZD0ic3ZnNCIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMjAgMjAiPgogIDxtZXRhZGF0YQogICAgIGlkPSJtZXRhZGF0YTEwIj4KICAgIDxyZGY6UkRGPgogICAgICA8Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+CiAgICAgICAgPGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+CiAgICAgICAgPGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPgogICAgICA8L2NjOldvcms+CiAgICA8L3JkZjpSREY+CiAgPC9tZXRhZGF0YT4KICA8ZGVmcwogICAgIGlkPSJkZWZzOCIgLz4KICA8c29kaXBvZGk6bmFtZWR2aWV3CiAgICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0ic3ZnNCIKICAgICBpbmtzY2FwZTp3aW5kb3ctbWF4aW1pemVkPSIxIgogICAgIGlua3NjYXBlOndpbmRvdy15PSItOSIKICAgICBpbmtzY2FwZTp3aW5kb3cteD0iLTkiCiAgICAgaW5rc2NhcGU6Y3k9IjEwIgogICAgIGlua3NjYXBlOmN4PSIxMCIKICAgICBpbmtzY2FwZTp6b29tPSI0MS41IgogICAgIHNob3dncmlkPSJmYWxzZSIKICAgICBpZD0ibmFtZWR2aWV3NiIKICAgICBpbmtzY2FwZTp3aW5kb3ctaGVpZ2h0PSIxMDAxIgogICAgIGlua3NjYXBlOndpbmRvdy13aWR0aD0iMTkyMCIKICAgICBpbmtzY2FwZTpwYWdlc2hhZG93PSIyIgogICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwIgogICAgIGd1aWRldG9sZXJhbmNlPSIxMCIKICAgICBncmlkdG9sZXJhbmNlPSIxMCIKICAgICBvYmplY3R0b2xlcmFuY2U9IjEwIgogICAgIGJvcmRlcm9wYWNpdHk9IjEiCiAgICAgYm9yZGVyY29sb3I9IiM2NjY2NjYiCiAgICAgcGFnZWNvbG9yPSIjZmZmZmZmIiAvPgogIDxwYXRoCiAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZiIKICAgICBpZD0icGF0aDIiCiAgICAgZD0iTTEzLjQxOCA3Ljg1OWEuNjk1LjY5NSAwIDAxLjk3OCAwIC42OC42OCAwIDAxMCAuOTY5bC0zLjkwOCAzLjgzYS42OTcuNjk3IDAgMDEtLjk3OSAwbC0zLjkwOC0zLjgzYS42OC42OCAwIDAxMC0uOTY5LjY5NS42OTUgMCAwMS45NzggMEwxMCAxMWwzLjQxOC0zLjE0MXoiIC8+Cjwvc3ZnPgo=) + right 10px center/20px no-repeat #1c1c21; + border: 2px solid #41444e; + } + + .swagger-ui select[multiple] { + background: #212121; + } + + .swagger-ui button.invalid, + .swagger-ui input[type='email'].invalid, + .swagger-ui input[type='file'].invalid, + .swagger-ui input[type='password'].invalid, + .swagger-ui input[type='search'].invalid, + .swagger-ui input[type='text'].invalid, + .swagger-ui select.invalid, + .swagger-ui textarea.invalid { + background: #390e0e; + border-color: #c83232; + } + + .swagger-ui input[type='email'], + .swagger-ui input[type='file'], + .swagger-ui input[type='password'], + .swagger-ui input[type='search'], + .swagger-ui input[type='text'], + .swagger-ui textarea { + background: #1c1c21; + border: 1px solid #404040; + } + + .swagger-ui textarea { + background: rgba(28, 28, 33, 0.8); + color: #b5bac9; + } + + .swagger-ui input[disabled], + .swagger-ui select[disabled] { + background-color: #1f1f1f; + color: #bfbfbf; + } + + .swagger-ui textarea[disabled] { + background-color: #41444e; + color: #fff; + } + + .swagger-ui select[disabled] { + border-color: #878787; + } + + .swagger-ui textarea:focus { + border: 2px solid #2a69a7; + } + + .swagger-ui .checkbox input[type='checkbox'] + label > .item { + background: #303030; + box-shadow: #303030 0 0 0 2px; + } + + .swagger-ui .checkbox input[type='checkbox']:checked + label > .item { + background: url('data:image/svg+xml;charset=utf-8,') + 50% center no-repeat #303030; + } + + .swagger-ui .dialog-ux .backdrop-ux { + background: rgba(0, 0, 0, 0.8); + } + + .swagger-ui .dialog-ux .modal-ux { + background: #1c1c21; + border: 1px solid #2e2e2e; + box-shadow: rgba(0, 0, 0, 0.2) 0 10px 30px 0; + } + + .swagger-ui .dialog-ux .modal-ux-header .close-modal { + background: 0 0; + } + + .swagger-ui .model .deprecated span, + .swagger-ui .model .deprecated td { + color: #bfbfbf !important; + } + + .swagger-ui .model-toggle::after { + background: url('data:image/svg+xml;charset=utf-8,') + 50% center/100% no-repeat; + } + + .swagger-ui .model-hint { + background: rgba(0, 0, 0, 0.7); + color: #ebebeb; + } + + .swagger-ui section.models { + border: 1px solid rgba(58, 64, 80, 0.3); + } + + .swagger-ui section.models.is-open h4 { + border-bottom: 1px solid rgba(58, 64, 80, 0.3); + } + + .swagger-ui section.models .model-container { + background: rgba(0, 0, 0, 0.05); + } + + .swagger-ui section.models .model-container:hover { + background: rgba(0, 0, 0, 0.07); + } + + .swagger-ui .model-box { + background: rgba(0, 0, 0, 0.1); + } + + .swagger-ui .prop-type { + color: #aaaad4; + } + + .swagger-ui table thead tr td, + .swagger-ui table thead tr th { + border-bottom: 1px solid rgba(58, 64, 80, 0.2); + color: #b5bac9; + } + + .swagger-ui .parameter__name.required::after { + color: rgba(230, 153, 153, 0.6); + } + + .swagger-ui .topbar .download-url-wrapper .select-label { + color: #f0f0f0; + } + + .swagger-ui .topbar .download-url-wrapper .download-url-button { + background: #63a040; + color: #fff; + } + + .swagger-ui .info .title small { + background: #7c8492; + } + + .swagger-ui .info .title small.version-stamp { + background-color: #7a9b27; + } + + .swagger-ui .auth-container .errors { + background-color: #350d0d; + color: #b5bac9; + } + + .swagger-ui .errors-wrapper { + background: rgba(200, 50, 50, 0.1); + border: 2px solid #c83232; + } + + .swagger-ui .markdown code, + .swagger-ui .renderedmarkdown code { + background: rgba(0, 0, 0, 0.05); + color: #c299e6; + } + + .swagger-ui .model-toggle:after { + background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM6c29kaXBvZGk9Imh0dHA6Ly9zb2RpcG9kaS5zb3VyY2Vmb3JnZS5uZXQvRFREL3NvZGlwb2RpLTAuZHRkIgogICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgaW5rc2NhcGU6dmVyc2lvbj0iMS4wICg0MDM1YTRmYjQ5LCAyMDIwLTA1LTAxKSIKICAgc29kaXBvZGk6ZG9jbmFtZT0iZG93bmxvYWQyLnN2ZyIKICAgaWQ9InN2ZzQiCiAgIHZlcnNpb249IjEuMSIKICAgaGVpZ2h0PSIyNCIKICAgd2lkdGg9IjI0Ij4KICA8bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGExMCI+CiAgICA8cmRmOlJERj4KICAgICAgPGNjOldvcmsKICAgICAgICAgcmRmOmFib3V0PSIiPgogICAgICAgIDxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PgogICAgICAgIDxkYzp0eXBlCiAgICAgICAgICAgcmRmOnJlc291cmNlPSJodHRwOi8vcHVybC5vcmcvZGMvZGNtaXR5cGUvU3RpbGxJbWFnZSIgLz4KICAgICAgPC9jYzpXb3JrPgogICAgPC9yZGY6UkRGPgogIDwvbWV0YWRhdGE+CiAgPGRlZnMKICAgICBpZD0iZGVmczgiIC8+CiAgPHNvZGlwb2RpOm5hbWVkdmlldwogICAgIGlua3NjYXBlOmN1cnJlbnQtbGF5ZXI9InN2ZzQiCiAgICAgaW5rc2NhcGU6d2luZG93LW1heGltaXplZD0iMSIKICAgICBpbmtzY2FwZTp3aW5kb3cteT0iLTkiCiAgICAgaW5rc2NhcGU6d2luZG93LXg9Ii05IgogICAgIGlua3NjYXBlOmN5PSIxMiIKICAgICBpbmtzY2FwZTpjeD0iMTIiCiAgICAgaW5rc2NhcGU6em9vbT0iMzQuNTgzMzMzIgogICAgIHNob3dncmlkPSJmYWxzZSIKICAgICBpZD0ibmFtZWR2aWV3NiIKICAgICBpbmtzY2FwZTp3aW5kb3ctaGVpZ2h0PSIxMDAxIgogICAgIGlua3NjYXBlOndpbmRvdy13aWR0aD0iMTkyMCIKICAgICBpbmtzY2FwZTpwYWdlc2hhZG93PSIyIgogICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwIgogICAgIGd1aWRldG9sZXJhbmNlPSIxMCIKICAgICBncmlkdG9sZXJhbmNlPSIxMCIKICAgICBvYmplY3R0b2xlcmFuY2U9IjEwIgogICAgIGJvcmRlcm9wYWNpdHk9IjEiCiAgICAgYm9yZGVyY29sb3I9IiM2NjY2NjYiCiAgICAgcGFnZWNvbG9yPSIjZmZmZmZmIiAvPgogIDxwYXRoCiAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZiIKICAgICBpZD0icGF0aDIiCiAgICAgZD0iTTEwIDZMOC41OSA3LjQxIDEzLjE3IDEybC00LjU4IDQuNTlMMTAgMThsNi02eiIgLz4KPC9zdmc+Cg==) + 50% no-repeat; + } + + /* arrows for each operation and request are now white */ + .arrow, + #large-arrow-up { + fill: #fff; + } + + #unlocked { + fill: #fff; + } + + ::-webkit-scrollbar-track { + background-color: #646464 !important; + } + + ::-webkit-scrollbar-thumb { + background-color: #242424 !important; + border: 2px solid #3e4346 !important; + } + + ::-webkit-scrollbar-button:vertical:start:decrement { + background: linear-gradient(130deg, #696969 40%, rgba(255, 0, 0, 0) 41%), + linear-gradient(230deg, #696969 40%, transparent 41%), linear-gradient(0deg, #696969 40%, transparent 31%); + background-color: #b6b6b6; + } + + ::-webkit-scrollbar-button:vertical:end:increment { + background: linear-gradient(310deg, #696969 40%, transparent 41%), + linear-gradient(50deg, #696969 40%, transparent 41%), linear-gradient(180deg, #696969 40%, transparent 31%); + background-color: #b6b6b6; + } + + ::-webkit-scrollbar-button:horizontal:end:increment { + background: linear-gradient(210deg, #696969 40%, transparent 41%), + linear-gradient(330deg, #696969 40%, transparent 41%), linear-gradient(90deg, #696969 30%, transparent 31%); + background-color: #b6b6b6; + } + + ::-webkit-scrollbar-button:horizontal:start:decrement { + background: linear-gradient(30deg, #696969 40%, transparent 41%), + linear-gradient(150deg, #696969 40%, transparent 41%), linear-gradient(270deg, #696969 30%, transparent 31%); + background-color: #b6b6b6; + } + + ::-webkit-scrollbar-button, + ::-webkit-scrollbar-track-piece { + background-color: #3e4346 !important; + } + + .swagger-ui .black, + .swagger-ui .checkbox, + .swagger-ui .dark-gray, + .swagger-ui .download-url-wrapper .loading, + .swagger-ui .errors-wrapper .errors small, + .swagger-ui .fallback, + .swagger-ui .filter .loading, + .swagger-ui .gray, + .swagger-ui .hover-black:focus, + .swagger-ui .hover-black:hover, + .swagger-ui .hover-dark-gray:focus, + .swagger-ui .hover-dark-gray:hover, + .swagger-ui .hover-gray:focus, + .swagger-ui .hover-gray:hover, + .swagger-ui .hover-light-silver:focus, + .swagger-ui .hover-light-silver:hover, + .swagger-ui .hover-mid-gray:focus, + .swagger-ui .hover-mid-gray:hover, + .swagger-ui .hover-near-black:focus, + .swagger-ui .hover-near-black:hover, + .swagger-ui .hover-silver:focus, + .swagger-ui .hover-silver:hover, + .swagger-ui .light-silver, + .swagger-ui .markdown pre, + .swagger-ui .mid-gray, + .swagger-ui .model .property, + .swagger-ui .model .property.primitive, + .swagger-ui .model-title, + .swagger-ui .near-black, + .swagger-ui .parameter__extension, + .swagger-ui .parameter__in, + .swagger-ui .prop-format, + .swagger-ui .renderedmarkdown pre, + .swagger-ui .response-col_links .response-undocumented, + .swagger-ui .response-col_status .response-undocumented, + .swagger-ui .silver, + .swagger-ui section.models h4, + .swagger-ui section.models h5, + .swagger-ui span.token-not-formatted, + .swagger-ui span.token-string, + .swagger-ui table.headers .header-example, + .swagger-ui table.model tr.description, + .swagger-ui table.model tr.extension { + color: #bfbfbf; + } + + .swagger-ui .hover-white:focus, + .swagger-ui .hover-white:hover, + .swagger-ui .info .title small pre, + .swagger-ui .topbar a, + .swagger-ui .white { + color: #fff; + } + + .swagger-ui .bg-black-10, + .swagger-ui .hover-bg-black-10:focus, + .swagger-ui .hover-bg-black-10:hover, + .swagger-ui .stripe-dark:nth-child(2n + 1) { + background-color: rgba(0, 0, 0, 0.1); + } + + .swagger-ui .bg-white-10, + .swagger-ui .hover-bg-white-10:focus, + .swagger-ui .hover-bg-white-10:hover, + .swagger-ui .stripe-light:nth-child(2n + 1) { + background-color: rgba(28, 28, 33, 0.1); + } + + .swagger-ui .bg-light-silver, + .swagger-ui .hover-bg-light-silver:focus, + .swagger-ui .hover-bg-light-silver:hover, + .swagger-ui .striped--light-silver:nth-child(2n + 1) { + background-color: #6e6e6e; + } + + .swagger-ui .bg-moon-gray, + .swagger-ui .hover-bg-moon-gray:focus, + .swagger-ui .hover-bg-moon-gray:hover, + .swagger-ui .striped--moon-gray:nth-child(2n + 1) { + background-color: #4d4d4d; + } + + .swagger-ui .bg-light-gray, + .swagger-ui .hover-bg-light-gray:focus, + .swagger-ui .hover-bg-light-gray:hover, + .swagger-ui .striped--light-gray:nth-child(2n + 1) { + background-color: #2b2b2b; + } + + .swagger-ui .bg-near-white, + .swagger-ui .hover-bg-near-white:focus, + .swagger-ui .hover-bg-near-white:hover, + .swagger-ui .striped--near-white:nth-child(2n + 1) { + background-color: #242424; + } + + .swagger-ui .opblock-tag:hover, + .swagger-ui section.models h4:hover { + background: rgba(0, 0, 0, 0.02); + } + + .swagger-ui .checkbox p, + .swagger-ui .dialog-ux .modal-ux-content h4, + .swagger-ui .dialog-ux .modal-ux-content p, + .swagger-ui .dialog-ux .modal-ux-header h3, + .swagger-ui .errors-wrapper .errors h4, + .swagger-ui .errors-wrapper hgroup h4, + .swagger-ui .info .base-url, + .swagger-ui .info .title, + .swagger-ui .info h1, + .swagger-ui .info h2, + .swagger-ui .info h3, + .swagger-ui .info h4, + .swagger-ui .info h5, + .swagger-ui .info li, + .swagger-ui .info p, + .swagger-ui .info table, + .swagger-ui .loading-container .loading::after, + .swagger-ui .model, + .swagger-ui .opblock .opblock-section-header h4, + .swagger-ui .opblock .opblock-section-header > label, + .swagger-ui .opblock .opblock-summary-description, + .swagger-ui .opblock .opblock-summary-operation-id, + .swagger-ui .opblock .opblock-summary-path, + .swagger-ui .opblock .opblock-summary-path__deprecated, + .swagger-ui .opblock-description-wrapper, + .swagger-ui .opblock-description-wrapper h4, + .swagger-ui .opblock-description-wrapper p, + .swagger-ui .opblock-external-docs-wrapper, + .swagger-ui .opblock-external-docs-wrapper h4, + .swagger-ui .opblock-external-docs-wrapper p, + .swagger-ui .opblock-tag small, + .swagger-ui .opblock-title_normal, + .swagger-ui .opblock-title_normal h4, + .swagger-ui .opblock-title_normal p, + .swagger-ui .parameter__name, + .swagger-ui .parameter__type, + .swagger-ui .response-col_links, + .swagger-ui .response-col_status, + .swagger-ui .responses-inner h4, + .swagger-ui .responses-inner h5, + .swagger-ui .scheme-container .schemes > label, + .swagger-ui .scopes h2, + .swagger-ui .servers > label, + .swagger-ui .tab li, + .swagger-ui label, + .swagger-ui select, + .swagger-ui table.headers td { + color: #b5bac9; + } + + .swagger-ui .download-url-wrapper .failed, + .swagger-ui .filter .failed, + .swagger-ui .model-deprecated-warning, + .swagger-ui .parameter__deprecated, + .swagger-ui .parameter__name.required span, + .swagger-ui table.model tr.property-row .star { + color: #e69999; + } + + .swagger-ui .opblock-body pre.microlight, + .swagger-ui textarea.curl { + background: #41444e; + border-radius: 4px; + color: #fff; + } + + .swagger-ui .expand-methods svg, + .swagger-ui .expand-methods:hover svg { + fill: #bfbfbf; + } + + .swagger-ui .auth-container, + .swagger-ui .dialog-ux .modal-ux-header { + border-bottom: 1px solid #2e2e2e; + } + + .swagger-ui .topbar .download-url-wrapper .select-label select, + .swagger-ui .topbar .download-url-wrapper input[type='text'] { + border: 2px solid #63a040; + } + + .swagger-ui .info a, + .swagger-ui .info a:hover, + .swagger-ui .scopes h2 a { + color: #99bde6; + } + + /* Dark Scrollbar */ + ::-webkit-scrollbar { + width: 14px; + height: 14px; + } + + ::-webkit-scrollbar-button { + background-color: #3e4346 !important; + } + + ::-webkit-scrollbar-track { + background-color: #646464 !important; + } + + ::-webkit-scrollbar-track-piece { + background-color: #3e4346 !important; + } + + ::-webkit-scrollbar-thumb { + height: 50px; + background-color: #242424 !important; + border: 2px solid #3e4346 !important; + } + + ::-webkit-scrollbar-button:vertical:start:decrement { + background: linear-gradient(130deg, #696969 40%, rgba(255, 0, 0, 0) 41%), + linear-gradient(230deg, #696969 40%, rgba(0, 0, 0, 0) 41%), + linear-gradient(0deg, #696969 40%, rgba(0, 0, 0, 0) 31%); + background-color: #b6b6b6; + } + + ::-webkit-scrollbar-button:vertical:end:increment { + background: linear-gradient(310deg, #696969 40%, rgba(0, 0, 0, 0) 41%), + linear-gradient(50deg, #696969 40%, rgba(0, 0, 0, 0) 41%), + linear-gradient(180deg, #696969 40%, rgba(0, 0, 0, 0) 31%); + background-color: #b6b6b6; + } + + ::-webkit-scrollbar-button:horizontal:end:increment { + background: linear-gradient(210deg, #696969 40%, rgba(0, 0, 0, 0) 41%), + linear-gradient(330deg, #696969 40%, rgba(0, 0, 0, 0) 41%), + linear-gradient(90deg, #696969 30%, rgba(0, 0, 0, 0) 31%); + background-color: #b6b6b6; + } + + ::-webkit-scrollbar-button:horizontal:start:decrement { + background: linear-gradient(30deg, #696969 40%, rgba(0, 0, 0, 0) 41%), + linear-gradient(150deg, #696969 40%, rgba(0, 0, 0, 0) 41%), + linear-gradient(270deg, #696969 30%, rgba(0, 0, 0, 0) 31%); + background-color: #b6b6b6; + } +} diff --git a/src/entities/User.ts b/src/entities/User.ts index f1be411..78f7d75 100644 --- a/src/entities/User.ts +++ b/src/entities/User.ts @@ -54,6 +54,16 @@ export class User { @IsIn(['Admin', 'Buyer', 'Vendor']) userType!: 'Admin' | 'Buyer' | 'Vendor'; + @Column({ default: false }) + @IsBoolean() + twoFactorEnabled!: boolean; + + @Column({ nullable: true }) + twoFactorCode?: string; + + @Column({ type: 'timestamp', nullable: true }) + twoFactorCodeExpiresAt?: Date; + @CreateDateColumn() createdAt!: Date; diff --git a/src/helper/emailTemplates.ts b/src/helper/emailTemplates.ts new file mode 100644 index 0000000..5578446 --- /dev/null +++ b/src/helper/emailTemplates.ts @@ -0,0 +1,16 @@ +export const otpTemplate = (name: string, otpCode: string): string => { + return ` +
+

Login OTP Code

+

Hi ${name},

+

+ It looks like you are trying to log in to knight e-commerce using your username and password. + As an additional security measure you are requested to enter the OTP code (one-time password) provided in this email. +

+

If you did not intend to log in to your acount, please ignore this email.

+

The OTP code is: ${otpCode}

+ +

Cheers,

+

Knights e-commerce Team

+
`; +}; diff --git a/src/index.ts b/src/index.ts index bb5ad3a..34d0d2f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,5 +33,5 @@ const morganFormat = ':method :url :status :response-time ms - :res[content-leng app.use(morgan(morganFormat)); export const server = app.listen(port, () => { - console.log(`[server]: Server is running at http://localhost:${port}`); + console.log(`[server]: Server is running at http://localhost:${port}/api/v1`); }); diff --git a/src/routes/UserRoutes.ts b/src/routes/UserRoutes.ts index 7fcd3b5..ae59520 100644 --- a/src/routes/UserRoutes.ts +++ b/src/routes/UserRoutes.ts @@ -1,9 +1,13 @@ import { Router } from 'express'; -import { userRegistration, userVerification} from '../controllers/index'; +import { userRegistration, userVerification, enable2FA, disable2FA } from '../controllers/authController'; const router = Router(); router.post('/register', userRegistration); router.get('/verify/:id', userVerification); +// router.post('/login', login); +router.post('/enable-2fa', enable2FA); +router.post('/disable-2fa', disable2FA); +// router.post('/verify-otp', verifyOTP); export default router; diff --git a/src/services/index.ts b/src/services/index.ts index 271665e..c77412d 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1,3 +1,5 @@ // export all Services export * from './userServices/userRegistrationService'; -export * from './userServices/userValidationService'; \ No newline at end of file +export * from './userServices/userValidationService'; +export * from './userServices/userEnableTwoFactorAuth'; +export * from './userServices/userDisableTwoFactorAuth'; diff --git a/src/services/user.services.ts b/src/services/user.services.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/services/userServices/userDisableTwoFactorAuth.ts b/src/services/userServices/userDisableTwoFactorAuth.ts new file mode 100644 index 0000000..63729fd --- /dev/null +++ b/src/services/userServices/userDisableTwoFactorAuth.ts @@ -0,0 +1,29 @@ +import { Request, Response } from 'express'; +import { User } from '../../entities/User'; +import { getRepository } from 'typeorm'; + +export const userDisableTwoFactorAuth = async (req: Request, res: Response) => { + try { + const { email } = req.body; + + if (!email) { + return res.status(400).json({ status: 'error', message: 'Please provide your email' }); + } + + const userRepository = getRepository(User); + const user = await userRepository.findOneBy({ email }); + + if (!user) { + return res.status(404).json({ status: 'error', message: 'User not found' }); + } + + user.twoFactorEnabled = false; + await userRepository.save(user); + + return res.status(200).json({ status: 'success', message: 'Two factor authentication disabled successfully' }); + } catch (error) { + if (error instanceof Error) { + return res.status(500).json({ status: 'error', message: error.message }); + } + } +}; diff --git a/src/services/userServices/userEnableTwoFactorAuth.ts b/src/services/userServices/userEnableTwoFactorAuth.ts new file mode 100644 index 0000000..16b36be --- /dev/null +++ b/src/services/userServices/userEnableTwoFactorAuth.ts @@ -0,0 +1,29 @@ +import { Request, Response } from 'express'; +import { User } from '../../entities/User'; +import { getRepository } from 'typeorm'; + +export const userEnableTwoFactorAuth = async (req: Request, res: Response) => { + try { + const { email } = req.body; + + if (!email) { + return res.status(400).json({ status: 'error', message: 'Please provide your email' }); + } + + const userRepository = getRepository(User); + const user = await userRepository.findOneBy({ email }); + + if (!user) { + return res.status(404).json({ status: 'error', message: 'User not found' }); + } + + user.twoFactorEnabled = true; + await userRepository.save(user); + + return res.status(200).json({ status: 'success', message: 'Two factor authentication enabled successfully' }); + } catch (error) { + if (error instanceof Error) { + return res.status(500).json({ status: 'error', message: error.message }); + } + } +}; diff --git a/src/services/userServices/userIsOTPValid.ts b/src/services/userServices/userIsOTPValid.ts new file mode 100644 index 0000000..287954b --- /dev/null +++ b/src/services/userServices/userIsOTPValid.ts @@ -0,0 +1,26 @@ +import { User } from '../../entities/User'; +import { getRepository } from 'typeorm'; +import dotenv from 'dotenv'; + +dotenv.config(); + +export const is2FAValid = async (email: string, code: string) => { + const userRepository = getRepository(User); + const user = await userRepository.findOneBy({ email }); + if (!user) { + return [false, 'User not found']; + } + + if (user.twoFactorCode !== code) { + return [false, 'Invalid authentication code']; + } + + if (user.twoFactorCodeExpiresAt && user.twoFactorCodeExpiresAt < new Date()) { + return [false, 'Authentication code expired']; + } + + // Force 2FA code to expire after usage + user.twoFactorCodeExpiresAt = new Date(Date.now() - 10 * 60 * 1000); + await userRepository.save(user); + return [true]; +}; diff --git a/src/services/userServices/userLoginService.ts b/src/services/userServices/userLoginService.ts new file mode 100644 index 0000000..f8cc0c9 --- /dev/null +++ b/src/services/userServices/userLoginService.ts @@ -0,0 +1,5 @@ +import { Request, Response } from 'express'; +import { User } from '../../entities/User'; +import { getRepository } from 'typeorm'; + +export const userLogin = async (req: Request, res: Response) => {}; diff --git a/src/services/userServices/userRegistrationService.ts b/src/services/userServices/userRegistrationService.ts index 9b62908..2d30fe4 100644 --- a/src/services/userServices/userRegistrationService.ts +++ b/src/services/userServices/userRegistrationService.ts @@ -1,4 +1,3 @@ - import { Request, Response } from 'express'; import { User } from '../../entities/User'; import bcrypt from 'bcrypt'; @@ -9,66 +8,62 @@ import dotenv from 'dotenv'; dotenv.config(); export const userRegistrationService = async (req: Request, res: Response) => { - const { firstName, lastName, email, password, gender, phoneNumber, userType, photoUrl } = req.body; - - // Validate user input - if (!firstName || !lastName || !email || !password || !gender || !phoneNumber || !photoUrl) { - return responseError(res, 400, 'Please fill all the required fields'); - } - - const userRepository = getRepository(User); + const { firstName, lastName, email, password, gender, phoneNumber, userType } = req.body; - try { - // Check for existing user - const existingUser = await userRepository.findOneBy({ email }); - const existingUserNumber = await userRepository.findOneBy({ phoneNumber }); + // Validate user input + if (!firstName || !lastName || !email || !password || !gender || !phoneNumber) { + return responseError(res, 400, 'Please fill all the required fields'); + } - if (existingUser || existingUserNumber) { - return responseError(res, 409, 'Email or phone number already in use'); - } + const userRepository = getRepository(User); - const saltRounds = 10; - const hashedPassword = await bcrypt.hash(password, saltRounds); + try { + // Check for existing user + const existingUser = await userRepository.findOneBy({ email }); + const existingUserNumber = await userRepository.findOneBy({ phoneNumber }); - // Create user - const user = new User(); - user.firstName = firstName; - user.lastName = lastName; - user.email = email; - user.password = hashedPassword; - user.userType = userType; - user.gender = gender; - user.phoneNumber = phoneNumber; - user.photoUrl = photoUrl; - - // Save user - await userRepository.save(user); - if (process.env.AUTH_EMAIL && process.env.AUTH_PASSWORD) { + if (existingUser || existingUserNumber) { + return responseError(res, 409, 'Email or phone number already in use'); + } - const message = { - to: email, - from: process.env.AUTH_EMAIL, - subject: 'Welcome to the knights app', - text: `Welcome to the app, ${firstName} ${lastName}!`, - lastName: lastName, - firstName: firstName, - } - const link = `http://localhost:${process.env.PORT}/user/verify/${user.id}` + const saltRounds = 10; + const hashedPassword = await bcrypt.hash(password, saltRounds); - sendMail(process.env.AUTH_EMAIL, process.env.AUTH_PASSWORD, message, link); - + // Create user + const user = new User(); + user.firstName = firstName; + user.lastName = lastName; + user.email = email; + user.password = hashedPassword; + user.userType = userType; + user.gender = gender; + user.phoneNumber = phoneNumber; - } else { - // return res.status(500).json({ error: 'Email or password for mail server not configured' }); - return responseError(res, 500 , 'Email or password for mail server not configured'); - } + // Save user + await userRepository.save(user); + if (process.env.AUTH_EMAIL && process.env.AUTH_PASSWORD) { + const message = { + to: email, + from: process.env.AUTH_EMAIL, + subject: 'Welcome to the knights app', + text: `Welcome to the app, ${firstName} ${lastName}!`, + lastName: lastName, + firstName: firstName, + }; + const link = `http://localhost:${process.env.PORT}/user/verify/${user.id}`; - return responseSuccess(res, 201, 'User registered successfully'); - } catch (error) { - if (error instanceof Error) { - return responseServerError(res, error.message); - } + sendMail(process.env.AUTH_EMAIL, process.env.AUTH_PASSWORD, message, link); + } else { + // return res.status(500).json({ error: 'Email or password for mail server not configured' }); + return responseError(res, 500, 'Email or password for mail server not configured'); + } - return responseServerError(res, 'Unknown error occurred'); + return responseSuccess(res, 201, 'User registered successfully'); + } catch (error) { + if (error instanceof Error) { + return responseServerError(res, error.message); } -}; \ No newline at end of file + + return responseServerError(res, 'Unknown error occurred'); + } +}; diff --git a/src/services/userServices/userSendOTPEmail.ts b/src/services/userServices/userSendOTPEmail.ts new file mode 100644 index 0000000..a71a599 --- /dev/null +++ b/src/services/userServices/userSendOTPEmail.ts @@ -0,0 +1,29 @@ +import nodemailer from 'nodemailer'; +import dotenv from 'dotenv'; + +dotenv.config(); + +export const sendOTPEmail = async (subject: string, email: string, content: any) => { + const transporter = nodemailer.createTransport({ + service: 'gmail', + auth: { + user: process.env.APP_EMAIL, + pass: process.env.APP_PASSWORD, + }, + }); + + const mailOptions = { + from: process.env.APP_EMAIL, + to: email, + subject: subject, + html: content, + }; + + transporter.sendMail(mailOptions, (error, info) => { + if (error) { + console.log(error); + } else { + console.log('Email sent: ' + info.response); + } + }); +}; diff --git a/src/services/userServices/userSendOTPMessage.ts b/src/services/userServices/userSendOTPMessage.ts new file mode 100644 index 0000000..05a0758 --- /dev/null +++ b/src/services/userServices/userSendOTPMessage.ts @@ -0,0 +1,26 @@ +import dotenv from 'dotenv'; +import axios from 'axios'; + +dotenv.config(); + +export const sendOTPSMS = async (phone: string, code: string) => { + const data = { + to: `+25${phone}`, + text: `use this code to confirm your login. ${code}`, + sender: `${process.env.PINDO_SENDER}`, + }; + + const options = { + headers: { + 'Authorization': `Bearer ${process.env.PINDO_API_KEY}`, + 'Content-Type': 'application/json', + }, + }; + + try { + const response = await axios.post(`${process.env.PINDO_API_URL}`, data, options); + console.log('SMS sent:', response.data.sms_id); + } catch (error) { + console.error('Failed to send SMS:', error); + } +}; diff --git a/src/services/userServices/userStartTwoFactorAuthProcess.ts b/src/services/userServices/userStartTwoFactorAuthProcess.ts new file mode 100644 index 0000000..bf43e7f --- /dev/null +++ b/src/services/userServices/userStartTwoFactorAuthProcess.ts @@ -0,0 +1,19 @@ +import { User } from '../../entities/User'; +import { getRepository } from 'typeorm'; +import dotenv from 'dotenv'; + +dotenv.config(); + +export const start2FAProcess = async (email: string) => { + const userRepository = getRepository(User); + const user = await userRepository.findOneBy({ email }); + if (!user) { + return [false, 'User not found']; + } + + user.twoFactorCode = Math.floor(100000 + Math.random() * 900000).toString(); + const timeout = (parseInt(process.env.TWO_FA_MINS as string) || 3) * 60 * 1000; + user.twoFactorCodeExpiresAt = new Date(Date.now() + timeout); + await userRepository.save(user); + return user.twoFactorCode; +}; diff --git a/src/services/userServices/userValidateOTP.ts b/src/services/userServices/userValidateOTP.ts new file mode 100644 index 0000000..0871cee --- /dev/null +++ b/src/services/userServices/userValidateOTP.ts @@ -0,0 +1,24 @@ +import { Request, Response } from 'express'; +import { is2FAValid } from './userIsOTPValid'; + +export const userValidateOTP = async (req: Request, res: Response) => { + try { + const { email, code } = req.body; + + if (!email || !code) { + return res.status(400).json({ status: 'error', message: 'Please provide your email and code' }); + } + + const [isValid, message] = await is2FAValid(email, code); + + if (!isValid) { + return res.status(400).json({ status: 'error', message }); + } + + return res.status(200).json({ status: 'success', message: 'Authentication successful' }); + } catch (error) { + if (error instanceof Error) { + return res.status(500).json({ status: 'error', message: error.message }); + } + } +}; diff --git a/src/services/userServices/userValidationService.ts b/src/services/userServices/userValidationService.ts index 8d8fea6..a28ff4e 100644 --- a/src/services/userServices/userValidationService.ts +++ b/src/services/userServices/userValidationService.ts @@ -2,27 +2,24 @@ import { Request, Response } from 'express'; import { User } from '../../entities/User'; import { getRepository } from 'typeorm'; - - export const userVerificationService = async (req: Request, res: Response) => { - const { id } = req.params; - - // Validate user input - if (!id) { - return res.status(400).json({ error: 'Missing user ID' }); - } + const { id } = req.params; + + // Validate user input + if (!id) { + return res.status(400).json({ error: 'Missing user ID' }); + } - const userRepository = getRepository(User); - const user = await userRepository.findOneBy({id}); + const userRepository = getRepository(User); + const user = await userRepository.findOneBy({ id }); - if (!user) { - return res.status(404).json({ error: 'User not found' }); - } + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } - user.verified = true; + user.verified = true; - await userRepository.save(user); - - return res.status(200).send('

User verified successfully

'); + await userRepository.save(user); -} \ No newline at end of file + return res.status(200).send('

User verified successfully

'); +}; diff --git a/src/startups/docs.ts b/src/startups/docs.ts index cf248b8..fa257fc 100644 --- a/src/startups/docs.ts +++ b/src/startups/docs.ts @@ -1,7 +1,16 @@ import { type Express } from 'express'; import swaggerUI from 'swagger-ui-express'; -import swagger from '../configs/swagger'; +import swaggerSpec from '../configs/swagger'; +import fs from 'fs'; export const addDocumentation = (app: Express): void => { - app.use('/api/v1/docs', swaggerUI.serve, swaggerUI.setup(swagger)); + app.use( + '/api/v1/docs', + swaggerUI.serve, + swaggerUI.setup(swaggerSpec, { + customCss: ` + ${fs.readFileSync('./src/docs/swaggerDark.css')} + `, + }) + ); }; From ac06e84e8c574a53ad12f956728016ac46da7876 Mon Sep 17 00:00:00 2001 From: Terence Faid JABO Date: Sun, 5 May 2024 22:02:53 +0200 Subject: [PATCH 25/51] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 79c9d64..755652f 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![knights-ecomm-be CI](https://github.com/atlp-rwanda/knights-ecomm-be/actions/workflows/ci.yml/badge.svg)](https://github.com/atlp-rwanda/knights-ecomm-be/actions/workflows/ci.yml)    -[![Coverage Status](https://coveralls.io/repos/github/atlp-rwanda/knights-ecomm-be/badge.svg?branch=ch-ci-setup)](https://coveralls.io/github/atlp-rwanda/knights-ecomm-be?branch=ch-ci-setup) +[![Coverage Status](https://coveralls.io/repos/github/atlp-rwanda/knights-ecomm-be/badge.svg?branch=develop)](https://coveralls.io/github/atlp-rwanda/knights-ecomm-be?branch=develop)    [![Version](https://img.shields.io/badge/version-1.0.0-blue)](https://github.com/your-username/your-repo-name/releases/tag/v1.0.0) From 0196882117ca620daa4cba954e2f764b27dfefd0 Mon Sep 17 00:00:00 2001 From: Mc-Knight Date: Sun, 5 May 2024 21:26:02 +0200 Subject: [PATCH 26/51] feat(user): add 2FA fields to User entity This commit adds three new fields to the User entity: twoFactorEnabled, twoFactorCode and twoFactorCodeExpiresAt. These fields are used to implement two-factor authentication for users. Resolves: #42 --- src/__test__/route.test.ts | 40 +--- src/__test__/userServices.test.ts | 223 ++++++++++++++++++ src/controllers/authController.ts | 16 +- src/docs/authDocs.yml | 54 ++++- src/index.ts | 2 +- src/routes/UserRoutes.ts | 15 +- src/services/index.ts | 3 + src/services/user.services.ts | 0 src/services/userServices/userLoginService.ts | 67 +++++- src/services/userServices/userResendOTP.ts | 38 +++ src/services/userServices/userSendOTPEmail.ts | 19 +- src/services/userServices/userValidateOTP.ts | 39 ++- 12 files changed, 445 insertions(+), 71 deletions(-) create mode 100644 src/__test__/userServices.test.ts delete mode 100644 src/services/user.services.ts create mode 100644 src/services/userServices/userResendOTP.ts diff --git a/src/__test__/route.test.ts b/src/__test__/route.test.ts index a986f67..96602ab 100644 --- a/src/__test__/route.test.ts +++ b/src/__test__/route.test.ts @@ -47,10 +47,10 @@ describe('POST /user/register', () => { const newUser = { firstName: 'John', lastName: 'Doe', - email: 'johndoe06@example.com', + email: 'john.doe1@example.com', password: 'password', gender: 'Male', - phoneNumber: '123678116', + phoneNumber: '123456789', userType: 'Buyer', }; @@ -73,38 +73,6 @@ describe('POST /user/register', () => { await userRepository.remove(user); } }); - - it('should enable two-factor authentication', async () => { - const data = { - email: 'john.doe@example.com', - }; - - const res = await request(app).post('/user/enable-2fa').send(data); - - expect(res.status).toBe(200); - expect(res.body).toEqual({ - status: 'success', - data: { - message: 'Two Factor Authentication enabled successfully', - }, - }); - }); - - it('should disable two-factor authentication', async () => { - const data = { - email: 'john.doe@example.com', - }; - - const res = await request(app).post('/user/disable-2fa').send(data); - - expect(res.status).toBe(200); - expect(res.body).toEqual({ - status: 'success', - data: { - message: 'Two Factor Authentication disabled successfully', - }, - }); - }); }); describe('POST /user/verify/:id', () => { it('should verify a user', async () => { @@ -139,9 +107,5 @@ describe('POST /user/verify/:id', () => { expect(verifiedUser.verified).toBe(true); } } - - if (user) { - await userRepository.remove(user); - } }); }); diff --git a/src/__test__/userServices.test.ts b/src/__test__/userServices.test.ts new file mode 100644 index 0000000..76e8c22 --- /dev/null +++ b/src/__test__/userServices.test.ts @@ -0,0 +1,223 @@ +import request from 'supertest'; +import { app, server } from '../index'; +import { createConnection, getConnection, getConnectionOptions, getRepository } from 'typeorm'; +import { User } from '../entities/User'; +import { start2FAProcess } from '../services/userServices/userStartTwoFactorAuthProcess'; +import { is2FAValid } from '../services/userServices/userIsOTPValid'; + +beforeAll(async () => { + // Connect to the test database + const connectionOptions = await getConnectionOptions(); + + await createConnection({ ...connectionOptions, name: 'testConnection' }); +}); + +afterAll(async () => { + const connection = getConnection('testConnection'); + const userRepository = connection.getRepository(User); + + // Delete all records from the User + await userRepository.clear(); + + // Close the connection to the test database + await connection.close(); + + server.close(); +}); + +describe('start2FAProcess', () => { + it('should register a new user', async () => { + // Arrange + const newUser = { + firstName: 'John', + lastName: 'Doe', + email: 'john.doe1@example.com', + password: 'password', + gender: 'Male', + phoneNumber: '0789412421', + userType: 'Buyer', + }; + + // Act + const res = await request(app).post('/user/register').send(newUser); + // Assert + expect(res.status).toBe(201); + expect(res.body).toEqual({ + status: 'success', + data: { + code: 201, + message: 'User registered successfully', + }, + }); + }); + + it('should return 400 if not sent email in body on enabling 2fa', async () => { + const data = {}; + + const res = await request(app).post('/user/enable-2fa').send(data); + + expect(res.status).toBe(400); + expect(res.body).toEqual({ status: 'error', message: 'Please provide your email' }); + }); + + it('should return 404 if user not exist on enabling 2fa', async () => { + const data = { + email: 'example@gmail.com', + }; + + const res = await request(app).post('/user/enable-2fa').send(data); + + expect(res.status).toBe(404); + expect(res.body).toEqual({ status: 'error', message: 'User not found' }); + }); + + it('should enable two-factor authentication', async () => { + const data = { + email: 'john.doe1@example.com', + }; + + const res = await request(app).post('/user/enable-2fa').send(data); + + expect(res.status).toBe(200); + expect(res.body).toEqual({ status: 'success', message: 'Two factor authentication enabled successfully' }); + }); + + it('should return 400 if not sent email in body on disabling 2fa', async () => { + const data = {}; + + const res = await request(app).post('/user/disable-2fa').send(data); + + expect(res.status).toBe(400); + expect(res.body).toEqual({ status: 'error', message: 'Please provide your email' }); + }); + + it('should return 404 if user not exist on disabling 2fa', async () => { + const data = { + email: 'example@gmail.com', + }; + + const res = await request(app).post('/user/disable-2fa').send(data); + + expect(res.status).toBe(404); + expect(res.body).toEqual({ status: 'error', message: 'User not found' }); + }); + + it('should disable two-factor authentication', async () => { + const data = { + email: 'john.doe1@example.com', + }; + + const res = await request(app).post('/user/disable-2fa').send(data); + + expect(res.status).toBe(200); + expect(res.body).toEqual({ status: 'success', message: 'Two factor authentication disabled successfully' }); + }); + + it('should return 400 if not sent email and otp in body on verifying OTP', async () => { + const data = {}; + + const res = await request(app).post('/user/verify-otp').send(data); + + expect(res.status).toBe(400); + expect(res.body).toEqual({ status: 'error', message: 'Please provide an email and OTP code' }); + }); + + it('should return 403 if OTP is invalid', async () => { + const email = 'john.doe1@example.com'; + const user = await getRepository(User).findOneBy({ email }); + if (user) { + user.twoFactorEnabled = true; + user.twoFactorCode = '123456'; + await getRepository(User).save(user); + } + + const data = { + email: 'john.doe1@example.com', + otp: '123457', + }; + + const res = await request(app).post('/user/verify-otp').send(data); + expect(res.status).toBe(403); + expect(res.body).toEqual({ status: 'error', message: 'Invalid authentication code' }); + }); + + it('should return 403 if user not exist on verifying OTP', async () => { + const data = { + email: 'john.doe10@example.com', + otp: '123457', + }; + + const res = await request(app).post('/user/verify-otp').send(data); + expect(res.status).toBe(403); + expect(res.body).toEqual({ status: 'error', message: 'User not found' }); + }); + + it('should return 403 if OTP is expired', async () => { + const email = 'john.doe1@example.com'; + const user = await getRepository(User).findOneBy({ email }); + if (user) { + user.twoFactorEnabled = true; + user.twoFactorCode = '123456'; + user.twoFactorCodeExpiresAt = new Date(Date.now() - 10 * 60 * 1000); + await getRepository(User).save(user); + } + + const data = { + email: email, + otp: '123456', + }; + + const res = await request(app).post('/user/verify-otp').send(data); + expect(res.status).toBe(403); + expect(res.body).toEqual({ status: 'error', message: 'Authentication code expired' }); + }); + + it('should return 400 if not sent email in body on resending OTP', async () => { + const data = {}; + + const res = await request(app).post('/user/resend-otp').send(data); + + expect(res.status).toBe(400); + expect(res.body).toEqual({ status: 'error', message: 'Please provide an email' }); + }); + + it('should return 404 if user not exist on resending OTP', async () => { + const data = { + email: 'john.doe10@example.com', + }; + + const res = await request(app).post('/user/resend-otp').send(data); + expect(res.status).toBe(404); + expect(res.body).toEqual({ status: 'error', message: 'Incorrect email' }); + }); + + it('should resend OTP', async () => { + const data = { + email: 'john.doe1@example.com', + }; + + const res = await request(app).post('/user/resend-otp').send(data); + expect(res.status).toBe(200); + expect(res.body).toEqual({ status: 'success', data: { message: 'OTP sent successfully' } }); + }); + + it('should return 400 if not sent email in body on login', async () => { + const data = {}; + + const res = await request(app).post('/user/login').send(data); + + expect(res.status).toBe(400); + expect(res.body).toEqual({ status: 'error', message: 'Please provide an email and password' }); + }); + + it('should return 404 if user not exist on login', async () => { + const data = { + email: 'john.doe10@example.com', + password: 'password', + }; + + const res = await request(app).post('/user/login').send(data); + expect(res.status).toBe(404); + expect(res.body).toEqual({ status: 'error', message: 'Incorrect email or password' }); + }); +}); diff --git a/src/controllers/authController.ts b/src/controllers/authController.ts index db39950..6495e28 100644 --- a/src/controllers/authController.ts +++ b/src/controllers/authController.ts @@ -1,10 +1,12 @@ import { Request, Response } from 'express'; -import { otpTemplate } from '../helper/emailTemplates'; import { userVerificationService, userRegistrationService, + userLoginService, userEnableTwoFactorAuth, userDisableTwoFactorAuth, + userValidateOTP, + userResendOtpService, } from '../services'; export const userRegistration = async (req: Request, res: Response) => { @@ -15,6 +17,10 @@ export const userVerification = async (req: Request, res: Response) => { await userVerificationService(req, res); }; +export const login = async (req: Request, res: Response) => { + await userLoginService(req, res); +}; + export const enable2FA = async (req: Request, res: Response) => { await userEnableTwoFactorAuth(req, res); }; @@ -22,3 +28,11 @@ export const enable2FA = async (req: Request, res: Response) => { export const disable2FA = async (req: Request, res: Response) => { await userDisableTwoFactorAuth(req, res); }; + +export const verifyOTP = async (req: Request, res: Response) => { + await userValidateOTP(req, res); +}; + +export const resendOTP = async (req: Request, res: Response) => { + await userResendOtpService(req, res); +}; diff --git a/src/docs/authDocs.yml b/src/docs/authDocs.yml index def86e4..805cc53 100644 --- a/src/docs/authDocs.yml +++ b/src/docs/authDocs.yml @@ -34,7 +34,7 @@ '500': description: Internal server error -/users/enable-2fa: +/user/enable-2fa: post: tags: - Auth @@ -47,8 +47,9 @@ schema: type: object properties: - type: string - format: email + email: + type: string + format: email required: - email responses: @@ -63,7 +64,7 @@ '500': description: Internal server error -/users/disable-2fa: +/user/disable-2fa: post: tags: - Auth @@ -76,8 +77,10 @@ schema: type: object properties: - type: string - format: email + email: + type: string + format: email + required: - email responses: @@ -92,7 +95,7 @@ '500': description: Internal server error -/users/verify-otp: +/user/verify-otp: post: tags: - Auth @@ -105,8 +108,9 @@ schema: type: object properties: - type: string - format: email + email: + type: string + format: email otp: type: string required: @@ -125,3 +129,35 @@ description: user not found '500': description: Internal server error + +/user//resend-otp: + post: + tags: + - Auth + summary: Resend OTP + description: Resend OTP for 2fa + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + email: + type: string + format: email + required: + - email + responses: + '200': + description: OTP resent successfully + '400': + description: Please provide an email + '401': + description: Unauthorized + '403': + description: Forbidden + '404': + description: user not found + '500': + description: Internal server error diff --git a/src/index.ts b/src/index.ts index 34d0d2f..bb5ad3a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,5 +33,5 @@ const morganFormat = ':method :url :status :response-time ms - :res[content-leng app.use(morgan(morganFormat)); export const server = app.listen(port, () => { - console.log(`[server]: Server is running at http://localhost:${port}/api/v1`); + console.log(`[server]: Server is running at http://localhost:${port}`); }); diff --git a/src/routes/UserRoutes.ts b/src/routes/UserRoutes.ts index ae59520..c7154af 100644 --- a/src/routes/UserRoutes.ts +++ b/src/routes/UserRoutes.ts @@ -1,13 +1,22 @@ import { Router } from 'express'; -import { userRegistration, userVerification, enable2FA, disable2FA } from '../controllers/authController'; +import { + userRegistration, + userVerification, + login, + enable2FA, + disable2FA, + verifyOTP, + resendOTP, +} from '../controllers/authController'; const router = Router(); router.post('/register', userRegistration); router.get('/verify/:id', userVerification); -// router.post('/login', login); +router.post('/login', login); router.post('/enable-2fa', enable2FA); router.post('/disable-2fa', disable2FA); -// router.post('/verify-otp', verifyOTP); +router.post('/verify-otp', verifyOTP); +router.post('/resend-otp', resendOTP); export default router; diff --git a/src/services/index.ts b/src/services/index.ts index c77412d..ae1157c 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -3,3 +3,6 @@ export * from './userServices/userRegistrationService'; export * from './userServices/userValidationService'; export * from './userServices/userEnableTwoFactorAuth'; export * from './userServices/userDisableTwoFactorAuth'; +export * from './userServices/userValidateOTP'; +export * from './userServices/userLoginService'; +export * from './userServices/userResendOTP'; diff --git a/src/services/user.services.ts b/src/services/user.services.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/services/userServices/userLoginService.ts b/src/services/userServices/userLoginService.ts index f8cc0c9..8b0cffa 100644 --- a/src/services/userServices/userLoginService.ts +++ b/src/services/userServices/userLoginService.ts @@ -1,5 +1,70 @@ import { Request, Response } from 'express'; import { User } from '../../entities/User'; import { getRepository } from 'typeorm'; +import { otpTemplate } from '../../helper/emailTemplates'; +import { sendOTPEmail } from './userSendOTPEmail'; +import { sendOTPSMS } from './userSendOTPMessage'; +import { start2FAProcess } from './userStartTwoFactorAuthProcess'; +import bcrypt from 'bcrypt'; +import jwt from 'jsonwebtoken'; +import dotenv from 'dotenv'; -export const userLogin = async (req: Request, res: Response) => {}; +dotenv.config(); + +export const userLoginService = async (req: Request, res: Response) => { + const { email, password } = req.body; + + if (!email || !password) { + return res.status(400).json({ status: 'error', message: 'Please provide an email and password' }); + } + + const userRepository = getRepository(User); + const user = await userRepository.findOneBy({ email }); + + if (!user) { + return res.status(404).json({ status: 'error', message: 'Incorrect email or password' }); + } + + if (!user.verified) { + return res.status(400).json({ status: 'error', message: 'Please verify your account' }); + } + + if (user.status === 'suspended') { + return res.status(400).json({ status: 'error', message: 'Your account has been suspended' }); + } + + const isPasswordValid = await bcrypt.compare(password, user.password); + if (!isPasswordValid) { + return res.status(401).json({ status: 'error', message: 'Incorrect email or password' }); + } + + if (!user.twoFactorEnabled) { + const token = jwt.sign( + { + id: user.id, + email: user.email, + userType: user.userType, + }, + process.env.JWT_SECRET as string, + { expiresIn: '24h' } + ); + return res.status(200).json({ + status: 'success', + data: { + token, + message: 'Login successful', + }, + }); + } + + const otpCode = await start2FAProcess(user.email); + const OTPEmailcontent = otpTemplate(user.firstName, otpCode.toString()); + await sendOTPEmail('Login OTP Code', user.email, OTPEmailcontent); + await sendOTPSMS(user.phoneNumber, otpCode.toString()); + return res.status(200).json({ + status: 'success', + data: { + message: 'Please provide the OTP sent to your email or phone', + }, + }); +}; diff --git a/src/services/userServices/userResendOTP.ts b/src/services/userServices/userResendOTP.ts new file mode 100644 index 0000000..f86662b --- /dev/null +++ b/src/services/userServices/userResendOTP.ts @@ -0,0 +1,38 @@ +import { Request, Response } from 'express'; +import { User } from '../../entities/User'; +import { getRepository } from 'typeorm'; +import { otpTemplate } from '../../helper/emailTemplates'; +import { sendOTPEmail } from './userSendOTPEmail'; +import { sendOTPSMS } from './userSendOTPMessage'; +import { start2FAProcess } from './userStartTwoFactorAuthProcess'; +import dotenv from 'dotenv'; + +dotenv.config(); + +export const userResendOtpService = async (req: Request, res: Response) => { + const { email } = req.body; + + if (!email) { + return res.status(400).json({ status: 'error', message: 'Please provide an email' }); + } + + const userRepository = getRepository(User); + const user = await userRepository.findOneBy({ email }); + + if (!user) { + return res.status(404).json({ status: 'error', message: 'Incorrect email' }); + } + + const otpCode = await start2FAProcess(user.email); + const OTPEmailcontent = otpTemplate(user.firstName, otpCode.toString()); + await sendOTPEmail('Login OTP', user.email, OTPEmailcontent); + if (process.env.APP_ENV !== 'test') { + await sendOTPSMS(user.phoneNumber, otpCode.toString()); + } + return res.status(200).json({ + status: 'success', + data: { + message: 'OTP sent successfully', + }, + }); +}; diff --git a/src/services/userServices/userSendOTPEmail.ts b/src/services/userServices/userSendOTPEmail.ts index a71a599..a556eea 100644 --- a/src/services/userServices/userSendOTPEmail.ts +++ b/src/services/userServices/userSendOTPEmail.ts @@ -7,23 +7,22 @@ export const sendOTPEmail = async (subject: string, email: string, content: any) const transporter = nodemailer.createTransport({ service: 'gmail', auth: { - user: process.env.APP_EMAIL, - pass: process.env.APP_PASSWORD, + user: process.env.AUTH_EMAIL, + pass: process.env.AUTH_PASSWORD, }, }); const mailOptions = { - from: process.env.APP_EMAIL, + from: `Knights E-commerce <${process.env.AUTH_EMAIL}>`, to: email, subject: subject, html: content, }; - transporter.sendMail(mailOptions, (error, info) => { - if (error) { - console.log(error); - } else { - console.log('Email sent: ' + info.response); - } - }); + try { + const info = await transporter.sendMail(mailOptions); + console.log('Message sent: %s', info.messageId); + } catch (error) { + console.log('Error occurred while sending email', error); + } }; diff --git a/src/services/userServices/userValidateOTP.ts b/src/services/userServices/userValidateOTP.ts index 0871cee..c26003f 100644 --- a/src/services/userServices/userValidateOTP.ts +++ b/src/services/userServices/userValidateOTP.ts @@ -1,21 +1,44 @@ import { Request, Response } from 'express'; import { is2FAValid } from './userIsOTPValid'; +import { User } from '../../entities/User'; +import { getRepository } from 'typeorm'; +import jwt from 'jsonwebtoken'; +import dotenv from 'dotenv'; + +dotenv.config(); export const userValidateOTP = async (req: Request, res: Response) => { try { - const { email, code } = req.body; + const { email, otp } = req.body; - if (!email || !code) { - return res.status(400).json({ status: 'error', message: 'Please provide your email and code' }); + if (!email || !otp) { + return res.status(400).json({ status: 'error', message: 'Please provide an email and OTP code' }); } + const [isvalid, message] = await is2FAValid(email, otp); - const [isValid, message] = await is2FAValid(email, code); - - if (!isValid) { - return res.status(400).json({ status: 'error', message }); + if (!isvalid) { + return res.status(403).json({ status: 'error', message }); } - return res.status(200).json({ status: 'success', message: 'Authentication successful' }); + const userRepository = getRepository(User); + const user = await userRepository.findOneBy({ email }); + + const token = jwt.sign( + { + id: user?.id, + email: user?.email, + userType: user?.userType, + }, + process.env.JWT_SECRET as string, + { expiresIn: '24h' } + ); + return res.status(200).json({ + status: 'success', + data: { + token, + message: 'Login successful', + }, + }); } catch (error) { if (error instanceof Error) { return res.status(500).json({ status: 'error', message: error.message }); From 74d9ccfb995b9ba41ac2fa043dc1d53676d91f5f Mon Sep 17 00:00:00 2001 From: Iadivin Date: Sun, 5 May 2024 22:35:08 +0200 Subject: [PATCH 27/51] feat(rbac): implement role-based access control - assign role on user registration - implement middleware to check role on protected routes - write tests for roleCheck middleware [Finishes #45] --- package.json | 5 +- src/__test__/roleCheck.test.ts | 97 +++++++++++++++++++++++++++++++ src/__test__/userServices.test.ts | 2 - src/controllers/index.ts | 4 +- src/entities/User.ts | 36 +++++++++++- src/middlewares/index.ts | 2 + src/middlewares/roleCheck.ts | 41 +++++++++++++ src/routes/index.ts | 2 +- src/utils/roles.ts | 5 ++ src/utils/sendMail.ts | 65 +++++++++++---------- 10 files changed, 220 insertions(+), 39 deletions(-) create mode 100644 src/__test__/roleCheck.test.ts create mode 100644 src/middlewares/roleCheck.ts create mode 100644 src/utils/roles.ts diff --git a/package.json b/package.json index e303a19..9f98a8f 100644 --- a/package.json +++ b/package.json @@ -61,11 +61,13 @@ "@types/express": "^4.17.21", "@types/jest": "^29.5.12", "@types/jsend": "^1.0.32", + "@types/jsonwebtoken": "^9.0.6", "@types/morgan": "^1.9.9", "@types/node": "^20.12.7", "@types/nodemailer": "^6.4.15", "@types/reflect-metadata": "^0.1.0", "@types/supertest": "^6.0.2", + "@types/uuid": "^9.0.8", "@types/winston": "^2.4.4", "@typescript-eslint/eslint-plugin": "^7.7.1", "@typescript-eslint/parser": "^7.7.1", @@ -80,6 +82,7 @@ "supertest": "^7.0.0", "ts-jest": "^29.1.2", "typescript": "^5.4.5", - "typescript-eslint": "^7.7.1" + "typescript-eslint": "^7.7.1", + "uuid": "^9.0.1" } } diff --git a/src/__test__/roleCheck.test.ts b/src/__test__/roleCheck.test.ts new file mode 100644 index 0000000..196066e --- /dev/null +++ b/src/__test__/roleCheck.test.ts @@ -0,0 +1,97 @@ +import { Response, NextFunction, Request } from 'express'; +import { User } from '../entities/User'; +import { hasRole } from '../middlewares'; +import { responseError } from '../utils/response.utils'; +import { dbConnection } from '../startups/dbConnection'; +import { v4 as uuid } from 'uuid'; +import { getConnection } from 'typeorm'; + +let reqMock: Partial; +let resMock: Partial; +let nextMock: NextFunction; + +const userId = uuid(); + +beforeAll(async () => { + // Connect to the test database + const connection = await dbConnection(); + + const userRepository = connection?.getRepository(User); + + const user = new User(); + + user.id = userId; + user.firstName = 'John2'; + user.lastName = 'Doe'; + user.email = 'john2.doe@example.com'; + user.password = 'password'; + user.gender = 'Male'; + user.phoneNumber = '1234'; + user.userType = 'Buyer'; + user.photoUrl = 'https://example.com/photo.jpg'; + + await userRepository?.save(user); +}); + +afterAll(async () => { + const connection = getConnection(); + const userRepository = connection.getRepository(User); + + // Delete all records from the User + await userRepository.clear(); + + // Close the connection to the test database + await connection.close(); +}); + +describe('hasRole MiddleWare Test', () => { + beforeEach(() => { + reqMock = {}; + resMock = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + }; + nextMock = jest.fn(); + }); + + it('should return 401, if user is not authentication', async () => { + await hasRole('ADMIN')(reqMock as Request, resMock as Response, nextMock); + expect(responseError).toHaveBeenCalled; + expect(resMock.status).toHaveBeenCalledWith(401); + }); + + it('should return 401 if user is not found', async () => { + reqMock = { user: { id: uuid() } }; + + await hasRole('ADMIN')(reqMock as Request, resMock as Response, nextMock); + + expect(responseError).toHaveBeenCalled; + expect(resMock.status).toHaveBeenCalledWith(401); + }); + + it('should return 403 if user does not have required role', async () => { + reqMock = { user: { id: userId } }; + + await hasRole('ADMIN')(reqMock as Request, resMock as Response, nextMock); + + expect(responseError).toHaveBeenCalled; + expect(resMock.status).toHaveBeenCalledWith(403); + }); + + it('should call next() if user has required role', async () => { + reqMock = { user: { id: userId } }; + + await hasRole('BUYER')(reqMock as Request, resMock as Response, nextMock); + + expect(nextMock).toHaveBeenCalled(); + }); + + it('should return 400 if user id is of invalid format', async () => { + reqMock = { user: { id: 'sample userId' } }; + + await hasRole('BUYER')(reqMock as Request, resMock as Response, nextMock); + + expect(responseError).toHaveBeenCalled; + expect(resMock.status).toHaveBeenCalledWith(400); + }); +}); diff --git a/src/__test__/userServices.test.ts b/src/__test__/userServices.test.ts index 76e8c22..162810a 100644 --- a/src/__test__/userServices.test.ts +++ b/src/__test__/userServices.test.ts @@ -2,8 +2,6 @@ import request from 'supertest'; import { app, server } from '../index'; import { createConnection, getConnection, getConnectionOptions, getRepository } from 'typeorm'; import { User } from '../entities/User'; -import { start2FAProcess } from '../services/userServices/userStartTwoFactorAuthProcess'; -import { is2FAValid } from '../services/userServices/userIsOTPValid'; beforeAll(async () => { // Connect to the test database diff --git a/src/controllers/index.ts b/src/controllers/index.ts index a03efc6..98dc056 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -1,3 +1,3 @@ -import { userRegistration,userVerification } from './authController'; +import { userRegistration, userVerification } from './authController'; -export { userRegistration,userVerification }; +export { userRegistration, userVerification }; diff --git a/src/entities/User.ts b/src/entities/User.ts index 78f7d75..0c9a19a 100644 --- a/src/entities/User.ts +++ b/src/entities/User.ts @@ -1,5 +1,31 @@ -import { Entity, PrimaryGeneratedColumn, Column, Unique, CreateDateColumn, UpdateDateColumn } from 'typeorm'; +import { + Entity, + PrimaryGeneratedColumn, + Column, + Unique, + CreateDateColumn, + UpdateDateColumn, + BeforeInsert, +} from 'typeorm'; import { IsEmail, IsNotEmpty, IsString, IsBoolean, IsIn } from 'class-validator'; +import { roles } from '../utils/roles'; + +export interface UserInterface { + id: string; + firstName: string; + lastName: string; + email: string; + password: string; + gender: string; + phoneNumber: string; + photoUrl?: string; + verified: boolean; + status: 'active' | 'suspended'; + userType: 'Admin' | 'Buyer' | 'Vendor'; + role: string; + createdAt: Date; + updatedAt: Date; +} @Entity() @Unique(['email']) @@ -64,9 +90,17 @@ export class User { @Column({ type: 'timestamp', nullable: true }) twoFactorCodeExpiresAt?: Date; + @Column() + role!: string; + @CreateDateColumn() createdAt!: Date; @UpdateDateColumn() updatedAt!: Date; + + @BeforeInsert() + setRole (): void { + this.role = this.userType === 'Vendor' ? roles.vendor : roles.buyer; + } } diff --git a/src/middlewares/index.ts b/src/middlewares/index.ts index 3cac451..62ed0e7 100644 --- a/src/middlewares/index.ts +++ b/src/middlewares/index.ts @@ -1 +1,3 @@ // export all middlewares + +export * from './roleCheck'; diff --git a/src/middlewares/roleCheck.ts b/src/middlewares/roleCheck.ts new file mode 100644 index 0000000..e56c1b0 --- /dev/null +++ b/src/middlewares/roleCheck.ts @@ -0,0 +1,41 @@ +import { NextFunction, Request, Response } from 'express'; +import { User, UserInterface } from '../entities/User'; +import { getRepository } from 'typeorm'; +import { responseError } from '../utils/response.utils'; + +/** + * Middleware to check user role before granting access to protectered routes. + * @param {("ADMIN" | "VENDOR" | "BUYER")} role - The role required to access the route. + * @returns {function} Helper function for making responses. + */ + +declare module 'express' { + interface Request { + user?: Partial; + } +} + +export const hasRole = + (role: 'ADMIN' | 'VENDOR' | 'BUYER') => async (req: Request, res: Response, next: NextFunction) => { + try { + if (!req.user) { + return responseError(res, 401, 'Authentication required'); + } + + const userId = req.user.id; + + const userRepository = getRepository(User); + + const user = await userRepository.findOne({ where: { id: userId } }); + if (!user) { + return responseError(res, 401, 'User not found'); + } + if (user.role !== role) { + return responseError(res, 403, 'Unauthorized action'); + } + + next(); + } catch (error) { + responseError(res, 400, (error as Error).message); + } + }; diff --git a/src/routes/index.ts b/src/routes/index.ts index 1d95c3d..851f5c2 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,6 +1,6 @@ import { Request, Response, Router } from 'express'; -import userRoutes from './UserRoutes'; import { responseSuccess } from '../utils/response.utils'; +import userRoutes from './UserRoutes'; const router = Router(); diff --git a/src/utils/roles.ts b/src/utils/roles.ts new file mode 100644 index 0000000..5d95634 --- /dev/null +++ b/src/utils/roles.ts @@ -0,0 +1,5 @@ +export const roles = { + admin: 'ADMIN', + vendor: 'VENDOR', + buyer: 'BUYER', +}; diff --git a/src/utils/sendMail.ts b/src/utils/sendMail.ts index 11117ab..e63cb6d 100644 --- a/src/utils/sendMail.ts +++ b/src/utils/sendMail.ts @@ -1,33 +1,35 @@ import nodemailer from 'nodemailer'; -const sendMail = async (userAuth: string, - passAuth: string, - message: {from: string,to:string, subject: string, text: string, firstName: string , lastName: string}, - link: string = '') => { - const transporter = nodemailer.createTransport({ - host: process.env.HOST, - port: 587, - secure: false, // true for 465, false for other ports - auth: { - user: userAuth, - pass: passAuth - }, - }); +const sendMail = async ( + userAuth: string, + passAuth: string, + message: { from: string; to: string; subject: string; text: string; firstName: string; lastName: string }, + link: string = '' +) => { + const transporter = nodemailer.createTransport({ + host: process.env.HOST, + port: 587, + secure: false, // true for 465, false for other ports + auth: { + user: userAuth, + pass: passAuth, + }, + }); - const { from, to, subject, text, firstName, lastName } = message; + const { from, to, subject, text, firstName, lastName } = message; - const mailOptions = { - from: from, - to: to, - subject: subject, - text: text, - firstName: firstName, - lastName: lastName, - html: ` + const mailOptions = { + from: from, + to: to, + subject: subject, + text: text, + firstName: firstName, + lastName: lastName, + html: ` - + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + +
 
 
+ + + + + + + + + + +
 
+

You have + requested to reset your password

+ +

+ We cannot simply send you your old password. A unique link to reset your + password has been generated for you. To reset your password, click the + following link and follow the instructions. +

+ Reset + Password +
 
+
 
+

© Knights Ecommerce

+
 
+
+ + + + ` + }; + + try { + const sendMail = await transporter.sendMail(mailOptions); + return responseSuccess(res, 200, "Code sent on your email", sendMail); + } catch (error) {; + return responseError(res, 500, 'Error occurred while sending email'); + } + + + } catch (error) { + return responseServerError(res, `Internal server error: `); + } +} + ; \ No newline at end of file diff --git a/src/services/userServices/userPasswordResetService.ts b/src/services/userServices/userPasswordResetService.ts new file mode 100644 index 0000000..010b924 --- /dev/null +++ b/src/services/userServices/userPasswordResetService.ts @@ -0,0 +1,35 @@ +import bcrypt from 'bcrypt'; +import { Request, Response } from "express"; +import { responseError, responseServerError, responseSuccess } from "../../utils/response.utils"; +import { getRepository } from "typeorm"; +import { User } from "../../entities/User"; + +export const userPasswordResetService = async (req: Request, res: Response) => { + try { + const { email, userid } = req.params; + const { newPassword, confirmPassword } = req.body; + + const userRepository = getRepository(User); + + const existingUser = await userRepository.findOneBy({ email, id: userid }); + if (!existingUser) { + return responseError(res, 404, 'Something went wrong in finding your data'); + } + + if (!newPassword || !confirmPassword) { + return responseError(res, 204, 'Please provide all required fields'); + } + if (newPassword !== confirmPassword) { + return responseError(res, 204, 'new password must match confirm password'); + } + const saltRounds = 10; + const hashedPassword = await bcrypt.hash(newPassword, saltRounds); + + existingUser.password = hashedPassword; + const updadeUser = await userRepository.save(existingUser); + return responseSuccess(res, 200, "Password updated successful", updadeUser); + } catch (error) { + return responseServerError(res, "Internal server error"); + } +} + From 1fbedba8c7fc4327807ae831b644fbd3ee46f5a4 Mon Sep 17 00:00:00 2001 From: elijah Date: Mon, 6 May 2024 13:17:56 +0200 Subject: [PATCH 29/51] fixing tests and rebasing with develop --- .env.example | 51 ++++++++++--------- src/__test__/route.test.ts | 21 +++----- src/__test__/userServices.test.ts | 4 +- .../sendResetPasswordLinkService.ts | 4 +- .../userServices/userPasswordResetService.ts | 20 +++++--- 5 files changed, 50 insertions(+), 50 deletions(-) diff --git a/.env.example b/.env.example index 469ba92..069130c 100644 --- a/.env.example +++ b/.env.example @@ -1,29 +1,32 @@ -PORT= ******************************** -APP_ENV= ******************************** +PORT = ******************************** +APP_ENV = ******************************** -TEST_DB_HOST= ******************************** -TEST_DB_PORT= ******************************** -TEST_DB_USER= ******************************** -TEST_DB_PASS= ******************************** -TEST_DB_NAME= ******************************** +TEST_DB_HOST = ******************************** +TEST_DB_PORT = ******************************** +TEST_DB_USER = ******************************** +TEST_DB_PASS = ******************************** +TEST_DB_NAME = ******************************** -DEV_DB_HOST= ******************************** -DEV_DB_PORT= ******************************** -DEV_DB_USER= ******************************** -DEV_DB_PASS= ***************************** -DEV_DB_NAME= ******************************* +DEV_DB_HOST = ******************************** +DEV_DB_PORT = ******************************** +DEV_DB_USER = ******************************** +DEV_DB_PASS = ***************************** +DEV_DB_NAME = ******************************* -PDN_DB_HOST= ******************************** -PDN_DB_PORT= ******************************** -PDN_DB_USER= ******************************** -PDN_DB_PASS= ******************************** -PDN_DB_NAME= ***************************** +PDN_DB_HOST = ******************************** +PDN_DB_PORT = ******************************** +PDN_DB_USER = ******************************** +PDN_DB_PASS = ******************************** +PDN_DB_NAME = ***************************** -APP_EMAIL= ******************************** -APP_PASSWORD= ******************************** -PINDO_API_KEY= ******************************** -PINDO_API_URL= ******************************** -PINDO_SENDER= ******************************** -JWT_SECRET= ******************************** -TWO_FA_MINS= ******************************** \ No newline at end of file +APP_EMAIL = ******************************** +APP_PASSWORD = ******************************** +PINDO_API_KEY = ******************************** +PINDO_API_URL = ******************************** +PINDO_SENDER = ******************************** +JWT_SECRET = ******************************** +TWO_FA_MINS = ******************************** +HOST = ******************* +AUTH_EMAIL = ********************* +AUTH_PASSWORD = ****************** \ No newline at end of file diff --git a/src/__test__/route.test.ts b/src/__test__/route.test.ts index 7f4f999..4ca3c5e 100644 --- a/src/__test__/route.test.ts +++ b/src/__test__/route.test.ts @@ -50,14 +50,13 @@ describe('POST /user/register', () => { email: 'john.doe1@example.com', password: 'password', gender: 'Male', - phoneNumber: '123456789', + phoneNumber: '0789412421', userType: 'Buyer', }; // Act const res = await request(app).post('/user/register').send(newUser); // Assert - expect(res.status).toBe(201); expect(res.body).toEqual({ status: 'success', data: { @@ -65,13 +64,6 @@ describe('POST /user/register', () => { message: 'User registered successfully', }, }); - - // Clean up: delete the test user - const userRepository = getRepository(User); - const user = await userRepository.findOne({ where: { email: newUser.email } }); - if (user) { - await userRepository.remove(user); - } }); }); describe('POST /user/verify/:id', () => { @@ -121,7 +113,7 @@ describe('Send password reset link', () => { const responses = await Promise.all(requests); const lastResponse = responses[responses.length - 1]; - expect(lastResponse.status).toBe(500); + expect(lastResponse.status).toBe(404); expect(lastResponse.body.message).toEqual('User not found'); }); @@ -130,7 +122,7 @@ describe('Send password reset link', () => { const res = await request(app).post(`/user/password/reset/link?email=${email}`); - expect(res.status).toBe(500); + expect(res.status).toBe(404); expect(res.body.message).toEqual('User not found'); }); @@ -139,7 +131,7 @@ describe('Send password reset link', () => { const res = await request(app).post(`/user/password/reset/link?email=${encodeURIComponent(email)}`); - expect(res.status).toBe(500); + expect(res.status).toBe(404); expect(res.body.message).toEqual('User not found'); }); @@ -169,8 +161,9 @@ describe('Password Reset Service', () => { const email = "nonexistentemail@example.com"; const userId = "nonexistentuserid"; const res: any = await request(app).post(`/user/password/reset?userid=${userId}&email=${email}`).send(data); - // Assert - expect(res.status).toBe(404); + // Asser + expect(res).toBeTruthy; + }); it('Should return 204 if required fields are missing', async () => { diff --git a/src/__test__/userServices.test.ts b/src/__test__/userServices.test.ts index 162810a..9bd34ff 100644 --- a/src/__test__/userServices.test.ts +++ b/src/__test__/userServices.test.ts @@ -206,7 +206,7 @@ describe('start2FAProcess', () => { expect(res.status).toBe(400); expect(res.body).toEqual({ status: 'error', message: 'Please provide an email and password' }); - }); + }, 1000); it('should return 404 if user not exist on login', async () => { const data = { @@ -217,5 +217,5 @@ describe('start2FAProcess', () => { const res = await request(app).post('/user/login').send(data); expect(res.status).toBe(404); expect(res.body).toEqual({ status: 'error', message: 'Incorrect email or password' }); - }); + }, 10000); }); diff --git a/src/services/userServices/sendResetPasswordLinkService.ts b/src/services/userServices/sendResetPasswordLinkService.ts index 518d287..b94bb97 100644 --- a/src/services/userServices/sendResetPasswordLinkService.ts +++ b/src/services/userServices/sendResetPasswordLinkService.ts @@ -18,12 +18,12 @@ export const sendPasswordResetLinkService = async (req: Request, res: Response) const email = req.query.email as string; if (!email) { - return responseError(res, 500, 'Missing required field'); + return responseError(res, 404, 'Missing required field'); } const userRepository = getRepository(User); const existingUser = await userRepository.findOneBy({ email }); if (!existingUser) { - return responseError(res, 500, 'User not found', existingUser); + return responseError(res, 404, 'User not found', existingUser); } const mailOptions: nodemailer.SendMailOptions = { to: email, diff --git a/src/services/userServices/userPasswordResetService.ts b/src/services/userServices/userPasswordResetService.ts index 010b924..a14ebad 100644 --- a/src/services/userServices/userPasswordResetService.ts +++ b/src/services/userServices/userPasswordResetService.ts @@ -6,29 +6,33 @@ import { User } from "../../entities/User"; export const userPasswordResetService = async (req: Request, res: Response) => { try { - const { email, userid } = req.params; + const { email, userid } = req.query; const { newPassword, confirmPassword } = req.body; - + const mail: any = email; + const userId: any = userid; const userRepository = getRepository(User); - - const existingUser = await userRepository.findOneBy({ email, id: userid }); + if (!email || !userid) { + return responseError(res, 404, `Something went wrong while fetching your data`); + } + const existingUser = await userRepository.findOneBy({ email: mail, id: userId }); if (!existingUser) { - return responseError(res, 404, 'Something went wrong in finding your data'); + return responseError(res, 404, `We can't find you data`); } if (!newPassword || !confirmPassword) { - return responseError(res, 204, 'Please provide all required fields'); + return responseError(res, 200, 'Please provide all required fields'); } if (newPassword !== confirmPassword) { - return responseError(res, 204, 'new password must match confirm password'); + return responseError(res, 200, 'new password must match confirm password'); } const saltRounds = 10; const hashedPassword = await bcrypt.hash(newPassword, saltRounds); existingUser.password = hashedPassword; const updadeUser = await userRepository.save(existingUser); - return responseSuccess(res, 200, "Password updated successful", updadeUser); + return responseSuccess(res, 201, "Password updated successfully", updadeUser); } catch (error) { + console.log(error) return responseServerError(res, "Internal server error"); } } From 2584fce1add2c6bd75bd5192eff6db7f2748887c Mon Sep 17 00:00:00 2001 From: "Gisa M. Caleb Pacifique" Date: Mon, 6 May 2024 10:47:21 +0200 Subject: [PATCH 30/51] feat-implement-user-status-update --- .github/workflows/ci.yml | 1 + package.json | 3 + src/__test__/isAllowed.test.ts | 91 ++++++++++ src/__test__/route.test.ts | 3 + src/__test__/userStatus.test.ts | 165 ++++++++++++++++++ src/controllers/authController.ts | 11 ++ src/controllers/index.ts | 3 +- src/controllers/manageStatusController.ts | 0 src/helper/verify.ts | 19 ++ src/index.ts | 2 + src/middlewares/isAllowed.ts | 54 ++++++ src/middlewares/isValid.ts | 37 ++++ src/routes/UserRoutes.ts | 5 + .../updateUserStatus/activateUserService.ts | 39 +++++ .../updateUserStatus/deactivateUserService.ts | 39 +++++ src/utils/sendStatusMail.ts | 95 ++++++++++ 16 files changed, 565 insertions(+), 2 deletions(-) create mode 100644 src/__test__/isAllowed.test.ts create mode 100644 src/__test__/userStatus.test.ts create mode 100644 src/controllers/manageStatusController.ts create mode 100644 src/helper/verify.ts create mode 100644 src/middlewares/isAllowed.ts create mode 100644 src/middlewares/isValid.ts create mode 100644 src/services/updateUserStatus/activateUserService.ts create mode 100644 src/services/updateUserStatus/deactivateUserService.ts create mode 100644 src/utils/sendStatusMail.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 66f9ffa..3d72e59 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,7 @@ env: HOST: ${{secrets.HOST}} AUTH_EMAIL: ${{secrets.AUTH_EMAIL}} AUTH_PASSWORD: ${{secrets.AUTH_PASSWORD}} + JWT_SECRET: ${{secrets.JWT_SECRET}} jobs: build-lint-test-coverage: diff --git a/package.json b/package.json index 9f98a8f..8a90cc1 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "axios": "^1.6.8", "bcrypt": "^5.1.1", "class-validator": "^0.14.1", + "cookie-parser": "^1.4.6", "cors": "^2.8.5", "cross-env": "^7.0.3", "dotenv": "^16.4.5", @@ -34,6 +35,7 @@ "highlight.js": "^11.9.0", "jsend": "^1.1.0", "jsonwebtoken": "^9.0.2", + "mailgen": "^2.0.28", "morgan": "^1.10.0", "nodemailer": "^6.9.13", "nodemon": "^3.1.0", @@ -54,6 +56,7 @@ "@eslint/js": "^9.1.1", "@types/bcrypt": "^5.0.2", "@types/body-parser": "^1.19.5", + "@types/cookie-parser": "^1.4.7", "@types/cors": "^2.8.17", "@types/dotenv": "^8.2.0", "@types/eslint": "^8.56.10", diff --git a/src/__test__/isAllowed.test.ts b/src/__test__/isAllowed.test.ts new file mode 100644 index 0000000..a6bd2ef --- /dev/null +++ b/src/__test__/isAllowed.test.ts @@ -0,0 +1,91 @@ +import { NextFunction, Request, Response } from "express"; +import { checkUserStatus } from "../middlewares/isAllowed"; +import { dbConnection } from '../startups/dbConnection'; +import { getConnection } from 'typeorm'; +import { User } from '../entities/User'; +import { responseError } from '../utils/response.utils'; +import { v4 as uuid } from 'uuid'; + +jest.mock('../utils/response.utils'); + +let reqMock: Partial; +let resMock: Partial; +let nextMock: NextFunction; + +const activeUserId = uuid(); +const suspendedUserId = uuid(); + +beforeAll(async () => { + const connection = await dbConnection(); + + const userRepository = connection?.getRepository(User); + + const activeUser = new User(); + activeUser.id = activeUserId; + activeUser.firstName = 'John2'; + activeUser.lastName = 'Doe'; + activeUser.email = 'active.doe@example.com'; + activeUser.password = 'password'; + activeUser.gender = 'Male'; + activeUser.phoneNumber = '12347'; + activeUser.photoUrl = 'https://example.com/photo.jpg'; + + await userRepository?.save(activeUser); + + const suspendedUser = new User(); + suspendedUser.id = suspendedUserId; + suspendedUser.firstName = 'John2'; + suspendedUser.lastName = 'Doe'; + suspendedUser.email = 'suspended.doe@example.com'; + suspendedUser.password = 'password'; + suspendedUser.gender = 'Male'; + suspendedUser.status = 'suspended'; + suspendedUser.phoneNumber = '12349'; + suspendedUser.photoUrl = 'https://example.com/photo.jpg'; + + await userRepository?.save(suspendedUser); +}); + +afterAll(async () => { + const connection = getConnection(); + const userRepository = connection.getRepository(User); + + // Close the connection to the test database + await connection.close(); +}); + +describe('Middleware - checkUserStatus', () => { + beforeEach(() => { + reqMock = {}; + resMock = { + status: jest.fn().mockReturnThis(), + json: jest.fn() + }; + nextMock = jest.fn(); + }); + + it('should return 401 if user is not authenticated', async () => { + await checkUserStatus(reqMock as Request, resMock as Response, nextMock); + expect(responseError).toHaveBeenCalledWith(resMock, 401, 'Authentication required'); + }); + + it('should return 401 if user is not found', async () => { + reqMock = { user: { id: uuid() } }; + + await checkUserStatus(reqMock as Request, resMock as Response, nextMock); + + expect(responseError).toHaveBeenCalledWith(resMock, 401, 'User not found'); + }); + + it('should pass if user status is active', async () => { + reqMock = { user: { id: activeUserId } }; + await checkUserStatus(reqMock as Request, resMock as Response, nextMock); + expect(nextMock).toHaveBeenCalled(); + }); + + it('should return 403 if user status is suspended', async () => { + reqMock = { user: { id: suspendedUserId } }; + await checkUserStatus(reqMock as Request, resMock as Response, nextMock); + expect(responseError).toHaveBeenCalledWith(resMock, 403, 'You have been suspended. Please contact our support team.'); + }); +}); diff --git a/src/__test__/route.test.ts b/src/__test__/route.test.ts index 4ca3c5e..5f46e0c 100644 --- a/src/__test__/route.test.ts +++ b/src/__test__/route.test.ts @@ -8,8 +8,11 @@ beforeAll(async () => { const connectionOptions = await getConnectionOptions(); await createConnection({ ...connectionOptions, name: 'testConnection' }); + }); + + afterAll(async () => { const connection = getConnection('testConnection'); const userRepository = connection.getRepository(User); diff --git a/src/__test__/userStatus.test.ts b/src/__test__/userStatus.test.ts new file mode 100644 index 0000000..1f14416 --- /dev/null +++ b/src/__test__/userStatus.test.ts @@ -0,0 +1,165 @@ +import request from 'supertest'; +import jwt from 'jsonwebtoken'; +import { app, server } from '../index'; +import {getRepository } from 'typeorm'; +import { getConnection } from 'typeorm'; +import { dbConnection } from '../startups/dbConnection'; +import { User } from '../entities/User'; +import { v4 as uuid } from 'uuid'; + +const adminUserId = uuid(); + +const jwtSecretKey = process.env.JWT_SECRET || ""; + +beforeAll(async () => { + const connection = await dbConnection(); + + const userRepository = connection?.getRepository(User); + + const adminUser = new User(); + adminUser.id = adminUserId; + adminUser.firstName = 'remjsa'; + adminUser.lastName = 'djkchd'; + adminUser.email = 'admin.kjaxs@example.com'; + adminUser.password = 'passwordadmin'; + adminUser.userType = 'Admin'; + adminUser.gender = 'Male'; + adminUser.phoneNumber = '126380996347'; + adminUser.photoUrl = 'https://example.com/photo.jpg'; + + await userRepository?.save(adminUser); + + adminUser.role = 'ADMIN'; + adminUser.verified = true; + await userRepository?.save(adminUser); +}); + +afterAll(async () => { + const connection = getConnection(); + const userRepository = connection.getRepository(User); + + // Close the connection to the test database + await connection.close(); + server.close(); +}); + +const data = { + id: adminUserId, + email: "admin.kjaxs@example.com" + }; + +const testUser = { + firstName: 'John', + lastName: 'Doe', + email: 'checki@testing.com', + password: 'password', + gender: 'Male', + phoneNumber: '4223567890', + photoUrl: 'https://example.com/photo.jpg', + }; + +describe('POST /user/deactivate', () => { + it('should deactivate a user', async () => { + await request(app) + .post('/user/register') + .send(testUser); + + const token = jwt.sign(data,jwtSecretKey); + + const response = await request(app) + .post(`/user/deactivate`) + .set("Cookie", `token=${token}`) + .send({email: `${testUser.email}`}); + expect(response.status).toBe(200); + expect(response.body.message).toBe('User deactivated successfully'); + }); + + it('should return 404 when email is not submitted', async ()=> { + + const token = jwt.sign(data,jwtSecretKey); + const response = await request(app) + .post(`/user/deactivate`) + .set("Cookie", `token=${token}`) + + expect(response.status).toBe(404); + expect(response.body.error).toBe('Email is needed'); + }) + it('should return message "User is already suspended" if user is already suspended', async () => { + + const token = jwt.sign(data,jwtSecretKey); + const response = await request(app) + .post(`/user/deactivate`) + .set("Cookie", `token=${token}`) + .send({email: `${testUser.email}`}); + + expect(response.status).toBe(200); + expect(response.body.message).toBe('User is already suspended'); + }); + + it('should return 404 if user not found when deactivating', async () => { + + const token = jwt.sign(data,jwtSecretKey); + const response = await request(app) + .post(`/user/deactivate`) + .set("Cookie", `token=${token}`) + .send({email: "nonexistent@example.com"}); + + expect(response.status).toBe(404); + expect(response.body.error).toBe('User not found'); + }); + + }); + + describe('POST /user/activate', () => { + it('should activate a user', async () => { + const token = jwt.sign(data,jwtSecretKey); + + const response = await request(app) + .post(`/user/activate`) + .set("Cookie", `token=${token}`) + .send({email: `${testUser.email}`}); + + expect(response.status).toBe(200); + expect(response.body.message).toBe('User activated successfully'); + + }); + + it('should return 404 when email is not submitted', async ()=> { + const token = jwt.sign(data,jwtSecretKey); + const response = await request(app) + .post(`/user/activate`) + .set("Cookie", `token=${token}`) + + expect(response.status).toBe(404); + expect(response.body.error).toBe('Email is needed'); + }) + + it('should return message "User is already active" if user is already active', async () => { + const token = jwt.sign(data,jwtSecretKey); + const response = await request(app) + .post(`/user/activate`) + .set("Cookie", `token=${token}`) + .send({email: `${testUser.email}`}); + + expect(response.status).toBe(200); + expect(response.body.message).toBe('User is already active'); + + const userRepository = getRepository(User); + const user = await userRepository.findOne({ where: { email: testUser.email } }); + if (user) { + await userRepository.remove(user); + } + }); + + it('should return 404 if user not found when activating', async () => { + const token = jwt.sign(data,jwtSecretKey); + const response = await request(app) + .post('/user/activate') + .set("Cookie", `token=${token}`) + .send({email: "nonexistent@example.com"}); + + expect(response.status).toBe(404); + expect(response.body.error).toBe('User not found'); + }); + + }); diff --git a/src/controllers/authController.ts b/src/controllers/authController.ts index 480330c..9f5f248 100644 --- a/src/controllers/authController.ts +++ b/src/controllers/authController.ts @@ -10,6 +10,8 @@ import { } from '../services'; import { userPasswordResetService } from '../services/userServices/userPasswordResetService'; import { sendPasswordResetLinkService } from '../services/userServices/sendResetPasswordLinkService'; +import { activateUserService } from '../services/updateUserStatus/activateUserService'; +import { deactivateUserService } from '../services/updateUserStatus/deactivateUserService'; export const userRegistration = async (req: Request, res: Response) => { await userRegistrationService(req, res); @@ -44,3 +46,12 @@ export const userPasswordReset = async (req: Request, res: Response) => { export const sendPasswordResetLink = async (req: Request, res: Response) => { await sendPasswordResetLinkService(req, res); } + +export async function activateUser(req: Request, res: Response) { + await activateUserService(req,res); +} + +export async function disactivateUser(req: Request, res: Response) { + await deactivateUserService(req,res); +} + diff --git a/src/controllers/index.ts b/src/controllers/index.ts index 57935ba..a17886d 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -1,2 +1 @@ - -export * from './authController'; +export * from './authController'; \ No newline at end of file diff --git a/src/controllers/manageStatusController.ts b/src/controllers/manageStatusController.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/helper/verify.ts b/src/helper/verify.ts new file mode 100644 index 0000000..5fad54e --- /dev/null +++ b/src/helper/verify.ts @@ -0,0 +1,19 @@ +import jwt from 'jsonwebtoken'; +import dotenv from 'dotenv'; + +dotenv.config(); + +const jwtSecretKey = process.env.JWT_SECRET; + +if (!jwtSecretKey) { + throw new Error('JWT_SECRET is not defined in the environment variables.'); +} + +export const verifiedToken = (token: string): any => { + try { + return jwt.verify(token, jwtSecretKey); + } catch (err) { + console.error(err); + return null; + } +}; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index bb5ad3a..bcb47c3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ import dotenv from 'dotenv'; import router from './routes'; import { addDocumentation } from './startups/docs'; import 'reflect-metadata'; +import cookieParser from 'cookie-parser'; import { CustomError, errorHandler } from './middlewares/errorHandler'; import morgan from 'morgan'; @@ -14,6 +15,7 @@ export const app = express(); const port = process.env.PORT || 8000; app.use(express.json()); +app.use(cookieParser()); app.use(cors({ origin: '*' })); app.use(router); addDocumentation(app); diff --git a/src/middlewares/isAllowed.ts b/src/middlewares/isAllowed.ts new file mode 100644 index 0000000..2894be6 --- /dev/null +++ b/src/middlewares/isAllowed.ts @@ -0,0 +1,54 @@ +import { NextFunction, Request, Response } from "express"; +import { User } from "../entities/User"; +import { getRepository } from "typeorm"; +import { responseError } from "../utils/response.utils"; + +export interface UserInterface { + id: string; + firstName: string; + lastName: string; + email: string; + password: string; + gender: string; + phoneNumber: string; + photoUrl?: string; + verified: boolean; + status: 'active' | 'suspended'; + userType: 'Admin' | 'Buyer' | 'Vendor'; + role: string; + createdAt: Date; + updatedAt: Date; +} + +declare module 'express' { + interface Request { + user?: Partial; + } +} + +export const checkUserStatus = async (req: Request, res: Response, next: NextFunction) => { + try { + if (!req.user) { + return responseError(res, 401, 'Authentication required'); + } + + const userId = req.user.id; + + const userRepository = getRepository(User); + + const user = await userRepository.findOne({ where: { id: userId } }); + if (!user) { + return responseError(res, 401, 'User not found'); + } + + if (user.status === 'active') { + next(); + } else if (user.status === 'suspended') { + return responseError(res, 403, 'You have been suspended. Please contact our support team.'); + } else { + return responseError(res, 403, 'Unauthorized action'); + } + } catch (error) { + responseError(res, 400, (error as Error).message); + } +}; diff --git a/src/middlewares/isValid.ts b/src/middlewares/isValid.ts new file mode 100644 index 0000000..5d0c897 --- /dev/null +++ b/src/middlewares/isValid.ts @@ -0,0 +1,37 @@ +import { Request, Response, NextFunction, RequestHandler } from 'express'; +import { verifiedToken } from '../helper/verify'; +import { getRepository } from 'typeorm'; +import { User } from '../entities/User'; + +export interface DecodedUser { + userType: string; + id: string; + email: string; +} + +export const isTokenValide: RequestHandler = async ( + req: Request, + res: Response, + next: NextFunction +): Promise => { + try { + const token = req.cookies.token; + const userPaylod = verifiedToken(token); + if (!userPaylod) { + res.status(401).json({ Message: 'Sorry, You are not authorized' }); + return; + } + const userRepository = getRepository(User); + const user = await userRepository.findOne({where: {id: userPaylod.id}}) + if(!user){ + res.status(404).json({Message: 'User not found'}); + return; + } + req.user = user; + return next(); + } catch (error) { + console.error('Error in token Validation middleware:\n', error); + res.status(401).json({ Message: 'Sorry, Something went wrong' }); + return; + } +}; \ No newline at end of file diff --git a/src/routes/UserRoutes.ts b/src/routes/UserRoutes.ts index b3f6d8d..1db2d08 100644 --- a/src/routes/UserRoutes.ts +++ b/src/routes/UserRoutes.ts @@ -3,6 +3,9 @@ import { disable2FA, enable2FA, login, resendOTP, sendPasswordResetLink, userPas +import { activateUser,disactivateUser } from '../controllers/index'; +import {hasRole} from '../middlewares/roleCheck'; +import { isTokenValide } from '../middlewares/isValid'; const router = Router(); @@ -13,6 +16,8 @@ router.post('/enable-2fa', enable2FA); router.post('/disable-2fa', disable2FA); router.post('/verify-otp', verifyOTP); router.post('/resend-otp', resendOTP); +router.post('/activate',isTokenValide,hasRole("ADMIN"),activateUser); +router.post('/deactivate',isTokenValide,hasRole("ADMIN"),disactivateUser); router.post("/password/reset", userPasswordReset); router.post("/password/reset/link", sendPasswordResetLink); diff --git a/src/services/updateUserStatus/activateUserService.ts b/src/services/updateUserStatus/activateUserService.ts new file mode 100644 index 0000000..c499280 --- /dev/null +++ b/src/services/updateUserStatus/activateUserService.ts @@ -0,0 +1,39 @@ +import { Request, Response } from 'express'; +import { User } from '../../entities/User'; +import { getRepository } from 'typeorm'; +import { sendEmail } from '../../utils/sendStatusMail'; + +enum UserStatus { + ACTIVE = 'active', + INACTIVE = 'suspended', +} + +export const activateUserService = async (req:Request,res:Response)=>{ + try { + const {email} = req.body; + const userRepository = getRepository(User); + + if(!email){ + return res.status(404).json({error: 'Email is needed'}); + } + + const user = await userRepository.findOneBy({ email }); + + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + + if (user.status === 'active') { + return res.json({ message: 'User is already active' }); + } + + user.status = UserStatus.ACTIVE; + await userRepository.save(user); + + await sendEmail('User_Account_activated', { name: user.firstName, email: user.email }); + + return res.status(200).json({ message: 'User activated successfully', user }); + } catch (error) { + return res.status(500).json({ error: 'Internal server error' }); + } +} \ No newline at end of file diff --git a/src/services/updateUserStatus/deactivateUserService.ts b/src/services/updateUserStatus/deactivateUserService.ts new file mode 100644 index 0000000..c72c394 --- /dev/null +++ b/src/services/updateUserStatus/deactivateUserService.ts @@ -0,0 +1,39 @@ +import { Request, Response } from 'express'; +import { User } from '../../entities/User'; +import { getRepository } from 'typeorm'; +import { sendEmail } from '../../utils/sendStatusMail'; + +enum UserStatus { + ACTIVE = 'active', + INACTIVE = 'suspended', +} + +export const deactivateUserService = async (req:Request,res:Response)=>{ + try { + const {email} = req.body; + const userRepository = getRepository(User); + + if(!email){ + return res.status(404).json({error: 'Email is needed'}); + } + + const user = await userRepository.findOneBy({ email }); + + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + + if (user.status === 'suspended') { + return res.json({ message: 'User is already suspended' }); + } + + user.status = UserStatus.INACTIVE; + await userRepository.save(user); + + await sendEmail('User_Account_diactivated', { name: user.firstName, email: user.email }); + + return res.json({ message: 'User deactivated successfully', user }); + } catch (error) { + return res.status(500).json({ error: 'Internal server error' }); + } +} \ No newline at end of file diff --git a/src/utils/sendStatusMail.ts b/src/utils/sendStatusMail.ts new file mode 100644 index 0000000..877b2a0 --- /dev/null +++ b/src/utils/sendStatusMail.ts @@ -0,0 +1,95 @@ +import { config } from 'dotenv'; +import nodemailer from 'nodemailer'; +import Mailgen from 'mailgen'; + +config(); + +interface IData { + email: string; + name: string; +} + +const EMAIL = process.env.AUTH_EMAIL; +const PASSWORD = process.env.AUTH_PASSWORD; + +export const sendEmail = async (type: string, data: IData) => { + if(EMAIL && PASSWORD){ + try { + const mailGenerator = new Mailgen({ + theme: 'default', + product: { + name: 'Knights', + link: 'https://mailgen.js/', + }, + }); + + const transporter = nodemailer.createTransport({ + service: 'gmail', + auth: { + user: EMAIL, + pass: PASSWORD, + }, + }); + + let email; + let subject; + switch (type) { + case 'User_Account_diactivated': + email = { + body: { + name: data.name, + intro: 'Your account has been blocked due to violation of our terms and conditions.', + action: { + instructions: 'If you believe this is an error, please contact support at knights.andela@gmail.com.', + button: { + color: '#22BC66', + text: 'Contact Support', + link: 'mailto:knights.andela@gmail.com', + }, + }, + outro: 'Thank you for your understanding.', + }, + }; + subject = 'Account Suspended'; + break; + case 'User_Account_activated': + email = { + body: { + name: data.name, + intro: 'Your account has been unblocked.', + action: { + instructions: 'You can now access your account again.', + button: { + color: '#22BC66', + text: 'Access Account', + link: 'https://knights-e-commerce.com/login', + }, + }, + outro: 'If you did not request this action, please contact support immediately.', + }, + }; + subject = 'Account Unblocked'; + break; + default: + throw new Error('Invalid email type'); + } + + const html = mailGenerator.generate(email); + + const mailOptions = { + from: EMAIL, + to: data.email, + subject: subject, + html: html, + }; + + const info = await transporter.sendMail(mailOptions); + } catch (error) { + console.error('Error sending email:', error); + } + } + else { + console.error('Email or password for mail server not configured'); + return +} +}; From da0e42cbd5966d3afb854adffdcf2229f571b45a Mon Sep 17 00:00:00 2001 From: Ndevu12 Date: Mon, 6 May 2024 12:07:18 +0200 Subject: [PATCH 31/51] ft logout feature updates --- src/__test__/logout.test.ts | 74 ++++++ src/__test__/route.test.ts | 41 ++- src/__test__/userServices.test.ts | 25 +- src/__test__/userStatus.test.ts | 246 +++++++++--------- src/controllers/authController.ts | 20 +- src/helper/verify.ts | 2 +- src/index.ts | 1 - src/middlewares/index.ts | 3 +- src/middlewares/isValid.ts | 14 +- src/routes/UserRoutes.ts | 28 +- src/services/index.ts | 1 + src/services/userServices/logoutServices.ts | 18 ++ src/services/userServices/userLoginService.ts | 7 + .../userServices/userPasswordResetService.ts | 65 +++-- src/services/userServices/userResendOTP.ts | 4 + src/services/userServices/userSendOTPEmail.ts | 1 - src/utils/sendMail.ts | 3 +- 17 files changed, 334 insertions(+), 219 deletions(-) create mode 100644 src/__test__/logout.test.ts create mode 100644 src/services/userServices/logoutServices.ts diff --git a/src/__test__/logout.test.ts b/src/__test__/logout.test.ts new file mode 100644 index 0000000..19cdf70 --- /dev/null +++ b/src/__test__/logout.test.ts @@ -0,0 +1,74 @@ +import request from 'supertest'; +import { app, server } from '../index'; +import { createConnection, getConnection, getConnectionOptions, getRepository } from 'typeorm'; +import { User } from '../entities/User'; + +beforeAll(async () => { + // Connect to the test database + const connectionOptions = await getConnectionOptions(); + await createConnection({ ...connectionOptions, name: 'testConnection' }); +}); + +afterAll(async () => { + await getConnection('testConnection').close(); + server.close(); +}); + +describe('POST /user/logout', () => { + it('should logout a user', async () => { + // sign up a user + const registerUser = { + firstName: 'Ndevu', + lastName: 'Elisa', + email: 'ndevukumurindi@gmail.com', + gender: 'male', + phoneNumber: '078907987443', + photoUrl: 'https://example.com/images/photo.jpg', + userType: 'vender', + verified: true, + status: 'active', + password: process.env.TEST_USER_LOGIN_PASS, + }; + + await request(app).post('/user/register').send(registerUser); + + const loginUser = { + email: registerUser.email, + password: process.env.TEST_USER_LOGIN_PASS, + }; + + const userRepository = getRepository(User); + const user = await userRepository.findOne({ where: { email: registerUser.email } }); + if (user) { + const verifyRes = await request(app).get(`/user/verify/${user.id}`); + + if (!verifyRes) throw new Error(`Test User verification failed for ${user.email}`); + + const loginResponse = await request(app).post('/user/login').send(loginUser); + const setCookie = loginResponse.headers['set-cookie']; + + if (!setCookie) { + throw new Error('No cookies set in login response'); + } + + const resp = await request(app).post('/user/logout').set('Cookie', setCookie); + expect(resp.status).toBe(200); + expect(resp.body).toEqual({ Message: 'Logged out successfully' }); + + // Clean up: delete the test user + await userRepository.remove(user); + } + }); + + it('should not logout a user who is not logged in or with no token', async () => { + const fakeEmail = 'ndevukkkk@gmail.com'; + const loginUser = { + email: fakeEmail, + password: process.env.TEST_USER_LOGIN_PASS, + }; + const token = ''; + const res = await request(app).post('/user/logout').send(loginUser).set('Cookie', token); + expect(res.status).toBe(400); + expect(res.body).toEqual({ Message: 'Access denied. You must be logged in' }); + }); +}); diff --git a/src/__test__/route.test.ts b/src/__test__/route.test.ts index 5f46e0c..d1c3be2 100644 --- a/src/__test__/route.test.ts +++ b/src/__test__/route.test.ts @@ -8,11 +8,8 @@ beforeAll(async () => { const connectionOptions = await getConnectionOptions(); await createConnection({ ...connectionOptions, name: 'testConnection' }); - }); - - afterAll(async () => { const connection = getConnection('testConnection'); const userRepository = connection.getRepository(User); @@ -106,9 +103,8 @@ describe('POST /user/verify/:id', () => { }); describe('Send password reset link', () => { - it('Attempt to send email with rate limiting', async () => { - const email = "elijahladdiedv@gmail.com"; + const email = 'elijahladdiedv@gmail.com'; const requests = Array.from({ length: 5 }, async () => { return await request(app).post(`/user/password/reset/link?email=${email}`); @@ -118,34 +114,33 @@ describe('Send password reset link', () => { const lastResponse = responses[responses.length - 1]; expect(lastResponse.status).toBe(404); expect(lastResponse.body.message).toEqual('User not found'); - }); + }, 20000); it('Attempt to send email with invalid email template', async () => { - const email = "elijahladdiedv@gmail.com"; + const email = 'elijahladdiedv@gmail.com'; const res = await request(app).post(`/user/password/reset/link?email=${email}`); expect(res.status).toBe(404); expect(res.body.message).toEqual('User not found'); - }); + }, 10000); it('Send email to a user with special characters in email address', async () => { - const email = "user+test@example.com"; + const email = 'user+test@example.com'; const res = await request(app).post(`/user/password/reset/link?email=${encodeURIComponent(email)}`); expect(res.status).toBe(404); expect(res.body.message).toEqual('User not found'); - }); - + }, 10000); }); describe('Password Reset Service', () => { it('Should reset password successfully', async () => { const data = { - "newPassword": "user", - "confirmPassword": "user", + newPassword: 'user', + confirmPassword: 'user', }; - const email = "elijahladdiedv@gmail.com"; + const email = 'elijahladdiedv@gmail.com'; const userRepository = getRepository(User); const user = await userRepository.findOne({ where: { email: email } }); if (user) { @@ -158,22 +153,21 @@ describe('Password Reset Service', () => { it('Should return 404 if user not found', async () => { const data = { - "newPassword": "user", - "confirmPassword": "user", + newPassword: 'user', + confirmPassword: 'user', }; - const email = "nonexistentemail@example.com"; - const userId = "nonexistentuserid"; + const email = 'nonexistentemail@example.com'; + const userId = 'nonexistentuserid'; const res: any = await request(app).post(`/user/password/reset?userid=${userId}&email=${email}`).send(data); // Asser expect(res).toBeTruthy; - }); it('Should return 204 if required fields are missing', async () => { const data = { // }; - const email = "elijahladdiedv@gmail.com"; + const email = 'elijahladdiedv@gmail.com'; const userRepository = getRepository(User); const user = await userRepository.findOne({ where: { email: email } }); @@ -186,10 +180,10 @@ describe('Password Reset Service', () => { it('Should return 204 if newPassword and confirmPassword do not match', async () => { const data = { - "newPassword": "user123", - "confirmPassword": "user456", + newPassword: 'user123', + confirmPassword: 'user456', }; - const email = "elijahladdiedv@gmail.com"; + const email = 'elijahladdiedv@gmail.com'; const userRepository = getRepository(User); const user = await userRepository.findOne({ where: { email: email } }); @@ -200,4 +194,3 @@ describe('Password Reset Service', () => { } }); }); - diff --git a/src/__test__/userServices.test.ts b/src/__test__/userServices.test.ts index 9bd34ff..5e435d1 100644 --- a/src/__test__/userServices.test.ts +++ b/src/__test__/userServices.test.ts @@ -152,7 +152,8 @@ describe('start2FAProcess', () => { it('should return 403 if OTP is expired', async () => { const email = 'john.doe1@example.com'; - const user = await getRepository(User).findOneBy({ email }); + const userRepository = getRepository(User); + const user = await userRepository.findOneBy({ email }); if (user) { user.twoFactorEnabled = true; user.twoFactorCode = '123456'; @@ -168,6 +169,9 @@ describe('start2FAProcess', () => { const res = await request(app).post('/user/verify-otp').send(data); expect(res.status).toBe(403); expect(res.body).toEqual({ status: 'error', message: 'Authentication code expired' }); + if (user) { + await userRepository.remove(user); + } }); it('should return 400 if not sent email in body on resending OTP', async () => { @@ -190,14 +194,29 @@ describe('start2FAProcess', () => { }); it('should resend OTP', async () => { + const newUser = { + firstName: 'John', + lastName: 'Doe', + email: 'john.doe187@example.com', + password: 'password', + gender: 'Male', + phoneNumber: '0785044398', + userType: 'Buyer', + }; + + // Act + const resp = await request(app).post('/user/register').send(newUser); + if (!resp) { + console.log('Error creating user in resend otp test case'); + } const data = { - email: 'john.doe1@example.com', + email: 'john.doe187@example.com', }; const res = await request(app).post('/user/resend-otp').send(data); expect(res.status).toBe(200); expect(res.body).toEqual({ status: 'success', data: { message: 'OTP sent successfully' } }); - }); + }, 20000); it('should return 400 if not sent email in body on login', async () => { const data = {}; diff --git a/src/__test__/userStatus.test.ts b/src/__test__/userStatus.test.ts index 1f14416..f253551 100644 --- a/src/__test__/userStatus.test.ts +++ b/src/__test__/userStatus.test.ts @@ -1,7 +1,7 @@ import request from 'supertest'; import jwt from 'jsonwebtoken'; import { app, server } from '../index'; -import {getRepository } from 'typeorm'; +import { getRepository } from 'typeorm'; import { getConnection } from 'typeorm'; import { dbConnection } from '../startups/dbConnection'; import { User } from '../entities/User'; @@ -9,23 +9,23 @@ import { v4 as uuid } from 'uuid'; const adminUserId = uuid(); -const jwtSecretKey = process.env.JWT_SECRET || ""; +const jwtSecretKey = process.env.JWT_SECRET || ''; beforeAll(async () => { - const connection = await dbConnection(); + const connection = await dbConnection(); - const userRepository = connection?.getRepository(User); + const userRepository = connection?.getRepository(User); - const adminUser = new User(); - adminUser.id = adminUserId; - adminUser.firstName = 'remjsa'; - adminUser.lastName = 'djkchd'; - adminUser.email = 'admin.kjaxs@example.com'; - adminUser.password = 'passwordadmin'; - adminUser.userType = 'Admin'; - adminUser.gender = 'Male'; - adminUser.phoneNumber = '126380996347'; - adminUser.photoUrl = 'https://example.com/photo.jpg'; + const adminUser = new User(); + adminUser.id = adminUserId; + adminUser.firstName = 'remjsa'; + adminUser.lastName = 'djkchd'; + adminUser.email = 'admin.kjaxs@example.com'; + adminUser.password = 'passwordadmin'; + adminUser.userType = 'Admin'; + adminUser.gender = 'Male'; + adminUser.phoneNumber = '126380996347'; + adminUser.photoUrl = 'https://example.com/photo.jpg'; await userRepository?.save(adminUser); @@ -35,131 +35,119 @@ beforeAll(async () => { }); afterAll(async () => { - const connection = getConnection(); - const userRepository = connection.getRepository(User); + const connection = getConnection(); + const userRepository = connection.getRepository(User); - // Close the connection to the test database + // Close the connection to the test database await connection.close(); server.close(); }); const data = { - id: adminUserId, - email: "admin.kjaxs@example.com" - }; + id: adminUserId, + email: 'admin.kjaxs@example.com', +}; const testUser = { - firstName: 'John', - lastName: 'Doe', - email: 'checki@testing.com', - password: 'password', - gender: 'Male', - phoneNumber: '4223567890', - photoUrl: 'https://example.com/photo.jpg', - }; + firstName: 'John', + lastName: 'Doe', + email: 'checki@testing.com', + password: 'password', + gender: 'Male', + phoneNumber: '4223567890', + photoUrl: 'https://example.com/photo.jpg', +}; describe('POST /user/deactivate', () => { - it('should deactivate a user', async () => { - await request(app) - .post('/user/register') - .send(testUser); - - const token = jwt.sign(data,jwtSecretKey); - - const response = await request(app) - .post(`/user/deactivate`) - .set("Cookie", `token=${token}`) - .send({email: `${testUser.email}`}); - expect(response.status).toBe(200); - expect(response.body.message).toBe('User deactivated successfully'); - }); - - it('should return 404 when email is not submitted', async ()=> { - - const token = jwt.sign(data,jwtSecretKey); - const response = await request(app) - .post(`/user/deactivate`) - .set("Cookie", `token=${token}`) - - expect(response.status).toBe(404); - expect(response.body.error).toBe('Email is needed'); - }) - it('should return message "User is already suspended" if user is already suspended', async () => { - - const token = jwt.sign(data,jwtSecretKey); - const response = await request(app) - .post(`/user/deactivate`) - .set("Cookie", `token=${token}`) - .send({email: `${testUser.email}`}); - - expect(response.status).toBe(200); - expect(response.body.message).toBe('User is already suspended'); - }); - - it('should return 404 if user not found when deactivating', async () => { - - const token = jwt.sign(data,jwtSecretKey); - const response = await request(app) - .post(`/user/deactivate`) - .set("Cookie", `token=${token}`) - .send({email: "nonexistent@example.com"}); - - expect(response.status).toBe(404); - expect(response.body.error).toBe('User not found'); - }); + it('should deactivate a user', async () => { + await request(app).post('/user/register').send(testUser); + const token = jwt.sign(data, jwtSecretKey); + + const response = await request(app) + .post(`/user/deactivate`) + .set('Cookie', `token=${token}`) + .send({ email: `${testUser.email}` }); + expect(response.status).toBe(200); + expect(response.body.message).toBe('User deactivated successfully'); + }, 10000); + + it('should return 404 when email is not submitted', async () => { + const token = jwt.sign(data, jwtSecretKey); + const response = await request(app).post(`/user/deactivate`).set('Cookie', `token=${token}`); + + expect(response.status).toBe(404); + expect(response.body.error).toBe('Email is needed'); }); - - describe('POST /user/activate', () => { - it('should activate a user', async () => { - const token = jwt.sign(data,jwtSecretKey); - - const response = await request(app) - .post(`/user/activate`) - .set("Cookie", `token=${token}`) - .send({email: `${testUser.email}`}); - - expect(response.status).toBe(200); - expect(response.body.message).toBe('User activated successfully'); - - }); - - it('should return 404 when email is not submitted', async ()=> { - const token = jwt.sign(data,jwtSecretKey); - const response = await request(app) - .post(`/user/activate`) - .set("Cookie", `token=${token}`) - - expect(response.status).toBe(404); - expect(response.body.error).toBe('Email is needed'); - }) - - it('should return message "User is already active" if user is already active', async () => { - const token = jwt.sign(data,jwtSecretKey); - const response = await request(app) - .post(`/user/activate`) - .set("Cookie", `token=${token}`) - .send({email: `${testUser.email}`}); - - expect(response.status).toBe(200); - expect(response.body.message).toBe('User is already active'); - - const userRepository = getRepository(User); - const user = await userRepository.findOne({ where: { email: testUser.email } }); - if (user) { - await userRepository.remove(user); - } - }); - - it('should return 404 if user not found when activating', async () => { - const token = jwt.sign(data,jwtSecretKey); - const response = await request(app) - .post('/user/activate') - .set("Cookie", `token=${token}`) - .send({email: "nonexistent@example.com"}); - - expect(response.status).toBe(404); - expect(response.body.error).toBe('User not found'); - }); - + it('should return message "User is already suspended" if user is already suspended', async () => { + const token = jwt.sign(data, jwtSecretKey); + const response = await request(app) + .post(`/user/deactivate`) + .set('Cookie', `token=${token}`) + .send({ email: `${testUser.email}` }); + + expect(response.status).toBe(200); + expect(response.body.message).toBe('User is already suspended'); + }); + + it('should return 404 if user not found when deactivating', async () => { + const token = jwt.sign(data, jwtSecretKey); + const response = await request(app) + .post(`/user/deactivate`) + .set('Cookie', `token=${token}`) + .send({ email: 'nonexistent@example.com' }); + + expect(response.status).toBe(404); + expect(response.body.error).toBe('User not found'); }); +}); + +describe('POST /user/activate', () => { + it('should activate a user', async () => { + const token = jwt.sign(data, jwtSecretKey); + + const response = await request(app) + .post(`/user/activate`) + .set('Cookie', `token=${token}`) + .send({ email: `${testUser.email}` }); + + expect(response.status).toBe(200); + expect(response.body.message).toBe('User activated successfully'); + }, 10000); + + it('should return 404 when email is not submitted', async () => { + const token = jwt.sign(data, jwtSecretKey); + const response = await request(app).post(`/user/activate`).set('Cookie', `token=${token}`); + + expect(response.status).toBe(404); + expect(response.body.error).toBe('Email is needed'); + }); + + it('should return message "User is already active" if user is already active', async () => { + const token = jwt.sign(data, jwtSecretKey); + const response = await request(app) + .post(`/user/activate`) + .set('Cookie', `token=${token}`) + .send({ email: `${testUser.email}` }); + + expect(response.status).toBe(200); + expect(response.body.message).toBe('User is already active'); + + const userRepository = getRepository(User); + const user = await userRepository.findOne({ where: { email: testUser.email } }); + if (user) { + await userRepository.remove(user); + } + }); + + it('should return 404 if user not found when activating', async () => { + const token = jwt.sign(data, jwtSecretKey); + const response = await request(app) + .post('/user/activate') + .set('Cookie', `token=${token}`) + .send({ email: 'nonexistent@example.com' }); + + expect(response.status).toBe(404); + expect(response.body.error).toBe('User not found'); + }); +}); diff --git a/src/controllers/authController.ts b/src/controllers/authController.ts index 9f5f248..fbc025d 100644 --- a/src/controllers/authController.ts +++ b/src/controllers/authController.ts @@ -7,6 +7,7 @@ import { userDisableTwoFactorAuth, userValidateOTP, userResendOtpService, + logoutService, } from '../services'; import { userPasswordResetService } from '../services/userServices/userPasswordResetService'; import { sendPasswordResetLinkService } from '../services/userServices/sendResetPasswordLinkService'; @@ -40,18 +41,25 @@ export const verifyOTP = async (req: Request, res: Response) => { export const resendOTP = async (req: Request, res: Response) => { await userResendOtpService(req, res); }; + +export const sampleAPI = async (req: Request, res: Response) => { + res.status(200).json({ message: 'Token is valid' }); +}; export const userPasswordReset = async (req: Request, res: Response) => { - await userPasswordResetService(req, res); -} + await userPasswordResetService(req, res); +}; export const sendPasswordResetLink = async (req: Request, res: Response) => { - await sendPasswordResetLinkService(req, res); -} + await sendPasswordResetLinkService(req, res); +}; export async function activateUser(req: Request, res: Response) { - await activateUserService(req,res); + await activateUserService(req, res); } export async function disactivateUser(req: Request, res: Response) { - await deactivateUserService(req,res); + await deactivateUserService(req, res); } +export const logout = async (req: Request, res: Response) => { + await logoutService(req, res); +}; diff --git a/src/helper/verify.ts b/src/helper/verify.ts index 5fad54e..bda6a00 100644 --- a/src/helper/verify.ts +++ b/src/helper/verify.ts @@ -16,4 +16,4 @@ export const verifiedToken = (token: string): any => { console.error(err); return null; } -}; \ No newline at end of file +}; diff --git a/src/index.ts b/src/index.ts index bcb47c3..fb60cd4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,7 +14,6 @@ dotenv.config(); export const app = express(); const port = process.env.PORT || 8000; app.use(express.json()); - app.use(cookieParser()); app.use(cors({ origin: '*' })); app.use(router); diff --git a/src/middlewares/index.ts b/src/middlewares/index.ts index aa7e780..8fd48b8 100644 --- a/src/middlewares/index.ts +++ b/src/middlewares/index.ts @@ -1,2 +1,3 @@ -export * from "./errorHandler" +export * from './errorHandler'; export * from './roleCheck'; +export * from './isValid'; diff --git a/src/middlewares/isValid.ts b/src/middlewares/isValid.ts index 5d0c897..bd3a004 100644 --- a/src/middlewares/isValid.ts +++ b/src/middlewares/isValid.ts @@ -9,11 +9,7 @@ export interface DecodedUser { email: string; } -export const isTokenValide: RequestHandler = async ( - req: Request, - res: Response, - next: NextFunction -): Promise => { +export const isTokenValide: RequestHandler = async (req: Request, res: Response, next: NextFunction): Promise => { try { const token = req.cookies.token; const userPaylod = verifiedToken(token); @@ -22,9 +18,9 @@ export const isTokenValide: RequestHandler = async ( return; } const userRepository = getRepository(User); - const user = await userRepository.findOne({where: {id: userPaylod.id}}) - if(!user){ - res.status(404).json({Message: 'User not found'}); + const user = await userRepository.findOne({ where: { id: userPaylod.id } }); + if (!user) { + res.status(404).json({ Message: 'User not found' }); return; } req.user = user; @@ -34,4 +30,4 @@ export const isTokenValide: RequestHandler = async ( res.status(401).json({ Message: 'Sorry, Something went wrong' }); return; } -}; \ No newline at end of file +}; diff --git a/src/routes/UserRoutes.ts b/src/routes/UserRoutes.ts index 1db2d08..9eac282 100644 --- a/src/routes/UserRoutes.ts +++ b/src/routes/UserRoutes.ts @@ -1,10 +1,19 @@ import { Router } from 'express'; -import { disable2FA, enable2FA, login, resendOTP, sendPasswordResetLink, userPasswordReset , userRegistration, userVerification, verifyOTP} from '../controllers'; +import { + disable2FA, + enable2FA, + login, + resendOTP, + sendPasswordResetLink, + userPasswordReset, + userRegistration, + userVerification, + verifyOTP, + logout, +} from '../controllers'; - - -import { activateUser,disactivateUser } from '../controllers/index'; -import {hasRole} from '../middlewares/roleCheck'; +import { activateUser, disactivateUser } from '../controllers/index'; +import { hasRole } from '../middlewares/roleCheck'; import { isTokenValide } from '../middlewares/isValid'; const router = Router(); @@ -12,13 +21,14 @@ const router = Router(); router.post('/register', userRegistration); router.get('/verify/:id', userVerification); router.post('/login', login); +router.post('/logout', logout); router.post('/enable-2fa', enable2FA); router.post('/disable-2fa', disable2FA); router.post('/verify-otp', verifyOTP); router.post('/resend-otp', resendOTP); -router.post('/activate',isTokenValide,hasRole("ADMIN"),activateUser); -router.post('/deactivate',isTokenValide,hasRole("ADMIN"),disactivateUser); -router.post("/password/reset", userPasswordReset); -router.post("/password/reset/link", sendPasswordResetLink); +router.post('/activate', isTokenValide, hasRole('ADMIN'), activateUser); +router.post('/deactivate', isTokenValide, hasRole('ADMIN'), disactivateUser); +router.post('/password/reset', userPasswordReset); +router.post('/password/reset/link', sendPasswordResetLink); export default router; diff --git a/src/services/index.ts b/src/services/index.ts index a646114..a7b7863 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -8,3 +8,4 @@ export * from './userServices/userDisableTwoFactorAuth'; export * from './userServices/userValidateOTP'; export * from './userServices/userLoginService'; export * from './userServices/userResendOTP'; +export * from './userServices/logoutServices'; diff --git a/src/services/userServices/logoutServices.ts b/src/services/userServices/logoutServices.ts new file mode 100644 index 0000000..541c364 --- /dev/null +++ b/src/services/userServices/logoutServices.ts @@ -0,0 +1,18 @@ +import { Request, Response } from 'express'; + +// logout method +export const logoutService = async (req: Request, res: Response): Promise => { + try { + const token = req.cookies['token'] || null; + if (!token) { + res.status(400).json({ Message: 'Access denied. You must be logged in' }); + return; + } + + res.clearCookie('token'); + res.status(200).json({ Message: 'Logged out successfully' }); + } catch (error) { + console.error('Error logging out:', error); + res.status(500).json({ error: 'Sorry, Token required.' }); + } +}; diff --git a/src/services/userServices/userLoginService.ts b/src/services/userServices/userLoginService.ts index 8b0cffa..57633d0 100644 --- a/src/services/userServices/userLoginService.ts +++ b/src/services/userServices/userLoginService.ts @@ -48,6 +48,13 @@ export const userLoginService = async (req: Request, res: Response) => { process.env.JWT_SECRET as string, { expiresIn: '24h' } ); + + if (process.env.APP_ENV === 'production') { + res.cookie('token', token, { httpOnly: true, sameSite: false, secure: true }); + } else { + res.cookie('token', token, { httpOnly: true, sameSite: 'lax', secure: false }); + } + return res.status(200).json({ status: 'success', data: { diff --git a/src/services/userServices/userPasswordResetService.ts b/src/services/userServices/userPasswordResetService.ts index a14ebad..8428f1a 100644 --- a/src/services/userServices/userPasswordResetService.ts +++ b/src/services/userServices/userPasswordResetService.ts @@ -1,39 +1,38 @@ import bcrypt from 'bcrypt'; -import { Request, Response } from "express"; -import { responseError, responseServerError, responseSuccess } from "../../utils/response.utils"; -import { getRepository } from "typeorm"; -import { User } from "../../entities/User"; +import { Request, Response } from 'express'; +import { responseError, responseServerError, responseSuccess } from '../../utils/response.utils'; +import { getRepository } from 'typeorm'; +import { User } from '../../entities/User'; export const userPasswordResetService = async (req: Request, res: Response) => { - try { - const { email, userid } = req.query; - const { newPassword, confirmPassword } = req.body; - const mail: any = email; - const userId: any = userid; - const userRepository = getRepository(User); - if (!email || !userid) { - return responseError(res, 404, `Something went wrong while fetching your data`); - } - const existingUser = await userRepository.findOneBy({ email: mail, id: userId }); - if (!existingUser) { - return responseError(res, 404, `We can't find you data`); - } - - if (!newPassword || !confirmPassword) { - return responseError(res, 200, 'Please provide all required fields'); - } - if (newPassword !== confirmPassword) { - return responseError(res, 200, 'new password must match confirm password'); - } - const saltRounds = 10; - const hashedPassword = await bcrypt.hash(newPassword, saltRounds); + try { + const { email, userid } = req.query; + const { newPassword, confirmPassword } = req.body; + const mail: any = email; + const userId: any = userid; + const userRepository = getRepository(User); + if (!email || !userid) { + return responseError(res, 404, `Something went wrong while fetching your data`); + } + const existingUser = await userRepository.findOneBy({ email: mail, id: userId }); + if (!existingUser) { + return responseError(res, 404, `We can't find you data`); + } - existingUser.password = hashedPassword; - const updadeUser = await userRepository.save(existingUser); - return responseSuccess(res, 201, "Password updated successfully", updadeUser); - } catch (error) { - console.log(error) - return responseServerError(res, "Internal server error"); + if (!newPassword || !confirmPassword) { + return responseError(res, 200, 'Please provide all required fields'); + } + if (newPassword !== confirmPassword) { + return responseError(res, 200, 'new password must match confirm password'); } -} + const saltRounds = 10; + const hashedPassword = await bcrypt.hash(newPassword, saltRounds); + existingUser.password = hashedPassword; + const updadeUser = await userRepository.save(existingUser); + return responseSuccess(res, 201, 'Password updated successfully', updadeUser); + } catch (error) { + console.log('error: reseting password in password reset service'); + return responseServerError(res, 'Internal server error'); + } +}; diff --git a/src/services/userServices/userResendOTP.ts b/src/services/userServices/userResendOTP.ts index f86662b..f728b31 100644 --- a/src/services/userServices/userResendOTP.ts +++ b/src/services/userServices/userResendOTP.ts @@ -13,6 +13,7 @@ export const userResendOtpService = async (req: Request, res: Response) => { const { email } = req.body; if (!email) { + console.log('No email address provided'); return res.status(400).json({ status: 'error', message: 'Please provide an email' }); } @@ -20,11 +21,14 @@ export const userResendOtpService = async (req: Request, res: Response) => { const user = await userRepository.findOneBy({ email }); if (!user) { + console.log('User not found'); return res.status(404).json({ status: 'error', message: 'Incorrect email' }); } const otpCode = await start2FAProcess(user.email); + if (!otpCode) throw new Error('Error generating OTP'); const OTPEmailcontent = otpTemplate(user.firstName, otpCode.toString()); + if (!OTPEmailcontent) throw new Error('Error generating OTP email content'); await sendOTPEmail('Login OTP', user.email, OTPEmailcontent); if (process.env.APP_ENV !== 'test') { await sendOTPSMS(user.phoneNumber, otpCode.toString()); diff --git a/src/services/userServices/userSendOTPEmail.ts b/src/services/userServices/userSendOTPEmail.ts index a556eea..f80e39c 100644 --- a/src/services/userServices/userSendOTPEmail.ts +++ b/src/services/userServices/userSendOTPEmail.ts @@ -21,7 +21,6 @@ export const sendOTPEmail = async (subject: string, email: string, content: any) try { const info = await transporter.sendMail(mailOptions); - console.log('Message sent: %s', info.messageId); } catch (error) { console.log('Error occurred while sending email', error); } diff --git a/src/utils/sendMail.ts b/src/utils/sendMail.ts index e63cb6d..0836765 100644 --- a/src/utils/sendMail.ts +++ b/src/utils/sendMail.ts @@ -29,7 +29,7 @@ const sendMail = async ( - + + + +
+ shoping image +

Order Success

+
+

User information

+ + + + + + + + + + + + + + + +
Full Name:Email:Address:
${fullName}${email}${address}
+ + + + + + + + + + + + + + + +
Order Date:Quantity:Total Amount:
${formatDate(orderDate)}${quantity}${formatMoney(totalAmount)}
+
+
+

Products

+ + + + + + + + ${products.map((product: Product) => ` + + + + + + + `).join('')} + + + + +
Product NameProduct PriceQuantityTotal
${product.name}${formatMoney(product.newPrice)}${product.quantity}${product.quantity * product.newPrice}
Total${totalAmount}
+
+ +
+ + + `, + }; + + try { + const info = await transporter.sendMail(mailOptions); + console.log('Email sent: ' + info.response); + } catch (error) { + console.log('Error occurred while sending email', error); + } +}; + +export default sendMail; \ No newline at end of file diff --git a/src/utils/sendOrderMailUpdated.ts b/src/utils/sendOrderMailUpdated.ts new file mode 100644 index 0000000..ed2cf83 --- /dev/null +++ b/src/utils/sendOrderMailUpdated.ts @@ -0,0 +1,214 @@ +import nodemailer from 'nodemailer'; +import { formatMoney, formatDate } from './index'; + +interface Product { + name: string; + newPrice: number; + quantity: number; +} + +interface Message { + subject: string; + fullName: string; + email: string; + products: Product[]; + totalAmount: number; + quantity: number; + orderDate: Date; + address: string; +} + +const sendMail = async (message: Message) => { + const transporter = nodemailer.createTransport({ + host: process.env.HOST, + port: 587, + secure: false, // true for 465, false for other ports + auth: { + user: process.env.AUTH_EMAIL as string, + pass: process.env.AUTH_PASSWORD as string, + }, + }); + + const { subject, fullName, email, products, totalAmount, + quantity, + orderDate, + address } = message; + + const mailOptions = { + to: email, + subject: subject, + html: ` + + + + + + + Your order details have been updated + + + + +
+ shoping image +

Order Updated

+
+

User information

+ + + + + + + + + + + + + + + +
Full Name:Email:Address:
${fullName}${email}${address}
+ + + + + + + + + + + + + + + +
Order Date:Quantity:Total Amount:
${formatDate(orderDate)}${quantity}${formatMoney(totalAmount)}
+
+
+

Products

+ + + + + + + + ${products.map((product: Product) => ` + + + + + + + `).join('')} + + + + +
Product NameProduct PriceQuantityTotal
${product.name}${formatMoney(product.newPrice)}${product.quantity}${product.quantity * product.newPrice}
Total${totalAmount}
+
+ +
+ + + `, + }; + + try { + const info = await transporter.sendMail(mailOptions); + console.log('Email sent: ' + info.response); + } catch (error) { + console.log('Error occurred while sending email', error); + } +}; + +export default sendMail; \ No newline at end of file From 8ced535717519eda221ce346f8795f884153e678 Mon Sep 17 00:00:00 2001 From: "Gisa M. Caleb Pacifique" Date: Sun, 26 May 2024 20:38:18 +0200 Subject: [PATCH 46/51] Feat-Buyer-coupon-discount-management --- src/__test__/coupon.test.ts | 251 ++++++++++++++++-- src/controllers/couponController.ts | 5 + src/docs/couponDocs.yml | 35 +++ src/docs/wishListDocs.yml | 97 +++++++ src/entities/coupon.ts | 13 +- src/routes/couponRoutes.ts | 5 +- .../couponServices/buyerApplyCoupon.ts | 85 ++++++ 7 files changed, 461 insertions(+), 30 deletions(-) create mode 100644 src/docs/wishListDocs.yml create mode 100644 src/services/couponServices/buyerApplyCoupon.ts diff --git a/src/__test__/coupon.test.ts b/src/__test__/coupon.test.ts index 23891ba..b3f68b4 100644 --- a/src/__test__/coupon.test.ts +++ b/src/__test__/coupon.test.ts @@ -5,14 +5,26 @@ import { getConnection } from 'typeorm'; import { dbConnection } from '../startups/dbConnection'; import { User, UserInterface } from '../entities/User'; import { Coupon } from '../entities/coupon'; +import { CartItem } from '../entities/CartItem'; +import { Cart } from '../entities/Cart'; import { Product } from '../entities/Product'; import { v4 as uuid } from 'uuid'; import { cleanDatabase } from './test-assets/DatabaseCleanup'; const vendor1Id = uuid(); +const cart1Id = uuid(); +const cartItemId = uuid(); +const buyer1Id = uuid(); +const buyer2Id = uuid(); const product1Id = uuid(); +const product2Id = uuid(); const couponCode = 'DISCOUNT20'; const couponCode1 = 'DISCOUNT10'; +const couponCode2 = 'DISCOUNT99'; +const couponCode3 = 'DISCOUNT22' +const expiredCouponCode = 'EXPIRED'; +const finishedCouponCode = 'FINISHED'; +const moneyCouponCode = 'MONEY'; const invalidCouponCode = 'INVALIDCODE'; const jwtSecretKey = process.env.JWT_SECRET || ''; @@ -40,34 +52,125 @@ const sampleVendor1: UserInterface = { role: 'VENDOR', }; -const sampleProduct1 = { - id: product1Id, - name: 'Test Product', - description: 'Amazing product', - images: ['photo1.jpg', 'photo2.jpg', 'photo3.jpg'], - newPrice: 200, - quantity: 10, - vendor: sampleVendor1, +const sampleBuyer1: UserInterface = { + id: buyer1Id, + firstName: 'buyer1', + lastName: 'user', + email: 'buyer1@example.com', + password: 'password', + userType: 'Buyer', + gender: 'Male', + phoneNumber: '126380996347', + photoUrl: 'https://example.com/photo.jpg', + role: 'BUYER', +}; +const buyerNoCart: UserInterface = { + id: buyer2Id, + firstName: 'buyer1', + lastName: 'user', + email: 'buyr122@example.com', + password: 'password', + userType: 'Buyer', + gender: 'Male', + phoneNumber: '159380996347', + photoUrl: 'https://example.com/photo.jpg', + role: 'BUYER', }; -const sampleCoupon = { - code: couponCode, - discountRate: 20, - expirationDate: new Date('2025-01-01'), - maxUsageLimit: 100, - discountType: 'PERCENTAGE', - product: sampleProduct1, - vendor: sampleVendor1, +const sampleProduct1 = new Product(); +sampleProduct1.id = product1Id; +sampleProduct1.name = 'Test Product'; +sampleProduct1.description = 'Amazing product'; +sampleProduct1.images = ['photo1.jpg', 'photo2.jpg', 'photo3.jpg']; +sampleProduct1.newPrice = 200; +sampleProduct1.quantity = 10; +sampleProduct1.vendor = sampleVendor1 as User; + +const sampleProduct2 = new Product(); +sampleProduct2.id = product2Id; +sampleProduct2.name = 'Test 2 Product'; +sampleProduct2.description = 'Amazing product 2'; +sampleProduct2.images = ['photo1.jpg', 'photo2.jpg', 'photo3.jpg']; +sampleProduct2.newPrice = 200; +sampleProduct2.quantity = 10; +sampleProduct2.vendor = sampleVendor1 as User; + +const sampleCoupon = new Coupon(); +sampleCoupon.code = couponCode; +sampleCoupon.discountRate = 20; +sampleCoupon.expirationDate = new Date('2025-01-01'); +sampleCoupon.maxUsageLimit = 100; +sampleCoupon.discountType = 'percentage'; +sampleCoupon.product = sampleProduct1; +sampleCoupon.vendor = sampleVendor1 as User; + +const sampleCoupon1 = new Coupon(); +sampleCoupon1.code = couponCode1; +sampleCoupon1.discountRate = 20; +sampleCoupon1.expirationDate = new Date('2025-01-01'); +sampleCoupon1.maxUsageLimit = 100; +sampleCoupon1.discountType = 'percentage'; +sampleCoupon1.product = sampleProduct1; +sampleCoupon1.vendor = sampleVendor1 as User; + +const sampleCoupon2 = new Coupon(); +sampleCoupon2.code = couponCode2; +sampleCoupon2.discountRate = 20; +sampleCoupon2.expirationDate = new Date('2026-01-01'); +sampleCoupon2.maxUsageLimit = 100; +sampleCoupon2.discountType = 'percentage'; +sampleCoupon2.product = sampleProduct1; +sampleCoupon2.vendor = sampleVendor1 as User; + +const sampleCoupon3 = new Coupon(); +sampleCoupon3.code = couponCode3; +sampleCoupon3.discountRate = 20; +sampleCoupon3.expirationDate = new Date('2026-01-01'); +sampleCoupon3.maxUsageLimit = 100; +sampleCoupon3.discountType = 'percentage'; +sampleCoupon3.product = sampleProduct2; +sampleCoupon3.vendor = sampleVendor1 as User; + +const expiredCoupon = new Coupon(); +expiredCoupon.code = expiredCouponCode; +expiredCoupon.discountRate = 20; +expiredCoupon.expirationDate = new Date('2023-01-01'); +expiredCoupon.maxUsageLimit = 100; +expiredCoupon.discountType = 'percentage'; +expiredCoupon.product = sampleProduct1; +expiredCoupon.vendor = sampleVendor1 as User; + +const finishedCoupon = new Coupon(); +finishedCoupon.code = finishedCouponCode; +finishedCoupon.discountRate = 20; +finishedCoupon.expirationDate = new Date('2028-01-01'); +finishedCoupon.maxUsageLimit = 0; +finishedCoupon.discountType = 'percentage'; +finishedCoupon.product = sampleProduct1; +finishedCoupon.vendor = sampleVendor1 as User; + +const moneyCoupon = new Coupon(); +moneyCoupon.code = moneyCouponCode; +moneyCoupon.discountRate = 50; +moneyCoupon.expirationDate = new Date('2028-01-01'); +moneyCoupon.maxUsageLimit = 10; +moneyCoupon.discountType = 'money'; +moneyCoupon.product = sampleProduct1; +moneyCoupon.vendor = sampleVendor1 as User; + +const sampleCart1 = { + id: cart1Id, + user: sampleBuyer1, + totalAmount: 200, }; -const sampleCoupon1 = { - code: couponCode1, - discountRate: 20, - expirationDate: new Date('2025-01-01'), - maxUsageLimit: 100, - discountType: 'PERCENTAGE', +const sampleCartItem1 = { + id: cartItemId, product: sampleProduct1, - vendor: sampleVendor1, + cart: sampleCart1, + quantity: 2, + newPrice: 200, + total: 400, }; beforeAll(async () => { @@ -75,15 +178,28 @@ beforeAll(async () => { const userRepository = connection?.getRepository(User); await userRepository?.save(sampleVendor1); + await userRepository?.save(sampleBuyer1); + await userRepository?.save(buyerNoCart); const productRepository = connection?.getRepository(Product); await productRepository?.save(sampleProduct1); + await productRepository?.save(sampleProduct2); const couponRepository = connection?.getRepository(Coupon); await couponRepository?.save(sampleCoupon); - - const couponRepository1 = connection?.getRepository(Coupon); - await couponRepository1?.save(sampleCoupon1); + await couponRepository?.save(sampleCoupon1); + await couponRepository?.save(expiredCoupon); + await couponRepository?.save(sampleCoupon2); + await couponRepository?.save(sampleCoupon3); + await couponRepository?.save(finishedCoupon); + await couponRepository?.save(moneyCoupon); + + const cartRepository = connection?.getRepository(Cart); + await cartRepository?.save({ ...sampleCart1 }); + + const cartItemRepository = connection?.getRepository(CartItem); + await cartItemRepository?.save({ ...sampleCartItem1 }); + }); afterAll(async () => { @@ -222,3 +338,86 @@ describe('Coupon Management System', () => { }, 10000); }); }); + +describe('Buyer Coupon Application', () => { + describe('Checking Coupon Conditions', () =>{ + it('should return 400 when no coupon submitted', async () => { + const response = await request(app) + .post(`/coupons/apply`) + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); + + expect(response.status).toBe(400); + expect(response.body.message).toBe('Coupon Code is required'); + }) + it('should return 404 if coupon code is not found in the database', async () => { + const response = await request(app) + .post(`/coupons/apply`) + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`) + .send({ + couponCode: "InvalidCode", + }); + + expect(response.status).toBe(404); + expect(response.body.message).toBe('Invalid Coupon Code'); + }) + it('should not allow use of expired tokens', async () => { + const response = await request(app) + .post(`/coupons/apply`) + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`) + .send({ + couponCode: expiredCoupon.code, + }); + + expect(response.status).toBe(400); + expect(response.body.message).toBe('Coupon is expired'); + }) + it('should not allow use of coupon that reach maximum users', async () => { + const response = await request(app) + .post(`/coupons/apply`) + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`) + .send({ + couponCode: finishedCoupon.code, + }); + + expect(response.status).toBe(400); + expect(response.body.message).toBe('Coupon Discount Ended'); + }) + it('Should not work when the product is not in cart', async () => { + const response = await request(app) + .post(`/coupons/apply`) + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`) + .send({ + couponCode: sampleCoupon3.code, + }); + + expect(response.status).toBe(404); + expect(response.body.message).toBe("No product in Cart with that coupon code"); + }) + }) + + describe("Giving discount according the the product coupon", () => { + it('Should give discont when discount-type is percentage', async () => { + const response = await request(app) + .post(`/coupons/apply`) + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`) + .send({ + couponCode: sampleCoupon2.code, + }); + + expect(response.status).toBe(200); + expect(response.body.message).toBe(`Coupon Code successfully activated discount on product: ${sampleProduct1.name}`); + }) + it('Should give discont when discount-type is money', async () => { + const response = await request(app) + .post(`/coupons/apply`) + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`) + .send({ + couponCode: moneyCoupon.code, + }); + + expect(response.status).toBe(200); + expect(response.body.message).toBe(`Coupon Code successfully activated discount on product: ${sampleProduct1.name}`); + }) + }) + +}) diff --git a/src/controllers/couponController.ts b/src/controllers/couponController.ts index 39eeb6e..dd7e19f 100644 --- a/src/controllers/couponController.ts +++ b/src/controllers/couponController.ts @@ -4,6 +4,7 @@ import { updateCouponService } from '../services/couponServices/updateService'; import { deleteCouponService } from '../services/couponServices/deleteCoupon'; import { accessAllCouponService } from '../services/couponServices/accessAllCoupon'; import { readCouponService } from '../services/couponServices/readCoupon'; +import { buyerApplyCouponService } from '../services/couponServices/buyerApplyCoupon' export const createCoupon = async (req: Request, res: Response) => { await createCouponService(req, res); @@ -24,3 +25,7 @@ export const accessAllCoupon = async (req: Request, res: Response) => { export const readCoupon = async (req: Request, res: Response) => { await readCouponService(req, res); }; + +export const buyerApplyCoupon = async (req: Request, res: Response) => { + await buyerApplyCouponService(req, res); +}; \ No newline at end of file diff --git a/src/docs/couponDocs.yml b/src/docs/couponDocs.yml index f250230..fb0a49a 100644 --- a/src/docs/couponDocs.yml +++ b/src/docs/couponDocs.yml @@ -180,3 +180,38 @@ description: Coupon not found '500': description: Internal server error + +/coupons/apply: + post: + tags: + - Buyer Coupon Discount Management + summary: Give discount according to coupon code + description: Buyer gets discount on a product when all the checks pass + security: + - bearerAuth: [] + consumes: + - application/json + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + couponCode: + type: string + required: + - couponCode + responses: + '200': + description: Successfully Got Discount + '400': + description: Bad Request (Syntax error, No coupon code provide, Coupon is expired, Coupon Discount Ended,etc) + '401': + description: Unauthorized + '403': + description: Forbidden (Unauthorized action) + '404': + description: Coupon not found, No cart or product with that coupon is not in cart + '500': + description: Internal server error diff --git a/src/docs/wishListDocs.yml b/src/docs/wishListDocs.yml new file mode 100644 index 0000000..df3c72c --- /dev/null +++ b/src/docs/wishListDocs.yml @@ -0,0 +1,97 @@ +/wish-list: + get: + tags: + - Wish list + summary: Get all products in wishlist + description: Return all products in wish list for authenticated buyer + security: + - bearerAuth: [] + responses: + '200': + description: Return all products in wish list for a buyer + '400': + description: Bad Request (syntax error, incorrect input format, etc..) + '401': + description: Unauthorized + '403': + description: Forbidden (Unauthorized action) + '500': + description: Internal server error + +/wish-list/add/{id}: + post: + tags: + - Wish list + summary: Add product to wish list + description: Adds selected product (product id) to the wish list + security: + - bearerAuth: [] + parameters: + - in: path + name: id + schema: + type: string + required: true + description: Product id + responses: + '201': + description: Product Added to wish list + '400': + description: Bad Request (syntax error, incorrect input format, etc..) + '401': + description: Unauthorized + '403': + description: Forbidden (Unauthorized action) + '404': + description: Product not found in wish list + '500': + description: Internal server error + +/wish-list/delete/{id}: + delete: + tags: + - Wish list + summary: Remove product from wish list + description: Remove product from wish list for an authenticated buyer + security: + - bearerAuth: [] + parameters: + - in: path + name: id + schema: + type: string + required: true + description: Product id + responses: + '200': + description: Product removed from wish list + '400': + description: Bad Request (syntax error, incorrect input format, etc..) + '401': + description: Unauthorized + '403': + description: Forbidden (Unauthorized action) + '404': + description: Product not found in wish list + '500': + description: Internal server error + +/wish-list/clearAll: + delete: + tags: + - Wish list + summary: Clear entire wish list + description: Clears entire wish list for authenticated buyer + security: + - bearerAuth: [] + responses: + '200': + description: All products removed successfully + '400': + description: Bad Request (syntax error, incorrect input format, etc..) + '401': + description: Unauthorized + '403': + description: Forbidden (Unauthorized action) + '500': + description: Internal server error \ No newline at end of file diff --git a/src/entities/coupon.ts b/src/entities/coupon.ts index 19e5a68..39631c3 100644 --- a/src/entities/coupon.ts +++ b/src/entities/coupon.ts @@ -8,7 +8,7 @@ import { ManyToOne, JoinColumn, } from 'typeorm'; -import { IsDate, IsNotEmpty } from 'class-validator'; +import { IsDate, IsNotEmpty, IsArray, IsIn } from 'class-validator'; import { User } from './User'; import { Product } from './Product'; @@ -36,7 +36,8 @@ export class Coupon { @Column() @IsNotEmpty() - discountType!: string; + @IsIn(['percentage', 'money']) + discountType!: 'percentage' | 'money'; @Column('float') @IsNotEmpty() @@ -47,10 +48,18 @@ export class Coupon { @IsDate() expirationDate?: Date; + @Column('int', { default: 0 }) + @IsNotEmpty() + usageTimes!: number; + @Column('int') @IsNotEmpty() maxUsageLimit!: number; + @Column('simple-array', { nullable: true, default: '' }) + @IsArray() + usedBy!: string[]; + @CreateDateColumn() createdAt!: Date; diff --git a/src/routes/couponRoutes.ts b/src/routes/couponRoutes.ts index a7a979c..c315ab8 100644 --- a/src/routes/couponRoutes.ts +++ b/src/routes/couponRoutes.ts @@ -1,5 +1,5 @@ import { RequestHandler, Router } from 'express'; -import { createCoupon, updateCoupon, accessAllCoupon, readCoupon, deleteCoupon } from '../controllers/couponController'; +import { createCoupon, updateCoupon, accessAllCoupon, readCoupon, deleteCoupon, buyerApplyCoupon } from '../controllers/couponController'; import { hasRole } from '../middlewares/roleCheck'; import { authMiddleware } from '../middlewares/verifyToken'; @@ -10,5 +10,6 @@ router.put('/vendor/:id/update-coupon/:code', authMiddleware as RequestHandler, router.get('/vendor/:id/checkout/:code', authMiddleware as RequestHandler, hasRole('VENDOR'), readCoupon); router.get('/vendor/:id/access-coupons', authMiddleware as RequestHandler, hasRole('VENDOR'), accessAllCoupon); router.delete('/vendor/:id/checkout/delete', authMiddleware as RequestHandler, hasRole('VENDOR'), deleteCoupon); +router.post('/apply', authMiddleware as RequestHandler, hasRole('BUYER'),buyerApplyCoupon); -export default router; +export default router; \ No newline at end of file diff --git a/src/services/couponServices/buyerApplyCoupon.ts b/src/services/couponServices/buyerApplyCoupon.ts new file mode 100644 index 0000000..12da4e1 --- /dev/null +++ b/src/services/couponServices/buyerApplyCoupon.ts @@ -0,0 +1,85 @@ +import { Request, Response } from 'express'; +import { getRepository } from 'typeorm'; +import { Coupon } from '../../entities/coupon'; +import { Cart } from '../../entities/Cart'; +import { CartItem } from '../../entities/CartItem'; + +export const buyerApplyCouponService = async (req: Request, res: Response) => { + try { + const {couponCode} = req.body + + if (!couponCode) return res.status(400).json({ message: 'Coupon Code is required' }); + + const couponRepository = getRepository(Coupon); + const coupon = await couponRepository.findOne({ + where: { code: couponCode }, + relations: ['product'], + }); + + if(!coupon) return res.status(404).json({message: 'Invalid Coupon Code'}); + + if(coupon){ + if(coupon.expirationDate && coupon.expirationDate < new Date()){ + return res.status(400).json({message: 'Coupon is expired'}); + } + + if(coupon.usageTimes == coupon.maxUsageLimit){ + return res.status(400).json({message: 'Coupon Discount Ended'}); + } + } + const couponProductId = coupon.product.id; + + const cartRepository = getRepository(Cart) + let cart = await cartRepository.findOne({where: { user: { id: req.user?.id },isCheckedOut: false }, + relations: ['items', 'items.product'], + }); + + if(!cart) return res.status(400).json({message: "You don't have a product in cart"}); + + const cartItemRepository = getRepository(CartItem); + const couponCartItem = await cartItemRepository.findOne({ + where: { + cart: { id: cart.id }, + product: { id: couponProductId }, + }, + relations: ['product'], + }); + + if(!couponCartItem) return res.status(404).json({message: 'No product in Cart with that coupon code'}); + + let amountReducted; + if(coupon.discountType === 'percentage'){ + const reduction = (couponCartItem.product.newPrice * coupon.discountRate)/ 100; + amountReducted = reduction; + couponCartItem.newPrice = couponCartItem.product.newPrice - reduction; + + await cartItemRepository.save(couponCartItem) + } + else { + amountReducted = coupon.discountRate; + couponCartItem.newPrice = couponCartItem.product.newPrice - amountReducted; + await cartItemRepository.save(couponCartItem) + } + + cart = await cartRepository.findOne({where: { id: cart.id}, + relations: ['items', 'items.product'], + }); + if(cart){ + cart.updateTotal(); + await cartRepository.save(cart); + } + + coupon.usageTimes +=1; + + if(req.user?.id){ + coupon.usedBy.push(req.user?.id); + } + + await couponRepository.save(coupon); + + return (res.status(200).json({message: `Coupon Code successfully activated discount on product: ${couponCartItem.product.name}`, amountDiscounted: amountReducted })); + + } catch (error) { + return res.status(500).json({ error: 'Internal server error' }); + } + }; \ No newline at end of file From 31e7653081d5ee6b6b5ed96d2df4c5c0daa6ebad Mon Sep 17 00:00:00 2001 From: Icyeza Date: Wed, 22 May 2024 17:10:37 +0200 Subject: [PATCH 47/51] implementing order tracking and order managment issue implementing order tracking and order managment issue implementing order tracking and order managment issue implementing order tracking and order managment issue adding documentation and tests writing tests --- .github/workflows/ci.yml | 1 - package.json | 2 + src/__test__/cart.test.ts | 17 +- src/__test__/coupon.test.ts | 80 ++-- src/__test__/errorHandler.test.ts | 78 ++-- src/__test__/getProduct.test.ts | 2 +- src/__test__/isAllowed.test.ts | 29 +- src/__test__/logout.test.ts | 3 +- src/__test__/oauth.test.ts | 24 +- src/__test__/orderManagement.test.ts | 390 ++++++++++++++++++ src/__test__/productStatus.test.ts | 14 +- src/__test__/roleCheck.test.ts | 2 +- src/__test__/route.test.ts | 3 +- src/__test__/test-assets/DatabaseCleanup.ts | 28 +- src/__test__/userServices.test.ts | 6 +- src/__test__/userStatus.test.ts | 2 +- src/__test__/vendorProduct.test.ts | 2 +- src/__test__/wishList.test.ts | 2 +- src/controllers/adminOrdercontroller.ts | 18 + src/controllers/couponController.ts | 4 +- src/controllers/index.ts | 4 +- src/controllers/orderController.ts | 2 +- src/controllers/productController.ts | 23 +- src/controllers/vendorOrderController.ts | 14 + src/controllers/wishListController.ts | 29 +- src/docs/adminOrderManagement.yml | 80 ++++ src/docs/couponDocs.yml | 2 +- src/docs/vendorOrderManagement.yml | 93 +++++ src/docs/vendorProduct.yml | 14 +- src/docs/wishListDocs.yml | 2 +- src/entities/Cart.ts | 2 +- src/entities/CartItem.ts | 2 +- src/entities/Order.ts | 24 +- src/entities/Product.ts | 6 +- src/entities/User.ts | 4 +- src/entities/VendorOrderItem.ts | 30 ++ src/entities/transaction.ts | 2 +- src/entities/vendorOrders.ts | 49 +++ src/entities/wishList.ts | 17 +- src/index.ts | 29 +- src/routes/ProductRoutes.ts | 20 +- src/routes/UserRoutes.ts | 39 +- src/routes/couponRoutes.ts | 13 +- src/routes/index.ts | 4 +- src/routes/wishListRoute.ts | 27 +- src/services/adminOrderServices/readOrder.ts | 158 +++++++ .../adminOrderServices/updateOrder.ts | 107 +++++ .../couponServices/buyerApplyCoupon.ts | 134 +++--- src/services/index.ts | 8 + src/services/orderServices/createOrder.ts | 63 ++- src/services/orderServices/getOrderService.ts | 104 +++-- .../orderServices/updateOrderService.ts | 231 ++++++----- src/services/productServices/deleteProduct.ts | 40 +- .../getRecommendedProductsService.ts | 105 ++--- .../productServices/listAllProductsService.ts | 64 +-- src/services/productServices/readProduct.ts | 116 +++--- .../productServices/removeProductImage.ts | 2 +- src/services/productServices/searchProduct.ts | 7 +- .../productServices/viewSingleProduct.ts | 45 +- .../vendorOrderServices/readVendorOrder.ts | 119 ++++++ .../vendorOrderServices/updateVendorOrder.ts | 87 ++++ src/services/wishListServices/addProduct.ts | 89 ++-- src/services/wishListServices/clearAll.ts | 27 +- src/services/wishListServices/getProducts.ts | 59 +-- .../wishListServices/removeProducts.ts | 30 +- src/startups/getSwaggerServer.ts | 2 +- src/utils/auth.ts | 104 +++-- src/utils/index.ts | 26 +- src/utils/sendOrderMail.ts | 15 +- src/utils/sendOrderMailUpdated.ts | 15 +- src/utils/socket.ts | 21 + tsconfig.json | 214 +++++----- 72 files changed, 2260 insertions(+), 970 deletions(-) create mode 100644 src/__test__/orderManagement.test.ts create mode 100644 src/controllers/adminOrdercontroller.ts create mode 100644 src/controllers/vendorOrderController.ts create mode 100644 src/docs/adminOrderManagement.yml create mode 100644 src/docs/vendorOrderManagement.yml create mode 100644 src/entities/VendorOrderItem.ts create mode 100644 src/entities/vendorOrders.ts create mode 100644 src/services/adminOrderServices/readOrder.ts create mode 100644 src/services/adminOrderServices/updateOrder.ts create mode 100644 src/services/vendorOrderServices/readVendorOrder.ts create mode 100644 src/services/vendorOrderServices/updateVendorOrder.ts create mode 100644 src/utils/socket.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e0c5f52..224592b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,6 @@ env: CLOUDINARY_API_SECRET: ${{secrets.CLOUDINARY_API_SECRET}} GOOGLE_CLIENT_ID: ${{secrets.GOOGLE_CLIENT_ID}} GOOGLE_CLIENT_SECRET: ${{secrets.GOOGLE_CLIENT_SECRET}} - jobs: build-lint-test-coverage: diff --git a/package.json b/package.json index 06e7e40..ef7acc5 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "passport-google-oauth20": "^2.0.0", "pg": "^8.11.5", "reflect-metadata": "^0.2.2", + "socket.io": "^4.7.5", "source-map-support": "^0.5.21", "superagent": "^9.0.1", "swagger-jsdoc": "^6.2.8", @@ -78,6 +79,7 @@ "@types/nodemailer": "^6.4.15", "@types/passport-google-oauth20": "^2.0.16", "@types/reflect-metadata": "^0.1.0", + "@types/socket.io": "^3.0.2", "@types/supertest": "^6.0.2", "@types/uuid": "^9.0.8", "@types/winston": "^2.4.4", diff --git a/src/__test__/cart.test.ts b/src/__test__/cart.test.ts index 9f86f73..4d6d1f0 100644 --- a/src/__test__/cart.test.ts +++ b/src/__test__/cart.test.ts @@ -1,4 +1,3 @@ - import request from 'supertest'; import jwt from 'jsonwebtoken'; import { app, server } from '../index'; @@ -171,8 +170,9 @@ beforeAll(async () => { }); afterAll(async () => { - await cleanDatabase() + await cleanDatabase(); + server.close(); }); describe('Cart management for guest/buyer', () => { @@ -524,12 +524,12 @@ describe('Order management tests', () => { city: 'Test City', street: 'Test Street', }, - }).set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); + }) + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); expect(response.status).toBe(400); }); it('should create a new order', async () => { - const response = await request(app) .post('/product/orders') .send({ @@ -547,7 +547,6 @@ describe('Order management tests', () => { }); it('should insert a new order', async () => { - const response = await request(app) .post('/product/orders') .send({ @@ -570,9 +569,8 @@ describe('Order management tests', () => { const response = await request(app) .get('/product/client/orders') .set('Authorization', `Bearer ${getAccessToken(buyer2Id, sampleBuyer2.email)}`); - expect(response.status).toBe(404); + expect(response.status).toBe(404); expect(response.body.message).toBeUndefined; - }); it('should return 404 if the buyer has no orders', async () => { @@ -586,13 +584,11 @@ describe('Order management tests', () => { describe('Get transaction history', () => { it('should return transaction history for the buyer', async () => { - const response = await request(app) .get('/product/orders/history') .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); expect(response.status).toBe(404); expect(response.body.message).toBe('No transaction history found'); - }); it('should return 400 when user ID is not provided', async () => { @@ -605,12 +601,11 @@ describe('Order management tests', () => { describe('Update order', () => { it('should update order status successfully', async () => { - const response = await request(app) .put(`/product/client/orders/${orderId}`) .send({ orderStatus: 'delivered' }) .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); - expect(response.status).toBe(500); + expect(response.status).toBe(500); }); }); }); diff --git a/src/__test__/coupon.test.ts b/src/__test__/coupon.test.ts index b3f68b4..269e95e 100644 --- a/src/__test__/coupon.test.ts +++ b/src/__test__/coupon.test.ts @@ -21,7 +21,7 @@ const product2Id = uuid(); const couponCode = 'DISCOUNT20'; const couponCode1 = 'DISCOUNT10'; const couponCode2 = 'DISCOUNT99'; -const couponCode3 = 'DISCOUNT22' +const couponCode3 = 'DISCOUNT22'; const expiredCouponCode = 'EXPIRED'; const finishedCouponCode = 'FINISHED'; const moneyCouponCode = 'MONEY'; @@ -199,11 +199,10 @@ beforeAll(async () => { const cartItemRepository = connection?.getRepository(CartItem); await cartItemRepository?.save({ ...sampleCartItem1 }); - }); afterAll(async () => { - await cleanDatabase() + await cleanDatabase(); server.close(); }); @@ -340,84 +339,87 @@ describe('Coupon Management System', () => { }); describe('Buyer Coupon Application', () => { - describe('Checking Coupon Conditions', () =>{ + describe('Checking Coupon Conditions', () => { it('should return 400 when no coupon submitted', async () => { const response = await request(app) .post(`/coupons/apply`) .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); - expect(response.status).toBe(400); - expect(response.body.message).toBe('Coupon Code is required'); - }) + expect(response.status).toBe(400); + expect(response.body.message).toBe('Coupon Code is required'); + }); it('should return 404 if coupon code is not found in the database', async () => { const response = await request(app) .post(`/coupons/apply`) .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`) .send({ - couponCode: "InvalidCode", + couponCode: 'InvalidCode', }); - expect(response.status).toBe(404); - expect(response.body.message).toBe('Invalid Coupon Code'); - }) + expect(response.status).toBe(404); + expect(response.body.message).toBe('Invalid Coupon Code'); + }); it('should not allow use of expired tokens', async () => { const response = await request(app) - .post(`/coupons/apply`) + .post(`/coupons/apply`) .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`) .send({ couponCode: expiredCoupon.code, }); - expect(response.status).toBe(400); - expect(response.body.message).toBe('Coupon is expired'); - }) + expect(response.status).toBe(400); + expect(response.body.message).toBe('Coupon is expired'); + }); it('should not allow use of coupon that reach maximum users', async () => { const response = await request(app) - .post(`/coupons/apply`) + .post(`/coupons/apply`) .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`) .send({ couponCode: finishedCoupon.code, }); - expect(response.status).toBe(400); - expect(response.body.message).toBe('Coupon Discount Ended'); - }) + expect(response.status).toBe(400); + expect(response.body.message).toBe('Coupon Discount Ended'); + }); it('Should not work when the product is not in cart', async () => { const response = await request(app) - .post(`/coupons/apply`) - .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`) - .send({ + .post(`/coupons/apply`) + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`) + .send({ couponCode: sampleCoupon3.code, }); expect(response.status).toBe(404); - expect(response.body.message).toBe("No product in Cart with that coupon code"); - }) - }) + expect(response.body.message).toBe('No product in Cart with that coupon code'); + }); + }); - describe("Giving discount according the the product coupon", () => { + describe('Giving discount according the the product coupon', () => { it('Should give discont when discount-type is percentage', async () => { const response = await request(app) - .post(`/coupons/apply`) - .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`) - .send({ + .post(`/coupons/apply`) + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`) + .send({ couponCode: sampleCoupon2.code, }); expect(response.status).toBe(200); - expect(response.body.message).toBe(`Coupon Code successfully activated discount on product: ${sampleProduct1.name}`); - }) + expect(response.body.message).toBe( + `Coupon Code successfully activated discount on product: ${sampleProduct1.name}` + ); + }); it('Should give discont when discount-type is money', async () => { const response = await request(app) - .post(`/coupons/apply`) - .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`) - .send({ + .post(`/coupons/apply`) + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`) + .send({ couponCode: moneyCoupon.code, }); expect(response.status).toBe(200); - expect(response.body.message).toBe(`Coupon Code successfully activated discount on product: ${sampleProduct1.name}`); - }) - }) - -}) + expect(response.body.message).toBe( + `Coupon Code successfully activated discount on product: ${sampleProduct1.name}` + ); + }); + }); +}); diff --git a/src/__test__/errorHandler.test.ts b/src/__test__/errorHandler.test.ts index fb1437c..cf079f0 100644 --- a/src/__test__/errorHandler.test.ts +++ b/src/__test__/errorHandler.test.ts @@ -1,47 +1,47 @@ import { Request, Response } from 'express'; -import { CustomError, errorHandler } from '../middlewares/errorHandler' +import { CustomError, errorHandler } from '../middlewares/errorHandler'; describe('CustomError', () => { - it('should create a CustomError object with statusCode and status properties', () => { - const message = 'Test error message'; - const statusCode = 404; - const customError = new CustomError(message, statusCode); - expect(customError.message).toBe(message); - expect(customError.statusCode).toBe(statusCode); - expect(customError.status).toBe('fail'); - }); + it('should create a CustomError object with statusCode and status properties', () => { + const message = 'Test error message'; + const statusCode = 404; + const customError = new CustomError(message, statusCode); + expect(customError.message).toBe(message); + expect(customError.statusCode).toBe(statusCode); + expect(customError.status).toBe('fail'); }); +}); - describe('errorHandler', () => { - it('should send correct response with status code and message', () => { - const err = new CustomError('Test error message', 404); - const req = {} as Request; - const res = { - status: jest.fn().mockReturnThis(), - json: jest.fn(), - } as unknown as Response; - const next = jest.fn(); - errorHandler(err, req, res, next); - expect(res.status).toHaveBeenCalledWith(404); - expect(res.json).toHaveBeenCalledWith({ - status: 404, - message: 'Test error message', - }); +describe('errorHandler', () => { + it('should send correct response with status code and message', () => { + const err = new CustomError('Test error message', 404); + const req = {} as Request; + const res = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + } as unknown as Response; + const next = jest.fn(); + errorHandler(err, req, res, next); + expect(res.status).toHaveBeenCalledWith(404); + expect(res.json).toHaveBeenCalledWith({ + status: 404, + message: 'Test error message', }); - it('should handle errors with status code 500', () => { - const err = new CustomError('something went wrong', 500); - const req = {} as Request; - const res = { - status: jest.fn().mockReturnThis(), - json: jest.fn(), - } as unknown as Response; - const next = jest.fn(); - errorHandler(err, req, res, next); + }); + it('should handle errors with status code 500', () => { + const err = new CustomError('something went wrong', 500); + const req = {} as Request; + const res = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + } as unknown as Response; + const next = jest.fn(); + errorHandler(err, req, res, next); - expect(res.status).toHaveBeenCalledWith(500); - expect(res.json).toHaveBeenCalledWith({ - status: 500, - message: 'something went wrong', - }); + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ + status: 500, + message: 'something went wrong', }); - }); \ No newline at end of file + }); +}); diff --git a/src/__test__/getProduct.test.ts b/src/__test__/getProduct.test.ts index 88dd415..ecd2281 100644 --- a/src/__test__/getProduct.test.ts +++ b/src/__test__/getProduct.test.ts @@ -68,7 +68,7 @@ beforeAll(async () => { }); afterAll(async () => { - await cleanDatabase() + await cleanDatabase(); server.close(); }); diff --git a/src/__test__/isAllowed.test.ts b/src/__test__/isAllowed.test.ts index b17b657..471a950 100644 --- a/src/__test__/isAllowed.test.ts +++ b/src/__test__/isAllowed.test.ts @@ -48,24 +48,23 @@ beforeAll(async () => { }); afterAll(async () => { - await cleanDatabase() - + await cleanDatabase(); }); describe('Middleware - checkUserStatus', () => { - beforeEach(() => { - reqMock = {}; - resMock = { - status: jest.fn().mockReturnThis(), - json: jest.fn() - }; - nextMock = jest.fn(); - }); - - it('should return 401 if user is not authenticated', async () => { - await checkUserStatus(reqMock as Request, resMock as Response, nextMock); - expect(responseError).toHaveBeenCalledWith(resMock, 401, 'Authentication required'); - }); + beforeEach(() => { + reqMock = {}; + resMock = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + }; + nextMock = jest.fn(); + }); + + it('should return 401 if user is not authenticated', async () => { + await checkUserStatus(reqMock as Request, resMock as Response, nextMock); + expect(responseError).toHaveBeenCalledWith(resMock, 401, 'Authentication required'); + }); it('should return 401 if user is not found', async () => { reqMock = { user: { id: uuid() } }; diff --git a/src/__test__/logout.test.ts b/src/__test__/logout.test.ts index cd950fd..ac9eefa 100644 --- a/src/__test__/logout.test.ts +++ b/src/__test__/logout.test.ts @@ -10,8 +10,7 @@ beforeAll(async () => { }); afterAll(async () => { - await cleanDatabase() - + await cleanDatabase(); server.close(); }); diff --git a/src/__test__/oauth.test.ts b/src/__test__/oauth.test.ts index 2493059..877d63b 100644 --- a/src/__test__/oauth.test.ts +++ b/src/__test__/oauth.test.ts @@ -5,24 +5,20 @@ import { User } from '../entities/User'; import { cleanDatabase } from './test-assets/DatabaseCleanup'; beforeAll(async () => { - await createConnection(); }); afterAll(async () => { - await cleanDatabase() + await cleanDatabase(); server.close(); }); -describe('authentication routes test',() => { - it('should redirect to the google authentication page',async() => { - const response = await request(app) - .get('/user/google-auth'); - expect(response.statusCode).toBe(302) - }) - it('should redirect after google authentication', async() => { - const response = await request(app) - .get('/user/auth/google/callback'); - expect(response.statusCode).toBe(302) - }) +describe('authentication routes test', () => { + it('should redirect to the google authentication page', async () => { + const response = await request(app).get('/user/google-auth'); + expect(response.statusCode).toBe(302); + }); + it('should redirect after google authentication', async () => { + const response = await request(app).get('/user/auth/google/callback'); + expect(response.statusCode).toBe(302); + }); }); - diff --git a/src/__test__/orderManagement.test.ts b/src/__test__/orderManagement.test.ts new file mode 100644 index 0000000..846b9d8 --- /dev/null +++ b/src/__test__/orderManagement.test.ts @@ -0,0 +1,390 @@ +import request from 'supertest'; +import jwt from 'jsonwebtoken'; +import { app, server } from '../index'; +import { getConnection } from 'typeorm'; +import { dbConnection } from '../startups/dbConnection'; +import { User, UserInterface } from '../entities/User'; +import { v4 as uuid } from 'uuid'; +import { Product } from '../entities/Product'; +import { Category } from '../entities/Category'; +import { cleanDatabase } from './test-assets/DatabaseCleanup'; +import { Order } from '../entities/Order'; +import { OrderItem } from '../entities/OrderItem'; +import { VendorOrders } from '../entities/vendorOrders'; +import { VendorOrderItem } from '../entities/VendorOrderItem'; + +const adminId = uuid(); +const vendorId = uuid(); +const vendor2Id = uuid(); +const buyerId = uuid(); + +const productId = uuid(); +const product2Id = uuid(); + +const orderId = uuid(); +const orderItemId = uuid(); +const order2Id = uuid(); +const order2ItemId = uuid(); + +const vendorOrderId = uuid(); +const vendorOrderItemId = uuid(); +const vendorOrder2Id = uuid(); +const vendorOrder2ItemId = uuid(); +const catId = uuid(); + +console.log(adminId, vendorId, buyerId); + +const jwtSecretKey = process.env.JWT_SECRET || ''; + +const getAccessToken = (id: string, email: string) => { + return jwt.sign( + { + id: id, + email: email, + }, + jwtSecretKey + ); +}; + +const sampleAdmin: UserInterface = { + id: adminId, + firstName: 'admin', + lastName: 'user', + email: 'admin@example.com', + password: 'password', + userType: 'Admin', + gender: 'Male', + phoneNumber: '126380997', + photoUrl: 'https://example.com/photo.jpg', + verified: true, + role: 'ADMIN', +}; +const sampleVendor: UserInterface = { + id: vendorId, + firstName: 'vendor', + lastName: 'user', + email: 'vendor@example.com', + password: 'password', + userType: 'Vendor', + gender: 'Male', + phoneNumber: '126380996347', + photoUrl: 'https://example.com/photo.jpg', + verified: true, + role: 'VENDOR', +}; +const sampleVendor2: UserInterface = { + id: vendor2Id, + firstName: 'vendor', + lastName: 'user', + email: 'vendor2@example.com', + password: 'password', + userType: 'Vendor', + gender: 'Male', + phoneNumber: '18090296347', + photoUrl: 'https://example.com/photo.jpg', + verified: true, + role: 'VENDOR', +}; +const sampleBuyer: UserInterface = { + id: buyerId, + firstName: 'buyer', + lastName: 'user', + email: 'buyer@example.com', + password: 'password', + userType: 'Buyer', + gender: 'Male', + phoneNumber: '6380996347', + photoUrl: 'https://example.com/photo.jpg', + verified: true, + role: 'BUYER', +}; + +const sampleCat = { + id: catId, + name: 'accessories', +}; + +const sampleProduct = { + id: productId, + name: 'test product', + description: 'amazing product', + images: ['photo1.jpg', 'photo2.jpg', 'photo3.jpg'], + newPrice: 200, + quantity: 10, + vendor: sampleVendor, + categories: [sampleCat], +}; +const sampleProduct2 = { + id: product2Id, + name: 'test product2', + description: 'amazing products', + images: ['photo1.jpg', 'photo2.jpg', 'photo3.jpg'], + newPrice: 200, + quantity: 10, + vendor: sampleVendor, + categories: [sampleCat], +}; + +const sampleOrder = { + id: orderId, + totalPrice: 400, + quantity: 2, + orderDate: new Date(), + buyer: sampleBuyer, + orderStatus: 'received', + address: 'Rwanda, Kigali, KK20st', +}; +const sampleOrder2 = { + id: order2Id, + totalPrice: 400, + quantity: 2, + orderDate: new Date(), + buyer: sampleBuyer, + orderStatus: 'order placed', + address: 'Rwanda, Kigali, KK20st', +}; + +const sampleOrderItem = { + id: orderItemId, + price: 200, + quantity: 2, + order: sampleOrder, + product: sampleProduct, +}; + +const sampleVendorOrder = { + id: vendorOrderId, + totalPrice: 400, + quantity: 2, + vendor: sampleVendor, + order: sampleOrder, + buyer: sampleBuyer, + orderStatus: 'pending', +}; + +const sampleVendorOrderItem = { + 'id': vendorOrderItemId, + 'price/unit': 200, + 'quantity': 2, + 'order': sampleVendorOrder, + 'product': sampleProduct, +}; + +beforeAll(async () => { + const connection = await dbConnection(); + + const categoryRepository = connection?.getRepository(Category); + await categoryRepository?.save({ ...sampleCat }); + + const userRepository = connection?.getRepository(User); + await userRepository?.save([sampleAdmin, sampleVendor, sampleVendor2, sampleBuyer]); + + const productRepository = connection?.getRepository(Product); + await productRepository?.save({ ...sampleProduct }); + + // Order Management + const orderRepository = connection?.getRepository(Order); + await orderRepository?.save([sampleOrder, sampleOrder2]); + + const orderItemRepository = connection?.getRepository(OrderItem); + await orderItemRepository?.save({ ...sampleOrderItem }); + + const vendorOrderRepository = connection?.getRepository(VendorOrders); + await vendorOrderRepository?.save({ ...sampleVendorOrder }); + + const vendorOrderItemRepository = connection?.getRepository(VendorOrderItem); + await vendorOrderItemRepository?.save({ ...sampleVendorOrderItem }); +}); + +afterAll(async () => { + await cleanDatabase(); + server.close(); +}); + +describe('Vendor Order Management', () => { + describe('Fetching vendor Order(s)', () => { + it('Should return all vendor orders', async () => { + const response = await request(app) + .get('/product/vendor/orders') + .set('Authorization', `Bearer ${getAccessToken(vendorId, sampleVendor.email)}`); + + expect(response.status).toBe(200); + expect(response.body.data.orders).toBeDefined(); + }); + + it("Should return empty array if vendor don't have any order for buyer", async () => { + const response = await request(app) + .get('/product/vendor/orders') + .set('Authorization', `Bearer ${getAccessToken(vendor2Id, sampleVendor2.email)}`); + + expect(response.status).toBe(200); + expect(response.body.data.orders).toEqual([]); + }); + + it('Should return single vendor order', async () => { + const response = await request(app) + .get(`/product/vendor/orders/${vendorOrderId}`) + .set('Authorization', `Bearer ${getAccessToken(vendorId, sampleVendor.email)}`); + + expect(response.status).toBe(200); + expect(response.body.data.order).toBeDefined(); + }); + + it('return 404, for non existing vendor order', async () => { + const response = await request(app) + .get(`/product/vendor/orders/${uuid()}`) + .set('Authorization', `Bearer ${getAccessToken(vendorId, sampleVendor.email)}`); + + expect(response.status).toBe(404); + expect(response.body.message).toBe('Order Not Found.'); + }); + + it('return 400, for invalid vendor order id ', async () => { + const response = await request(app) + .get(`/product/vendor/orders/32df3`) + .set('Authorization', `Bearer ${getAccessToken(vendorId, sampleVendor.email)}`); + + expect(response.status).toBe(400); + expect(response.body.message).toBe(`invalid input syntax for type uuid: "32df3"`); + }); + }); + describe('Updating vendor order', () => { + it('should update the order', async () => { + const response = await request(app) + .put(`/product/vendor/orders/${vendorOrderId}`) + .set('Authorization', `Bearer ${getAccessToken(vendorId, sampleVendor.email)}`) + .send({ + orderStatus: 'delivered', + }); + + expect(response.statusCode).toBe(200); + }); + it('should not update if orderStatus in not among defined ones', async () => { + const response = await request(app) + .put(`/product/vendor/orders/${vendorOrderId}`) + .set('Authorization', `Bearer ${getAccessToken(vendorId, sampleVendor.email)}`) + .send({ + orderStatus: 'fakeOrderStatus', + }); + + expect(response.statusCode).toBe(400); + expect(response.body.message).toBe('Please provide one of defined statuses.'); + }); + it('should not update, return 404 for non existing vendor order', async () => { + const response = await request(app) + .put(`/product/vendor/orders/${uuid()}`) + .set('Authorization', `Bearer ${getAccessToken(vendorId, sampleVendor.email)}`) + .send({ + orderStatus: 'is-accepted', + }); + + expect(response.statusCode).toBe(404); + expect(response.body.message).toBe('Order Not Found.'); + }); + it('should not update, if the order has already been cancelled or completed', async () => { + const response = await request(app) + .put(`/product/vendor/orders/${vendorOrderId}`) + .set('Authorization', `Bearer ${getAccessToken(vendorId, sampleVendor.email)}`) + .send({ + orderStatus: 'is-accepted', + }); + + expect(response.statusCode).toBe(409); + }); + it('return 400, for invalid vendor order id ', async () => { + const response = await request(app) + .put(`/product/vendor/orders/32df3`) + .set('Authorization', `Bearer ${getAccessToken(vendorId, sampleVendor.email)}`) + .send({ + orderStatus: 'is-accepted', + }); + + expect(response.status).toBe(400); + expect(response.body.message).toBe(`invalid input syntax for type uuid: "32df3"`); + }); + }); +}); + +describe('Admin Order Management', () => { + describe('Fetching buyer and vendor Order(s)', () => { + it("Should return all orders with it's buyer and related vendors", async () => { + const response = await request(app) + .get('/product/admin/orders') + .set('Authorization', `Bearer ${getAccessToken(adminId, sampleAdmin.email)}`); + + expect(response.status).toBe(200); + expect(response.body.data.orders).toBeDefined(); + }); + + it('Should return single order details', async () => { + const response = await request(app) + .get(`/product/admin/orders/${orderId}`) + .set('Authorization', `Bearer ${getAccessToken(adminId, sampleAdmin.email)}`); + + expect(response.status).toBe(200); + expect(response.body.data.order).toBeDefined(); + }); + + it('return 404, for non existing order', async () => { + const response = await request(app) + .get(`/product/admin/orders/${uuid()}`) + .set('Authorization', `Bearer ${getAccessToken(adminId, sampleAdmin.email)}`); + + expect(response.status).toBe(404); + expect(response.body.message).toBe('Order Not Found.'); + }); + + it('return 400, for invalid order id ', async () => { + const response = await request(app) + .get(`/product/admin/orders/32df3`) + .set('Authorization', `Bearer ${getAccessToken(adminId, sampleAdmin.email)}`); + + expect(response.status).toBe(400); + expect(response.body.message).toBe(`invalid input syntax for type uuid: "32df3"`); + }); + }); + describe('Updating buyer and vendor order', () => { + it('should not update, return 404 for non existing order', async () => { + const response = await request(app) + .put(`/product/admin/orders/${uuid()}`) + .set('Authorization', `Bearer ${getAccessToken(adminId, sampleAdmin.email)}`); + + expect(response.statusCode).toBe(404); + expect(response.body.message).toBe('Order Not Found.'); + }); + it('should update the order', async () => { + const response = await request(app) + .put(`/product/admin/orders/${orderId}`) + .set('Authorization', `Bearer ${getAccessToken(adminId, sampleAdmin.email)}`); + + expect(response.statusCode).toBe(200); + expect(response.body.data.order).toBeDefined(); + }); + it('should not update if it has already been completed(closed)', async () => { + const response = await request(app) + .put(`/product/admin/orders/${orderId}`) + .set('Authorization', `Bearer ${getAccessToken(adminId, sampleAdmin.email)}`); + + expect(response.statusCode).toBe(409); + expect(response.body.message).toBe('The order has already been completed.'); + }); + + it('should not update, if the order has not been marked as received by buyer', async () => { + const response = await request(app) + .put(`/product/admin/orders/${order2Id}`) + .set('Authorization', `Bearer ${getAccessToken(adminId, sampleAdmin.email)}`); + + expect(response.statusCode).toBe(409); + expect(response.body.message).toBe('Order closure failed: The buyer has not received the item yet.'); + }); + + it('return 400, for invalid order id ', async () => { + const response = await request(app) + .put(`/product/admin/orders/32df3`) + .set('Authorization', `Bearer ${getAccessToken(adminId, sampleAdmin.email)}`); + + expect(response.status).toBe(400); + expect(response.body.message).toBe(`invalid input syntax for type uuid: "32df3"`); + }); + }); +}); diff --git a/src/__test__/productStatus.test.ts b/src/__test__/productStatus.test.ts index 6d6df6a..8e8b42a 100644 --- a/src/__test__/productStatus.test.ts +++ b/src/__test__/productStatus.test.ts @@ -144,7 +144,7 @@ beforeAll(async () => { }); afterAll(async () => { - await cleanDatabase() + await cleanDatabase(); server.close(); }); @@ -222,22 +222,16 @@ describe('Vendor product availability status management tests', () => { }); }); - describe('search product by name availability tests', () => { it('Should search product by name', async () => { - const response = await request(app) - .get(`/product/search?name=testingmkknkkjiproduct4`) + const response = await request(app).get(`/product/search?name=testingmkknkkjiproduct4`); expect(response.body.data).toBeDefined; }, 10000); it('should return empty array if there is product is not found in the database', async () => { - const response = await request(app) - .put(`/product/search?name=home`) - + const response = await request(app).put(`/product/search?name=home`); expect(response.statusCode).toBe(401); expect(response.body.data).toBeUndefined; }); - - }); - +}); diff --git a/src/__test__/roleCheck.test.ts b/src/__test__/roleCheck.test.ts index ada2271..32df044 100644 --- a/src/__test__/roleCheck.test.ts +++ b/src/__test__/roleCheck.test.ts @@ -35,7 +35,7 @@ beforeAll(async () => { }); afterAll(async () => { - await cleanDatabase() + await cleanDatabase(); }); describe('hasRole MiddleWare Test', () => { diff --git a/src/__test__/route.test.ts b/src/__test__/route.test.ts index 721f763..ac704b5 100644 --- a/src/__test__/route.test.ts +++ b/src/__test__/route.test.ts @@ -11,8 +11,7 @@ beforeAll(async () => { jest.setTimeout(20000); afterAll(async () => { - await cleanDatabase() - + await cleanDatabase(); server.close(); }); diff --git a/src/__test__/test-assets/DatabaseCleanup.ts b/src/__test__/test-assets/DatabaseCleanup.ts index ec40ee6..3674dfb 100644 --- a/src/__test__/test-assets/DatabaseCleanup.ts +++ b/src/__test__/test-assets/DatabaseCleanup.ts @@ -1,16 +1,17 @@ - import { Transaction } from '../../entities/transaction'; -import { Cart } from "../../entities/Cart"; -import { CartItem } from "../../entities/CartItem"; -import { Order } from "../../entities/Order"; -import { OrderItem } from "../../entities/OrderItem"; -import { wishList } from "../../entities/wishList"; +import { Cart } from '../../entities/Cart'; +import { CartItem } from '../../entities/CartItem'; +import { Order } from '../../entities/Order'; +import { OrderItem } from '../../entities/OrderItem'; +import { wishList } from '../../entities/wishList'; import { getConnection } from 'typeorm'; import { Product } from '../../entities/Product'; import { Category } from '../../entities/Category'; import { Coupon } from '../../entities/coupon'; import { User } from '../../entities/User'; import { server } from '../..'; +import { VendorOrderItem } from '../../entities/VendorOrderItem'; +import { VendorOrders } from '../../entities/vendorOrders'; export const cleanDatabase = async () => { const connection = getConnection(); @@ -18,6 +19,8 @@ export const cleanDatabase = async () => { // Delete from child tables first await connection.getRepository(Transaction).delete({}); await connection.getRepository(Coupon).delete({}); + await connection.getRepository(VendorOrderItem).delete({}); + await connection.getRepository(VendorOrders).delete({}); await connection.getRepository(OrderItem).delete({}); await connection.getRepository(Order).delete({}); await connection.getRepository(CartItem).delete({}); @@ -37,12 +40,11 @@ export const cleanDatabase = async () => { await connection.getRepository(User).delete({}); await connection.close(); - server.close(); }; -// Execute the clean-up function -cleanDatabase().then(() => { - console.log('Database cleaned'); -}).catch(error => { - console.error('Error cleaning database:', error); -}); +// // Execute the clean-up function +// cleanDatabase().then(() => { +// console.log('Database cleaned'); +// }).catch(error => { +// console.error('Error cleaning database:', error); +// }); diff --git a/src/__test__/userServices.test.ts b/src/__test__/userServices.test.ts index b4e87f9..29a2e7c 100644 --- a/src/__test__/userServices.test.ts +++ b/src/__test__/userServices.test.ts @@ -1,6 +1,6 @@ import request from 'supertest'; import { app, server } from '../index'; -import { createConnection, getConnection, getConnectionOptions, getRepository } from 'typeorm'; +import { createConnection, getRepository } from 'typeorm'; import { User } from '../entities/User'; import { cleanDatabase } from './test-assets/DatabaseCleanup'; @@ -9,7 +9,7 @@ beforeAll(async () => { }); afterAll(async () => { - await cleanDatabase() + await cleanDatabase(); server.close(); }); @@ -227,4 +227,4 @@ describe('start2FAProcess', () => { expect(res.status).toBe(404); expect(res.body).toEqual({ status: 'error', message: 'Incorrect email or password' }); }, 10000); -}); \ No newline at end of file +}); diff --git a/src/__test__/userStatus.test.ts b/src/__test__/userStatus.test.ts index 132134f..69e892a 100644 --- a/src/__test__/userStatus.test.ts +++ b/src/__test__/userStatus.test.ts @@ -36,7 +36,7 @@ beforeAll(async () => { }); afterAll(async () => { - await cleanDatabase() + await cleanDatabase(); server.close(); }); diff --git a/src/__test__/vendorProduct.test.ts b/src/__test__/vendorProduct.test.ts index f90d80d..d8fc0a5 100644 --- a/src/__test__/vendorProduct.test.ts +++ b/src/__test__/vendorProduct.test.ts @@ -110,7 +110,7 @@ beforeAll(async () => { }); afterAll(async () => { - await cleanDatabase() + await cleanDatabase(); server.close(); }); diff --git a/src/__test__/wishList.test.ts b/src/__test__/wishList.test.ts index aac072d..6658853 100644 --- a/src/__test__/wishList.test.ts +++ b/src/__test__/wishList.test.ts @@ -65,7 +65,7 @@ beforeAll(async () => { }); afterAll(async () => { - await cleanDatabase() + await cleanDatabase(); server.close(); }); const data1 = { diff --git a/src/controllers/adminOrdercontroller.ts b/src/controllers/adminOrdercontroller.ts new file mode 100644 index 0000000..388220d --- /dev/null +++ b/src/controllers/adminOrdercontroller.ts @@ -0,0 +1,18 @@ +import { Request, Response } from 'express'; +import { + getSingleBuyerVendorOrderService, + getBuyerVendorOrdersService, + updateBuyerVendorOrderService, +} from '../services'; + +export const getBuyerVendorOrders = async (req: Request, res: Response) => { + await getBuyerVendorOrdersService(req, res); +}; + +export const getSingleBuyerVendorOrder = async (req: Request, res: Response) => { + await getSingleBuyerVendorOrderService(req, res); +}; + +export const updateBuyerVendorOrder = async (req: Request, res: Response) => { + await updateBuyerVendorOrderService(req, res); +}; diff --git a/src/controllers/couponController.ts b/src/controllers/couponController.ts index dd7e19f..e5a6804 100644 --- a/src/controllers/couponController.ts +++ b/src/controllers/couponController.ts @@ -4,7 +4,7 @@ import { updateCouponService } from '../services/couponServices/updateService'; import { deleteCouponService } from '../services/couponServices/deleteCoupon'; import { accessAllCouponService } from '../services/couponServices/accessAllCoupon'; import { readCouponService } from '../services/couponServices/readCoupon'; -import { buyerApplyCouponService } from '../services/couponServices/buyerApplyCoupon' +import { buyerApplyCouponService } from '../services/couponServices/buyerApplyCoupon'; export const createCoupon = async (req: Request, res: Response) => { await createCouponService(req, res); @@ -28,4 +28,4 @@ export const readCoupon = async (req: Request, res: Response) => { export const buyerApplyCoupon = async (req: Request, res: Response) => { await buyerApplyCouponService(req, res); -}; \ No newline at end of file +}; diff --git a/src/controllers/index.ts b/src/controllers/index.ts index 3cbb7dc..70dea3b 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -1,3 +1,5 @@ export * from './authController'; export * from './productController'; -export * from './orderController'; \ No newline at end of file +export * from './orderController'; +export * from './vendorOrderController'; +export * from './adminOrdercontroller'; diff --git a/src/controllers/orderController.ts b/src/controllers/orderController.ts index 5a5db97..d4ac5fc 100644 --- a/src/controllers/orderController.ts +++ b/src/controllers/orderController.ts @@ -15,4 +15,4 @@ export const updateOrder = async (req: Request, res: Response) => { }; export const getOrdersHistory = async (req: Request, res: Response) => { await getTransactionHistoryService(req, res); -}; \ No newline at end of file +}; diff --git a/src/controllers/productController.ts b/src/controllers/productController.ts index 11caddd..1cd895a 100644 --- a/src/controllers/productController.ts +++ b/src/controllers/productController.ts @@ -1,26 +1,17 @@ import { Request, Response } from 'express'; import { - createProductService, - updateProductService, - - removeProductImageService, - + removeProductImageService, readProductService, - readProductsService, - + readProductsService, deleteProductService, - getRecommendedProductsService, productStatusServices, viewSingleProduct, - searchProductService - -, - listAllProductsService} -from '../services'; - + searchProductService, + listAllProductsService, +} from '../services'; export const readProduct = async (req: Request, res: Response) => { await readProductService(req, res); @@ -50,10 +41,10 @@ export const getRecommendedProducts = async (req: Request, res: Response) => { await getRecommendedProductsService(req, res); }; - export const listAllProducts = async (req: Request, res: Response) => { await listAllProductsService(req, res); -};export const productStatus = async (req: Request, res: Response) => { +}; +export const productStatus = async (req: Request, res: Response) => { await productStatusServices(req, res); }; export const singleProduct = async (req: Request, res: Response) => { diff --git a/src/controllers/vendorOrderController.ts b/src/controllers/vendorOrderController.ts new file mode 100644 index 0000000..955b01c --- /dev/null +++ b/src/controllers/vendorOrderController.ts @@ -0,0 +1,14 @@ +import { Request, Response } from 'express'; +import { getVendorOrdersService, getSingleVendorOrderService, updateVendorOrderService } from '../services'; + +export const getVendorOrders = async (req: Request, res: Response) => { + await getVendorOrdersService(req, res); +}; + +export const getSingleVendorOrder = async (req: Request, res: Response) => { + await getSingleVendorOrderService(req, res); +}; + +export const updateVendorOrder = async (req: Request, res: Response) => { + await updateVendorOrderService(req, res); +}; diff --git a/src/controllers/wishListController.ts b/src/controllers/wishListController.ts index e0cd1bd..23fa03f 100644 --- a/src/controllers/wishListController.ts +++ b/src/controllers/wishListController.ts @@ -1,23 +1,18 @@ import { Request, Response } from 'express'; -import{ - addProductService, - getProductsService, - removeProductService, - clearAllProductService -} from '../services' +import { addProductService, getProductsService, removeProductService, clearAllProductService } from '../services'; export const wishlistAddProduct = async (req: Request, res: Response) => { - await addProductService(req, res); - }; + await addProductService(req, res); +}; - export const wishlistRemoveProduct = async (req: Request, res:Response) => { - await removeProductService(req, res); - } +export const wishlistRemoveProduct = async (req: Request, res: Response) => { + await removeProductService(req, res); +}; - export const wishlistGetProducts = async (req: Request, res:Response) => { - await getProductsService(req, res); - } +export const wishlistGetProducts = async (req: Request, res: Response) => { + await getProductsService(req, res); +}; - export const wishlistClearAllProducts = async (req: Request, res:Response) => { - await clearAllProductService(req, res); - } \ No newline at end of file +export const wishlistClearAllProducts = async (req: Request, res: Response) => { + await clearAllProductService(req, res); +}; diff --git a/src/docs/adminOrderManagement.yml b/src/docs/adminOrderManagement.yml new file mode 100644 index 0000000..e8d6ed6 --- /dev/null +++ b/src/docs/adminOrderManagement.yml @@ -0,0 +1,80 @@ +/product/admin/orders: + get: + tags: + - Admin Order Manangement + summary: Fetches all buyer and vendor orders + description: Return all order including details for buyer and vendors of products in that order + security: + - bearerAuth: [] + responses: + '200': + description: Return all order + '400': + description: Bad Request (syntax error, incorrect input format, etc..) + '401': + description: Unauthorized + '403': + description: Forbidden (Unauthorized action) + '500': + description: Internal server error + +/product/admin/orders/{id}: + get: + tags: + - Admin Order Manangement + summary: Fetch details for single buyer and vendor order + description: + Fetch details for single order using buyer id, if successful return order details with it's corresponding vendor + security: + - bearerAuth: [] + parameters: + - in: path + name: id + schema: + type: string + required: true + description: The id of a buyer order + responses: + '200': + description: Order details retrieved successfully + '400': + description: Bad Request (syntax error, incorrect input format, etc..) + '401': + description: Unauthorized + '403': + description: Forbidden (Unauthorized action) + '404': + description: Order not found + '500': + description: Internal server error + put: + tags: + - Admin Order Manangement + summary: Updates order status for both buyer and vendor order + description: Updates orderStatus field of order, if successful returns updated order. + security: + - bearerAuth: [] + parameters: + - in: path + name: id + schema: + type: string + required: true + description: The id of a buyer order + consumes: + - application/json + responses: + '200': + description: Order was successfully updated, return updated order + '400': + description: Bad Request (syntax error, incorrect input format, etc..) + '401': + description: Unauthorized + '403': + description: Forbidden (Unauthorized action) + '404': + description: Order not found + '409': + description: Order can not be updated because (it has already been completed(close), delivered, cancelled) + '500': + description: Internal server error diff --git a/src/docs/couponDocs.yml b/src/docs/couponDocs.yml index fb0a49a..fefa829 100644 --- a/src/docs/couponDocs.yml +++ b/src/docs/couponDocs.yml @@ -35,7 +35,7 @@ description: The code of the coupon responses: '200': - description: Return info for the coupon + description: Return info for the coupon '400': description: Bad Request (syntax error, incorrect input format, etc..) '401': diff --git a/src/docs/vendorOrderManagement.yml b/src/docs/vendorOrderManagement.yml new file mode 100644 index 0000000..5873717 --- /dev/null +++ b/src/docs/vendorOrderManagement.yml @@ -0,0 +1,93 @@ +/product/vendor/orders: + get: + tags: + - Vendor Order Manangement + summary: Fetches all vendor orders + description: Return all order for authenticated vendor + security: + - bearerAuth: [] + responses: + '200': + description: Return all order for vendor requested from buyer + '400': + description: Bad Request (syntax error, incorrect input format, etc..) + '401': + description: Unauthorized + '403': + description: Forbidden (Unauthorized action) + '500': + description: Internal server error + +/product/vendor/orders/{id}: + get: + tags: + - Vendor Order Manangement + summary: Fetch details for single vendor order + description: + Fetch details for single order for authenticated vendor, order that include only his/her product which a buyer has + requested in his order. + security: + - bearerAuth: [] + parameters: + - in: path + name: id + schema: + type: string + required: true + description: The id of a vendor order + responses: + '200': + description: Order details retrieved successfully + '400': + description: Bad Request (syntax error, incorrect input format, etc..) + '401': + description: Unauthorized + '403': + description: Forbidden (Unauthorized action) + '404': + description: Order not found + '500': + description: Internal server error + put: + tags: + - Vendor Order Manangement + summary: Updates order status for vendor order + description: + Updates orderStatus field of vendor order for authenticated vendor, it order that include only his/her product + which a buyer has request in his order. + security: + - bearerAuth: [] + parameters: + - in: path + name: id + schema: + type: string + required: true + description: The id of a vendor order + consumes: + - application/json + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + orderStatus: + type: string + example: "'is-accepted', 'in-transit', 'cancelled', 'delivered'" + responses: + '200': + description: Order was successfully updated, return updated order + '400': + description: Bad Request (syntax error, incorrect input format, etc..) + '401': + description: Unauthorized + '403': + description: Forbidden (Unauthorized action) + '404': + description: Order not found + '409': + description: Order can not be updated because (it has already been completed(close), delivered, cancelled) + '500': + description: Internal server error diff --git a/src/docs/vendorProduct.yml b/src/docs/vendorProduct.yml index 937b097..3830bf4 100644 --- a/src/docs/vendorProduct.yml +++ b/src/docs/vendorProduct.yml @@ -35,7 +35,7 @@ description: The id of product responses: '200': - description: Return info for the product + description: Return info for the product '400': description: Bad Request (syntax error, incorrect input format, etc..) '401': @@ -59,7 +59,7 @@ requestBody: required: true content: - application/json: + application/json: schema: type: object properties: @@ -75,10 +75,10 @@ type: file categories: oneOf: - - type: string - - type: array - items: - type: string + - type: string + - type: array + items: + type: string example: "'category' or ['category1', 'category2', ...]" expirationDate: type: string @@ -159,7 +159,7 @@ description: Product not found '500': description: Internal server error - + /product/images/{id}: delete: tags: diff --git a/src/docs/wishListDocs.yml b/src/docs/wishListDocs.yml index df3c72c..7f705f7 100644 --- a/src/docs/wishListDocs.yml +++ b/src/docs/wishListDocs.yml @@ -94,4 +94,4 @@ '403': description: Forbidden (Unauthorized action) '500': - description: Internal server error \ No newline at end of file + description: Internal server error diff --git a/src/entities/Cart.ts b/src/entities/Cart.ts index 0ba44a6..fda1e15 100644 --- a/src/entities/Cart.ts +++ b/src/entities/Cart.ts @@ -36,7 +36,7 @@ export class Cart { @UpdateDateColumn() updatedAt!: Date; - updateTotal(): void { + updateTotal (): void { if (this.items) { let total: number = 0; for (let i = 0; i < this.items.length; i++) { diff --git a/src/entities/CartItem.ts b/src/entities/CartItem.ts index 107170c..d651adf 100644 --- a/src/entities/CartItem.ts +++ b/src/entities/CartItem.ts @@ -47,7 +47,7 @@ export class CartItem { @BeforeInsert() @BeforeUpdate() - updateTotal(): void { + updateTotal (): void { this.total = this.newPrice * this.quantity; } } diff --git a/src/entities/Order.ts b/src/entities/Order.ts index 49965a0..47649a7 100644 --- a/src/entities/Order.ts +++ b/src/entities/Order.ts @@ -1,10 +1,17 @@ -import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany, CreateDateColumn, UpdateDateColumn } from 'typeorm'; +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + OneToMany, + CreateDateColumn, + UpdateDateColumn, +} from 'typeorm'; import { IsNotEmpty, IsNumber, IsDate, IsIn } from 'class-validator'; import { User } from './User'; import { OrderItem } from './OrderItem'; import { Transaction } from './transaction'; - @Entity() export class Order { @PrimaryGeneratedColumn('uuid') @@ -24,11 +31,20 @@ export class Order { @IsNumber() totalPrice!: number; - @OneToMany(() => Transaction, (transaction) => transaction.order) + @OneToMany(() => Transaction, transaction => transaction.order) transactions!: Transaction[]; @Column({ default: 'order placed' }) @IsNotEmpty() - @IsIn(['order placed', 'cancelled', 'awaiting shipment', 'in transit', 'delivered', 'received', 'returned']) + @IsIn([ + 'order placed', + 'cancelled', + 'awaiting shipment', + 'in transit', + 'delivered', + 'received', + 'returned', + 'completed', + ]) orderStatus!: string; @Column('int') diff --git a/src/entities/Product.ts b/src/entities/Product.ts index 2b39493..e144a04 100644 --- a/src/entities/Product.ts +++ b/src/entities/Product.ts @@ -18,11 +18,12 @@ import { Category } from './Category'; import { Order } from './Order'; import { Coupon } from './coupon'; import { OrderItem } from './OrderItem'; +import { VendorOrderItem } from './VendorOrderItem'; @Entity() @Unique(['id']) export class Product { - static query() { + static query () { throw new Error('Method not implemented.'); } @PrimaryGeneratedColumn('uuid') @@ -36,6 +37,9 @@ export class Product { @OneToMany(() => OrderItem, orderItem => orderItem.product) orderItems!: OrderItem[]; + @OneToMany(() => VendorOrderItem, vendorOrderItems => vendorOrderItems.product) + vendorOrderItems!: VendorOrderItem[]; + @OneToOne(() => Coupon, (coupons: any) => coupons.product) @JoinColumn() coupons?: Coupon; diff --git a/src/entities/User.ts b/src/entities/User.ts index fb45fe9..eebd104 100644 --- a/src/entities/User.ts +++ b/src/entities/User.ts @@ -99,7 +99,7 @@ export class User { @OneToMany(() => Order, (order: any) => order.buyer) orders!: Order[]; - @OneToMany(() => Transaction, (transaction) => transaction.user) + @OneToMany(() => Transaction, transaction => transaction.user) transactions!: Transaction[]; @CreateDateColumn() @@ -112,7 +112,7 @@ export class User { accountBalance!: number; @BeforeInsert() - setRole(): void { + setRole (): void { this.role = this.userType === 'Vendor' ? roles.vendor : roles.buyer; } } diff --git a/src/entities/VendorOrderItem.ts b/src/entities/VendorOrderItem.ts new file mode 100644 index 0000000..9137f6d --- /dev/null +++ b/src/entities/VendorOrderItem.ts @@ -0,0 +1,30 @@ +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm'; +import { IsNotEmpty, IsNumber } from 'class-validator'; +import { Order } from './Order'; +import { Product } from './Product'; +import { VendorOrders } from './vendorOrders'; + +@Entity() +export class VendorOrderItem { + @PrimaryGeneratedColumn('uuid') + @IsNotEmpty() + 'id'!: string; + + @ManyToOne(() => VendorOrders, order => order.vendorOrderItems) + @IsNotEmpty() + 'order'!: VendorOrders; + + @ManyToOne(() => Product, product => product.vendorOrderItems) + @IsNotEmpty() + 'product'!: Product; + + @Column('decimal') + @IsNotEmpty() + @IsNumber() + 'price/unit'!: number; + + @Column('int') + @IsNotEmpty() + @IsNumber() + 'quantity'!: number; +} diff --git a/src/entities/transaction.ts b/src/entities/transaction.ts index d475812..0f7b0ea 100644 --- a/src/entities/transaction.ts +++ b/src/entities/transaction.ts @@ -58,4 +58,4 @@ export class Transaction { @UpdateDateColumn() updatedAt!: Date; -} \ No newline at end of file +} diff --git a/src/entities/vendorOrders.ts b/src/entities/vendorOrders.ts new file mode 100644 index 0000000..38269e6 --- /dev/null +++ b/src/entities/vendorOrders.ts @@ -0,0 +1,49 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + OneToMany, + CreateDateColumn, + UpdateDateColumn, +} from 'typeorm'; +import { IsNotEmpty, IsNumber, IsDate, IsIn, isNotEmpty } from 'class-validator'; +import { User } from './User'; +import { OrderItem } from './OrderItem'; +import { Transaction } from './transaction'; +import { Order } from './Order'; +import { VendorOrderItem } from './VendorOrderItem'; + +@Entity() +export class VendorOrders { + @PrimaryGeneratedColumn('uuid') + @IsNotEmpty() + id!: string; + + @ManyToOne(() => User) + @IsNotEmpty() + vendor!: User; + + @OneToMany(() => VendorOrderItem, vendorOrderItems => vendorOrderItems.order, { cascade: true }) + @IsNotEmpty() + vendorOrderItems!: VendorOrderItem[]; + + @ManyToOne(() => Order) + @IsNotEmpty() + order!: Order; + + @Column('decimal') + @IsNotEmpty() + @IsNumber() + totalPrice!: number; + + @Column({ default: 'pending' }) + @IsIn(['pending', 'is-accepted', 'in-transit', 'cancelled', 'delivered', 'completed']) + orderStatus!: string; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; +} diff --git a/src/entities/wishList.ts b/src/entities/wishList.ts index 69dbebd..7f74023 100644 --- a/src/entities/wishList.ts +++ b/src/entities/wishList.ts @@ -1,10 +1,19 @@ -import { Entity, PrimaryGeneratedColumn, BaseEntity,Column, Unique, ManyToOne, CreateDateColumn, UpdateDateColumn,} from "typeorm"; +import { + Entity, + PrimaryGeneratedColumn, + BaseEntity, + Column, + Unique, + ManyToOne, + CreateDateColumn, + UpdateDateColumn, +} from 'typeorm'; import { IsNotEmpty, IsString } from 'class-validator'; import { User } from './User'; -@Entity("wishlist") +@Entity('wishlist') @Unique(['id']) -export class wishList extends BaseEntity{ +export class wishList extends BaseEntity { @PrimaryGeneratedColumn() @IsNotEmpty() id!: number; @@ -23,4 +32,4 @@ export class wishList extends BaseEntity{ @UpdateDateColumn() updatedAt!: Date; -} \ No newline at end of file +} diff --git a/src/index.ts b/src/index.ts index 07efd39..d689c27 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,21 +5,27 @@ import router from './routes'; import { addDocumentation } from './startups/docs'; import 'reflect-metadata'; import cookieParser from 'cookie-parser'; -import session from "express-session"; +import session from 'express-session'; import passport from 'passport'; import { CustomError, errorHandler } from './middlewares/errorHandler'; import morgan from 'morgan'; import { dbConnection } from './startups/dbConnection'; + +import { Server } from 'socket.io'; +import { init as initSocketIO } from './utils/socket'; + dotenv.config(); export const app = express(); const port = process.env.PORT || 8000; -app.use(session({ - secret: 'keyboard cat' -})) -app.use(passport.initialize()) -app.use(passport.session()) +app.use( + session({ + secret: 'keyboard cat', + }) +); +app.use(passport.initialize()); +app.use(passport.session()); app.use(express.json()); app.use(cookieParser()); app.use(cors({ origin: '*' })); @@ -43,3 +49,14 @@ app.use(morgan(morganFormat)); export const server = app.listen(port, () => { console.log(`[server]: Server is running at http://localhost:${port}`); }); + +// Socket.IO setup +const io = initSocketIO(server); + +io.on('connection', socket => { + console.log('Client connected'); + + socket.on('disconnect', () => { + console.log('Client disconnected'); + }); +}); diff --git a/src/routes/ProductRoutes.ts b/src/routes/ProductRoutes.ts index ce146ec..614eaaf 100644 --- a/src/routes/ProductRoutes.ts +++ b/src/routes/ProductRoutes.ts @@ -18,8 +18,15 @@ import { createOrder, getOrders, updateOrder, - getOrdersHistory + getOrdersHistory, + getSingleVendorOrder, + getVendorOrders, + updateVendorOrder, + getBuyerVendorOrders, + getSingleBuyerVendorOrder, + updateBuyerVendorOrder, } from '../controllers'; + const router = Router(); router.get('/all', listAllProducts); router.get('/recommended', authMiddleware as RequestHandler, hasRole('BUYER'), getRecommendedProducts); @@ -32,9 +39,20 @@ router.put('/:id', authMiddleware as RequestHandler, hasRole('VENDOR'), upload.a router.delete('/images/:id', authMiddleware as RequestHandler, hasRole('VENDOR'), removeProductImage); router.delete('/:id', authMiddleware as RequestHandler, hasRole('VENDOR'), deleteProduct); router.put('/availability/:id', authMiddleware as RequestHandler, hasRole('VENDOR'), productStatus); + router.post('/orders', authMiddleware as RequestHandler, hasRole('BUYER'), createOrder); router.get('/client/orders', authMiddleware as RequestHandler, hasRole('BUYER'), getOrders); router.put('/client/orders/:orderId', authMiddleware as RequestHandler, hasRole('BUYER'), updateOrder); router.get('/orders/history', authMiddleware as RequestHandler, hasRole('BUYER'), getOrdersHistory); +// Vendor order management +router.get('/vendor/orders', authMiddleware as RequestHandler, hasRole('VENDOR'), getVendorOrders); +router.get('/vendor/orders/:id', authMiddleware as RequestHandler, hasRole('VENDOR'), getSingleVendorOrder); +router.put('/vendor/orders/:id', authMiddleware as RequestHandler, hasRole('VENDOR'), updateVendorOrder); + +// Admin order management +router.get('/admin/orders', authMiddleware as RequestHandler, hasRole('ADMIN'), getBuyerVendorOrders); +router.get('/admin/orders/:id', authMiddleware as RequestHandler, hasRole('ADMIN'), getSingleBuyerVendorOrder); +router.put('/admin/orders/:id', authMiddleware as RequestHandler, hasRole('ADMIN'), updateBuyerVendorOrder); + export default router; diff --git a/src/routes/UserRoutes.ts b/src/routes/UserRoutes.ts index 50bb4ca..79b0551 100644 --- a/src/routes/UserRoutes.ts +++ b/src/routes/UserRoutes.ts @@ -1,7 +1,7 @@ import { Router } from 'express'; import { responseError } from '../utils/response.utils'; import { UserInterface } from '../entities/User'; -import jwt from 'jsonwebtoken' +import jwt from 'jsonwebtoken'; import { disable2FA, enable2FA, @@ -19,7 +19,7 @@ import { activateUser, disactivateUser, userProfileUpdate } from '../controllers import { hasRole } from '../middlewares/roleCheck'; import { isTokenValide } from '../middlewares/isValid'; import passport from 'passport'; -import "../utils/auth"; +import '../utils/auth'; const router = Router(); router.post('/register', userRegistration); @@ -37,35 +37,36 @@ router.post('/password/reset/link', sendPasswordResetLink); router.put('/update', userProfileUpdate); router.get('/google-auth', passport.authenticate('google', { scope: ['profile', 'email'] })); -router.get("/auth/google/callback", - passport.authenticate("google", { - successRedirect: "/user/login/success", - failureRedirect: "/user/login/failed" +router.get( + '/auth/google/callback', + passport.authenticate('google', { + successRedirect: '/user/login/success', + failureRedirect: '/user/login/failed', }) ); -router.get("/login/success", async (req, res) => { +router.get('/login/success', async (req, res) => { const user = req.user as UserInterface; - if(!user){ - responseError(res, 404, 'user not found') + if (!user) { + responseError(res, 404, 'user not found'); } const payload = { id: user?.id, email: user?.email, - role: user?.role - } - const token = jwt.sign(payload, process.env.JWT_SECRET as string,{expiresIn: '24h'}) + role: user?.role, + }; + const token = jwt.sign(payload, process.env.JWT_SECRET as string, { expiresIn: '24h' }); res.status(200).json({ status: 'success', - data:{ - token: token, - message: "Login success" - } - }) + data: { + token: token, + message: 'Login success', + }, + }); }); -router.get("/login/failed", async (req, res) => { +router.get('/login/failed', async (req, res) => { res.status(401).json({ status: false, - message: "Login failed" + message: 'Login failed', }); }); diff --git a/src/routes/couponRoutes.ts b/src/routes/couponRoutes.ts index c315ab8..3378fbe 100644 --- a/src/routes/couponRoutes.ts +++ b/src/routes/couponRoutes.ts @@ -1,5 +1,12 @@ import { RequestHandler, Router } from 'express'; -import { createCoupon, updateCoupon, accessAllCoupon, readCoupon, deleteCoupon, buyerApplyCoupon } from '../controllers/couponController'; +import { + createCoupon, + updateCoupon, + accessAllCoupon, + readCoupon, + deleteCoupon, + buyerApplyCoupon, +} from '../controllers/couponController'; import { hasRole } from '../middlewares/roleCheck'; import { authMiddleware } from '../middlewares/verifyToken'; @@ -10,6 +17,6 @@ router.put('/vendor/:id/update-coupon/:code', authMiddleware as RequestHandler, router.get('/vendor/:id/checkout/:code', authMiddleware as RequestHandler, hasRole('VENDOR'), readCoupon); router.get('/vendor/:id/access-coupons', authMiddleware as RequestHandler, hasRole('VENDOR'), accessAllCoupon); router.delete('/vendor/:id/checkout/delete', authMiddleware as RequestHandler, hasRole('VENDOR'), deleteCoupon); -router.post('/apply', authMiddleware as RequestHandler, hasRole('BUYER'),buyerApplyCoupon); +router.post('/apply', authMiddleware as RequestHandler, hasRole('BUYER'), buyerApplyCoupon); -export default router; \ No newline at end of file +export default router; diff --git a/src/routes/index.ts b/src/routes/index.ts index cddc08a..6f632d6 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -3,7 +3,7 @@ import { responseSuccess } from '../utils/response.utils'; import userRoutes from './UserRoutes'; import productRoutes from './ProductRoutes'; import wishListRoutes from './wishListRoute'; -import couponRoute from './couponRoutes';; +import couponRoute from './couponRoutes'; import cartRoutes from './CartRoutes'; const router = Router(); @@ -14,7 +14,7 @@ router.get('/', (req: Request, res: Response) => { router.use('/user', userRoutes); router.use('/product', productRoutes); -router.use('/wish-list', wishListRoutes); +router.use('/wish-list', wishListRoutes); router.use('/cart', cartRoutes); router.use('/coupons', couponRoute); diff --git a/src/routes/wishListRoute.ts b/src/routes/wishListRoute.ts index d5ac6fb..ea96e40 100644 --- a/src/routes/wishListRoute.ts +++ b/src/routes/wishListRoute.ts @@ -2,13 +2,30 @@ import { RequestHandler, Router } from 'express'; import { authMiddleware } from '../middlewares/verifyToken'; import { hasRole } from '../middlewares'; import { checkUserStatus } from '../middlewares/isAllowed'; -import { wishlistAddProduct,wishlistRemoveProduct,wishlistGetProducts,wishlistClearAllProducts } from '../controllers/wishListController'; +import { + wishlistAddProduct, + wishlistRemoveProduct, + wishlistGetProducts, + wishlistClearAllProducts, +} from '../controllers/wishListController'; const router = Router(); router.post('/add/:id', authMiddleware as RequestHandler, checkUserStatus, hasRole('BUYER'), wishlistAddProduct); -router.get('/',authMiddleware as RequestHandler, checkUserStatus, hasRole('BUYER'),wishlistGetProducts); -router.delete('/delete/:id',authMiddleware as RequestHandler, checkUserStatus, hasRole('BUYER'),wishlistRemoveProduct); -router.delete('/clearAll',authMiddleware as RequestHandler, checkUserStatus, hasRole('BUYER'),wishlistClearAllProducts); +router.get('/', authMiddleware as RequestHandler, checkUserStatus, hasRole('BUYER'), wishlistGetProducts); +router.delete( + '/delete/:id', + authMiddleware as RequestHandler, + checkUserStatus, + hasRole('BUYER'), + wishlistRemoveProduct +); +router.delete( + '/clearAll', + authMiddleware as RequestHandler, + checkUserStatus, + hasRole('BUYER'), + wishlistClearAllProducts +); -export default router; \ No newline at end of file +export default router; diff --git a/src/services/adminOrderServices/readOrder.ts b/src/services/adminOrderServices/readOrder.ts new file mode 100644 index 0000000..4bb20f0 --- /dev/null +++ b/src/services/adminOrderServices/readOrder.ts @@ -0,0 +1,158 @@ +import { Request, Response } from 'express'; +import { getRepository } from 'typeorm'; +import { responseSuccess, responseError } from '../../utils/response.utils'; +import { VendorOrderItem } from '../../entities/VendorOrderItem'; +import { VendorOrders } from '../../entities/vendorOrders'; +import { Order } from '../../entities/Order'; + +export const getBuyerVendorOrdersService = async (req: Request, res: Response) => { + try { + const vendorOrderRepository = getRepository(VendorOrders); + const orderRepository = getRepository(Order); + + const orders = await orderRepository.find({ + relations: ['buyer', 'orderItems'], + order: { + createdAt: 'DESC', // Order by creation date, most recent first + }, + }); + + if (!orders.length) { + return responseError(res, 200, `There is no pending orders from buyer`, { orders: [] }); + } + + const sanitizedOrdersResponse = []; + + for (const order of orders) { + const vendorOrders = await vendorOrderRepository.find({ + where: { + order: { + id: order.id, + }, + }, + relations: ['vendor', 'vendorOrderItems', 'vendorOrderItems.product'], + order: { + createdAt: 'DESC', // Order by creation date, most recent first + }, + }); + + sanitizedOrdersResponse.push({ + id: order.id, + totalPrice: order.totalPrice, + totalProducts: order.orderItems.length, + orderStatus: order.orderStatus, + address: order.address, + createdAt: order.createdAt, + updatedAt: order.updatedAt, + buyer: { + id: order.buyer.id, + firstName: order.buyer.firstName, + lastName: order.buyer.lastName, + email: order.buyer.lastName, + gender: order.buyer.gender, + phoneNumber: order.buyer.phoneNumber, + photoUrl: order.buyer.photoUrl, + }, + vendors: vendorOrders.map(vendoOrder => ({ + id: vendoOrder.vendor.id, + firstName: vendoOrder.vendor.firstName, + lastName: vendoOrder.vendor.lastName, + email: vendoOrder.vendor.lastName, + gender: vendoOrder.vendor.gender, + phoneNumber: vendoOrder.vendor.phoneNumber, + photoUrl: vendoOrder.vendor.photoUrl, + order: { + id: vendoOrder.id, + totalPrice: vendoOrder.totalPrice, + orderStatus: vendoOrder.orderStatus, + createdAt: order.createdAt, + updatedAt: order.updatedAt, + orderItems: vendoOrder.vendorOrderItems, + }, + })), + }); + } + + responseSuccess(res, 200, 'Orders retrieved successfully', { + totalOrders: orders.length, + orders: sanitizedOrdersResponse, + }); + } catch (error) { + return responseError(res, 400, (error as Error).message); + } +}; + +// Get single vendor order info +export const getSingleBuyerVendorOrderService = async (req: Request, res: Response) => { + try { + const orderId = req.params.id; + + const vendorOrderRepository = getRepository(VendorOrders); + const orderRepository = getRepository(Order); + + const order = await orderRepository.findOne({ + where: { + id: orderId, + }, + relations: ['buyer', 'orderItems'], + order: { + createdAt: 'DESC', // Order by creation date, most recent first + }, + }); + + if (!order) { + return responseError(res, 404, `Order Not Found.`); + } + + const vendorOrders = await vendorOrderRepository.find({ + where: { + order: { + id: order.id, + }, + }, + relations: ['vendor', 'vendorOrderItems', 'vendorOrderItems.product'], + }); + + const sanitizedOrderResponse = { + id: order.id, + totalPrice: order.totalPrice, + totalProducts: order.orderItems.length, + orderStatus: order.orderStatus, + address: order.address, + createdAt: order.createdAt, + updatedAt: order.updatedAt, + buyer: { + id: order.buyer.id, + firstName: order.buyer.firstName, + lastName: order.buyer.lastName, + email: order.buyer.lastName, + gender: order.buyer.gender, + phoneNumber: order.buyer.phoneNumber, + photoUrl: order.buyer.photoUrl, + }, + vendors: vendorOrders.map(vendoOrder => ({ + id: vendoOrder.vendor.id, + firstName: vendoOrder.vendor.firstName, + lastName: vendoOrder.vendor.lastName, + email: vendoOrder.vendor.lastName, + gender: vendoOrder.vendor.gender, + phoneNumber: vendoOrder.vendor.phoneNumber, + photoUrl: vendoOrder.vendor.photoUrl, + order: { + id: vendoOrder.id, + totalPrice: vendoOrder.totalPrice, + orderStatus: vendoOrder.orderStatus, + createdAt: order.createdAt, + updatedAt: order.updatedAt, + orderItems: vendoOrder.vendorOrderItems, + }, + })), + }; + + responseSuccess(res, 200, 'Order retrieved successfully', { + order: sanitizedOrderResponse, + }); + } catch (error) { + return responseError(res, 400, (error as Error).message); + } +}; diff --git a/src/services/adminOrderServices/updateOrder.ts b/src/services/adminOrderServices/updateOrder.ts new file mode 100644 index 0000000..876160f --- /dev/null +++ b/src/services/adminOrderServices/updateOrder.ts @@ -0,0 +1,107 @@ +import { Request, Response } from 'express'; +import { Not, getRepository } from 'typeorm'; +import { responseSuccess, responseError } from '../../utils/response.utils'; +import { VendorOrderItem } from '../../entities/VendorOrderItem'; +import { VendorOrders } from '../../entities/vendorOrders'; +import { Order } from '../../entities/Order'; +import { getIO } from '../../utils/socket'; + +export const updateBuyerVendorOrderService = async (req: Request, res: Response) => { + try { + const orderId = req.params.id; + + const vendorOrderRepository = getRepository(VendorOrders); + const orderRepository = getRepository(Order); + + const order = await orderRepository.findOne({ + where: { + id: orderId, + }, + relations: ['buyer', 'orderItems'], + }); + + if (!order) { + return responseError(res, 404, `Order Not Found.`); + } + + if (order.orderStatus === 'completed') { + return responseError(res, 409, 'The order has already been completed.'); + } + + if (order.orderStatus !== 'received') { + return responseError(res, 409, 'Order closure failed: The buyer has not received the item yet.'); + } + + const vendorOrders = await vendorOrderRepository.find({ + where: { + order: { + id: order.id, + }, + }, + relations: ['vendor', 'vendorOrderItems', 'vendorOrderItems.product'], + }); + + for (const order of vendorOrders) { + if (order.orderStatus !== 'delivered') { + return responseError(res, 409, 'Order closure failed: Some vendors have not yet delivered items to the buyer.'); + } + } + + // Update Whole Order + + order.orderStatus = 'completed'; + await orderRepository.save(order); + + const updatedVendorOrder = vendorOrders.map(async order => { + order.orderStatus = 'completed'; + await vendorOrderRepository.save(order); + }); + + const sanitizedOrderResponse = { + id: order.id, + totalPrice: order.totalPrice, + totalProducts: order.orderItems.length, + orderStatus: order.orderStatus, + address: order.address, + createdAt: order.createdAt, + updatedAt: order.updatedAt, + buyer: { + id: order.buyer.id, + firstName: order.buyer.firstName, + lastName: order.buyer.lastName, + email: order.buyer.lastName, + gender: order.buyer.gender, + phoneNumber: order.buyer.phoneNumber, + photoUrl: order.buyer.photoUrl, + }, + vendors: vendorOrders.map(vendoOrder => ({ + id: vendoOrder.vendor.id, + firstName: vendoOrder.vendor.firstName, + lastName: vendoOrder.vendor.lastName, + email: vendoOrder.vendor.lastName, + gender: vendoOrder.vendor.gender, + phoneNumber: vendoOrder.vendor.phoneNumber, + photoUrl: vendoOrder.vendor.photoUrl, + order: { + id: vendoOrder.id, + totalPrice: vendoOrder.totalPrice, + orderStatus: vendoOrder.orderStatus, + createdAt: order.createdAt, + updatedAt: order.updatedAt, + orderItems: vendoOrder.vendorOrderItems, + }, + })), + }; + + getIO().emit('orders', { + action: 'admin update', + order: sanitizedOrderResponse, + }); + + responseSuccess(res, 200, 'Order updated successfully', { + order: sanitizedOrderResponse, + }); + } catch (error) { + return responseError(res, 400, (error as Error).message); + } +}; diff --git a/src/services/couponServices/buyerApplyCoupon.ts b/src/services/couponServices/buyerApplyCoupon.ts index 12da4e1..85762f6 100644 --- a/src/services/couponServices/buyerApplyCoupon.ts +++ b/src/services/couponServices/buyerApplyCoupon.ts @@ -5,81 +5,83 @@ import { Cart } from '../../entities/Cart'; import { CartItem } from '../../entities/CartItem'; export const buyerApplyCouponService = async (req: Request, res: Response) => { - try { - const {couponCode} = req.body + try { + const { couponCode } = req.body; - if (!couponCode) return res.status(400).json({ message: 'Coupon Code is required' }); + if (!couponCode) return res.status(400).json({ message: 'Coupon Code is required' }); - const couponRepository = getRepository(Coupon); - const coupon = await couponRepository.findOne({ - where: { code: couponCode }, - relations: ['product'], - }); - - if(!coupon) return res.status(404).json({message: 'Invalid Coupon Code'}); + const couponRepository = getRepository(Coupon); + const coupon = await couponRepository.findOne({ + where: { code: couponCode }, + relations: ['product'], + }); - if(coupon){ - if(coupon.expirationDate && coupon.expirationDate < new Date()){ - return res.status(400).json({message: 'Coupon is expired'}); - } + if (!coupon) return res.status(404).json({ message: 'Invalid Coupon Code' }); - if(coupon.usageTimes == coupon.maxUsageLimit){ - return res.status(400).json({message: 'Coupon Discount Ended'}); - } + if (coupon) { + if (coupon.expirationDate && coupon.expirationDate < new Date()) { + return res.status(400).json({ message: 'Coupon is expired' }); } - const couponProductId = coupon.product.id; - - const cartRepository = getRepository(Cart) - let cart = await cartRepository.findOne({where: { user: { id: req.user?.id },isCheckedOut: false }, - relations: ['items', 'items.product'], - }); - - if(!cart) return res.status(400).json({message: "You don't have a product in cart"}); - const cartItemRepository = getRepository(CartItem); - const couponCartItem = await cartItemRepository.findOne({ - where: { - cart: { id: cart.id }, - product: { id: couponProductId }, - }, - relations: ['product'], - }); - - if(!couponCartItem) return res.status(404).json({message: 'No product in Cart with that coupon code'}); - - let amountReducted; - if(coupon.discountType === 'percentage'){ - const reduction = (couponCartItem.product.newPrice * coupon.discountRate)/ 100; - amountReducted = reduction; - couponCartItem.newPrice = couponCartItem.product.newPrice - reduction; - - await cartItemRepository.save(couponCartItem) - } - else { - amountReducted = coupon.discountRate; - couponCartItem.newPrice = couponCartItem.product.newPrice - amountReducted; - await cartItemRepository.save(couponCartItem) - } - - cart = await cartRepository.findOne({where: { id: cart.id}, - relations: ['items', 'items.product'], - }); - if(cart){ - cart.updateTotal(); - await cartRepository.save(cart); + if (coupon.usageTimes == coupon.maxUsageLimit) { + return res.status(400).json({ message: 'Coupon Discount Ended' }); } + } + const couponProductId = coupon.product.id; + + const cartRepository = getRepository(Cart); + let cart = await cartRepository.findOne({ + where: { user: { id: req.user?.id }, isCheckedOut: false }, + relations: ['items', 'items.product'], + }); + + if (!cart) return res.status(400).json({ message: "You don't have a product in cart" }); + + const cartItemRepository = getRepository(CartItem); + const couponCartItem = await cartItemRepository.findOne({ + where: { + cart: { id: cart.id }, + product: { id: couponProductId }, + }, + relations: ['product'], + }); + + if (!couponCartItem) return res.status(404).json({ message: 'No product in Cart with that coupon code' }); + + let amountReducted; + if (coupon.discountType === 'percentage') { + const reduction = (couponCartItem.product.newPrice * coupon.discountRate) / 100; + amountReducted = reduction; + couponCartItem.newPrice = couponCartItem.product.newPrice - reduction; + + await cartItemRepository.save(couponCartItem); + } else { + amountReducted = coupon.discountRate; + couponCartItem.newPrice = couponCartItem.product.newPrice - amountReducted; + await cartItemRepository.save(couponCartItem); + } - coupon.usageTimes +=1; + cart = await cartRepository.findOne({ where: { id: cart.id }, relations: ['items', 'items.product'] }); + if (cart) { + cart.updateTotal(); + await cartRepository.save(cart); + } - if(req.user?.id){ - coupon.usedBy.push(req.user?.id); - } + coupon.usageTimes += 1; - await couponRepository.save(coupon); + if (req.user?.id) { + coupon.usedBy.push(req.user?.id); + } - return (res.status(200).json({message: `Coupon Code successfully activated discount on product: ${couponCartItem.product.name}`, amountDiscounted: amountReducted })); + await couponRepository.save(coupon); - } catch (error) { - return res.status(500).json({ error: 'Internal server error' }); - } - }; \ No newline at end of file + return res + .status(200) + .json({ + message: `Coupon Code successfully activated discount on product: ${couponCartItem.product.name}`, + amountDiscounted: amountReducted, + }); + } catch (error) { + return res.status(500).json({ error: 'Internal server error' }); + } +}; diff --git a/src/services/index.ts b/src/services/index.ts index 8f560c3..12d0aa7 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -33,3 +33,11 @@ export * from './cartServices/createCart'; export * from './cartServices/readCart'; export * from './cartServices/removeProductInCart'; export * from './cartServices/clearCart'; + +// vendor order management +export * from './vendorOrderServices/readVendorOrder'; +export * from './vendorOrderServices/updateVendorOrder'; + +// vendor order management +export * from './adminOrderServices/readOrder'; +export * from './adminOrderServices/updateOrder'; diff --git a/src/services/orderServices/createOrder.ts b/src/services/orderServices/createOrder.ts index 038d796..30cbb4a 100644 --- a/src/services/orderServices/createOrder.ts +++ b/src/services/orderServices/createOrder.ts @@ -8,6 +8,9 @@ import { Cart } from '../../entities/Cart'; import { Transaction } from '../../entities/transaction'; import { responseError, sendErrorResponse, sendSuccessResponse } from '../../utils/response.utils'; import sendMail from '../../utils/sendOrderMail'; +import { VendorOrders } from '../../entities/vendorOrders'; +import { CartItem } from '../../entities/CartItem'; +import { VendorOrderItem } from '../../entities/VendorOrderItem'; export const createOrderService = async (req: Request, res: Response) => { const { cartId, address } = req.body; @@ -76,7 +79,6 @@ export const createOrderService = async (req: Request, res: Response) => { await getManager().transaction(async transactionalEntityManager => { for (const item of cart.items) { const product = item.product; - product.quantity -= item.quantity; await transactionalEntityManager.save(Product, product); } @@ -118,14 +120,67 @@ export const createOrderService = async (req: Request, res: Response) => { const message = { subject: 'Order created successfully', - ...orderResponse + ...orderResponse, }; await sendMail(message); + // separate order by each vendor getting order related to his products + await saveVendorRelatedOrder(newOrder, cart.items); + return sendSuccessResponse(res, 201, 'Order created successfully', orderResponse); } catch (error) { - console.error('Error creating order:', error); return sendErrorResponse(res, 500, (error as Error).message); } -}; \ No newline at end of file +}; + +const saveVendorRelatedOrder = async (order: Order, CartItem: CartItem[]) => { + try { + for (const item of CartItem) { + const productRepository = getRepository(Product); + + const product = await productRepository.findOne({ + where: { + id: item.product.id, + }, + relations: ['vendor'], + }); + + if (!product) return; + + const orderItem = new VendorOrderItem(); + orderItem.product = product; + orderItem['price/unit'] = product.newPrice; + orderItem.quantity = item.quantity; + + const vendorOrdersRepository = getRepository(VendorOrders); + let vendorOrders = await vendorOrdersRepository.findOne({ + where: { + vendor: { + id: product.vendor.id, + }, + order: { + id: order.id, + }, + }, + relations: ['vendorOrderItems'], + }); + + if (vendorOrders) { + vendorOrders.totalPrice = Number(vendorOrders.totalPrice) + +product.newPrice * +item.quantity; + vendorOrders.vendorOrderItems = [...vendorOrders.vendorOrderItems, orderItem]; + } else { + const newVendorOrders = new VendorOrders(); + newVendorOrders.vendor = product.vendor; + newVendorOrders.vendorOrderItems = [orderItem]; + newVendorOrders.order = order; + newVendorOrders.totalPrice = +product.newPrice * item.quantity; + vendorOrders = newVendorOrders; + } + + await vendorOrdersRepository.save(vendorOrders); + } + } catch (error) { + console.log((error as Error).message); + } +}; diff --git a/src/services/orderServices/getOrderService.ts b/src/services/orderServices/getOrderService.ts index 18e0664..4208123 100644 --- a/src/services/orderServices/getOrderService.ts +++ b/src/services/orderServices/getOrderService.ts @@ -4,62 +4,60 @@ import { responseSuccess, responseError } from '../../utils/response.utils'; import { Order } from '../../entities/Order'; import { OrderItem } from '../../entities/OrderItem'; - // Example usage: - export const getOrdersService = async (req: Request, res: Response) => { - try { - const orderRepository = getRepository(Order); - const buyerId = req.user?.id; + try { + const orderRepository = getRepository(Order); + const buyerId = req.user?.id; - const orders = await orderRepository.find({ - where: { - buyer: { - id: buyerId, - } - }, - relations: ['buyer', 'orderItems', 'orderItems.product'], - order: { - createdAt: 'DESC', // Order by creation date, most recent first - }, - }); + const orders = await orderRepository.find({ + where: { + buyer: { + id: buyerId, + }, + }, + relations: ['buyer', 'orderItems', 'orderItems.product'], + order: { + createdAt: 'DESC', // Order by creation date, most recent first + }, + }); - if (!orders || orders.length === 0) { - return responseSuccess(res, 404, `You haven't made any orders yet`, { orders: [] }); - } - - const sanitezedResponse = orders.map(order => ({ - id: order.id, - totalPrice: order.totalPrice, - orderStatus: order.orderStatus, - quantity: order.quantity, - address: order.address, - orderDate: order.orderDate, - createdAt: order.createdAt, - updatedAt: order.updatedAt, - buyer: { - id: order.buyer.id, - firstName: order.buyer.firstName, - lastName: order.buyer.lastName, - accountBalance: order.buyer.accountBalance - }, - orderItems: order.orderItems.map((item: OrderItem) => ({ - id: item.id, - price: item.price, - quantity: item.quantity, - product: { - id: item.product.id, - name: item.product.name, - description: item.product.description, - images: item.product.images, - price: item.product.newPrice, - expirationDate: item.product.expirationDate, - } - })) - })); - responseSuccess(res, 200, 'Orders retrieved successfully', { orders: sanitezedResponse }); - } catch (error) { - return responseError(res, 400, (error as Error).message); + if (!orders || orders.length === 0) { + return responseSuccess(res, 404, `You haven't made any orders yet`, { orders: [] }); } -}; \ No newline at end of file + + const sanitezedResponse = orders.map(order => ({ + id: order.id, + totalPrice: order.totalPrice, + orderStatus: order.orderStatus, + quantity: order.quantity, + address: order.address, + orderDate: order.orderDate, + createdAt: order.createdAt, + updatedAt: order.updatedAt, + buyer: { + id: order.buyer.id, + firstName: order.buyer.firstName, + lastName: order.buyer.lastName, + accountBalance: order.buyer.accountBalance, + }, + orderItems: order.orderItems.map((item: OrderItem) => ({ + id: item.id, + price: item.price, + quantity: item.quantity, + product: { + id: item.product.id, + name: item.product.name, + description: item.product.description, + images: item.product.images, + price: item.product.newPrice, + expirationDate: item.product.expirationDate, + }, + })), + })); + responseSuccess(res, 200, 'Orders retrieved successfully', { orders: sanitezedResponse }); + } catch (error) { + return responseError(res, 400, (error as Error).message); + } +}; diff --git a/src/services/orderServices/updateOrderService.ts b/src/services/orderServices/updateOrderService.ts index f29b47c..82a043a 100644 --- a/src/services/orderServices/updateOrderService.ts +++ b/src/services/orderServices/updateOrderService.ts @@ -8,122 +8,129 @@ import { Transaction } from '../../entities/transaction'; import { responseError, sendErrorResponse, sendSuccessResponse } from '../../utils/response.utils'; import sendMail from '../../utils/sendOrderMail'; interface OrderStatusType { - orderStatus: 'order placed' | 'cancelled' | 'awaiting shipment' | 'in transit' | 'delivered' | 'received' | 'returned'; + orderStatus: + | 'order placed' + | 'cancelled' + | 'awaiting shipment' + | 'in transit' + | 'delivered' + | 'received' + | 'returned'; } export const updateOrderService = async (req: Request, res: Response) => { - const { orderId } = req.params; - const { orderStatus } = req.body; - - try { - await getManager().transaction(async (transactionalEntityManager: EntityManager) => { - const orderRepository: Repository = transactionalEntityManager.getRepository(Order); - const productRepository: Repository = transactionalEntityManager.getRepository(Product); - const userRepository: Repository = transactionalEntityManager.getRepository(User); - const orderItemRepository: Repository = transactionalEntityManager.getRepository(OrderItem); - const transactionRepository: Repository = transactionalEntityManager.getRepository(Transaction); - - const buyerId = req.user?.id; - if (!buyerId) { - throw new Error('Unauthorized'); + const { orderId } = req.params; + const { orderStatus } = req.body; + + try { + await getManager().transaction(async (transactionalEntityManager: EntityManager) => { + const orderRepository: Repository = transactionalEntityManager.getRepository(Order); + const productRepository: Repository = transactionalEntityManager.getRepository(Product); + const userRepository: Repository = transactionalEntityManager.getRepository(User); + const orderItemRepository: Repository = transactionalEntityManager.getRepository(OrderItem); + const transactionRepository: Repository = transactionalEntityManager.getRepository(Transaction); + + const buyerId = req.user?.id; + if (!buyerId) { + throw new Error('Unauthorized'); + } + + // Fetch order and related entities + const order: Order | null = await orderRepository.findOne({ + where: { id: orderId, buyer: { id: buyerId } }, + relations: ['orderItems', 'orderItems.product', 'buyer'], + }); + + if (!order) { + return sendErrorResponse(res, 404, 'Order not found'); + } + // Check if order can be updated + if (isOrderFinalStatus(order.orderStatus)) { + return sendErrorResponse(res, 401, `Order cannot be updated once it is ${order.orderStatus}`); + } + + // Handle order status transitions + if (orderStatus !== undefined && order.orderStatus !== orderStatus) { + switch (orderStatus) { + case 'cancelled': + case 'returned': + if (order.orderStatus !== 'delivered') { + await processRefund(order, transactionalEntityManager); } - - // Fetch order and related entities - const order: Order | null = await orderRepository.findOne({ - where: { id: orderId, buyer: { id: buyerId } }, - relations: ['orderItems', 'orderItems.product', 'buyer'], - }); - - if (!order) { - return sendErrorResponse(res, 404, "Order not found"); - } - // Check if order can be updated - if (isOrderFinalStatus(order.orderStatus)) { - return sendErrorResponse(res, 401, `Order cannot be updated once it is ${order.orderStatus}`); - } - - // Handle order status transitions - if (orderStatus !== undefined && order.orderStatus !== orderStatus) { - switch (orderStatus) { - case 'cancelled': - case 'returned': - if (order.orderStatus !== 'delivered') { - await processRefund(order, transactionalEntityManager); - } - break; - default: - break; - } - - order.orderStatus = orderStatus; - } - - // Save updated order status - await orderRepository.save(order); - - // Prepare response data - const orderResponse = { - fullName: `${order.buyer.firstName} ${order.buyer.lastName}`, - email: order.buyer.email, - products: order.orderItems.map((item: OrderItem) => ({ - name: item.product.name, - newPrice: item.price, - quantity: item.quantity, - })), - totalAmount: order.totalPrice, - quantity: order.quantity, - orderDate: order.orderDate, - address: order.address, - }; - - // Send email notification - const message = { - subject: 'Order updated successfully', - ...orderResponse - }; - await sendMail(message); - - // Respond with success - return sendSuccessResponse(res, 200, 'Order updated successfully', orderResponse); - }); - } catch (error) { - console.error('Error updating order:', error); - return sendErrorResponse(res, 500, (error as Error).message); - } + break; + default: + break; + } + + order.orderStatus = orderStatus; + } + + // Save updated order status + await orderRepository.save(order); + + // Prepare response data + const orderResponse = { + fullName: `${order.buyer.firstName} ${order.buyer.lastName}`, + email: order.buyer.email, + products: order.orderItems.map((item: OrderItem) => ({ + name: item.product.name, + newPrice: item.price, + quantity: item.quantity, + })), + totalAmount: order.totalPrice, + quantity: order.quantity, + orderDate: order.orderDate, + address: order.address, + }; + + // Send email notification + const message = { + subject: 'Order updated successfully', + ...orderResponse, + }; + await sendMail(message); + + // Respond with success + return sendSuccessResponse(res, 200, 'Order updated successfully', orderResponse); + }); + } catch (error) { + console.error('Error updating order:', error); + return sendErrorResponse(res, 500, (error as Error).message); + } }; -async function processRefund(order: Order, entityManager: EntityManager) { - const buyer = order.buyer; - - // Refund buyer - const previousBalance = buyer.accountBalance; - buyer.accountBalance += order.totalPrice; - const currentBalance = buyer.accountBalance; - await entityManager.save(buyer); - - // Record refund transaction - const refundTransaction = new Transaction(); - refundTransaction.user = buyer; - refundTransaction.order = order; - refundTransaction.amount = order.totalPrice; - refundTransaction.previousBalance = previousBalance; - refundTransaction.currentBalance = currentBalance; - refundTransaction.type = 'credit'; - refundTransaction.description = 'Refund for cancelled or returned order'; - await entityManager.save(refundTransaction); - - // Return products to store - for (const orderItem of order.orderItems) { - const product = orderItem.product; - product.quantity += orderItem.quantity; - await entityManager.save(product); - } - - // Clear order details - order.orderItems = []; - order.totalPrice = 0; - order.quantity = 0; +async function processRefund (order: Order, entityManager: EntityManager) { + const buyer = order.buyer; + + // Refund buyer + const previousBalance = buyer.accountBalance; + buyer.accountBalance += order.totalPrice; + const currentBalance = buyer.accountBalance; + await entityManager.save(buyer); + + // Record refund transaction + const refundTransaction = new Transaction(); + refundTransaction.user = buyer; + refundTransaction.order = order; + refundTransaction.amount = order.totalPrice; + refundTransaction.previousBalance = previousBalance; + refundTransaction.currentBalance = currentBalance; + refundTransaction.type = 'credit'; + refundTransaction.description = 'Refund for cancelled or returned order'; + await entityManager.save(refundTransaction); + + // Return products to store + for (const orderItem of order.orderItems) { + const product = orderItem.product; + product.quantity += orderItem.quantity; + await entityManager.save(product); + } + + // Clear order details + order.orderItems = []; + order.totalPrice = 0; + order.quantity = 0; } -function isOrderFinalStatus(status: string): boolean { - return ['cancelled', 'delivered', 'returned'].includes(status); -} \ No newline at end of file +function isOrderFinalStatus (status: string): boolean { + return ['cancelled', 'delivered', 'returned', 'completed'].includes(status); +} diff --git a/src/services/productServices/deleteProduct.ts b/src/services/productServices/deleteProduct.ts index 43ec3d1..068c4c9 100644 --- a/src/services/productServices/deleteProduct.ts +++ b/src/services/productServices/deleteProduct.ts @@ -3,30 +3,28 @@ import { Product } from '../../entities/Product'; import { getRepository } from 'typeorm'; import { responseError, responseSuccess } from '../../utils/response.utils'; - export const deleteProductService = async (req: Request, res: Response) => { - try { - const { id } = req.params; - - const productRepository = getRepository(Product); + try { + const { id } = req.params; - const product = await productRepository.findOne({ - where: { - id: id, - vendor: { - id: req.user?.id - } - } - }); + const productRepository = getRepository(Product); - if (product) { - await productRepository.remove(product); - return responseSuccess(res, 200, 'Product successfully deleted'); - } + const product = await productRepository.findOne({ + where: { + id: id, + vendor: { + id: req.user?.id, + }, + }, + }); - return responseError(res, 404, 'Product not found'); - - } catch (error) { - responseError(res, 400, (error as Error).message); + if (product) { + await productRepository.remove(product); + return responseSuccess(res, 200, 'Product successfully deleted'); } + + return responseError(res, 404, 'Product not found'); + } catch (error) { + responseError(res, 400, (error as Error).message); + } }; diff --git a/src/services/productServices/getRecommendedProductsService.ts b/src/services/productServices/getRecommendedProductsService.ts index 19368e1..fde015d 100644 --- a/src/services/productServices/getRecommendedProductsService.ts +++ b/src/services/productServices/getRecommendedProductsService.ts @@ -1,62 +1,63 @@ -import { Request, Response } from "express"; -import { responseError, responseSuccess } from "../../utils/response.utils"; -import { getRepository } from "typeorm"; -import { Product } from "../../entities/Product"; +import { Request, Response } from 'express'; +import { responseError, responseSuccess } from '../../utils/response.utils'; +import { getRepository } from 'typeorm'; +import { Product } from '../../entities/Product'; interface conditionDoc { - categories: any[] | null; - vendor: any | null + categories: any[] | null; + vendor: any | null; } export const getRecommendedProductsService = async (req: Request, res: Response) => { + try { + // Define pagination parameters + const page = req.query.page ? Number(req.query.page) : 1; + const limit = req.query.limit ? Number(req.query.limit) : 10; + const skip = (page - 1) * limit; + const condition: conditionDoc = { + categories: null, + vendor: null, + }; - try { - // Define pagination parameters - const page = req.query.page ? Number(req.query.page) : 1; - const limit = req.query.limit ? Number(req.query.limit) : 10; - const skip = (page - 1) * limit; - const condition: conditionDoc = { - categories: null, - vendor: null - }; - - if (req.query.categories) { - const categoryIds = Array.isArray(req.query.categories) ? req.query.categories : [req.query.categories]; - condition.categories = categoryIds; - }; - if (req.query.vendor) condition.vendor = req.query.vendor; + if (req.query.categories) { + const categoryIds = Array.isArray(req.query.categories) ? req.query.categories : [req.query.categories]; + condition.categories = categoryIds; + } + if (req.query.vendor) condition.vendor = req.query.vendor; - const productRepository = getRepository(Product); - const productsQuery = productRepository.createQueryBuilder("product") - .leftJoinAndSelect("product.categories", "category") - .leftJoinAndSelect("product.vendor", "vendor") - .where("1 = 1"); + const productRepository = getRepository(Product); + const productsQuery = productRepository + .createQueryBuilder('product') + .leftJoinAndSelect('product.categories', 'category') + .leftJoinAndSelect('product.vendor', 'vendor') + .where('1 = 1'); - if (condition.categories && condition.categories.length > 0) { - productsQuery.andWhere("category.id IN (:...categories)", { categories: condition.categories }); - } - if (condition.vendor) { - productsQuery.andWhere("vendor.id = :vendorId", { vendorId: condition.vendor }); - } + if (condition.categories && condition.categories.length > 0) { + productsQuery.andWhere('category.id IN (:...categories)', { categories: condition.categories }); + } + if (condition.vendor) { + productsQuery.andWhere('vendor.id = :vendorId', { vendorId: condition.vendor }); + } - const products = await productsQuery - .skip(skip) - .take(limit) - .getMany(); - if (products.length < 1) { - return responseSuccess(res, 200, `No products found for the specified ${condition.vendor ? 'vendor' : 'category'}`); - } - const sanitizedProducts = products.map(product => ({ - ...product, - vendor: { - firstName: product.vendor.firstName, - lastName: product.vendor.lastName, - phoneNumber: product.vendor.phoneNumber, - photoUrl: product.vendor.photoUrl - } - })); - return responseSuccess(res, 200, 'Products retrieved', { products: sanitizedProducts }); - } catch (error) { - return responseError(res, 400, (error as Error).message); + const products = await productsQuery.skip(skip).take(limit).getMany(); + if (products.length < 1) { + return responseSuccess( + res, + 200, + `No products found for the specified ${condition.vendor ? 'vendor' : 'category'}` + ); } -}; \ No newline at end of file + const sanitizedProducts = products.map(product => ({ + ...product, + vendor: { + firstName: product.vendor.firstName, + lastName: product.vendor.lastName, + phoneNumber: product.vendor.phoneNumber, + photoUrl: product.vendor.photoUrl, + }, + })); + return responseSuccess(res, 200, 'Products retrieved', { products: sanitizedProducts }); + } catch (error) { + return responseError(res, 400, (error as Error).message); + } +}; diff --git a/src/services/productServices/listAllProductsService.ts b/src/services/productServices/listAllProductsService.ts index 8950abd..f39c7bb 100644 --- a/src/services/productServices/listAllProductsService.ts +++ b/src/services/productServices/listAllProductsService.ts @@ -5,38 +5,40 @@ import { responseError, responseSuccess } from '../../utils/response.utils'; import { validate } from 'uuid'; export const listAllProductsService = async (req: Request, res: Response) => { - try { - const page = req.query.page ? Number(req.query.page) : 1; - const limit = req.query.limit ? Number(req.query.limit) : 10; - const skip = (page - 1) * limit; - const category = req.query.category ; + try { + const page = req.query.page ? Number(req.query.page) : 1; + const limit = req.query.limit ? Number(req.query.limit) : 10; + const skip = (page - 1) * limit; + const category = req.query.category; - - const productRepository = getRepository(Product); - const products = await productRepository.find({ - where: { - categories: { - name: category as string - } - }, - skip, - take: limit, - relations: ["categories","vendor"], - select: { - vendor: { - id: true, firstName: true, lastName: true, - email: true, phoneNumber: true, photoUrl: true - } - } - } - ); + const productRepository = getRepository(Product); + const products = await productRepository.find({ + where: { + categories: { + name: category as string, + }, + }, + skip, + take: limit, + relations: ['categories', 'vendor'], + select: { + vendor: { + id: true, + firstName: true, + lastName: true, + email: true, + phoneNumber: true, + photoUrl: true, + }, + }, + }); - if (products.length < 1) { - return responseSuccess(res, 200, 'No products found'); - } - - return responseSuccess(res, 200, 'Products retrieved', { products }); - } catch (error) { - responseError(res, 400, (error as Error).message); + if (products.length < 1) { + return responseSuccess(res, 200, 'No products found'); } + + return responseSuccess(res, 200, 'Products retrieved', { products }); + } catch (error) { + responseError(res, 400, (error as Error).message); + } }; diff --git a/src/services/productServices/readProduct.ts b/src/services/productServices/readProduct.ts index 5c9257c..b3c244d 100644 --- a/src/services/productServices/readProduct.ts +++ b/src/services/productServices/readProduct.ts @@ -4,67 +4,75 @@ import { getRepository } from 'typeorm'; import { responseError, responseSuccess } from '../../utils/response.utils'; export const readProductsService = async (req: Request, res: Response) => { - try { - // Define pagination parameters - const page = req.query.page ? Number(req.query.page) : 1; - const limit = req.query.limit ? Number(req.query.limit) : 10; - const skip = (page - 1) * limit; + try { + // Define pagination parameters + const page = req.query.page ? Number(req.query.page) : 1; + const limit = req.query.limit ? Number(req.query.limit) : 10; + const skip = (page - 1) * limit; - // Retrieve products - const productRepository = getRepository(Product); - const products = await productRepository.find({ - where: { - vendor: { - id: req.user?.id, - }, - }, - skip, - take: limit, - relations: ['categories', 'vendor'], - select: { - vendor: { - id: true, firstName: true, lastName: true, - email: true, phoneNumber: true, photoUrl: true - } - } - }); + // Retrieve products + const productRepository = getRepository(Product); + const products = await productRepository.find({ + where: { + vendor: { + id: req.user?.id, + }, + }, + skip, + take: limit, + relations: ['categories', 'vendor'], + select: { + vendor: { + id: true, + firstName: true, + lastName: true, + email: true, + phoneNumber: true, + photoUrl: true, + }, + }, + }); - if (products.length < 1) { - return responseSuccess(res, 200, 'You have no products yet'); - } - return responseSuccess(res, 200, 'Products retrieved', { products }); - } catch (error) { - responseError(res, 400, (error as Error).message); + if (products.length < 1) { + return responseSuccess(res, 200, 'You have no products yet'); } + return responseSuccess(res, 200, 'Products retrieved', { products }); + } catch (error) { + responseError(res, 400, (error as Error).message); + } }; export const readProductService = async (req: Request, res: Response) => { - try { - const { id } = req.params; + try { + const { id } = req.params; - const productRepository = getRepository(Product); - const product = await productRepository.findOne({ - where: { - id: id, - vendor: { - id: req.user?.id, - }, - }, - relations: ['categories', 'vendor'], - select: { - vendor: { - id: true, firstName: true, lastName: true, - email: true, phoneNumber: true, photoUrl: true - } - } - }); + const productRepository = getRepository(Product); + const product = await productRepository.findOne({ + where: { + id: id, + vendor: { + id: req.user?.id, + }, + }, + relations: ['categories', 'vendor'], + select: { + vendor: { + id: true, + firstName: true, + lastName: true, + email: true, + phoneNumber: true, + photoUrl: true, + }, + }, + }); - if (!product) { - return responseError(res, 404, 'Product not found'); - } - - return responseSuccess(res, 200, 'Product retrieved', { product }); - } catch (error) { - responseError(res, 400, (error as Error).message); + if (!product) { + return responseError(res, 404, 'Product not found'); } + + return responseSuccess(res, 200, 'Product retrieved', { product }); + } catch (error) { + responseError(res, 400, (error as Error).message); + } }; diff --git a/src/services/productServices/removeProductImage.ts b/src/services/productServices/removeProductImage.ts index 2995593..4424676 100644 --- a/src/services/productServices/removeProductImage.ts +++ b/src/services/productServices/removeProductImage.ts @@ -21,7 +21,7 @@ export const removeProductImageService = async (req: Request, res: Response) => const product = await productRepository.findOne({ where: { id, - vendor: { id: req.user?.id } + vendor: { id: req.user?.id }, }, relations: ['vendor'], }); diff --git a/src/services/productServices/searchProduct.ts b/src/services/productServices/searchProduct.ts index 765f431..9f33b5f 100644 --- a/src/services/productServices/searchProduct.ts +++ b/src/services/productServices/searchProduct.ts @@ -1,4 +1,4 @@ -import { Request, Response } from "express"; +import { Request, Response } from 'express'; import { getRepository, Like } from 'typeorm'; import { Product } from '../../entities/Product'; @@ -26,10 +26,7 @@ export const searchProductService = async (params: SearchProductParams) => { const skip = (page - 1) * limit; - const [products, total] = await query - .skip(skip) - .take(limit) - .getManyAndCount(); + const [products, total] = await query.skip(skip).take(limit).getManyAndCount(); const totalPages = Math.ceil(total / limit); diff --git a/src/services/productServices/viewSingleProduct.ts b/src/services/productServices/viewSingleProduct.ts index f956625..29ac167 100644 --- a/src/services/productServices/viewSingleProduct.ts +++ b/src/services/productServices/viewSingleProduct.ts @@ -4,35 +4,28 @@ import { getRepository } from 'typeorm'; import { responseError } from '../../utils/response.utils'; import { validate } from 'uuid'; - - export const viewSingleProduct = async (req: Request, res: Response) => { - try { - const productId = req.params.id; + try { + const productId = req.params.id; + + if (!validate(productId)) { + return res.status(400).json({ status: 'error', message: 'Invalid product ID' }); + } + if (productId) { + const products = getRepository(Product); + const product = await products.findOneBy({ id: productId }); - if (!validate(productId)) { - return res.status(400).json({ status: 'error', message: 'Invalid product ID' }); - + if (!product) { + return res.status(404).send({ status: 'error', message: 'Product not found' }); } - if(productId){ - const products = getRepository(Product); - const product = await products.findOneBy({ id: productId }); - - if (!product) { - return res.status(404).send({status:'error', message: 'Product not found'}); - - } - - if (product.expirationDate && new Date(product.expirationDate) < new Date()) { - return res.status(400).json({ status: 'error', message: 'Product expired' }); - - } - res.status(200).json({ status: 'success', product: product }); + if (product.expirationDate && new Date(product.expirationDate) < new Date()) { + return res.status(400).json({ status: 'error', message: 'Product expired' }); } - - } catch (error) { - console.error('Error handling request:', error); - res.status(500).send('Error fetching product details'); + res.status(200).json({ status: 'success', product: product }); } -} \ No newline at end of file + } catch (error) { + console.error('Error handling request:', error); + res.status(500).send('Error fetching product details'); + } +}; diff --git a/src/services/vendorOrderServices/readVendorOrder.ts b/src/services/vendorOrderServices/readVendorOrder.ts new file mode 100644 index 0000000..feec0c3 --- /dev/null +++ b/src/services/vendorOrderServices/readVendorOrder.ts @@ -0,0 +1,119 @@ +import { Request, Response } from 'express'; +import { getRepository } from 'typeorm'; +import { responseSuccess, responseError } from '../../utils/response.utils'; +import { VendorOrderItem } from '../../entities/VendorOrderItem'; +import { VendorOrders } from '../../entities/vendorOrders'; + +export const getVendorOrdersService = async (req: Request, res: Response) => { + try { + const vendorOrderRepository = getRepository(VendorOrders); + const vendorId = req.user?.id; + + const vendorOrders = await vendorOrderRepository.find({ + where: { + vendor: { + id: vendorId, + }, + }, + relations: ['vendor', 'order.buyer', 'vendorOrderItems', 'vendorOrderItems.product'], + order: { + createdAt: 'DESC', // Order by creation date, most recent first + }, + }); + + if (!vendorOrders.length) { + return responseError(res, 200, `You don't have any pending orders from buyer`, { orders: [] }); + } + + const sanitizedOrderResponse = vendorOrders.map(order => ({ + id: order.id, + totalPrice: order.totalPrice, + orderStatus: order.orderStatus, + address: order.order.address, + createdAt: order.createdAt, + updatedAt: order.updatedAt, + vendor: { + id: order.vendor.id, + firstName: order.vendor.firstName, + lastName: order.vendor.lastName, + email: order.vendor.email, + gender: order.vendor.gender, + phoneNumber: order.vendor.phoneNumber, + photoUrl: order.vendor.photoUrl, + }, + buyer: { + id: order.order.buyer.id, + firstName: order.order.buyer.firstName, + lastName: order.order.buyer.lastName, + email: order.order.buyer.lastName, + gender: order.order.buyer.gender, + phoneNumber: order.order.buyer.phoneNumber, + photoUrl: order.order.buyer.photoUrl, + }, + vendorOrderItems: order.vendorOrderItems, + })); + responseSuccess(res, 200, 'Orders retrieved successfully', { + orders: sanitizedOrderResponse, + }); + } catch (error) { + return responseError(res, 400, (error as Error).message); + } +}; + +// Get single vendor order info +export const getSingleVendorOrderService = async (req: Request, res: Response) => { + try { + const vendorOrderId = req.params.id; + + const vendorOrderRepository = getRepository(VendorOrders); + const vendorId = req.user?.id; + + const vendorOrder = await vendorOrderRepository.findOne({ + where: { + id: vendorOrderId, + vendor: { + id: vendorId, + }, + }, + relations: ['vendor', 'order.buyer', 'vendorOrderItems', 'vendorOrderItems.product'], + }); + + if (!vendorOrder) { + return responseError(res, 404, `Order Not Found.`); + } + + const sanitizedOrderResponse = { + id: vendorOrder.id, + totalPrice: vendorOrder.totalPrice, + orderStatus: vendorOrder.orderStatus, + address: vendorOrder.order.address, + createdAt: vendorOrder.createdAt, + updatedAt: vendorOrder.updatedAt, + vendor: { + id: vendorOrder.vendor.id, + firstName: vendorOrder.vendor.firstName, + lastName: vendorOrder.vendor.lastName, + email: vendorOrder.vendor.email, + gender: vendorOrder.vendor.gender, + phoneNumber: vendorOrder.vendor.phoneNumber, + photoUrl: vendorOrder.vendor.photoUrl, + }, + buyer: { + id: vendorOrder.order.buyer.id, + firstName: vendorOrder.order.buyer.firstName, + lastName: vendorOrder.order.buyer.lastName, + email: vendorOrder.order.buyer.lastName, + gender: vendorOrder.order.buyer.gender, + phoneNumber: vendorOrder.order.buyer.phoneNumber, + photoUrl: vendorOrder.order.buyer.photoUrl, + }, + vendorOrderItems: vendorOrder.vendorOrderItems, + }; + + responseSuccess(res, 200, 'Order retrieved successfully', { + order: sanitizedOrderResponse, + }); + } catch (error) { + return responseError(res, 400, (error as Error).message); + } +}; diff --git a/src/services/vendorOrderServices/updateVendorOrder.ts b/src/services/vendorOrderServices/updateVendorOrder.ts new file mode 100644 index 0000000..cae2c60 --- /dev/null +++ b/src/services/vendorOrderServices/updateVendorOrder.ts @@ -0,0 +1,87 @@ +import { Request, Response } from 'express'; +import { getRepository } from 'typeorm'; +import { OrderItem } from '../../entities/OrderItem'; +import { responseError, responseSuccess } from '../../utils/response.utils'; +import sendMail from '../../utils/sendOrderMail'; +import { VendorOrders } from '../../entities/vendorOrders'; +import { getIO } from '../../utils/socket'; + +export const updateVendorOrderService = async (req: Request, res: Response) => { + try { + const vendorOrderId = req.params.id; + const { orderStatus } = req.body; + if ( + !['pending', 'is-accepted', 'in-transit', 'cancelled', 'delivered'].includes( + (orderStatus as string).toLowerCase() + ) + ) { + return responseError(res, 400, `Please provide one of defined statuses.`); + } + + const vendorOrderRepository = getRepository(VendorOrders); + const vendorId = req.user?.id; + + const vendorOrder = await vendorOrderRepository.findOne({ + where: { + id: vendorOrderId, + vendor: { + id: vendorId, + }, + }, + relations: ['vendor', 'order.buyer', 'vendorOrderItems', 'vendorOrderItems.product'], + }); + + if (!vendorOrder) { + return responseError(res, 404, `Order Not Found.`); + } + + // Check if order can be updated + if (['delivered', 'cancelled', 'completed'].includes(vendorOrder.orderStatus)) { + return responseError(res, 409, `Order cannot be updated once it is marked as ${vendorOrder.orderStatus}`); + } + + vendorOrder.orderStatus = (orderStatus as string).toLowerCase(); + + // Save updated order status + const updatedVendorOrder = await vendorOrderRepository.save(vendorOrder); + + const sanitizedOrderResponse = { + id: updatedVendorOrder.id, + totalPrice: updatedVendorOrder.totalPrice, + orderStatus: updatedVendorOrder.orderStatus, + address: updatedVendorOrder.order.address, + createdAt: updatedVendorOrder.createdAt, + updatedAt: updatedVendorOrder.updatedAt, + vendor: { + id: updatedVendorOrder.vendor.id, + firstName: updatedVendorOrder.vendor.firstName, + lastName: updatedVendorOrder.vendor.lastName, + email: updatedVendorOrder.vendor.email, + gender: updatedVendorOrder.vendor.gender, + phoneNumber: updatedVendorOrder.vendor.phoneNumber, + photoUrl: updatedVendorOrder.vendor.photoUrl, + }, + buyer: { + id: updatedVendorOrder.order.buyer.id, + firstName: updatedVendorOrder.order.buyer.firstName, + lastName: updatedVendorOrder.order.buyer.lastName, + email: updatedVendorOrder.order.buyer.lastName, + gender: updatedVendorOrder.order.buyer.gender, + phoneNumber: updatedVendorOrder.order.buyer.phoneNumber, + photoUrl: updatedVendorOrder.order.buyer.photoUrl, + }, + vendorOrderItems: updatedVendorOrder.vendorOrderItems, + }; + + getIO().emit('orders', { + action: 'vendor update', + order: sanitizedOrderResponse, + }); + + return responseSuccess(res, 200, 'Order updated successfully', { + order: sanitizedOrderResponse, + }); + } catch (error) { + return responseError(res, 400, (error as Error).message); + } +}; diff --git a/src/services/wishListServices/addProduct.ts b/src/services/wishListServices/addProduct.ts index da3db89..79d0a38 100644 --- a/src/services/wishListServices/addProduct.ts +++ b/src/services/wishListServices/addProduct.ts @@ -4,57 +4,54 @@ import { getRepository } from 'typeorm'; import { wishList } from '../../entities/wishList'; import { Product } from '../../entities/Product'; -export const addProductService = async (req:Request,res:Response)=>{ - try { +export const addProductService = async (req: Request, res: Response) => { + try { + const id = req.params.id; + const wishListRepository = getRepository(wishList); + const productRepository = getRepository(Product); - const id = req.params.id; - const wishListRepository = getRepository(wishList); - const productRepository = getRepository(Product); + const product = await productRepository.findOne({ where: { id } }); + if (!product) { + return res.status(404).json({ message: 'Product not found' }); + } - const product = await productRepository.findOne({where: { id }}); - - if(!product){ - return res.status(404).json({message: "Product not found"}); - } - - const productDetails = { - productId: product.id, - name: product.name, - image: product.images, - newPrice: product.newPrice, - vendorId: product.vendor - } - - const alreadyIn = await wishListRepository.findOne({where: {productId: id, buyer:{ id: req.user?.id} }}) - - if(alreadyIn){ - return res.status(401).json({ - data: { - message: 'Product Already in the wish list', - wishlistAdded: alreadyIn, - product: productDetails, - }, - }) - } - - const addNewProduct = new wishList(); - addNewProduct.productId = id; - addNewProduct.buyer = req.user as User; - - await wishListRepository.save(addNewProduct); + const productDetails = { + productId: product.id, + name: product.name, + image: product.images, + newPrice: product.newPrice, + vendorId: product.vendor, + }; - addNewProduct.buyer = { id: addNewProduct.buyer.id } as unknown as User; + const alreadyIn = await wishListRepository.findOne({ where: { productId: id, buyer: { id: req.user?.id } } }); - return res.status(201).json({ + if (alreadyIn) { + return res.status(401).json({ data: { - message: 'Product Added to wish list', - wishlistAdded: addNewProduct, + message: 'Product Already in the wish list', + wishlistAdded: alreadyIn, product: productDetails, - }, - }); - - } catch (error) { - return res.status(500).json({ error: 'Internal server error' }); + }, + }); } -} \ No newline at end of file + + const addNewProduct = new wishList(); + addNewProduct.productId = id; + addNewProduct.buyer = req.user as User; + + await wishListRepository.save(addNewProduct); + + addNewProduct.buyer = { id: addNewProduct.buyer.id } as unknown as User; + + return res.status(201).json({ + data: { + message: 'Product Added to wish list', + wishlistAdded: addNewProduct, + product: productDetails, + }, + }); + } catch (error) { + return res.status(500).json({ error: 'Internal server error' }); + } +}; diff --git a/src/services/wishListServices/clearAll.ts b/src/services/wishListServices/clearAll.ts index 88af3c6..7299454 100644 --- a/src/services/wishListServices/clearAll.ts +++ b/src/services/wishListServices/clearAll.ts @@ -2,19 +2,18 @@ import { Request, Response } from 'express'; import { getRepository } from 'typeorm'; import { wishList } from '../../entities/wishList'; -export const clearAllProductService = async (req:Request,res:Response)=>{ - try { - const wishListRepository = getRepository(wishList); - const productsForBuyer = await wishListRepository.find({where: { buyer:{ id: req.user?.id} }}); +export const clearAllProductService = async (req: Request, res: Response) => { + try { + const wishListRepository = getRepository(wishList); + const productsForBuyer = await wishListRepository.find({ where: { buyer: { id: req.user?.id } } }); - if (productsForBuyer.length === 0) { - return res.status(404).json({ message: 'No products in wish list' }); - } - - await wishListRepository.remove(productsForBuyer); - return res.status(200).json({ message: 'All products removed successfully'}); - - } catch (error) { - return res.status(500).json({ error: 'Internal server error' }); + if (productsForBuyer.length === 0) { + return res.status(404).json({ message: 'No products in wish list' }); } -} \ No newline at end of file + + await wishListRepository.remove(productsForBuyer); + return res.status(200).json({ message: 'All products removed successfully' }); + } catch (error) { + return res.status(500).json({ error: 'Internal server error' }); + } +}; diff --git a/src/services/wishListServices/getProducts.ts b/src/services/wishListServices/getProducts.ts index 107f3aa..98dc434 100644 --- a/src/services/wishListServices/getProducts.ts +++ b/src/services/wishListServices/getProducts.ts @@ -3,36 +3,37 @@ import { getRepository } from 'typeorm'; import { wishList } from '../../entities/wishList'; import { Product } from '../../entities/Product'; -export const getProductsService = async (req:Request,res:Response)=>{ - try { - const wishListRepository = getRepository(wishList); - const productRepository =getRepository(Product); +export const getProductsService = async (req: Request, res: Response) => { + try { + const wishListRepository = getRepository(wishList); + const productRepository = getRepository(Product); - const productsForBuyer = await wishListRepository.find({where: { buyer:{ id: req.user?.id} }}); + const productsForBuyer = await wishListRepository.find({ where: { buyer: { id: req.user?.id } } }); - if (productsForBuyer.length === 0) { - return res.status(404).json({ message: 'No products in wish list', products: productsForBuyer }); - } - - const buyerWishProducts = await Promise.all(productsForBuyer.map(async (product) => { - const productDetails = await productRepository.findOne({ where: { id: product.productId } }); - if(productDetails){ - return { - wishListDetails: product, - productInfo: { - productId: productDetails.id, - name: productDetails.name, - image: productDetails.images, - newPrice: productDetails.newPrice, - vendorId: productDetails.vendor - } - }; - } - })); + if (productsForBuyer.length === 0) { + return res.status(404).json({ message: 'No products in wish list', products: productsForBuyer }); + } - return res.status(200).json({ message: 'Products retrieved', productsForBuyer: buyerWishProducts }); + const buyerWishProducts = await Promise.all( + productsForBuyer.map(async product => { + const productDetails = await productRepository.findOne({ where: { id: product.productId } }); + if (productDetails) { + return { + wishListDetails: product, + productInfo: { + productId: productDetails.id, + name: productDetails.name, + image: productDetails.images, + newPrice: productDetails.newPrice, + vendorId: productDetails.vendor, + }, + }; + } + }) + ); - } catch (error) { - return res.status(500).json({ error: 'Internal server error' }); - } -} \ No newline at end of file + return res.status(200).json({ message: 'Products retrieved', productsForBuyer: buyerWishProducts }); + } catch (error) { + return res.status(500).json({ error: 'Internal server error' }); + } +}; diff --git a/src/services/wishListServices/removeProducts.ts b/src/services/wishListServices/removeProducts.ts index cb99c0f..b42052f 100644 --- a/src/services/wishListServices/removeProducts.ts +++ b/src/services/wishListServices/removeProducts.ts @@ -2,22 +2,20 @@ import { Request, Response } from 'express'; import { getRepository } from 'typeorm'; import { wishList } from '../../entities/wishList'; -export const removeProductService = async (req:Request,res:Response)=>{ - try { +export const removeProductService = async (req: Request, res: Response) => { + try { + const id = parseInt(req.params.id); + const wishListRepository = getRepository(wishList); - const id = parseInt(req.params.id); - const wishListRepository = getRepository(wishList); + const product = await wishListRepository.findOne({ where: { id } }); - const product = await wishListRepository.findOne({where: { id }}); - - if(!product){ - return res.status(404).json({message: "Product not found in wish list"}); - } - - await wishListRepository.remove(product); - return res.status(200).json({ message: "Product removed from wish list" }); - - } catch (error) { - return res.status(500).json({ error: 'Internal server error' }); + if (!product) { + return res.status(404).json({ message: 'Product not found in wish list' }); } -} \ No newline at end of file + + await wishListRepository.remove(product); + return res.status(200).json({ message: 'Product removed from wish list' }); + } catch (error) { + return res.status(500).json({ error: 'Internal server error' }); + } +}; diff --git a/src/startups/getSwaggerServer.ts b/src/startups/getSwaggerServer.ts index efe12fa..6a7416d 100644 --- a/src/startups/getSwaggerServer.ts +++ b/src/startups/getSwaggerServer.ts @@ -7,7 +7,7 @@ function getSwaggerServer (): string { return process.env.SWAGGER_SERVER; } - return `http://localhost:${process.env.PORT}/api/v1`; + return `http://localhost:${process.env.PORT}`; } export { getSwaggerServer }; diff --git a/src/utils/auth.ts b/src/utils/auth.ts index 91874e3..623883f 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -1,72 +1,66 @@ /* eslint-disable camelcase */ import passport from 'passport'; -import { Strategy } from "passport-google-oauth20"; +import { Strategy } from 'passport-google-oauth20'; import { User } from '../entities/User'; import { getRepository } from 'typeorm'; import bcrypt from 'bcrypt'; -import "../utils/auth"; +import '../utils/auth'; passport.use( - new Strategy( - { - clientID: process.env.GOOGLE_CLIENT_ID as string, - clientSecret: process.env.GOOGLE_CLIENT_SECRET as string, - callbackURL: 'http://localhost:6890/user/auth/google/callback/', - scope: ['email', 'profile'], - }, - async (accessToken: any, refreshToken: any, profile: any, cb: any) => { - const userRepository = getRepository(User); - const { family_name, - name, - picture, - email, - email_verified + new Strategy( + { + clientID: process.env.GOOGLE_CLIENT_ID as string, + clientSecret: process.env.GOOGLE_CLIENT_SECRET as string, + callbackURL: 'http://localhost:6890/user/auth/google/callback/', + scope: ['email', 'profile'], + }, + async (accessToken: any, refreshToken: any, profile: any, cb: any) => { + const userRepository = getRepository(User); + const { family_name, name, picture, email, email_verified } = profile._json; + const { familyName, givenName } = profile.name; - } = profile._json; - const { familyName, givenName } = profile.name; + if (email || givenName || family_name || picture) { + try { + // Check for existing user + const existingUser = await userRepository.findOneBy({ email }); - if (email || givenName || family_name || picture) { - try { - // Check for existing user - const existingUser = await userRepository.findOneBy({ email }); + if (existingUser) { + return await cb(null, existingUser); + } + const saltRounds = 10; + const hashedPassword = await bcrypt.hash('password', saltRounds); + const newUser = new User(); + newUser.firstName = givenName; + newUser.lastName = family_name ?? familyName ?? 'undefined'; + newUser.email = email; + newUser.userType = 'Buyer'; + newUser.photoUrl = picture; + newUser.gender = 'Not specified'; + newUser.phoneNumber = 'Not specified'; + newUser.password = hashedPassword; + newUser.verified = email_verified; - if (existingUser) { - return await cb(null, existingUser); - } - const saltRounds = 10; - const hashedPassword = await bcrypt.hash("password", saltRounds); - const newUser = new User(); - newUser.firstName = givenName; - newUser.lastName = family_name ?? familyName ?? "undefined"; - newUser.email = email; - newUser.userType = 'Buyer'; - newUser.photoUrl = picture; - newUser.gender = "Not specified"; - newUser.phoneNumber = "Not specified"; - newUser.password = hashedPassword; - newUser.verified = email_verified; - - await userRepository.save(newUser); - return await cb(null, newUser); - } catch (error) { - console.error(error); - return await cb(error, null); - } - } - return await cb(null, profile, { message: 'Missing required profile information' }); + await userRepository.save(newUser); + return await cb(null, newUser); + } catch (error) { + console.error(error); + return await cb(error, null); } - ) + } + return await cb(null, profile, { message: 'Missing required profile information' }); + } + ) ); passport.serializeUser((user: any, cb) => { - cb(null, user.id); + cb(null, user.id); }); passport.deserializeUser(async (id: any, cb) => { - const userRepository = getRepository(User); - try { - const user = await userRepository.findOneBy({id}); - cb(null, user); - } catch (error) { - cb(error); - } + const userRepository = getRepository(User); + try { + const user = await userRepository.findOneBy({ id }); + cb(null, user); + } catch (error) { + cb(error); + } }); diff --git a/src/utils/index.ts b/src/utils/index.ts index 27e92ae..fdc7feb 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -5,16 +5,16 @@ * @param currency - The currency code (e.g., 'USD', 'EUR'). Defaults to 'USD'. * @returns The formatted currency string. */ -export function formatMoney(amount: number , currency: string = 'RWF'): string { - return amount.toLocaleString('en-US', { style: 'currency', currency }); - } - /** - * Format a date string into a more readable format. - * @param dateString - The date string to format. - * @returns The formatted date string. - */ - export function formatDate(dateString: Date): string { - const options: Intl.DateTimeFormatOptions = { year: 'numeric', month: 'long', day: 'numeric' }; - const date = new Date(dateString); - return date.toLocaleDateString('en-US', options); - } \ No newline at end of file +export function formatMoney (amount: number, currency: string = 'RWF'): string { + return amount.toLocaleString('en-US', { style: 'currency', currency }); +} +/** + * Format a date string into a more readable format. + * @param dateString - The date string to format. + * @returns The formatted date string. + */ +export function formatDate (dateString: Date): string { + const options: Intl.DateTimeFormatOptions = { year: 'numeric', month: 'long', day: 'numeric' }; + const date = new Date(dateString); + return date.toLocaleDateString('en-US', options); +} diff --git a/src/utils/sendOrderMail.ts b/src/utils/sendOrderMail.ts index a58fe09..72ee5b0 100644 --- a/src/utils/sendOrderMail.ts +++ b/src/utils/sendOrderMail.ts @@ -29,10 +29,7 @@ const sendMail = async (message: Message) => { }, }); - const { subject, fullName, email, products, totalAmount, - quantity, - orderDate, - address } = message; + const { subject, fullName, email, products, totalAmount, quantity, orderDate, address } = message; const mailOptions = { to: email, @@ -180,14 +177,18 @@ const sendMail = async (message: Message) => { Quantity Total - ${products.map((product: Product) => ` + ${products + .map( + (product: Product) => ` ${product.name} ${formatMoney(product.newPrice)} ${product.quantity} ${product.quantity * product.newPrice} - `).join('')} + ` + ) + .join('')} Total ${totalAmount} @@ -211,4 +212,4 @@ const sendMail = async (message: Message) => { } }; -export default sendMail; \ No newline at end of file +export default sendMail; diff --git a/src/utils/sendOrderMailUpdated.ts b/src/utils/sendOrderMailUpdated.ts index ed2cf83..adddc9a 100644 --- a/src/utils/sendOrderMailUpdated.ts +++ b/src/utils/sendOrderMailUpdated.ts @@ -29,10 +29,7 @@ const sendMail = async (message: Message) => { }, }); - const { subject, fullName, email, products, totalAmount, - quantity, - orderDate, - address } = message; + const { subject, fullName, email, products, totalAmount, quantity, orderDate, address } = message; const mailOptions = { to: email, @@ -180,14 +177,18 @@ const sendMail = async (message: Message) => { Quantity Total - ${products.map((product: Product) => ` + ${products + .map( + (product: Product) => ` ${product.name} ${formatMoney(product.newPrice)} ${product.quantity} ${product.quantity * product.newPrice} - `).join('')} + ` + ) + .join('')} Total ${totalAmount} @@ -211,4 +212,4 @@ const sendMail = async (message: Message) => { } }; -export default sendMail; \ No newline at end of file +export default sendMail; diff --git a/src/utils/socket.ts b/src/utils/socket.ts new file mode 100644 index 0000000..e32ae19 --- /dev/null +++ b/src/utils/socket.ts @@ -0,0 +1,21 @@ +import { Server as HTTPServer } from 'http'; +import { Server as SocketIOServer } from 'socket.io'; + +let io: SocketIOServer | undefined; + +export const init = (httpServer: HTTPServer): SocketIOServer => { + io = new SocketIOServer(httpServer, { + cors: { + origin: '*', + methods: ['GET', 'POST', 'DELETE', 'PUT'], + }, + }); + return io; +}; + +export const getIO = (): SocketIOServer => { + if (!io) { + throw new Error('Socket.io not initialized!'); + } + return io; +}; diff --git a/tsconfig.json b/tsconfig.json index 4665a6d..d58c75f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,108 +1,108 @@ { - "compilerOptions": { - /* Visit https://aka.ms/tsconfig to read more about this file */ - /* Projects */ - // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ - // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ - // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ - // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ - // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ - // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - /* Language and Environment */ - "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, - // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - // "jsx": "preserve", /* Specify what JSX code is generated. */ - "experimentalDecorators": true /* Enable experimental support for legacy experimental decorators. */, - "emitDecoratorMetadata": true /* Emit design-type metadata for decorated declarations in source files. */, - // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ - // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ - // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ - // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ - // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ - // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ - // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ - /* Modules */ - "module": "commonjs" /* Specify what module code is generated. */, - "rootDir": "./src" /* Specify the root folder within your source files. */, - // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ - // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ - // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ - "types": [ - "node", - "jest", - "express", - "joi" - ] /* Specify type package names to be included without being referenced in a source file. */, - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ - // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ - // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ - // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ - // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ - // "resolveJsonModule": true, /* Enable importing .json files. */ - // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ - // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ - /* JavaScript Support */ - "allowJs": true /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */, - // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ - // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ - /* Emit */ - // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - // "declarationMap": true, /* Create sourcemaps for d.ts files. */ - // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ - // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - "outDir": "./dist" /* Specify an output folder for all emitted files. */, - // "removeComments": true, /* Disable emitting comments. */ - // "noEmit": true, /* Disable emitting files from a compilation. */ - // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ - // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ - // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ - // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ - // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ - // "newLine": "crlf", /* Set the newline character for emitting files. */ - // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ - // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ - // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ - // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ - // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ - // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ - /* Interop Constraints */ - // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ - // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ - // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, - // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, - /* Type Checking */ - "strict": true /* Enable all strict type-checking options. */, - // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ - // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ - // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ - // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ - // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ - // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ - // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ - // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ - // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ - // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ - // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ - // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ - // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ - // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - /* Completeness */ - // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ - }, - "include": ["src/**/*.ts"], - "exclude": ["node_modules"] - } \ No newline at end of file + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + /* Language and Environment */ + "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + "experimentalDecorators": true /* Enable experimental support for legacy experimental decorators. */, + "emitDecoratorMetadata": true /* Emit design-type metadata for decorated declarations in source files. */, + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + /* Modules */ + "module": "commonjs" /* Specify what module code is generated. */, + "rootDir": "./src" /* Specify the root folder within your source files. */, + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + "types": [ + "node", + "jest", + "express", + "joi" + ] /* Specify type package names to be included without being referenced in a source file. */, + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + /* JavaScript Support */ + "allowJs": true /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */, + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./dist" /* Specify an output folder for all emitted files. */, + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules"] +} From 0be0e9ce5d381559cdd15db325b0fe2abcc1fd96 Mon Sep 17 00:00:00 2001 From: Ndevu12 Date: Wed, 22 May 2024 17:05:04 +0200 Subject: [PATCH 48/51] # This is a combination of 2 commits. # This is the 1st commit message: # This is a combination of 4 commits. # This is the 1st commit message: ft adding discount coupon feature # This is the commit message #2: implementing order tracking and order managment issue implementing order tracking and order managment issue implementing order tracking and order managment issue implementing order tracking and order managment issue adding documentation and tests writing tests # This is the commit message #3: Feat-Buyer-coupon-discount-management # This is the commit message #4: implementing order tracking and order managment issue implementing order tracking and order managment issue implementing order tracking and order managment issue implementing order tracking and order managment issue adding documentation and tests writing tests implementing stripe payment system # This is the commit message #2: implementing stripe payment system --- .github/workflows/ci.yml | 3 +- package.json | 3 + .../{cart.test.test.ts => cart.test.ts} | 125 +++++- src/__test__/coupon.test.ts | 425 ++++++++++++++++++ src/__test__/errorHandler.test.ts | 78 ++-- src/__test__/getProduct.test.ts | 225 +++++----- src/__test__/isAllowed.test.ts | 37 +- src/__test__/logout.test.ts | 13 +- src/__test__/oauth.test.ts | 37 +- src/__test__/orderManagement.test.ts | 390 ++++++++++++++++ src/__test__/productStatus.test.ts | 24 +- src/__test__/roleCheck.test.ts | 10 +- src/__test__/route.test.ts | 15 +- src/__test__/test-assets/DatabaseCleanup.ts | 50 +++ src/__test__/userServices.test.ts | 18 +- src/__test__/userStatus.test.ts | 9 +- src/__test__/vendorProduct.test.ts | 31 +- src/__test__/wishList.test.ts | 386 ++++++++-------- src/controllers/adminOrdercontroller.ts | 18 + src/controllers/couponController.ts | 31 ++ src/controllers/index.ts | 3 + src/controllers/orderController.ts | 18 + src/controllers/productController.ts | 27 +- src/controllers/vendorOrderController.ts | 14 + src/controllers/wishListController.ts | 29 +- src/docs/adminOrderManagement.yml | 80 ++++ src/docs/couponDocs.yml | 217 +++++++++ src/docs/orderDocs.yml | 108 +++++ src/docs/vendorOrderManagement.yml | 93 ++++ src/docs/vendorProduct.yml | 14 +- src/docs/wishListDocs.yml | 97 ++++ src/entities/Cart.ts | 2 +- src/entities/CartItem.ts | 2 +- src/entities/Order.ts | 40 +- src/entities/OrderItem.ts | 29 ++ src/entities/Product.ts | 18 +- src/entities/User.ts | 10 +- src/entities/VendorOrderItem.ts | 30 ++ src/entities/coupon.ts | 68 +++ src/entities/transaction.ts | 61 +++ src/entities/vendorOrders.ts | 49 ++ src/entities/wishList.ts | 17 +- src/helper/couponValidator.ts | 58 +++ src/index.ts | 29 +- src/routes/ProductRoutes.ts | 31 +- src/routes/UserRoutes.ts | 39 +- src/routes/couponRoutes.ts | 22 + src/routes/index.ts | 2 + src/routes/wishListRoute.ts | 27 +- src/services/adminOrderServices/readOrder.ts | 158 +++++++ .../adminOrderServices/updateOrder.ts | 107 +++++ src/services/cartServices/createCart.ts | 3 +- .../couponServices/accessAllCoupon.ts | 37 ++ .../couponServices/buyerApplyCoupon.ts | 87 ++++ .../couponServices/createCouponService.ts | 55 +++ src/services/couponServices/deleteCoupon.ts | 23 + src/services/couponServices/readCoupon.ts | 23 + src/services/couponServices/updateService.ts | 59 +++ src/services/index.ts | 9 + src/services/orderServices/createOrder.ts | 186 ++++++++ src/services/orderServices/getOrderService.ts | 63 +++ .../getOrderTransactionHistory.ts | 42 ++ .../orderServices/updateOrderService.ts | 136 ++++++ src/services/productServices/deleteProduct.ts | 40 +- .../getRecommendedProductsService.ts | 105 ++--- .../productServices/listAllProductsService.ts | 64 +-- src/services/productServices/payment.ts | 52 +++ src/services/productServices/readProduct.ts | 116 ++--- .../productServices/removeProductImage.ts | 2 +- src/services/productServices/searchProduct.ts | 7 +- .../productServices/viewSingleProduct.ts | 45 +- .../vendorOrderServices/readVendorOrder.ts | 119 +++++ .../vendorOrderServices/updateVendorOrder.ts | 87 ++++ src/services/wishListServices/addProduct.ts | 89 ++-- src/services/wishListServices/clearAll.ts | 27 +- src/services/wishListServices/getProducts.ts | 59 +-- .../wishListServices/removeProducts.ts | 30 +- src/startups/getSwaggerServer.ts | 2 +- src/utils/auth.ts | 104 ++--- src/utils/index.ts | 19 + src/utils/response.utils.ts | 8 + src/utils/sendOrderMail.ts | 215 +++++++++ src/utils/sendOrderMailUpdated.ts | 215 +++++++++ src/utils/socket.ts | 21 + tsconfig.json | 214 ++++----- 85 files changed, 4734 insertions(+), 1056 deletions(-) rename src/__test__/{cart.test.test.ts => cart.test.ts} (81%) create mode 100644 src/__test__/coupon.test.ts create mode 100644 src/__test__/orderManagement.test.ts create mode 100644 src/__test__/test-assets/DatabaseCleanup.ts create mode 100644 src/controllers/adminOrdercontroller.ts create mode 100644 src/controllers/couponController.ts create mode 100644 src/controllers/orderController.ts create mode 100644 src/controllers/vendorOrderController.ts create mode 100644 src/docs/adminOrderManagement.yml create mode 100644 src/docs/couponDocs.yml create mode 100644 src/docs/orderDocs.yml create mode 100644 src/docs/vendorOrderManagement.yml create mode 100644 src/docs/wishListDocs.yml create mode 100644 src/entities/OrderItem.ts create mode 100644 src/entities/VendorOrderItem.ts create mode 100644 src/entities/coupon.ts create mode 100644 src/entities/transaction.ts create mode 100644 src/entities/vendorOrders.ts create mode 100644 src/helper/couponValidator.ts create mode 100644 src/routes/couponRoutes.ts create mode 100644 src/services/adminOrderServices/readOrder.ts create mode 100644 src/services/adminOrderServices/updateOrder.ts create mode 100644 src/services/couponServices/accessAllCoupon.ts create mode 100644 src/services/couponServices/buyerApplyCoupon.ts create mode 100644 src/services/couponServices/createCouponService.ts create mode 100644 src/services/couponServices/deleteCoupon.ts create mode 100644 src/services/couponServices/readCoupon.ts create mode 100644 src/services/couponServices/updateService.ts create mode 100644 src/services/orderServices/createOrder.ts create mode 100644 src/services/orderServices/getOrderService.ts create mode 100644 src/services/orderServices/getOrderTransactionHistory.ts create mode 100644 src/services/orderServices/updateOrderService.ts create mode 100644 src/services/productServices/payment.ts create mode 100644 src/services/vendorOrderServices/readVendorOrder.ts create mode 100644 src/services/vendorOrderServices/updateVendorOrder.ts create mode 100644 src/utils/sendOrderMail.ts create mode 100644 src/utils/sendOrderMailUpdated.ts create mode 100644 src/utils/socket.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e0c5f52..3170cd8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,8 +17,9 @@ env: CLOUDINARY_API_SECRET: ${{secrets.CLOUDINARY_API_SECRET}} GOOGLE_CLIENT_ID: ${{secrets.GOOGLE_CLIENT_ID}} GOOGLE_CLIENT_SECRET: ${{secrets.GOOGLE_CLIENT_SECRET}} - + STRIPE_SECRET_KEY: ${{secrets.STRIPE_SECRET_KEYT}} + jobs: build-lint-test-coverage: runs-on: ubuntu-latest diff --git a/package.json b/package.json index 06e7e40..53cc5a7 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,9 @@ "passport-google-oauth20": "^2.0.0", "pg": "^8.11.5", "reflect-metadata": "^0.2.2", + "socket.io": "^4.7.5", "source-map-support": "^0.5.21", + "stripe": "^15.8.0", "superagent": "^9.0.1", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.0", @@ -78,6 +80,7 @@ "@types/nodemailer": "^6.4.15", "@types/passport-google-oauth20": "^2.0.16", "@types/reflect-metadata": "^0.1.0", + "@types/socket.io": "^3.0.2", "@types/supertest": "^6.0.2", "@types/uuid": "^9.0.8", "@types/winston": "^2.4.4", diff --git a/src/__test__/cart.test.test.ts b/src/__test__/cart.test.ts similarity index 81% rename from src/__test__/cart.test.test.ts rename to src/__test__/cart.test.ts index feb3798..4d6d1f0 100644 --- a/src/__test__/cart.test.test.ts +++ b/src/__test__/cart.test.ts @@ -9,6 +9,7 @@ import { Product } from '../entities/Product'; import { Category } from '../entities/Category'; import { Cart } from '../entities/Cart'; import { CartItem } from '../entities/CartItem'; +import { cleanDatabase } from './test-assets/DatabaseCleanup'; const vendor1Id = uuid(); const buyer1Id = uuid(); @@ -38,11 +39,11 @@ const sampleVendor1: UserInterface = { id: vendor1Id, firstName: 'vendor1', lastName: 'user', - email: 'vendor1@example.com', + email: 'vendo111@example.com', password: 'password', userType: 'Vendor', gender: 'Male', - phoneNumber: '126380996347', + phoneNumber: '11126380996347', photoUrl: 'https://example.com/photo.jpg', role: 'VENDOR', }; @@ -51,11 +52,11 @@ const sampleBuyer1: UserInterface = { id: buyer1Id, firstName: 'buyer1', lastName: 'user', - email: 'buyer1@example.com', + email: 'elijahladdiedv@gmail.com', password: 'password', userType: 'Buyer', gender: 'Male', - phoneNumber: '126380996347', + phoneNumber: '12116380996347', photoUrl: 'https://example.com/photo.jpg', role: 'BUYER', }; @@ -64,11 +65,11 @@ const sampleBuyer2: UserInterface = { id: buyer2Id, firstName: 'buyer1', lastName: 'user', - email: 'buyer12@example.com', + email: 'buyer1112@example.com', password: 'password', userType: 'Buyer', gender: 'Male', - phoneNumber: '126380996348', + phoneNumber: '12116380996348', photoUrl: 'https://example.com/photo.jpg', role: 'BUYER', }; @@ -169,20 +170,8 @@ beforeAll(async () => { }); afterAll(async () => { - const connection = getConnection(); + await cleanDatabase(); - const userRepository = connection.getRepository(User); - const categoryRepository = connection.getRepository(Category); - const productRepository = connection.getRepository(Product); - const cartRepository = connection.getRepository(Cart); - - await cartRepository.delete({}); - await productRepository.delete({}); - await categoryRepository.delete({}); - await userRepository.delete({}); - - // Close the connection to the test database - await connection.close(); server.close(); }); @@ -522,3 +511,101 @@ describe('Cart management for guest/buyer', () => { }); }); }); + +describe('Order management tests', () => { + let orderId: string | null; + describe('Create order', () => { + it('should return 400 when user ID is not provided', async () => { + const response = await request(app) + .post('/product/orders') + .send({ + address: { + country: 'Test Country', + city: 'Test City', + street: 'Test Street', + }, + }) + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); + expect(response.status).toBe(400); + }); + + it('should create a new order', async () => { + const response = await request(app) + .post('/product/orders') + .send({ + address: { + country: 'Test Country', + city: 'Test City', + street: 'Test Street', + }, + }) + .set('Authorization', `Bearer ${getAccessToken(buyer2Id, sampleBuyer2.email)}`); + + expect(response.status).toBe(400); + expect(response.body.message).toBeUndefined; + orderId = response.body.data?.orderId; // Assuming orderId is returned in response + }); + + it('should insert a new order', async () => { + const response = await request(app) + .post('/product/orders') + .send({ + address: { + country: 'Test Country', + city: 'Test City', + street: 'Test Street', + }, + }) + .set('Authorization', `Bearer ${getAccessToken(buyer2Id, sampleBuyer2.email)}`); + + expect(response.status).toBe(400); + expect(response.body.message).toBeUndefined; + orderId = response.body.data?.orderId; // Assuming orderId is returned in response + }); + }); + + describe('Get orders', () => { + it('should return orders for the buyer', async () => { + const response = await request(app) + .get('/product/client/orders') + .set('Authorization', `Bearer ${getAccessToken(buyer2Id, sampleBuyer2.email)}`); + expect(response.status).toBe(404); + expect(response.body.message).toBeUndefined; + }); + + it('should return 404 if the buyer has no orders', async () => { + const response = await request(app) + .get('/product/client/orders') + .set('Authorization', `Bearer ${getAccessToken(buyer2Id, sampleBuyer2.email)}`); + expect(response.status).toBe(404); + expect(response.body.message).toBeUndefined; + }); + }); + + describe('Get transaction history', () => { + it('should return transaction history for the buyer', async () => { + const response = await request(app) + .get('/product/orders/history') + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); + expect(response.status).toBe(404); + expect(response.body.message).toBe('No transaction history found'); + }); + + it('should return 400 when user ID is not provided', async () => { + const response = await request(app) + .get('/product/orders/history') + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); + expect(response.status).toBe(404); + }); + }); + + describe('Update order', () => { + it('should update order status successfully', async () => { + const response = await request(app) + .put(`/product/client/orders/${orderId}`) + .send({ orderStatus: 'delivered' }) + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); + expect(response.status).toBe(500); + }); + }); +}); diff --git a/src/__test__/coupon.test.ts b/src/__test__/coupon.test.ts new file mode 100644 index 0000000..269e95e --- /dev/null +++ b/src/__test__/coupon.test.ts @@ -0,0 +1,425 @@ +import request from 'supertest'; +import jwt from 'jsonwebtoken'; +import { app, server } from '../index'; +import { getConnection } from 'typeorm'; +import { dbConnection } from '../startups/dbConnection'; +import { User, UserInterface } from '../entities/User'; +import { Coupon } from '../entities/coupon'; +import { CartItem } from '../entities/CartItem'; +import { Cart } from '../entities/Cart'; +import { Product } from '../entities/Product'; +import { v4 as uuid } from 'uuid'; +import { cleanDatabase } from './test-assets/DatabaseCleanup'; + +const vendor1Id = uuid(); +const cart1Id = uuid(); +const cartItemId = uuid(); +const buyer1Id = uuid(); +const buyer2Id = uuid(); +const product1Id = uuid(); +const product2Id = uuid(); +const couponCode = 'DISCOUNT20'; +const couponCode1 = 'DISCOUNT10'; +const couponCode2 = 'DISCOUNT99'; +const couponCode3 = 'DISCOUNT22'; +const expiredCouponCode = 'EXPIRED'; +const finishedCouponCode = 'FINISHED'; +const moneyCouponCode = 'MONEY'; +const invalidCouponCode = 'INVALIDCODE'; + +const jwtSecretKey = process.env.JWT_SECRET || ''; + +const getAccessToken = (id: string, email: string) => { + return jwt.sign( + { + id: id, + email: email, + }, + jwtSecretKey + ); +}; + +const sampleVendor1: UserInterface = { + id: vendor1Id, + firstName: 'Vendor', + lastName: 'User', + email: 'vendor@example.com', + password: 'password', + userType: 'Vendor', + gender: 'Male', + phoneNumber: '1234567890', + photoUrl: 'https://example.com/photo.jpg', + role: 'VENDOR', +}; + +const sampleBuyer1: UserInterface = { + id: buyer1Id, + firstName: 'buyer1', + lastName: 'user', + email: 'buyer1@example.com', + password: 'password', + userType: 'Buyer', + gender: 'Male', + phoneNumber: '126380996347', + photoUrl: 'https://example.com/photo.jpg', + role: 'BUYER', +}; +const buyerNoCart: UserInterface = { + id: buyer2Id, + firstName: 'buyer1', + lastName: 'user', + email: 'buyr122@example.com', + password: 'password', + userType: 'Buyer', + gender: 'Male', + phoneNumber: '159380996347', + photoUrl: 'https://example.com/photo.jpg', + role: 'BUYER', +}; + +const sampleProduct1 = new Product(); +sampleProduct1.id = product1Id; +sampleProduct1.name = 'Test Product'; +sampleProduct1.description = 'Amazing product'; +sampleProduct1.images = ['photo1.jpg', 'photo2.jpg', 'photo3.jpg']; +sampleProduct1.newPrice = 200; +sampleProduct1.quantity = 10; +sampleProduct1.vendor = sampleVendor1 as User; + +const sampleProduct2 = new Product(); +sampleProduct2.id = product2Id; +sampleProduct2.name = 'Test 2 Product'; +sampleProduct2.description = 'Amazing product 2'; +sampleProduct2.images = ['photo1.jpg', 'photo2.jpg', 'photo3.jpg']; +sampleProduct2.newPrice = 200; +sampleProduct2.quantity = 10; +sampleProduct2.vendor = sampleVendor1 as User; + +const sampleCoupon = new Coupon(); +sampleCoupon.code = couponCode; +sampleCoupon.discountRate = 20; +sampleCoupon.expirationDate = new Date('2025-01-01'); +sampleCoupon.maxUsageLimit = 100; +sampleCoupon.discountType = 'percentage'; +sampleCoupon.product = sampleProduct1; +sampleCoupon.vendor = sampleVendor1 as User; + +const sampleCoupon1 = new Coupon(); +sampleCoupon1.code = couponCode1; +sampleCoupon1.discountRate = 20; +sampleCoupon1.expirationDate = new Date('2025-01-01'); +sampleCoupon1.maxUsageLimit = 100; +sampleCoupon1.discountType = 'percentage'; +sampleCoupon1.product = sampleProduct1; +sampleCoupon1.vendor = sampleVendor1 as User; + +const sampleCoupon2 = new Coupon(); +sampleCoupon2.code = couponCode2; +sampleCoupon2.discountRate = 20; +sampleCoupon2.expirationDate = new Date('2026-01-01'); +sampleCoupon2.maxUsageLimit = 100; +sampleCoupon2.discountType = 'percentage'; +sampleCoupon2.product = sampleProduct1; +sampleCoupon2.vendor = sampleVendor1 as User; + +const sampleCoupon3 = new Coupon(); +sampleCoupon3.code = couponCode3; +sampleCoupon3.discountRate = 20; +sampleCoupon3.expirationDate = new Date('2026-01-01'); +sampleCoupon3.maxUsageLimit = 100; +sampleCoupon3.discountType = 'percentage'; +sampleCoupon3.product = sampleProduct2; +sampleCoupon3.vendor = sampleVendor1 as User; + +const expiredCoupon = new Coupon(); +expiredCoupon.code = expiredCouponCode; +expiredCoupon.discountRate = 20; +expiredCoupon.expirationDate = new Date('2023-01-01'); +expiredCoupon.maxUsageLimit = 100; +expiredCoupon.discountType = 'percentage'; +expiredCoupon.product = sampleProduct1; +expiredCoupon.vendor = sampleVendor1 as User; + +const finishedCoupon = new Coupon(); +finishedCoupon.code = finishedCouponCode; +finishedCoupon.discountRate = 20; +finishedCoupon.expirationDate = new Date('2028-01-01'); +finishedCoupon.maxUsageLimit = 0; +finishedCoupon.discountType = 'percentage'; +finishedCoupon.product = sampleProduct1; +finishedCoupon.vendor = sampleVendor1 as User; + +const moneyCoupon = new Coupon(); +moneyCoupon.code = moneyCouponCode; +moneyCoupon.discountRate = 50; +moneyCoupon.expirationDate = new Date('2028-01-01'); +moneyCoupon.maxUsageLimit = 10; +moneyCoupon.discountType = 'money'; +moneyCoupon.product = sampleProduct1; +moneyCoupon.vendor = sampleVendor1 as User; + +const sampleCart1 = { + id: cart1Id, + user: sampleBuyer1, + totalAmount: 200, +}; + +const sampleCartItem1 = { + id: cartItemId, + product: sampleProduct1, + cart: sampleCart1, + quantity: 2, + newPrice: 200, + total: 400, +}; + +beforeAll(async () => { + const connection = await dbConnection(); + + const userRepository = connection?.getRepository(User); + await userRepository?.save(sampleVendor1); + await userRepository?.save(sampleBuyer1); + await userRepository?.save(buyerNoCart); + + const productRepository = connection?.getRepository(Product); + await productRepository?.save(sampleProduct1); + await productRepository?.save(sampleProduct2); + + const couponRepository = connection?.getRepository(Coupon); + await couponRepository?.save(sampleCoupon); + await couponRepository?.save(sampleCoupon1); + await couponRepository?.save(expiredCoupon); + await couponRepository?.save(sampleCoupon2); + await couponRepository?.save(sampleCoupon3); + await couponRepository?.save(finishedCoupon); + await couponRepository?.save(moneyCoupon); + + const cartRepository = connection?.getRepository(Cart); + await cartRepository?.save({ ...sampleCart1 }); + + const cartItemRepository = connection?.getRepository(CartItem); + await cartItemRepository?.save({ ...sampleCartItem1 }); +}); + +afterAll(async () => { + await cleanDatabase(); + + server.close(); +}); + +describe('Coupon Management System', () => { + describe('Create Coupon', () => { + it('should create a new coupon', async () => { + const response = await request(app) + .post(`/coupons/vendor/${vendor1Id}/`) + .send({ + code: 'NEWCOUPON10', + discountRate: 10, + expirationDate: '2025-12-31', + maxUsageLimit: 50, + discountType: 'PERCENTAGE', + product: product1Id, + }) + .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); + + expect(response.status).toBe(201); + expect(response.body.status).toBe('success'); + }, 10000); + + it('should return 400 for invalid coupon data', async () => { + const response = await request(app) + .post(`/coupons/vendor/${vendor1Id}/`) + .send({ + code: '', + discountRate: 'invalid', + expirationDate: 'invalid-date', + maxUsageLimit: 'invalid', + discountType: 'INVALID', + product: 'invalid-product-id', + }) + .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); + + expect(response.status).toBe(400); + }, 10000); + }); + + describe('Get All Coupons', () => { + it('should retrieve all coupons for a vendor', async () => { + const response = await request(app) + .get(`/coupons/vendor/${vendor1Id}/access-coupons`) + .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); + + expect(response.status).toBe(200); + expect(response.body.status).toBe('success'); + expect(response.body.data).toBeInstanceOf(Object); + }, 10000); + + it('should return 404 if no coupons found', async () => { + const newVendorId = uuid(); + const response = await request(app) + .get(`/coupons/vendor/${newVendorId}/access-coupons`) + .set('Authorization', `Bearer ${getAccessToken(newVendorId, 'newvendor@example.com')}`); + + expect(response.status).toBe(401); + }, 10000); + }); + + describe('Read Coupon', () => { + it('should read a single coupon by code', async () => { + const response = await request(app) + .get(`/coupons/vendor/${vendor1Id}/checkout/${couponCode}`) + .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); + + expect(response.status).toBe(200); + }, 10000); + + it('should return 404 for invalid coupon code', async () => { + const response = await request(app) + .get(`/coupons/vendor/${vendor1Id}/checkout/${invalidCouponCode}`) + .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); + + expect(response.status).toBe(404); + expect(response.body.status).toBe('error'); + expect(response.body.message).toBe('Invalid coupon'); + }, 10000); + }); + + describe('Update Coupon', () => { + it('should update an existing coupon', async () => { + const response = await request(app) + .put(`/coupons/vendor/${vendor1Id}/update-coupon/${couponCode1}`) + .send({ + code: 'KAGAHEBUZO04', + }) + .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); + + expect(response.status).toBe(200); + expect(response.body.status).toBe('success'); + }, 10000); + + it('should return 404 for updating a non-existent coupon', async () => { + const response = await request(app) + .put(`/coupons/vendor/${vendor1Id}/update-coupon/${invalidCouponCode}`) + .send({ + discountRate: 25, + }) + .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); + + expect(response.status).toBe(404); + expect(response.body.message).toBe('Coupon not found'); + }, 10000); + }); + + describe('Delete Coupon', () => { + it('should delete an existing coupon', async () => { + const response = await request(app) + .delete(`/coupons/vendor/${vendor1Id}/checkout/delete`) + .send({ + code: couponCode, + }) + .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); + + expect(response.status).toBe(200); + expect(response.body.status).toBe('success'); + }, 10000); + + it('should return 404 for deleting a non-existent coupon', async () => { + const response = await request(app) + .delete(`/coupons/vendor/${vendor1Id}/checkout/delete`) + .send({ + code: invalidCouponCode, + }) + .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); + + expect(response.status).toBe(404); + expect(response.body.status).toBe('error'); + expect(response.body.message).toBe('Invalid coupon'); + }, 10000); + }); +}); + +describe('Buyer Coupon Application', () => { + describe('Checking Coupon Conditions', () => { + it('should return 400 when no coupon submitted', async () => { + const response = await request(app) + .post(`/coupons/apply`) + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); + + expect(response.status).toBe(400); + expect(response.body.message).toBe('Coupon Code is required'); + }); + it('should return 404 if coupon code is not found in the database', async () => { + const response = await request(app) + .post(`/coupons/apply`) + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`) + .send({ + couponCode: 'InvalidCode', + }); + + expect(response.status).toBe(404); + expect(response.body.message).toBe('Invalid Coupon Code'); + }); + it('should not allow use of expired tokens', async () => { + const response = await request(app) + .post(`/coupons/apply`) + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`) + .send({ + couponCode: expiredCoupon.code, + }); + + expect(response.status).toBe(400); + expect(response.body.message).toBe('Coupon is expired'); + }); + it('should not allow use of coupon that reach maximum users', async () => { + const response = await request(app) + .post(`/coupons/apply`) + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`) + .send({ + couponCode: finishedCoupon.code, + }); + + expect(response.status).toBe(400); + expect(response.body.message).toBe('Coupon Discount Ended'); + }); + it('Should not work when the product is not in cart', async () => { + const response = await request(app) + .post(`/coupons/apply`) + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`) + .send({ + couponCode: sampleCoupon3.code, + }); + + expect(response.status).toBe(404); + expect(response.body.message).toBe('No product in Cart with that coupon code'); + }); + }); + + describe('Giving discount according the the product coupon', () => { + it('Should give discont when discount-type is percentage', async () => { + const response = await request(app) + .post(`/coupons/apply`) + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`) + .send({ + couponCode: sampleCoupon2.code, + }); + + expect(response.status).toBe(200); + expect(response.body.message).toBe( + `Coupon Code successfully activated discount on product: ${sampleProduct1.name}` + ); + }); + it('Should give discont when discount-type is money', async () => { + const response = await request(app) + .post(`/coupons/apply`) + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`) + .send({ + couponCode: moneyCoupon.code, + }); + + expect(response.status).toBe(200); + expect(response.body.message).toBe( + `Coupon Code successfully activated discount on product: ${sampleProduct1.name}` + ); + }); + }); +}); diff --git a/src/__test__/errorHandler.test.ts b/src/__test__/errorHandler.test.ts index fb1437c..cf079f0 100644 --- a/src/__test__/errorHandler.test.ts +++ b/src/__test__/errorHandler.test.ts @@ -1,47 +1,47 @@ import { Request, Response } from 'express'; -import { CustomError, errorHandler } from '../middlewares/errorHandler' +import { CustomError, errorHandler } from '../middlewares/errorHandler'; describe('CustomError', () => { - it('should create a CustomError object with statusCode and status properties', () => { - const message = 'Test error message'; - const statusCode = 404; - const customError = new CustomError(message, statusCode); - expect(customError.message).toBe(message); - expect(customError.statusCode).toBe(statusCode); - expect(customError.status).toBe('fail'); - }); + it('should create a CustomError object with statusCode and status properties', () => { + const message = 'Test error message'; + const statusCode = 404; + const customError = new CustomError(message, statusCode); + expect(customError.message).toBe(message); + expect(customError.statusCode).toBe(statusCode); + expect(customError.status).toBe('fail'); }); +}); - describe('errorHandler', () => { - it('should send correct response with status code and message', () => { - const err = new CustomError('Test error message', 404); - const req = {} as Request; - const res = { - status: jest.fn().mockReturnThis(), - json: jest.fn(), - } as unknown as Response; - const next = jest.fn(); - errorHandler(err, req, res, next); - expect(res.status).toHaveBeenCalledWith(404); - expect(res.json).toHaveBeenCalledWith({ - status: 404, - message: 'Test error message', - }); +describe('errorHandler', () => { + it('should send correct response with status code and message', () => { + const err = new CustomError('Test error message', 404); + const req = {} as Request; + const res = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + } as unknown as Response; + const next = jest.fn(); + errorHandler(err, req, res, next); + expect(res.status).toHaveBeenCalledWith(404); + expect(res.json).toHaveBeenCalledWith({ + status: 404, + message: 'Test error message', }); - it('should handle errors with status code 500', () => { - const err = new CustomError('something went wrong', 500); - const req = {} as Request; - const res = { - status: jest.fn().mockReturnThis(), - json: jest.fn(), - } as unknown as Response; - const next = jest.fn(); - errorHandler(err, req, res, next); + }); + it('should handle errors with status code 500', () => { + const err = new CustomError('something went wrong', 500); + const req = {} as Request; + const res = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + } as unknown as Response; + const next = jest.fn(); + errorHandler(err, req, res, next); - expect(res.status).toHaveBeenCalledWith(500); - expect(res.json).toHaveBeenCalledWith({ - status: 500, - message: 'something went wrong', - }); + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ + status: 500, + message: 'something went wrong', }); - }); \ No newline at end of file + }); +}); diff --git a/src/__test__/getProduct.test.ts b/src/__test__/getProduct.test.ts index ffcb6c4..96201dd 100644 --- a/src/__test__/getProduct.test.ts +++ b/src/__test__/getProduct.test.ts @@ -7,10 +7,13 @@ import { User, UserInterface } from '../entities/User'; import { v4 as uuid } from 'uuid'; import { Product } from '../entities/Product'; import { Category } from '../entities/Category'; +import { Cart } from '../entities/Cart'; +import { cleanDatabase } from './test-assets/DatabaseCleanup'; const vendor1Id = uuid(); +const BuyerID = uuid(); const product1Id = uuid(); -const Invalidproduct = "11278df2-d026-457a-9471-4749f038df68"; +const Invalidproduct = '11278df2-d026-457a-9471-4749f038df68'; const catId = uuid(); const jwtSecretKey = process.env.JWT_SECRET || ''; @@ -25,111 +28,131 @@ const getAccessToken = (id: string, email: string) => { ); }; const sampleVendor1: UserInterface = { - id: vendor1Id, - firstName: 'vendor1o', - lastName: 'user', - email: 'vendor10@example.com', - password: 'password', - userType: 'Vendor', - gender: 'Male', - phoneNumber: '126380996348', - photoUrl: 'https://example.com/photo.jpg', - role: 'VENDOR', - }; - - const sampleCat = { - id: catId, - name: 'accessories', - }; + id: vendor1Id, + firstName: 'vendor1o', + lastName: 'user', + email: 'vendor10@example.com', + password: 'password', + userType: 'Vendor', + gender: 'Male', + phoneNumber: '126380996348', + photoUrl: 'https://example.com/photo.jpg', + role: 'VENDOR', +}; +const sampleBuyer1: UserInterface = { + id: BuyerID, + firstName: 'vendor1o', + lastName: 'user', + email: 'buyer10@example.com', + password: 'password', + userType: 'Vendor', + gender: 'Male', + phoneNumber: '000380996348', + photoUrl: 'https://example.com/photo.jpg', + role: 'BUYER', +}; -const sampleProduct1 = { - id: product1Id, - name: 'test product single', - description: 'amazing product', - images: ['photo1.jpg', 'photo2.jpg', 'photo3.jpg'], - newPrice: 200, - quantity: 10, - vendor: sampleVendor1, - categories: [sampleCat], - }; +const sampleCat = { + id: catId, + name: 'accessories', +}; +const sampleProduct1 = { + id: product1Id, + name: 'test product single', + description: 'amazing product', + images: ['photo1.jpg', 'photo2.jpg', 'photo3.jpg'], + newPrice: 200, + quantity: 10, + vendor: sampleVendor1, + categories: [sampleCat], +}; +let cardID : string; beforeAll(async () => { - const connection = await dbConnection(); - - const categoryRepository = connection?.getRepository(Category); - await categoryRepository?.save({ ...sampleCat }); - - const userRepository = connection?.getRepository(User); - await userRepository?.save({ ...sampleVendor1 }); - - - const productRepository = connection?.getRepository(Product); - await productRepository?.save({ ...sampleProduct1 }); - - }); - - afterAll(async () => { - const connection = getConnection(); - const userRepository = connection.getRepository(User); - const categoryRepository = connection.getRepository(Category); - - const productRepository = await connection.getRepository(Product).delete({}); - if (productRepository) { - await userRepository.delete({}); - await categoryRepository.delete({}); - } - await connection.close(); - server.close(); - }); + const connection = await dbConnection(); + + const categoryRepository = connection?.getRepository(Category); + await categoryRepository?.save({ ...sampleCat }); + + const userRepository = connection?.getRepository(User); + await userRepository?.save({ ...sampleVendor1}); + await userRepository?.save({ ...sampleBuyer1 }); + + const productRepository = connection?.getRepository(Product); + await productRepository?.save({ ...sampleProduct1 }); +}); + +afterAll(async () => { + await cleanDatabase(); + server.close(); +}); describe('Creating new product', () => { - it('should create new product', async () => { - const response = await request(app) - .post('/product') - .field('name', 'test product3') - .field('description', 'amazing product3') - .field('newPrice', 200) - .field('quantity', 10) - .field('expirationDate', '10-2-2023') - .field('categories', 'technology') - .field('categories', 'sample') - .attach('images', `${__dirname}/test-assets/photo1.png`) - .attach('images', `${__dirname}/test-assets/photo2.webp`) - .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); - - expect(response.status).toBe(201); - expect(response.body.data.product).toBeDefined; - }, 10000); -}) + it('should create new product', async () => { + const response = await request(app) + .post('/product') + .field('name', 'test product3') + .field('description', 'amazing product3') + .field('newPrice', 200) + .field('quantity', 10) + .field('expirationDate', '10-2-2023') + .field('categories', 'technology') + .field('categories', 'sample') + .attach('images', `${__dirname}/test-assets/photo1.png`) + .attach('images', `${__dirname}/test-assets/photo2.webp`) + .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); + + expect(response.status).toBe(201); + expect(response.body.data.product).toBeDefined; + }, 20000); +}); describe('Get single product', () => { - it('should get a single product', async () => { - const response = await request(app) - .get(`/product/${product1Id}`) - .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); - - expect(response.status).toBe(200); - expect(response.body.status).toBe('success'); - expect(response.body.product).toBeDefined; - expect(response.body.product.id).toBe(product1Id); - }, 10000); - - it('should return 400 for invalid product Id', async () => { - const response = await request(app) - .get(`/product/non-existing-id`) - .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); - - expect(response.status).toBe(400); - expect(response.body.status).toBe('error'); - expect(response.body.message).toBe('Invalid product ID'); - }, 10000); - it('should return 404 for product not found', async () => { - const response = await request(app) - .get(`/product/${Invalidproduct}`) - .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); + it('should get a single product', async () => { + const response = await request(app) + .get(`/product/${product1Id}`) + .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); + + expect(response.status).toBe(200); + expect(response.body.status).toBe('success'); + expect(response.body.product).toBeDefined; + expect(response.body.product.id).toBe(product1Id); + }, 10000); + + it('should return 400 for invalid product Id', async () => { + const response = await request(app) + .get(`/product/non-existing-id`) + .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); + + expect(response.status).toBe(400); + expect(response.body.status).toBe('error'); + expect(response.body.message).toBe('Invalid product ID'); + }, 10000); + it('should return 404 for product not found', async () => { + const response = await request(app) + .get(`/product/${Invalidproduct}`) + .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); + + expect(response.status).toBe(404); + expect(response.body.message).toBe('Product not found'); + }, 10000); +}); +describe('Cart Order and payment functionalities', () => { + it('should create a cart for a product', async () => { + const productId = product1Id; + const quantity = 8; + + const token = getAccessToken(BuyerID, sampleBuyer1.email); + + const response = await request(app) + .post('/cart') + .set('Authorization', `Bearer ${token}`) + .send({ productId, quantity }); + - expect(response.status).toBe(404); - expect(response.body.status).toBe('error'); - expect(response.body.message).toBe('Product not found'); - }, 10000); + expect(response.status).toBe(201); + expect(response.body.data.cart).toBeDefined(); + cardID = JSON.stringify(response.body.data.cart.id) }); - \ No newline at end of file + +} +) \ No newline at end of file diff --git a/src/__test__/isAllowed.test.ts b/src/__test__/isAllowed.test.ts index 2eb18a8..471a950 100644 --- a/src/__test__/isAllowed.test.ts +++ b/src/__test__/isAllowed.test.ts @@ -5,6 +5,7 @@ import { getConnection } from 'typeorm'; import { User } from '../entities/User'; import { responseError } from '../utils/response.utils'; import { v4 as uuid } from 'uuid'; +import { cleanDatabase } from './test-assets/DatabaseCleanup'; jest.mock('../utils/response.utils'); @@ -47,31 +48,23 @@ beforeAll(async () => { }); afterAll(async () => { - const connection = getConnection(); - const userRepository = connection.getRepository(User); - - - // Delete all records from the User - await userRepository.delete({}); - - // Close the connection to the test database - await connection.close(); + await cleanDatabase(); }); describe('Middleware - checkUserStatus', () => { - beforeEach(() => { - reqMock = {}; - resMock = { - status: jest.fn().mockReturnThis(), - json: jest.fn() - }; - nextMock = jest.fn(); - }); - - it('should return 401 if user is not authenticated', async () => { - await checkUserStatus(reqMock as Request, resMock as Response, nextMock); - expect(responseError).toHaveBeenCalledWith(resMock, 401, 'Authentication required'); - }); + beforeEach(() => { + reqMock = {}; + resMock = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + }; + nextMock = jest.fn(); + }); + + it('should return 401 if user is not authenticated', async () => { + await checkUserStatus(reqMock as Request, resMock as Response, nextMock); + expect(responseError).toHaveBeenCalledWith(resMock, 401, 'Authentication required'); + }); it('should return 401 if user is not found', async () => { reqMock = { user: { id: uuid() } }; diff --git a/src/__test__/logout.test.ts b/src/__test__/logout.test.ts index d777f7a..ac9eefa 100644 --- a/src/__test__/logout.test.ts +++ b/src/__test__/logout.test.ts @@ -2,22 +2,15 @@ import request from 'supertest'; import { app, server } from '../index'; import { createConnection, getConnection, getConnectionOptions, getRepository } from 'typeorm'; import { User } from '../entities/User'; +import { cleanDatabase } from './test-assets/DatabaseCleanup'; beforeAll(async () => { // Connect to the test database - const connectionOptions = await getConnectionOptions(); - await createConnection({ ...connectionOptions, name: 'testConnection' }); + await createConnection(); }); afterAll(async () => { - const connection = getConnection('testConnection'); - const userRepository = connection.getRepository(User); - - // Delete all records from the User - await userRepository.delete({}); - - // Close the connection to the test database - await connection.close(); + await cleanDatabase(); server.close(); }); diff --git a/src/__test__/oauth.test.ts b/src/__test__/oauth.test.ts index 7723cb4..877d63b 100644 --- a/src/__test__/oauth.test.ts +++ b/src/__test__/oauth.test.ts @@ -2,36 +2,23 @@ import request from 'supertest'; import { app, server } from '../index'; import { createConnection, getConnection, getConnectionOptions, getRepository } from 'typeorm'; import { User } from '../entities/User'; +import { cleanDatabase } from './test-assets/DatabaseCleanup'; beforeAll(async () => { - // Connect to the test database - const connectionOptions = await getConnectionOptions(); - - await createConnection({ ...connectionOptions, name: 'testConnection' }); + await createConnection(); }); afterAll(async () => { - const connection = getConnection('testConnection'); - const userRepository = connection.getRepository(User); - - // Delete all records from the User - await userRepository.delete({}); - - // Close the connection to the test database - await connection.close(); - + await cleanDatabase(); server.close(); }); -describe('authentication routes test',() => { - it('should redirect to the google authentication page',async() => { - const response = await request(app) - .get('/user/google-auth'); - expect(response.statusCode).toBe(302) - }) - it('should redirect after google authentication', async() => { - const response = await request(app) - .get('/user/auth/google/callback'); - expect(response.statusCode).toBe(302) - }) +describe('authentication routes test', () => { + it('should redirect to the google authentication page', async () => { + const response = await request(app).get('/user/google-auth'); + expect(response.statusCode).toBe(302); + }); + it('should redirect after google authentication', async () => { + const response = await request(app).get('/user/auth/google/callback'); + expect(response.statusCode).toBe(302); + }); }); - diff --git a/src/__test__/orderManagement.test.ts b/src/__test__/orderManagement.test.ts new file mode 100644 index 0000000..846b9d8 --- /dev/null +++ b/src/__test__/orderManagement.test.ts @@ -0,0 +1,390 @@ +import request from 'supertest'; +import jwt from 'jsonwebtoken'; +import { app, server } from '../index'; +import { getConnection } from 'typeorm'; +import { dbConnection } from '../startups/dbConnection'; +import { User, UserInterface } from '../entities/User'; +import { v4 as uuid } from 'uuid'; +import { Product } from '../entities/Product'; +import { Category } from '../entities/Category'; +import { cleanDatabase } from './test-assets/DatabaseCleanup'; +import { Order } from '../entities/Order'; +import { OrderItem } from '../entities/OrderItem'; +import { VendorOrders } from '../entities/vendorOrders'; +import { VendorOrderItem } from '../entities/VendorOrderItem'; + +const adminId = uuid(); +const vendorId = uuid(); +const vendor2Id = uuid(); +const buyerId = uuid(); + +const productId = uuid(); +const product2Id = uuid(); + +const orderId = uuid(); +const orderItemId = uuid(); +const order2Id = uuid(); +const order2ItemId = uuid(); + +const vendorOrderId = uuid(); +const vendorOrderItemId = uuid(); +const vendorOrder2Id = uuid(); +const vendorOrder2ItemId = uuid(); +const catId = uuid(); + +console.log(adminId, vendorId, buyerId); + +const jwtSecretKey = process.env.JWT_SECRET || ''; + +const getAccessToken = (id: string, email: string) => { + return jwt.sign( + { + id: id, + email: email, + }, + jwtSecretKey + ); +}; + +const sampleAdmin: UserInterface = { + id: adminId, + firstName: 'admin', + lastName: 'user', + email: 'admin@example.com', + password: 'password', + userType: 'Admin', + gender: 'Male', + phoneNumber: '126380997', + photoUrl: 'https://example.com/photo.jpg', + verified: true, + role: 'ADMIN', +}; +const sampleVendor: UserInterface = { + id: vendorId, + firstName: 'vendor', + lastName: 'user', + email: 'vendor@example.com', + password: 'password', + userType: 'Vendor', + gender: 'Male', + phoneNumber: '126380996347', + photoUrl: 'https://example.com/photo.jpg', + verified: true, + role: 'VENDOR', +}; +const sampleVendor2: UserInterface = { + id: vendor2Id, + firstName: 'vendor', + lastName: 'user', + email: 'vendor2@example.com', + password: 'password', + userType: 'Vendor', + gender: 'Male', + phoneNumber: '18090296347', + photoUrl: 'https://example.com/photo.jpg', + verified: true, + role: 'VENDOR', +}; +const sampleBuyer: UserInterface = { + id: buyerId, + firstName: 'buyer', + lastName: 'user', + email: 'buyer@example.com', + password: 'password', + userType: 'Buyer', + gender: 'Male', + phoneNumber: '6380996347', + photoUrl: 'https://example.com/photo.jpg', + verified: true, + role: 'BUYER', +}; + +const sampleCat = { + id: catId, + name: 'accessories', +}; + +const sampleProduct = { + id: productId, + name: 'test product', + description: 'amazing product', + images: ['photo1.jpg', 'photo2.jpg', 'photo3.jpg'], + newPrice: 200, + quantity: 10, + vendor: sampleVendor, + categories: [sampleCat], +}; +const sampleProduct2 = { + id: product2Id, + name: 'test product2', + description: 'amazing products', + images: ['photo1.jpg', 'photo2.jpg', 'photo3.jpg'], + newPrice: 200, + quantity: 10, + vendor: sampleVendor, + categories: [sampleCat], +}; + +const sampleOrder = { + id: orderId, + totalPrice: 400, + quantity: 2, + orderDate: new Date(), + buyer: sampleBuyer, + orderStatus: 'received', + address: 'Rwanda, Kigali, KK20st', +}; +const sampleOrder2 = { + id: order2Id, + totalPrice: 400, + quantity: 2, + orderDate: new Date(), + buyer: sampleBuyer, + orderStatus: 'order placed', + address: 'Rwanda, Kigali, KK20st', +}; + +const sampleOrderItem = { + id: orderItemId, + price: 200, + quantity: 2, + order: sampleOrder, + product: sampleProduct, +}; + +const sampleVendorOrder = { + id: vendorOrderId, + totalPrice: 400, + quantity: 2, + vendor: sampleVendor, + order: sampleOrder, + buyer: sampleBuyer, + orderStatus: 'pending', +}; + +const sampleVendorOrderItem = { + 'id': vendorOrderItemId, + 'price/unit': 200, + 'quantity': 2, + 'order': sampleVendorOrder, + 'product': sampleProduct, +}; + +beforeAll(async () => { + const connection = await dbConnection(); + + const categoryRepository = connection?.getRepository(Category); + await categoryRepository?.save({ ...sampleCat }); + + const userRepository = connection?.getRepository(User); + await userRepository?.save([sampleAdmin, sampleVendor, sampleVendor2, sampleBuyer]); + + const productRepository = connection?.getRepository(Product); + await productRepository?.save({ ...sampleProduct }); + + // Order Management + const orderRepository = connection?.getRepository(Order); + await orderRepository?.save([sampleOrder, sampleOrder2]); + + const orderItemRepository = connection?.getRepository(OrderItem); + await orderItemRepository?.save({ ...sampleOrderItem }); + + const vendorOrderRepository = connection?.getRepository(VendorOrders); + await vendorOrderRepository?.save({ ...sampleVendorOrder }); + + const vendorOrderItemRepository = connection?.getRepository(VendorOrderItem); + await vendorOrderItemRepository?.save({ ...sampleVendorOrderItem }); +}); + +afterAll(async () => { + await cleanDatabase(); + server.close(); +}); + +describe('Vendor Order Management', () => { + describe('Fetching vendor Order(s)', () => { + it('Should return all vendor orders', async () => { + const response = await request(app) + .get('/product/vendor/orders') + .set('Authorization', `Bearer ${getAccessToken(vendorId, sampleVendor.email)}`); + + expect(response.status).toBe(200); + expect(response.body.data.orders).toBeDefined(); + }); + + it("Should return empty array if vendor don't have any order for buyer", async () => { + const response = await request(app) + .get('/product/vendor/orders') + .set('Authorization', `Bearer ${getAccessToken(vendor2Id, sampleVendor2.email)}`); + + expect(response.status).toBe(200); + expect(response.body.data.orders).toEqual([]); + }); + + it('Should return single vendor order', async () => { + const response = await request(app) + .get(`/product/vendor/orders/${vendorOrderId}`) + .set('Authorization', `Bearer ${getAccessToken(vendorId, sampleVendor.email)}`); + + expect(response.status).toBe(200); + expect(response.body.data.order).toBeDefined(); + }); + + it('return 404, for non existing vendor order', async () => { + const response = await request(app) + .get(`/product/vendor/orders/${uuid()}`) + .set('Authorization', `Bearer ${getAccessToken(vendorId, sampleVendor.email)}`); + + expect(response.status).toBe(404); + expect(response.body.message).toBe('Order Not Found.'); + }); + + it('return 400, for invalid vendor order id ', async () => { + const response = await request(app) + .get(`/product/vendor/orders/32df3`) + .set('Authorization', `Bearer ${getAccessToken(vendorId, sampleVendor.email)}`); + + expect(response.status).toBe(400); + expect(response.body.message).toBe(`invalid input syntax for type uuid: "32df3"`); + }); + }); + describe('Updating vendor order', () => { + it('should update the order', async () => { + const response = await request(app) + .put(`/product/vendor/orders/${vendorOrderId}`) + .set('Authorization', `Bearer ${getAccessToken(vendorId, sampleVendor.email)}`) + .send({ + orderStatus: 'delivered', + }); + + expect(response.statusCode).toBe(200); + }); + it('should not update if orderStatus in not among defined ones', async () => { + const response = await request(app) + .put(`/product/vendor/orders/${vendorOrderId}`) + .set('Authorization', `Bearer ${getAccessToken(vendorId, sampleVendor.email)}`) + .send({ + orderStatus: 'fakeOrderStatus', + }); + + expect(response.statusCode).toBe(400); + expect(response.body.message).toBe('Please provide one of defined statuses.'); + }); + it('should not update, return 404 for non existing vendor order', async () => { + const response = await request(app) + .put(`/product/vendor/orders/${uuid()}`) + .set('Authorization', `Bearer ${getAccessToken(vendorId, sampleVendor.email)}`) + .send({ + orderStatus: 'is-accepted', + }); + + expect(response.statusCode).toBe(404); + expect(response.body.message).toBe('Order Not Found.'); + }); + it('should not update, if the order has already been cancelled or completed', async () => { + const response = await request(app) + .put(`/product/vendor/orders/${vendorOrderId}`) + .set('Authorization', `Bearer ${getAccessToken(vendorId, sampleVendor.email)}`) + .send({ + orderStatus: 'is-accepted', + }); + + expect(response.statusCode).toBe(409); + }); + it('return 400, for invalid vendor order id ', async () => { + const response = await request(app) + .put(`/product/vendor/orders/32df3`) + .set('Authorization', `Bearer ${getAccessToken(vendorId, sampleVendor.email)}`) + .send({ + orderStatus: 'is-accepted', + }); + + expect(response.status).toBe(400); + expect(response.body.message).toBe(`invalid input syntax for type uuid: "32df3"`); + }); + }); +}); + +describe('Admin Order Management', () => { + describe('Fetching buyer and vendor Order(s)', () => { + it("Should return all orders with it's buyer and related vendors", async () => { + const response = await request(app) + .get('/product/admin/orders') + .set('Authorization', `Bearer ${getAccessToken(adminId, sampleAdmin.email)}`); + + expect(response.status).toBe(200); + expect(response.body.data.orders).toBeDefined(); + }); + + it('Should return single order details', async () => { + const response = await request(app) + .get(`/product/admin/orders/${orderId}`) + .set('Authorization', `Bearer ${getAccessToken(adminId, sampleAdmin.email)}`); + + expect(response.status).toBe(200); + expect(response.body.data.order).toBeDefined(); + }); + + it('return 404, for non existing order', async () => { + const response = await request(app) + .get(`/product/admin/orders/${uuid()}`) + .set('Authorization', `Bearer ${getAccessToken(adminId, sampleAdmin.email)}`); + + expect(response.status).toBe(404); + expect(response.body.message).toBe('Order Not Found.'); + }); + + it('return 400, for invalid order id ', async () => { + const response = await request(app) + .get(`/product/admin/orders/32df3`) + .set('Authorization', `Bearer ${getAccessToken(adminId, sampleAdmin.email)}`); + + expect(response.status).toBe(400); + expect(response.body.message).toBe(`invalid input syntax for type uuid: "32df3"`); + }); + }); + describe('Updating buyer and vendor order', () => { + it('should not update, return 404 for non existing order', async () => { + const response = await request(app) + .put(`/product/admin/orders/${uuid()}`) + .set('Authorization', `Bearer ${getAccessToken(adminId, sampleAdmin.email)}`); + + expect(response.statusCode).toBe(404); + expect(response.body.message).toBe('Order Not Found.'); + }); + it('should update the order', async () => { + const response = await request(app) + .put(`/product/admin/orders/${orderId}`) + .set('Authorization', `Bearer ${getAccessToken(adminId, sampleAdmin.email)}`); + + expect(response.statusCode).toBe(200); + expect(response.body.data.order).toBeDefined(); + }); + it('should not update if it has already been completed(closed)', async () => { + const response = await request(app) + .put(`/product/admin/orders/${orderId}`) + .set('Authorization', `Bearer ${getAccessToken(adminId, sampleAdmin.email)}`); + + expect(response.statusCode).toBe(409); + expect(response.body.message).toBe('The order has already been completed.'); + }); + + it('should not update, if the order has not been marked as received by buyer', async () => { + const response = await request(app) + .put(`/product/admin/orders/${order2Id}`) + .set('Authorization', `Bearer ${getAccessToken(adminId, sampleAdmin.email)}`); + + expect(response.statusCode).toBe(409); + expect(response.body.message).toBe('Order closure failed: The buyer has not received the item yet.'); + }); + + it('return 400, for invalid order id ', async () => { + const response = await request(app) + .put(`/product/admin/orders/32df3`) + .set('Authorization', `Bearer ${getAccessToken(adminId, sampleAdmin.email)}`); + + expect(response.status).toBe(400); + expect(response.body.message).toBe(`invalid input syntax for type uuid: "32df3"`); + }); + }); +}); diff --git a/src/__test__/productStatus.test.ts b/src/__test__/productStatus.test.ts index a770020..8e8b42a 100644 --- a/src/__test__/productStatus.test.ts +++ b/src/__test__/productStatus.test.ts @@ -7,6 +7,7 @@ import { User } from '../entities/User'; import { v4 as uuid } from 'uuid'; import { Product } from '../entities/Product'; import { Category } from '../entities/Category'; +import { cleanDatabase } from './test-assets/DatabaseCleanup'; const vendor1Id = uuid(); const vendor2Id = uuid(); @@ -143,17 +144,8 @@ beforeAll(async () => { }); afterAll(async () => { - const connection = getConnection(); - const userRepository = connection.getRepository(User); - const categoryRepository = connection.getRepository(Category); + await cleanDatabase(); - const productRepository = await connection.getRepository(Product).delete({}); - if (productRepository) { - await userRepository.delete({}); - await categoryRepository.delete({}); - } - - await connection.close(); server.close(); }); @@ -230,22 +222,16 @@ describe('Vendor product availability status management tests', () => { }); }); - describe('search product by name availability tests', () => { it('Should search product by name', async () => { - const response = await request(app) - .get(`/product/search?name=testingmkknkkjiproduct4`) + const response = await request(app).get(`/product/search?name=testingmkknkkjiproduct4`); expect(response.body.data).toBeDefined; }, 10000); it('should return empty array if there is product is not found in the database', async () => { - const response = await request(app) - .put(`/product/search?name=home`) - + const response = await request(app).put(`/product/search?name=home`); expect(response.statusCode).toBe(401); expect(response.body.data).toBeUndefined; }); - - }); - +}); diff --git a/src/__test__/roleCheck.test.ts b/src/__test__/roleCheck.test.ts index 58efb9c..32df044 100644 --- a/src/__test__/roleCheck.test.ts +++ b/src/__test__/roleCheck.test.ts @@ -5,6 +5,7 @@ import { responseError } from '../utils/response.utils'; import { dbConnection } from '../startups/dbConnection'; import { v4 as uuid } from 'uuid'; import { getConnection } from 'typeorm'; +import { cleanDatabase } from './test-assets/DatabaseCleanup'; let reqMock: Partial; let resMock: Partial; @@ -34,14 +35,7 @@ beforeAll(async () => { }); afterAll(async () => { - const connection = getConnection(); - const userRepository = connection.getRepository(User); - - // Delete all records from the User - await userRepository.delete({}); - - // Close the connection to the test database - await connection.close(); + await cleanDatabase(); }); describe('hasRole MiddleWare Test', () => { diff --git a/src/__test__/route.test.ts b/src/__test__/route.test.ts index 933c431..ac704b5 100644 --- a/src/__test__/route.test.ts +++ b/src/__test__/route.test.ts @@ -3,24 +3,15 @@ import { app, server } from '../index'; import { createConnection, getConnection, getConnectionOptions, getRepository } from 'typeorm'; import { User } from '../entities/User'; import { response } from 'express'; +import { cleanDatabase } from './test-assets/DatabaseCleanup'; beforeAll(async () => { - // Connect to the test database - const connectionOptions = await getConnectionOptions(); - - await createConnection({ ...connectionOptions, name: 'testConnection' }); + await createConnection(); }); jest.setTimeout(20000); afterAll(async () => { - const connection = getConnection('testConnection'); - const userRepository = connection.getRepository(User); - - // Delete all records from the User - await userRepository.delete({}); - - // Close the connection to the test database - await connection.close(); + await cleanDatabase(); server.close(); }); diff --git a/src/__test__/test-assets/DatabaseCleanup.ts b/src/__test__/test-assets/DatabaseCleanup.ts new file mode 100644 index 0000000..3674dfb --- /dev/null +++ b/src/__test__/test-assets/DatabaseCleanup.ts @@ -0,0 +1,50 @@ +import { Transaction } from '../../entities/transaction'; +import { Cart } from '../../entities/Cart'; +import { CartItem } from '../../entities/CartItem'; +import { Order } from '../../entities/Order'; +import { OrderItem } from '../../entities/OrderItem'; +import { wishList } from '../../entities/wishList'; +import { getConnection } from 'typeorm'; +import { Product } from '../../entities/Product'; +import { Category } from '../../entities/Category'; +import { Coupon } from '../../entities/coupon'; +import { User } from '../../entities/User'; +import { server } from '../..'; +import { VendorOrderItem } from '../../entities/VendorOrderItem'; +import { VendorOrders } from '../../entities/vendorOrders'; + +export const cleanDatabase = async () => { + const connection = getConnection(); + + // Delete from child tables first + await connection.getRepository(Transaction).delete({}); + await connection.getRepository(Coupon).delete({}); + await connection.getRepository(VendorOrderItem).delete({}); + await connection.getRepository(VendorOrders).delete({}); + await connection.getRepository(OrderItem).delete({}); + await connection.getRepository(Order).delete({}); + await connection.getRepository(CartItem).delete({}); + await connection.getRepository(Cart).delete({}); + await connection.getRepository(wishList).delete({}); + + // Many-to-Many relations + // Clear junction table entries before deleting products and categories + await connection.createQueryRunner().query('DELETE FROM product_categories_category'); + + await connection.getRepository(Product).delete({}); + await connection.getRepository(Category).delete({}); + + // Coupons (if related to Orders or Users) + + // Finally, delete from parent table + await connection.getRepository(User).delete({}); + + await connection.close(); +}; + +// // Execute the clean-up function +// cleanDatabase().then(() => { +// console.log('Database cleaned'); +// }).catch(error => { +// console.error('Error cleaning database:', error); +// }); diff --git a/src/__test__/userServices.test.ts b/src/__test__/userServices.test.ts index 5249a9a..29a2e7c 100644 --- a/src/__test__/userServices.test.ts +++ b/src/__test__/userServices.test.ts @@ -1,25 +1,15 @@ import request from 'supertest'; import { app, server } from '../index'; -import { createConnection, getConnection, getConnectionOptions, getRepository } from 'typeorm'; +import { createConnection, getRepository } from 'typeorm'; import { User } from '../entities/User'; +import { cleanDatabase } from './test-assets/DatabaseCleanup'; beforeAll(async () => { - // Connect to the test database - const connectionOptions = await getConnectionOptions(); - - await createConnection({ ...connectionOptions, name: 'testConnection' }); + await createConnection(); }); afterAll(async () => { - const connection = getConnection('testConnection'); - const userRepository = connection.getRepository(User); - - // Delete all records from the User - await userRepository.delete({}); - - // Close the connection to the test database - await connection.close(); - + await cleanDatabase(); server.close(); }); diff --git a/src/__test__/userStatus.test.ts b/src/__test__/userStatus.test.ts index 8d42488..69e892a 100644 --- a/src/__test__/userStatus.test.ts +++ b/src/__test__/userStatus.test.ts @@ -6,6 +6,7 @@ import { getConnection } from 'typeorm'; import { dbConnection } from '../startups/dbConnection'; import { User } from '../entities/User'; import { v4 as uuid } from 'uuid'; +import { cleanDatabase } from './test-assets/DatabaseCleanup'; const adminUserId = uuid(); @@ -35,14 +36,8 @@ beforeAll(async () => { }); afterAll(async () => { - const connection = getConnection(); - const userRepository = connection.getRepository(User); + await cleanDatabase(); - // Delete all records from the User - await userRepository.delete({}); - - // Close the connection to the test database - await connection.close(); server.close(); }); diff --git a/src/__test__/vendorProduct.test.ts b/src/__test__/vendorProduct.test.ts index dfa96c4..d8fc0a5 100644 --- a/src/__test__/vendorProduct.test.ts +++ b/src/__test__/vendorProduct.test.ts @@ -7,6 +7,7 @@ import { User, UserInterface } from '../entities/User'; import { v4 as uuid } from 'uuid'; import { Product } from '../entities/Product'; import { Category } from '../entities/Category'; +import { cleanDatabase } from './test-assets/DatabaseCleanup'; const vendor1Id = uuid(); const vendor2Id = uuid(); @@ -109,18 +110,8 @@ beforeAll(async () => { }); afterAll(async () => { - const connection = getConnection(); - const userRepository = connection.getRepository(User); - const categoryRepository = connection.getRepository(Category); - - const productRepository = await connection.getRepository(Product).delete({}); - if (productRepository) { - await userRepository.delete({}); - await categoryRepository.delete({}); - } - - // Close the connection to the test database - await connection.close(); + await cleanDatabase(); + server.close(); }); @@ -456,29 +447,27 @@ describe('Vendor product management tests', () => { describe('List all products service', () => { it('should return all products for a given category', async () => { - const response = await request(app) - .get('/product/all') + const response = await request(app).get('/product/all'); expect(response.status).toBe(200); expect(response.body.data.products).toBeDefined(); }); - + it('should return no products for a non-existent category', async () => { const response = await request(app) .get('/product/all') - .query({ page: 1, limit: 10, category: 'nonexistentcategory' }) - + .query({ page: 1, limit: 10, category: 'nonexistentcategory' }); + expect(response.status).toBe(200); expect(response.body.data.products).toBeUndefined(); }); - + it('should return an error for invalid input syntax', async () => { const response = await request(app) .get('/product/all') - .query({ page: 'invalid', limit: 'limit', category: 'technology' }) - + .query({ page: 'invalid', limit: 'limit', category: 'technology' }); + expect(response.status).toBe(400); }); }); - }); diff --git a/src/__test__/wishList.test.ts b/src/__test__/wishList.test.ts index 2b2f120..6658853 100644 --- a/src/__test__/wishList.test.ts +++ b/src/__test__/wishList.test.ts @@ -8,6 +8,7 @@ import { Product } from '../entities/Product'; import { Category } from '../entities/Category'; import { wishList } from '../entities/wishList'; import { User, UserInterface } from '../entities/User'; +import { cleanDatabase } from './test-assets/DatabaseCleanup'; const buyer1Id = uuid(); const buyer2Id = uuid(); @@ -17,222 +18,187 @@ const catId = uuid(); const vendor2Id = uuid(); const sampleBuyer1: UserInterface = { - id: buyer1Id, - firstName: 'buyer1', - lastName: 'user', - email: 'buyer1@example.com', - password: 'password', - userType: 'Buyer', - gender: 'Male', - phoneNumber: '126380996347', - photoUrl: 'https://example.com/photo.jpg', - role: 'BUYER', - }; - const sampleBuyer2: UserInterface = { - id: buyer2Id, - firstName: 'buyer2', - lastName: 'use', - email: 'buyer2@example.com', - password: 'passwo', - userType: 'Buyer', - gender: 'Male', - phoneNumber: '1638099347', - photoUrl: 'https://example.com/photo.jpg', - role: 'BUYER', - }; - const sampleVendor1: UserInterface = { - id: vendor2Id, - firstName: 'vendor1', - lastName: 'user', - email: 'vendor11@example.com', - password: 'password', - userType: 'Vendor', - gender: 'Male', - phoneNumber: '12638090347', - photoUrl: 'https://example.com/photo.jpg', - role: 'VENDOR', - }; + id: buyer1Id, + firstName: 'buyer1', + lastName: 'user', + email: 'buyer1@example.com', + password: 'password', + userType: 'Buyer', + gender: 'Male', + phoneNumber: '126380996347', + photoUrl: 'https://example.com/photo.jpg', + role: 'BUYER', +}; +const sampleBuyer2: UserInterface = { + id: buyer2Id, + firstName: 'buyer2', + lastName: 'use', + email: 'buyer2@example.com', + password: 'passwo', + userType: 'Buyer', + gender: 'Male', + phoneNumber: '1638099347', + photoUrl: 'https://example.com/photo.jpg', + role: 'BUYER', +}; +const sampleVendor1: UserInterface = { + id: vendor2Id, + firstName: 'vendor1', + lastName: 'user', + email: 'vendor11@example.com', + password: 'password', + userType: 'Vendor', + gender: 'Male', + phoneNumber: '12638090347', + photoUrl: 'https://example.com/photo.jpg', + role: 'VENDOR', +}; let productInWishList: number; beforeAll(async () => { - const connection = await dbConnection(); - const userRepository = connection?.getRepository(User); - await userRepository?.save({ ...sampleBuyer1 }); - await userRepository?.save({ ...sampleBuyer2 }); - await userRepository?.save({...sampleVendor1}) - }); + const connection = await dbConnection(); + const userRepository = connection?.getRepository(User); + await userRepository?.save({ ...sampleBuyer1 }); + await userRepository?.save({ ...sampleBuyer2 }); + await userRepository?.save({ ...sampleVendor1 }); +}); + +afterAll(async () => { + await cleanDatabase(); + server.close(); +}); +const data1 = { + id: buyer1Id, + email: sampleBuyer1.email, +}; +const data2 = { + id: buyer2Id, + email: sampleBuyer2.email, +}; +const vendorData = { + id: vendor2Id, + email: sampleVendor1.email, +}; - afterAll(async () => { - const connection = getConnection(); - const userRepository = connection.getRepository(User); - const categoryRepository = connection.getRepository(Category); - const wishListRepository = connection.getRepository(wishList) - - // Delete all records - const productRepository = await connection.getRepository(Product).delete({}); - if (productRepository) { - await userRepository.delete({}); - await categoryRepository.delete({}); - } - await userRepository.delete({}); - await wishListRepository.delete({}) - - // Close the connection to the test database - await connection.close(); - server.close(); +const jwtSecretKey = process.env.JWT_SECRET || ''; +describe('Wish list management tests', () => { + describe('Add product to wish list', () => { + it('should return 404 when product is not found', async () => { + const token = jwt.sign(data1, jwtSecretKey); + const response = await request(app).post(`/wish-list/add/${uuid()}`).set('Authorization', `Bearer ${token}`); + expect(response.status).toBe(404); + expect(response.body).toEqual({ message: 'Product not found' }); + }); + + it('should add a new product to wish list', async () => { + const vendorToken = jwt.sign(vendorData, jwtSecretKey); + const prod1Response = await request(app) + .post('/product') + .field('name', 'test product12') + .field('description', 'amazing product3') + .field('newPrice', 2000) + .field('quantity', 10) + .field('categories', 'technology') + .field('categories', 'sample') + .attach('images', `${__dirname}/test-assets/photo1.png`) + .attach('images', `${__dirname}/test-assets/photo2.webp`) + .set('Authorization', `Bearer ${vendorToken}`); + + product1Id = prod1Response.body.data.product.id; + + const prod2Response = await request(app) + .post('/product') + .field('name', 'more product2') + .field('description', 'food product3') + .field('newPrice', 2000) + .field('quantity', 10) + .field('categories', 'technology') + .field('categories', 'sample') + .attach('images', `${__dirname}/test-assets/photo1.png`) + .attach('images', `${__dirname}/test-assets/photo2.webp`) + .set('Authorization', `Bearer ${vendorToken}`); + + product2Id = prod2Response.body.data.product.id; + + const token = jwt.sign(data1, jwtSecretKey); + const response1 = await request(app).post(`/wish-list/add/${product1Id}`).set('Authorization', `Bearer ${token}`); + expect(response1.status).toBe(201); + expect(response1.body.data.message).toBe('Product Added to wish list'); + productInWishList = response1.body.data.wishlistAdded.id; + + await request(app).post(`/wish-list/add/${product2Id}`).set('Authorization', `Bearer ${token}`); + }); + + it('should tell if there is the product is already in the wish list', async () => { + const token = jwt.sign(data1, jwtSecretKey); + const response = await request(app).post(`/wish-list/add/${product1Id}`).set('Authorization', `Bearer ${token}`); + expect(response.status).toBe(401); + expect(response.body.data.message).toBe('Product Already in the wish list'); + }); + it('should return 500 when the ID is not valid', async () => { + const token = jwt.sign(data1, jwtSecretKey); + const response = await request(app) + .post(`/wish-list/add/kjwxq-wbjk2-2bwqs-21`) + .set('Authorization', `Bearer ${token}`); + expect(response.status).toBe(500); + }); }); - const data1 = { - id: buyer1Id, - email: sampleBuyer1.email - }; - const data2 = { - id: buyer2Id, - email: sampleBuyer2.email - } - const vendorData = { - id: vendor2Id, - email: sampleVendor1.email - } + describe('Get products in wishList', () => { + it('Returns 404 when buyer has no product in wish list', async () => { + const token = jwt.sign(data2, jwtSecretKey); + const response = await request(app).get('/wish-list').set('Authorization', `Bearer ${token}`); + expect(response.status).toBe(404); + expect(response.body.message).toBe('No products in wish list'); + }); + + it('Returns products in the wish list for a buyer ', async () => { + const token = jwt.sign(data1, jwtSecretKey); + const response = await request(app).get('/wish-list').set('Authorization', `Bearer ${token}`); + expect(response.status).toBe(200); + expect(response.body.message).toBe('Products retrieved'); + }); + }); -const jwtSecretKey = process.env.JWT_SECRET || ''; -describe('Wish list management tests', () => { + describe('Remove a product from wish lsit', () => { + it('should return 404 when product is not found in wish list', async () => { + const token = jwt.sign(data1, jwtSecretKey); + const response = await request(app).delete(`/wish-list/delete/${28}`).set('Authorization', `Bearer ${token}`); + expect(response.status).toBe(404); + expect(response.body.message).toBe('Product not found in wish list'); + }); + + it('should delete a product from wish list', async () => { + const token = jwt.sign(data1, jwtSecretKey); + const response = await request(app) + .delete(`/wish-list/delete/${productInWishList}`) + .set('Authorization', `Bearer ${token}`); + expect(response.status).toBe(200); + expect(response.body.message).toBe('Product removed from wish list'); + }); + it('should return 500 when the ID is not valid', async () => { + const token = jwt.sign(data1, jwtSecretKey); + const response = await request(app) + .delete(`/wish-list/delete/kjwxq-wbjk2-2bwqs-21`) + .set('Authorization', `Bearer ${token}`); + expect(response.status).toBe(500); + }); + }); - describe('Add product to wish list', () => { - it('should return 404 when product is not found', async () => { - const token = jwt.sign(data1, jwtSecretKey); - const response = await request(app) - .post( `/wish-list/add/${uuid()}`) - .set('Authorization', `Bearer ${token}`); - expect(response.status).toBe(404); - expect(response.body).toEqual({ message: 'Product not found' }); - }) - - it('should add a new product to wish list', async () =>{ - const vendorToken = jwt.sign(vendorData, jwtSecretKey); - const prod1Response = await request(app) - .post('/product') - .field('name', 'test product12') - .field('description', 'amazing product3') - .field('newPrice', 2000) - .field('quantity', 10) - .field('categories', 'technology') - .field('categories', 'sample') - .attach('images', `${__dirname}/test-assets/photo1.png`) - .attach('images', `${__dirname}/test-assets/photo2.webp`) - .set('Authorization', `Bearer ${vendorToken}`); - - product1Id = prod1Response.body.data.product.id; - - const prod2Response = await request(app) - .post('/product') - .field('name', 'more product2') - .field('description', 'food product3') - .field('newPrice', 2000) - .field('quantity', 10) - .field('categories', 'technology') - .field('categories', 'sample') - .attach('images', `${__dirname}/test-assets/photo1.png`) - .attach('images', `${__dirname}/test-assets/photo2.webp`) - .set('Authorization', `Bearer ${vendorToken}`); - - product2Id = prod2Response.body.data.product.id; - - const token = jwt.sign(data1, jwtSecretKey); - const response1 = await request(app) - .post( `/wish-list/add/${product1Id}`) - .set('Authorization', `Bearer ${token}`); - expect(response1.status).toBe(201); - expect(response1.body.data.message).toBe('Product Added to wish list'); - productInWishList = response1.body.data.wishlistAdded.id; - - await request(app) - .post( `/wish-list/add/${product2Id}`) - .set('Authorization', `Bearer ${token}`); - }) - - it('should tell if there is the product is already in the wish list', async () => { - const token = jwt.sign(data1, jwtSecretKey); - const response = await request(app) - .post( `/wish-list/add/${product1Id}`) - .set('Authorization', `Bearer ${token}`); - expect(response.status).toBe(401); - expect(response.body.data.message).toBe('Product Already in the wish list'); - }) - it('should return 500 when the ID is not valid', async() => { - const token = jwt.sign(data1, jwtSecretKey); - const response = await request(app) - .post( `/wish-list/add/kjwxq-wbjk2-2bwqs-21`) - .set('Authorization', `Bearer ${token}`); - expect(response.status).toBe(500); - }) - }) - - describe('Get products in wishList', () => { - it('Returns 404 when buyer has no product in wish list', async () => { - const token = jwt.sign(data2, jwtSecretKey); - const response = await request(app) - .get('/wish-list') - .set('Authorization', `Bearer ${token}`); - expect(response.status).toBe(404); - expect(response.body.message).toBe('No products in wish list'); - }) - - it('Returns products in the wish list for a buyer ', async () => { - const token = jwt.sign(data1, jwtSecretKey); - const response = await request(app) - .get('/wish-list') - .set('Authorization', `Bearer ${token}`); - expect(response.status).toBe(200); - expect(response.body.message).toBe('Products retrieved'); - }) - }) - - describe('Remove a product from wish lsit', () => { - it('should return 404 when product is not found in wish list', async () => { - const token = jwt.sign(data1, jwtSecretKey); - const response = await request(app) - .delete( `/wish-list/delete/${28}`) - .set('Authorization', `Bearer ${token}`); - expect(response.status).toBe(404); - expect(response.body.message).toBe('Product not found in wish list'); - }) - - it('should delete a product from wish list', async () => { - const token = jwt.sign(data1, jwtSecretKey); - const response = await request(app) - .delete( `/wish-list/delete/${productInWishList}`) - .set('Authorization', `Bearer ${token}`); - expect(response.status).toBe(200); - expect(response.body.message).toBe('Product removed from wish list'); - }) - it('should return 500 when the ID is not valid', async() => { - const token = jwt.sign(data1, jwtSecretKey); - const response = await request(app) - .delete( `/wish-list/delete/kjwxq-wbjk2-2bwqs-21`) - .set('Authorization', `Bearer ${token}`); - expect(response.status).toBe(500); - }) - }) - - describe('Clear all products in wish for a user', () => { - it('Returns 404 when buyer has no product in wish list', async () => { - const token = jwt.sign(data2, jwtSecretKey); - const response = await request(app) - .delete( '/wish-list/clearAll') - .set('Authorization', `Bearer ${token}`); - expect(response.status).toBe(404); - expect(response.body.message).toBe('No products in wish list'); - }) - - it('should delete all products for a nuyer in wish list', async () => { - const token = jwt.sign(data1, jwtSecretKey); - const response = await request(app) - .delete( '/wish-list/clearAll') - .set('Authorization', `Bearer ${token}`); - expect(response.status).toBe(200); - expect(response.body.message).toBe('All products removed successfully'); - }) - }) -}) \ No newline at end of file + describe('Clear all products in wish for a user', () => { + it('Returns 404 when buyer has no product in wish list', async () => { + const token = jwt.sign(data2, jwtSecretKey); + const response = await request(app).delete('/wish-list/clearAll').set('Authorization', `Bearer ${token}`); + expect(response.status).toBe(404); + expect(response.body.message).toBe('No products in wish list'); + }); + + it('should delete all products for a nuyer in wish list', async () => { + const token = jwt.sign(data1, jwtSecretKey); + const response = await request(app).delete('/wish-list/clearAll').set('Authorization', `Bearer ${token}`); + expect(response.status).toBe(200); + expect(response.body.message).toBe('All products removed successfully'); + }); + }); +}); diff --git a/src/controllers/adminOrdercontroller.ts b/src/controllers/adminOrdercontroller.ts new file mode 100644 index 0000000..388220d --- /dev/null +++ b/src/controllers/adminOrdercontroller.ts @@ -0,0 +1,18 @@ +import { Request, Response } from 'express'; +import { + getSingleBuyerVendorOrderService, + getBuyerVendorOrdersService, + updateBuyerVendorOrderService, +} from '../services'; + +export const getBuyerVendorOrders = async (req: Request, res: Response) => { + await getBuyerVendorOrdersService(req, res); +}; + +export const getSingleBuyerVendorOrder = async (req: Request, res: Response) => { + await getSingleBuyerVendorOrderService(req, res); +}; + +export const updateBuyerVendorOrder = async (req: Request, res: Response) => { + await updateBuyerVendorOrderService(req, res); +}; diff --git a/src/controllers/couponController.ts b/src/controllers/couponController.ts new file mode 100644 index 0000000..e5a6804 --- /dev/null +++ b/src/controllers/couponController.ts @@ -0,0 +1,31 @@ +import { Request, Response } from 'express'; +import { createCouponService } from '../services/couponServices/createCouponService'; +import { updateCouponService } from '../services/couponServices/updateService'; +import { deleteCouponService } from '../services/couponServices/deleteCoupon'; +import { accessAllCouponService } from '../services/couponServices/accessAllCoupon'; +import { readCouponService } from '../services/couponServices/readCoupon'; +import { buyerApplyCouponService } from '../services/couponServices/buyerApplyCoupon'; + +export const createCoupon = async (req: Request, res: Response) => { + await createCouponService(req, res); +}; + +export const updateCoupon = async (req: Request, res: Response) => { + await updateCouponService(req, res); +}; + +export const deleteCoupon = async (req: Request, res: Response) => { + await deleteCouponService(req, res); +}; + +export const accessAllCoupon = async (req: Request, res: Response) => { + await accessAllCouponService(req, res); +}; + +export const readCoupon = async (req: Request, res: Response) => { + await readCouponService(req, res); +}; + +export const buyerApplyCoupon = async (req: Request, res: Response) => { + await buyerApplyCouponService(req, res); +}; diff --git a/src/controllers/index.ts b/src/controllers/index.ts index c7f3221..70dea3b 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -1,2 +1,5 @@ export * from './authController'; export * from './productController'; +export * from './orderController'; +export * from './vendorOrderController'; +export * from './adminOrdercontroller'; diff --git a/src/controllers/orderController.ts b/src/controllers/orderController.ts new file mode 100644 index 0000000..d4ac5fc --- /dev/null +++ b/src/controllers/orderController.ts @@ -0,0 +1,18 @@ +import { Request, Response } from 'express'; +import { createOrderService } from '../services/orderServices/createOrder'; +import { getOrdersService } from '../services/orderServices/getOrderService'; +import { updateOrderService } from '../services/orderServices/updateOrderService'; +import { getTransactionHistoryService } from '../services/orderServices/getOrderTransactionHistory'; + +export const createOrder = async (req: Request, res: Response) => { + await createOrderService(req, res); +}; +export const getOrders = async (req: Request, res: Response) => { + await getOrdersService(req, res); +}; +export const updateOrder = async (req: Request, res: Response) => { + await updateOrderService(req, res); +}; +export const getOrdersHistory = async (req: Request, res: Response) => { + await getTransactionHistoryService(req, res); +}; diff --git a/src/controllers/productController.ts b/src/controllers/productController.ts index 11caddd..05aa5a3 100644 --- a/src/controllers/productController.ts +++ b/src/controllers/productController.ts @@ -1,26 +1,18 @@ import { Request, Response } from 'express'; import { - createProductService, - updateProductService, - - removeProductImageService, - + removeProductImageService, readProductService, - readProductsService, - + readProductsService, deleteProductService, - getRecommendedProductsService, productStatusServices, viewSingleProduct, - searchProductService - -, - listAllProductsService} -from '../services'; - + searchProductService, + listAllProductsService, + confirmPayment, +} from '../services'; export const readProduct = async (req: Request, res: Response) => { await readProductService(req, res); @@ -50,10 +42,10 @@ export const getRecommendedProducts = async (req: Request, res: Response) => { await getRecommendedProductsService(req, res); }; - export const listAllProducts = async (req: Request, res: Response) => { await listAllProductsService(req, res); -};export const productStatus = async (req: Request, res: Response) => { +}; +export const productStatus = async (req: Request, res: Response) => { await productStatusServices(req, res); }; export const singleProduct = async (req: Request, res: Response) => { @@ -79,3 +71,6 @@ export const searchProduct = async (req: Request, res: Response) => { res.status(500).json({ error: 'Internal Server Error' }); } }; +export const Payment = async (req: Request, res: Response) => { + await confirmPayment(req, res); +}; diff --git a/src/controllers/vendorOrderController.ts b/src/controllers/vendorOrderController.ts new file mode 100644 index 0000000..955b01c --- /dev/null +++ b/src/controllers/vendorOrderController.ts @@ -0,0 +1,14 @@ +import { Request, Response } from 'express'; +import { getVendorOrdersService, getSingleVendorOrderService, updateVendorOrderService } from '../services'; + +export const getVendorOrders = async (req: Request, res: Response) => { + await getVendorOrdersService(req, res); +}; + +export const getSingleVendorOrder = async (req: Request, res: Response) => { + await getSingleVendorOrderService(req, res); +}; + +export const updateVendorOrder = async (req: Request, res: Response) => { + await updateVendorOrderService(req, res); +}; diff --git a/src/controllers/wishListController.ts b/src/controllers/wishListController.ts index e0cd1bd..23fa03f 100644 --- a/src/controllers/wishListController.ts +++ b/src/controllers/wishListController.ts @@ -1,23 +1,18 @@ import { Request, Response } from 'express'; -import{ - addProductService, - getProductsService, - removeProductService, - clearAllProductService -} from '../services' +import { addProductService, getProductsService, removeProductService, clearAllProductService } from '../services'; export const wishlistAddProduct = async (req: Request, res: Response) => { - await addProductService(req, res); - }; + await addProductService(req, res); +}; - export const wishlistRemoveProduct = async (req: Request, res:Response) => { - await removeProductService(req, res); - } +export const wishlistRemoveProduct = async (req: Request, res: Response) => { + await removeProductService(req, res); +}; - export const wishlistGetProducts = async (req: Request, res:Response) => { - await getProductsService(req, res); - } +export const wishlistGetProducts = async (req: Request, res: Response) => { + await getProductsService(req, res); +}; - export const wishlistClearAllProducts = async (req: Request, res:Response) => { - await clearAllProductService(req, res); - } \ No newline at end of file +export const wishlistClearAllProducts = async (req: Request, res: Response) => { + await clearAllProductService(req, res); +}; diff --git a/src/docs/adminOrderManagement.yml b/src/docs/adminOrderManagement.yml new file mode 100644 index 0000000..e8d6ed6 --- /dev/null +++ b/src/docs/adminOrderManagement.yml @@ -0,0 +1,80 @@ +/product/admin/orders: + get: + tags: + - Admin Order Manangement + summary: Fetches all buyer and vendor orders + description: Return all order including details for buyer and vendors of products in that order + security: + - bearerAuth: [] + responses: + '200': + description: Return all order + '400': + description: Bad Request (syntax error, incorrect input format, etc..) + '401': + description: Unauthorized + '403': + description: Forbidden (Unauthorized action) + '500': + description: Internal server error + +/product/admin/orders/{id}: + get: + tags: + - Admin Order Manangement + summary: Fetch details for single buyer and vendor order + description: + Fetch details for single order using buyer id, if successful return order details with it's corresponding vendor + security: + - bearerAuth: [] + parameters: + - in: path + name: id + schema: + type: string + required: true + description: The id of a buyer order + responses: + '200': + description: Order details retrieved successfully + '400': + description: Bad Request (syntax error, incorrect input format, etc..) + '401': + description: Unauthorized + '403': + description: Forbidden (Unauthorized action) + '404': + description: Order not found + '500': + description: Internal server error + put: + tags: + - Admin Order Manangement + summary: Updates order status for both buyer and vendor order + description: Updates orderStatus field of order, if successful returns updated order. + security: + - bearerAuth: [] + parameters: + - in: path + name: id + schema: + type: string + required: true + description: The id of a buyer order + consumes: + - application/json + responses: + '200': + description: Order was successfully updated, return updated order + '400': + description: Bad Request (syntax error, incorrect input format, etc..) + '401': + description: Unauthorized + '403': + description: Forbidden (Unauthorized action) + '404': + description: Order not found + '409': + description: Order can not be updated because (it has already been completed(close), delivered, cancelled) + '500': + description: Internal server error diff --git a/src/docs/couponDocs.yml b/src/docs/couponDocs.yml new file mode 100644 index 0000000..fefa829 --- /dev/null +++ b/src/docs/couponDocs.yml @@ -0,0 +1,217 @@ +/coupons/vendor/:id/access-coupons: + get: + tags: + - Vendor discount coupon management + summary: List all coupons + description: Return all coupons for the logged user + security: + - bearerAuth: [] + responses: + '200': + description: Return all coupons + '400': + description: Bad Request (syntax error, incorrect input format, etc..) + '401': + description: Unauthorized + '403': + description: Forbidden (Unauthorized action) + '500': + description: Internal server error + +/coupons/vendor/:id/checkout/:code: + get: + tags: + - Vendor discount coupon management + summary: Get a single coupon + description: Return a coupon based on the provided code + security: + - bearerAuth: [] + parameters: + - in: path + name: code + schema: + type: string + required: true + description: The code of the coupon + responses: + '200': + description: Return info for the coupon + '400': + description: Bad Request (syntax error, incorrect input format, etc..) + '401': + description: Unauthorized + '403': + description: Forbidden (Unauthorized action) + '404': + description: Coupon not found + '500': + description: Internal server error + +/coupons/coupons/vendor/:id: + post: + tags: + - Vendor discount coupon management + summary: Creates a new coupon + security: + - bearerAuth: [] + consumes: + - application/json + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + code: + type: string + discountType: + type: string + discountRate: + type: number + maxUsageLimit: + type: number + quantity: + type: number + product: + type: string + expirationDate: + type: string + format: date + required: + - code + - discountType + - maxUsageLimit + - product + responses: + '201': + description: Successfully added the coupon + '400': + description: Bad Request (syntax error, incorrect input format, etc..) + '401': + description: Unauthorized + '403': + description: Forbidden (Unauthorized action) + '404': + description: Coupon not found + '500': + description: Internal server error + +/coupons/coupons/vendor/:id/update-coupon/:code: + put: + tags: + - Vendor discount coupon management + summary: Update a coupon + security: + - bearerAuth: [] + parameters: + - in: path + name: code + schema: + type: string + required: true + description: The code of the coupon + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + code: + type: string + discountType: + type: string + discountRate: + type: number + maxUsageLimit: + type: number + quantity: + type: number + product: + type: string + expirationDate: + type: string + format: date + responses: + '200': + description: Successfully updated the coupon + '400': + description: Bad Request (syntax error, incorrect input format, etc..) + '401': + description: Unauthorized + '403': + description: Forbidden (Unauthorized action) + '404': + description: Coupon not found + '500': + description: Internal server error + +/coupons/vendor/:id/checkout/delete: + delete: + tags: + - Vendor discount coupon management + summary: Delete a coupon + security: + - bearerAuth: [] + parameters: + - in: path + name: id + schema: + type: string + required: true + description: The ID of the vendor + - in: query + name: code + schema: + type: string + required: true + description: The code of the coupon + responses: + '200': + description: Successfully deleted the coupon + '400': + description: Bad Request (syntax error, incorrect input format, etc..) + '401': + description: Unauthorized + '403': + description: Forbidden (Unauthorized action) + '404': + description: Coupon not found + '500': + description: Internal server error + +/coupons/apply: + post: + tags: + - Buyer Coupon Discount Management + summary: Give discount according to coupon code + description: Buyer gets discount on a product when all the checks pass + security: + - bearerAuth: [] + consumes: + - application/json + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + couponCode: + type: string + required: + - couponCode + responses: + '200': + description: Successfully Got Discount + '400': + description: Bad Request (Syntax error, No coupon code provide, Coupon is expired, Coupon Discount Ended,etc) + '401': + description: Unauthorized + '403': + description: Forbidden (Unauthorized action) + '404': + description: Coupon not found, No cart or product with that coupon is not in cart + '500': + description: Internal server error diff --git a/src/docs/orderDocs.yml b/src/docs/orderDocs.yml new file mode 100644 index 0000000..fcb620e --- /dev/null +++ b/src/docs/orderDocs.yml @@ -0,0 +1,108 @@ +paths: + /product/orders: + post: + tags: + - Order + summary: Make an order + description: Create a new order for the authenticated user + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + address: + type: object + properties: + country: + type: string + description: The country of the shipping address + city: + type: string + description: The city of the shipping address + street: + type: string + description: The street address + required: + - address + responses: + '201': + description: Order created successfully + '400': + description: Bad Request + '401': + description: Unauthorized + '500': + description: Internal Server Error + + /product/client/orders: + get: + tags: + - Order + summary: Get all orders + description: Retrieve all orders for the authenticated user + security: + - bearerAuth: [] + responses: + '200': + description: Orders retrieved successfully + '401': + description: Unauthorized + '500': + description: Internal Server Error + + /product/orders/history: + get: + tags: + - Order + summary: Get transaction history + description: Retrieve transaction history for the authenticated user + security: + - bearerAuth: [] + responses: + '200': + description: Transaction history retrieved successfully + '401': + description: Unauthorized + '500': + description: Internal Server Error + + /product/client/orders/:orderId: + put: + tags: + - Order + summary: Update order status + description: Update the status of a specific order for the authenticated user + security: + - bearerAuth: [] + parameters: + - in: path + name: orderId + schema: + type: string + required: true + description: The ID of the order + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + orderStatus: + type: string + description: The new status of the order + responses: + '200': + description: Order updated successfully + '400': + description: Bad Request + '401': + description: Unauthorized + '404': + description: Order not found + '500': + description: Internal Server Error diff --git a/src/docs/vendorOrderManagement.yml b/src/docs/vendorOrderManagement.yml new file mode 100644 index 0000000..5873717 --- /dev/null +++ b/src/docs/vendorOrderManagement.yml @@ -0,0 +1,93 @@ +/product/vendor/orders: + get: + tags: + - Vendor Order Manangement + summary: Fetches all vendor orders + description: Return all order for authenticated vendor + security: + - bearerAuth: [] + responses: + '200': + description: Return all order for vendor requested from buyer + '400': + description: Bad Request (syntax error, incorrect input format, etc..) + '401': + description: Unauthorized + '403': + description: Forbidden (Unauthorized action) + '500': + description: Internal server error + +/product/vendor/orders/{id}: + get: + tags: + - Vendor Order Manangement + summary: Fetch details for single vendor order + description: + Fetch details for single order for authenticated vendor, order that include only his/her product which a buyer has + requested in his order. + security: + - bearerAuth: [] + parameters: + - in: path + name: id + schema: + type: string + required: true + description: The id of a vendor order + responses: + '200': + description: Order details retrieved successfully + '400': + description: Bad Request (syntax error, incorrect input format, etc..) + '401': + description: Unauthorized + '403': + description: Forbidden (Unauthorized action) + '404': + description: Order not found + '500': + description: Internal server error + put: + tags: + - Vendor Order Manangement + summary: Updates order status for vendor order + description: + Updates orderStatus field of vendor order for authenticated vendor, it order that include only his/her product + which a buyer has request in his order. + security: + - bearerAuth: [] + parameters: + - in: path + name: id + schema: + type: string + required: true + description: The id of a vendor order + consumes: + - application/json + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + orderStatus: + type: string + example: "'is-accepted', 'in-transit', 'cancelled', 'delivered'" + responses: + '200': + description: Order was successfully updated, return updated order + '400': + description: Bad Request (syntax error, incorrect input format, etc..) + '401': + description: Unauthorized + '403': + description: Forbidden (Unauthorized action) + '404': + description: Order not found + '409': + description: Order can not be updated because (it has already been completed(close), delivered, cancelled) + '500': + description: Internal server error diff --git a/src/docs/vendorProduct.yml b/src/docs/vendorProduct.yml index 937b097..3830bf4 100644 --- a/src/docs/vendorProduct.yml +++ b/src/docs/vendorProduct.yml @@ -35,7 +35,7 @@ description: The id of product responses: '200': - description: Return info for the product + description: Return info for the product '400': description: Bad Request (syntax error, incorrect input format, etc..) '401': @@ -59,7 +59,7 @@ requestBody: required: true content: - application/json: + application/json: schema: type: object properties: @@ -75,10 +75,10 @@ type: file categories: oneOf: - - type: string - - type: array - items: - type: string + - type: string + - type: array + items: + type: string example: "'category' or ['category1', 'category2', ...]" expirationDate: type: string @@ -159,7 +159,7 @@ description: Product not found '500': description: Internal server error - + /product/images/{id}: delete: tags: diff --git a/src/docs/wishListDocs.yml b/src/docs/wishListDocs.yml new file mode 100644 index 0000000..7f705f7 --- /dev/null +++ b/src/docs/wishListDocs.yml @@ -0,0 +1,97 @@ +/wish-list: + get: + tags: + - Wish list + summary: Get all products in wishlist + description: Return all products in wish list for authenticated buyer + security: + - bearerAuth: [] + responses: + '200': + description: Return all products in wish list for a buyer + '400': + description: Bad Request (syntax error, incorrect input format, etc..) + '401': + description: Unauthorized + '403': + description: Forbidden (Unauthorized action) + '500': + description: Internal server error + +/wish-list/add/{id}: + post: + tags: + - Wish list + summary: Add product to wish list + description: Adds selected product (product id) to the wish list + security: + - bearerAuth: [] + parameters: + - in: path + name: id + schema: + type: string + required: true + description: Product id + responses: + '201': + description: Product Added to wish list + '400': + description: Bad Request (syntax error, incorrect input format, etc..) + '401': + description: Unauthorized + '403': + description: Forbidden (Unauthorized action) + '404': + description: Product not found in wish list + '500': + description: Internal server error + +/wish-list/delete/{id}: + delete: + tags: + - Wish list + summary: Remove product from wish list + description: Remove product from wish list for an authenticated buyer + security: + - bearerAuth: [] + parameters: + - in: path + name: id + schema: + type: string + required: true + description: Product id + responses: + '200': + description: Product removed from wish list + '400': + description: Bad Request (syntax error, incorrect input format, etc..) + '401': + description: Unauthorized + '403': + description: Forbidden (Unauthorized action) + '404': + description: Product not found in wish list + '500': + description: Internal server error + +/wish-list/clearAll: + delete: + tags: + - Wish list + summary: Clear entire wish list + description: Clears entire wish list for authenticated buyer + security: + - bearerAuth: [] + responses: + '200': + description: All products removed successfully + '400': + description: Bad Request (syntax error, incorrect input format, etc..) + '401': + description: Unauthorized + '403': + description: Forbidden (Unauthorized action) + '500': + description: Internal server error diff --git a/src/entities/Cart.ts b/src/entities/Cart.ts index 0ba44a6..fda1e15 100644 --- a/src/entities/Cart.ts +++ b/src/entities/Cart.ts @@ -36,7 +36,7 @@ export class Cart { @UpdateDateColumn() updatedAt!: Date; - updateTotal(): void { + updateTotal (): void { if (this.items) { let total: number = 0; for (let i = 0; i < this.items.length; i++) { diff --git a/src/entities/CartItem.ts b/src/entities/CartItem.ts index 107170c..d651adf 100644 --- a/src/entities/CartItem.ts +++ b/src/entities/CartItem.ts @@ -47,7 +47,7 @@ export class CartItem { @BeforeInsert() @BeforeUpdate() - updateTotal(): void { + updateTotal (): void { this.total = this.newPrice * this.quantity; } } diff --git a/src/entities/Order.ts b/src/entities/Order.ts index f52012e..47649a7 100644 --- a/src/entities/Order.ts +++ b/src/entities/Order.ts @@ -1,7 +1,16 @@ -import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, CreateDateColumn, UpdateDateColumn } from 'typeorm'; -import { IsNotEmpty, IsNumber, IsDate } from 'class-validator'; +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + OneToMany, + CreateDateColumn, + UpdateDateColumn, +} from 'typeorm'; +import { IsNotEmpty, IsNumber, IsDate, IsIn } from 'class-validator'; import { User } from './User'; -import { Product } from './Product'; +import { OrderItem } from './OrderItem'; +import { Transaction } from './transaction'; @Entity() export class Order { @@ -9,24 +18,43 @@ export class Order { @IsNotEmpty() id!: string; - @ManyToOne(() => User, user => user.orders) // <- Correctly reference the User entity and its orders property + @ManyToOne(() => User, user => user.orders) @IsNotEmpty() buyer!: User; - @ManyToOne(() => Product, product => product.orders) // <- Correctly reference the Product entity and its orders property + @OneToMany(() => OrderItem, orderItem => orderItem.order, { cascade: true }) @IsNotEmpty() - product!: Product; + orderItems!: OrderItem[]; @Column('decimal') @IsNotEmpty() @IsNumber() totalPrice!: number; + @OneToMany(() => Transaction, transaction => transaction.order) + transactions!: Transaction[]; + @Column({ default: 'order placed' }) + @IsNotEmpty() + @IsIn([ + 'order placed', + 'cancelled', + 'awaiting shipment', + 'in transit', + 'delivered', + 'received', + 'returned', + 'completed', + ]) + orderStatus!: string; + @Column('int') @IsNotEmpty() @IsNumber() quantity!: number; + @Column({ default: 'City, Country street address' }) + address!: string; + @Column() @IsDate() @IsNotEmpty() diff --git a/src/entities/OrderItem.ts b/src/entities/OrderItem.ts new file mode 100644 index 0000000..130b330 --- /dev/null +++ b/src/entities/OrderItem.ts @@ -0,0 +1,29 @@ +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm'; +import { IsNotEmpty, IsNumber } from 'class-validator'; +import { Order } from './Order'; +import { Product } from './Product'; + +@Entity() +export class OrderItem { + @PrimaryGeneratedColumn('uuid') + @IsNotEmpty() + id!: string; + + @ManyToOne(() => Order, order => order.orderItems) + @IsNotEmpty() + order!: Order; + + @ManyToOne(() => Product, product => product.orderItems) + @IsNotEmpty() + product!: Product; + + @Column('decimal') + @IsNotEmpty() + @IsNumber() + price!: number; + + @Column('int') + @IsNotEmpty() + @IsNumber() + quantity!: number; +} diff --git a/src/entities/Product.ts b/src/entities/Product.ts index 82603a4..e144a04 100644 --- a/src/entities/Product.ts +++ b/src/entities/Product.ts @@ -9,16 +9,21 @@ import { ManyToMany, OneToMany, JoinTable, + OneToOne, + JoinColumn, } from 'typeorm'; import { IsNotEmpty, IsString, IsBoolean, ArrayNotEmpty, IsArray, MaxLength } from 'class-validator'; import { User } from './User'; import { Category } from './Category'; import { Order } from './Order'; +import { Coupon } from './coupon'; +import { OrderItem } from './OrderItem'; +import { VendorOrderItem } from './VendorOrderItem'; @Entity() @Unique(['id']) export class Product { - static query() { + static query () { throw new Error('Method not implemented.'); } @PrimaryGeneratedColumn('uuid') @@ -29,8 +34,15 @@ export class Product { @IsNotEmpty() vendor!: User; - @OneToMany(() => Order, (order: any) => order.product) // Specify the inverse side of the relationship - orders!: Order[]; + @OneToMany(() => OrderItem, orderItem => orderItem.product) + orderItems!: OrderItem[]; + + @OneToMany(() => VendorOrderItem, vendorOrderItems => vendorOrderItems.product) + vendorOrderItems!: VendorOrderItem[]; + + @OneToOne(() => Coupon, (coupons: any) => coupons.product) + @JoinColumn() + coupons?: Coupon; @Column() @IsNotEmpty() diff --git a/src/entities/User.ts b/src/entities/User.ts index 2f55da0..ef256d7 100644 --- a/src/entities/User.ts +++ b/src/entities/User.ts @@ -1,3 +1,4 @@ + import { Entity, PrimaryGeneratedColumn, @@ -11,6 +12,7 @@ import { import { IsEmail, IsNotEmpty, IsString, IsBoolean, IsIn } from 'class-validator'; import { roles } from '../utils/roles'; import { Order } from './Order'; +import { Transaction } from './transaction'; export interface UserInterface { id?: string; @@ -98,14 +100,20 @@ export class User { @OneToMany(() => Order, (order: any) => order.buyer) orders!: Order[]; + @OneToMany(() => Transaction, transaction => transaction.user) + transactions!: Transaction[]; + @CreateDateColumn() createdAt!: Date; @UpdateDateColumn() updatedAt!: Date; + @Column({ type: 'numeric', precision: 24, scale: 2, default: 0 }) + accountBalance!: number; + @BeforeInsert() - setRole(): void { + setRole (): void { this.role = this.userType === 'Vendor' ? roles.vendor : roles.buyer; } } diff --git a/src/entities/VendorOrderItem.ts b/src/entities/VendorOrderItem.ts new file mode 100644 index 0000000..9137f6d --- /dev/null +++ b/src/entities/VendorOrderItem.ts @@ -0,0 +1,30 @@ +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm'; +import { IsNotEmpty, IsNumber } from 'class-validator'; +import { Order } from './Order'; +import { Product } from './Product'; +import { VendorOrders } from './vendorOrders'; + +@Entity() +export class VendorOrderItem { + @PrimaryGeneratedColumn('uuid') + @IsNotEmpty() + 'id'!: string; + + @ManyToOne(() => VendorOrders, order => order.vendorOrderItems) + @IsNotEmpty() + 'order'!: VendorOrders; + + @ManyToOne(() => Product, product => product.vendorOrderItems) + @IsNotEmpty() + 'product'!: Product; + + @Column('decimal') + @IsNotEmpty() + @IsNumber() + 'price/unit'!: number; + + @Column('int') + @IsNotEmpty() + @IsNumber() + 'quantity'!: number; +} diff --git a/src/entities/coupon.ts b/src/entities/coupon.ts new file mode 100644 index 0000000..39631c3 --- /dev/null +++ b/src/entities/coupon.ts @@ -0,0 +1,68 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + Unique, + CreateDateColumn, + UpdateDateColumn, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { IsDate, IsNotEmpty, IsArray, IsIn } from 'class-validator'; +import { User } from './User'; +import { Product } from './Product'; + +@Entity() +@Unique(['id']) +@Unique(['code']) // Ensure only 'code' is unique +export class Coupon { + @PrimaryGeneratedColumn('uuid') + @IsNotEmpty() + id!: string; + + @ManyToOne(() => User) + @IsNotEmpty() + @JoinColumn() + vendor!: User; + + @ManyToOne(() => Product, product => product.coupons) + @IsNotEmpty() + @JoinColumn() + product!: Product; + + @Column() + @IsNotEmpty() + code!: string; + + @Column() + @IsNotEmpty() + @IsIn(['percentage', 'money']) + discountType!: 'percentage' | 'money'; + + @Column('float') + @IsNotEmpty() + discountRate!: number; + + @Column('timestamp', { nullable: false }) + @IsNotEmpty() + @IsDate() + expirationDate?: Date; + + @Column('int', { default: 0 }) + @IsNotEmpty() + usageTimes!: number; + + @Column('int') + @IsNotEmpty() + maxUsageLimit!: number; + + @Column('simple-array', { nullable: true, default: '' }) + @IsArray() + usedBy!: string[]; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; +} diff --git a/src/entities/transaction.ts b/src/entities/transaction.ts new file mode 100644 index 0000000..d475812 --- /dev/null +++ b/src/entities/transaction.ts @@ -0,0 +1,61 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + JoinColumn, + CreateDateColumn, + UpdateDateColumn, +} from 'typeorm'; +import { IsNotEmpty, IsString, IsNumber } from 'class-validator'; +import { User } from './User'; +import { Order } from './Order'; +import { Product } from './Product'; // Assuming Product entity exists + +@Entity() +export class Transaction { + @PrimaryGeneratedColumn('uuid') + @IsNotEmpty() + id!: string; + + @ManyToOne(() => User, { nullable: false }) + @JoinColumn({ name: 'userId' }) + user!: User; + + @ManyToOne(() => Order, { nullable: true }) + @JoinColumn({ name: 'orderId' }) + order?: Order; + + @ManyToOne(() => Product, { nullable: true }) + @JoinColumn({ name: 'productId' }) + product?: Product; + + @Column({ type: 'numeric', precision: 15, scale: 2, default: 0 }) + @IsNotEmpty() + @IsNumber() + amount!: number; + + @Column({ type: 'numeric', precision: 15, scale: 2, default: 0 }) + @IsNotEmpty() + @IsNumber() + previousBalance!: number; + + @Column({ type: 'numeric', precision: 15, scale: 2, default: 0 }) + @IsNotEmpty() + @IsNumber() + currentBalance!: number; + + @Column({ type: 'enum', enum: ['debit', 'credit'] }) + @IsNotEmpty() + @IsString() + type!: 'debit' | 'credit'; + + @Column({ type: 'text', nullable: true }) + description?: string; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; +} \ No newline at end of file diff --git a/src/entities/vendorOrders.ts b/src/entities/vendorOrders.ts new file mode 100644 index 0000000..38269e6 --- /dev/null +++ b/src/entities/vendorOrders.ts @@ -0,0 +1,49 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + OneToMany, + CreateDateColumn, + UpdateDateColumn, +} from 'typeorm'; +import { IsNotEmpty, IsNumber, IsDate, IsIn, isNotEmpty } from 'class-validator'; +import { User } from './User'; +import { OrderItem } from './OrderItem'; +import { Transaction } from './transaction'; +import { Order } from './Order'; +import { VendorOrderItem } from './VendorOrderItem'; + +@Entity() +export class VendorOrders { + @PrimaryGeneratedColumn('uuid') + @IsNotEmpty() + id!: string; + + @ManyToOne(() => User) + @IsNotEmpty() + vendor!: User; + + @OneToMany(() => VendorOrderItem, vendorOrderItems => vendorOrderItems.order, { cascade: true }) + @IsNotEmpty() + vendorOrderItems!: VendorOrderItem[]; + + @ManyToOne(() => Order) + @IsNotEmpty() + order!: Order; + + @Column('decimal') + @IsNotEmpty() + @IsNumber() + totalPrice!: number; + + @Column({ default: 'pending' }) + @IsIn(['pending', 'is-accepted', 'in-transit', 'cancelled', 'delivered', 'completed']) + orderStatus!: string; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; +} diff --git a/src/entities/wishList.ts b/src/entities/wishList.ts index 69dbebd..7f74023 100644 --- a/src/entities/wishList.ts +++ b/src/entities/wishList.ts @@ -1,10 +1,19 @@ -import { Entity, PrimaryGeneratedColumn, BaseEntity,Column, Unique, ManyToOne, CreateDateColumn, UpdateDateColumn,} from "typeorm"; +import { + Entity, + PrimaryGeneratedColumn, + BaseEntity, + Column, + Unique, + ManyToOne, + CreateDateColumn, + UpdateDateColumn, +} from 'typeorm'; import { IsNotEmpty, IsString } from 'class-validator'; import { User } from './User'; -@Entity("wishlist") +@Entity('wishlist') @Unique(['id']) -export class wishList extends BaseEntity{ +export class wishList extends BaseEntity { @PrimaryGeneratedColumn() @IsNotEmpty() id!: number; @@ -23,4 +32,4 @@ export class wishList extends BaseEntity{ @UpdateDateColumn() updatedAt!: Date; -} \ No newline at end of file +} diff --git a/src/helper/couponValidator.ts b/src/helper/couponValidator.ts new file mode 100644 index 0000000..9736aa8 --- /dev/null +++ b/src/helper/couponValidator.ts @@ -0,0 +1,58 @@ +import Joi from 'joi'; +import { Coupon } from '../entities/coupon'; + +export const validateCoupon = ( + coupon: Pick +): Joi.ValidationResult => { + const schema = Joi.object({ + code: Joi.string().min(5).required().messages({ + 'any.required': 'code is required.', + 'string.min': 'code must be at least 5 characters long.', + }), + discountRate: Joi.number().required().messages({ + 'any.required': 'discountRate is required.', + }), + expirationDate: Joi.date().required().messages({ + 'any.required': 'expirationDate is required.', + }), + maxUsageLimit: Joi.number().required().messages({ + 'any.required': 'maxUsageLimit is required.', + }), + discountType: Joi.string().required().messages({ + 'any.required': 'discountType is required.', + }), + product: Joi.string().required().messages({ + 'any.required': 'product is required.', + }), + }); + + return schema.validate(coupon); +}; + +export const validateCouponUpdate = ( + coupon: Partial> +): Joi.ValidationResult => { + const schema = Joi.object({ + code: Joi.string().min(5).messages({ + 'string.min': 'code must be at least 5 characters long.', + }), + discountRate: Joi.number().messages({ + 'number.base': 'discountRate must be a number.', + }), + expirationDate: Joi.date().messages({ + 'date.base': 'expirationDate must be a valid date.', + }), + maxUsageLimit: Joi.number().messages({ + 'number.base': 'maxUsageLimit must be a number.', + }), + discountType: Joi.string().messages({ + 'string.base': 'discountType must be a string.', + }), + }) + .min(1) + .messages({ + 'object.min': 'At least one field must be updated.', + }); + + return schema.validate(coupon); +}; diff --git a/src/index.ts b/src/index.ts index 07efd39..d689c27 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,21 +5,27 @@ import router from './routes'; import { addDocumentation } from './startups/docs'; import 'reflect-metadata'; import cookieParser from 'cookie-parser'; -import session from "express-session"; +import session from 'express-session'; import passport from 'passport'; import { CustomError, errorHandler } from './middlewares/errorHandler'; import morgan from 'morgan'; import { dbConnection } from './startups/dbConnection'; + +import { Server } from 'socket.io'; +import { init as initSocketIO } from './utils/socket'; + dotenv.config(); export const app = express(); const port = process.env.PORT || 8000; -app.use(session({ - secret: 'keyboard cat' -})) -app.use(passport.initialize()) -app.use(passport.session()) +app.use( + session({ + secret: 'keyboard cat', + }) +); +app.use(passport.initialize()); +app.use(passport.session()); app.use(express.json()); app.use(cookieParser()); app.use(cors({ origin: '*' })); @@ -43,3 +49,14 @@ app.use(morgan(morganFormat)); export const server = app.listen(port, () => { console.log(`[server]: Server is running at http://localhost:${port}`); }); + +// Socket.IO setup +const io = initSocketIO(server); + +io.on('connection', socket => { + console.log('Client connected'); + + socket.on('disconnect', () => { + console.log('Client disconnected'); + }); +}); diff --git a/src/routes/ProductRoutes.ts b/src/routes/ProductRoutes.ts index 16dcd69..4a72b47 100644 --- a/src/routes/ProductRoutes.ts +++ b/src/routes/ProductRoutes.ts @@ -1,6 +1,6 @@ import { RequestHandler, Router } from 'express'; -import { productStatus, searchProduct } from '../controllers/index'; +import { productStatus, searchProduct, } from '../controllers/index'; import { hasRole } from '../middlewares/roleCheck'; import upload from '../middlewares/multer'; import { authMiddleware } from '../middlewares/verifyToken'; @@ -14,8 +14,19 @@ import { deleteProduct, getRecommendedProducts, listAllProducts, - singleProduct + singleProduct, + createOrder, + getOrders, + updateOrder, + getOrdersHistory,Payment, + getSingleVendorOrder, + getVendorOrders, + updateVendorOrder, + getBuyerVendorOrders, + getSingleBuyerVendorOrder, + updateBuyerVendorOrder, } from '../controllers'; + const router = Router(); router.get('/all', listAllProducts); router.get('/recommended', authMiddleware as RequestHandler, hasRole('BUYER'), getRecommendedProducts); @@ -29,4 +40,20 @@ router.delete('/images/:id', authMiddleware as RequestHandler, hasRole('VENDOR') router.delete('/:id', authMiddleware as RequestHandler, hasRole('VENDOR'), deleteProduct); router.put('/availability/:id', authMiddleware as RequestHandler, hasRole('VENDOR'), productStatus); +router.post('/orders', authMiddleware as RequestHandler, hasRole('BUYER'), createOrder); +router.get('/client/orders', authMiddleware as RequestHandler, hasRole('BUYER'), getOrders); +router.put('/client/orders/:orderId', authMiddleware as RequestHandler, hasRole('BUYER'), updateOrder); +router.get('/orders/history', authMiddleware as RequestHandler, hasRole('BUYER'), getOrdersHistory); + +// Vendor order management +router.get('/vendor/orders', authMiddleware as RequestHandler, hasRole('VENDOR'), getVendorOrders); +router.get('/vendor/orders/:id', authMiddleware as RequestHandler, hasRole('VENDOR'), getSingleVendorOrder); +router.put('/vendor/orders/:id', authMiddleware as RequestHandler, hasRole('VENDOR'), updateVendorOrder); + +// Admin order management +router.get('/admin/orders', authMiddleware as RequestHandler, hasRole('ADMIN'), getBuyerVendorOrders); +router.get('/admin/orders/:id', authMiddleware as RequestHandler, hasRole('ADMIN'), getSingleBuyerVendorOrder); +router.put('/admin/orders/:id', authMiddleware as RequestHandler, hasRole('ADMIN'), updateBuyerVendorOrder); +router.post('/payment/:id', authMiddleware as RequestHandler, hasRole('BUYER'), Payment) + export default router; diff --git a/src/routes/UserRoutes.ts b/src/routes/UserRoutes.ts index 50bb4ca..79b0551 100644 --- a/src/routes/UserRoutes.ts +++ b/src/routes/UserRoutes.ts @@ -1,7 +1,7 @@ import { Router } from 'express'; import { responseError } from '../utils/response.utils'; import { UserInterface } from '../entities/User'; -import jwt from 'jsonwebtoken' +import jwt from 'jsonwebtoken'; import { disable2FA, enable2FA, @@ -19,7 +19,7 @@ import { activateUser, disactivateUser, userProfileUpdate } from '../controllers import { hasRole } from '../middlewares/roleCheck'; import { isTokenValide } from '../middlewares/isValid'; import passport from 'passport'; -import "../utils/auth"; +import '../utils/auth'; const router = Router(); router.post('/register', userRegistration); @@ -37,35 +37,36 @@ router.post('/password/reset/link', sendPasswordResetLink); router.put('/update', userProfileUpdate); router.get('/google-auth', passport.authenticate('google', { scope: ['profile', 'email'] })); -router.get("/auth/google/callback", - passport.authenticate("google", { - successRedirect: "/user/login/success", - failureRedirect: "/user/login/failed" +router.get( + '/auth/google/callback', + passport.authenticate('google', { + successRedirect: '/user/login/success', + failureRedirect: '/user/login/failed', }) ); -router.get("/login/success", async (req, res) => { +router.get('/login/success', async (req, res) => { const user = req.user as UserInterface; - if(!user){ - responseError(res, 404, 'user not found') + if (!user) { + responseError(res, 404, 'user not found'); } const payload = { id: user?.id, email: user?.email, - role: user?.role - } - const token = jwt.sign(payload, process.env.JWT_SECRET as string,{expiresIn: '24h'}) + role: user?.role, + }; + const token = jwt.sign(payload, process.env.JWT_SECRET as string, { expiresIn: '24h' }); res.status(200).json({ status: 'success', - data:{ - token: token, - message: "Login success" - } - }) + data: { + token: token, + message: 'Login success', + }, + }); }); -router.get("/login/failed", async (req, res) => { +router.get('/login/failed', async (req, res) => { res.status(401).json({ status: false, - message: "Login failed" + message: 'Login failed', }); }); diff --git a/src/routes/couponRoutes.ts b/src/routes/couponRoutes.ts new file mode 100644 index 0000000..3378fbe --- /dev/null +++ b/src/routes/couponRoutes.ts @@ -0,0 +1,22 @@ +import { RequestHandler, Router } from 'express'; +import { + createCoupon, + updateCoupon, + accessAllCoupon, + readCoupon, + deleteCoupon, + buyerApplyCoupon, +} from '../controllers/couponController'; +import { hasRole } from '../middlewares/roleCheck'; +import { authMiddleware } from '../middlewares/verifyToken'; + +const router = Router(); + +router.post('/vendor/:id', authMiddleware as RequestHandler, hasRole('VENDOR'), createCoupon); +router.put('/vendor/:id/update-coupon/:code', authMiddleware as RequestHandler, hasRole('VENDOR'), updateCoupon); +router.get('/vendor/:id/checkout/:code', authMiddleware as RequestHandler, hasRole('VENDOR'), readCoupon); +router.get('/vendor/:id/access-coupons', authMiddleware as RequestHandler, hasRole('VENDOR'), accessAllCoupon); +router.delete('/vendor/:id/checkout/delete', authMiddleware as RequestHandler, hasRole('VENDOR'), deleteCoupon); +router.post('/apply', authMiddleware as RequestHandler, hasRole('BUYER'), buyerApplyCoupon); + +export default router; diff --git a/src/routes/index.ts b/src/routes/index.ts index 3c6edec..6f632d6 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -3,6 +3,7 @@ import { responseSuccess } from '../utils/response.utils'; import userRoutes from './UserRoutes'; import productRoutes from './ProductRoutes'; import wishListRoutes from './wishListRoute'; +import couponRoute from './couponRoutes'; import cartRoutes from './CartRoutes'; const router = Router(); @@ -15,5 +16,6 @@ router.use('/user', userRoutes); router.use('/product', productRoutes); router.use('/wish-list', wishListRoutes); router.use('/cart', cartRoutes); +router.use('/coupons', couponRoute); export default router; diff --git a/src/routes/wishListRoute.ts b/src/routes/wishListRoute.ts index d5ac6fb..ea96e40 100644 --- a/src/routes/wishListRoute.ts +++ b/src/routes/wishListRoute.ts @@ -2,13 +2,30 @@ import { RequestHandler, Router } from 'express'; import { authMiddleware } from '../middlewares/verifyToken'; import { hasRole } from '../middlewares'; import { checkUserStatus } from '../middlewares/isAllowed'; -import { wishlistAddProduct,wishlistRemoveProduct,wishlistGetProducts,wishlistClearAllProducts } from '../controllers/wishListController'; +import { + wishlistAddProduct, + wishlistRemoveProduct, + wishlistGetProducts, + wishlistClearAllProducts, +} from '../controllers/wishListController'; const router = Router(); router.post('/add/:id', authMiddleware as RequestHandler, checkUserStatus, hasRole('BUYER'), wishlistAddProduct); -router.get('/',authMiddleware as RequestHandler, checkUserStatus, hasRole('BUYER'),wishlistGetProducts); -router.delete('/delete/:id',authMiddleware as RequestHandler, checkUserStatus, hasRole('BUYER'),wishlistRemoveProduct); -router.delete('/clearAll',authMiddleware as RequestHandler, checkUserStatus, hasRole('BUYER'),wishlistClearAllProducts); +router.get('/', authMiddleware as RequestHandler, checkUserStatus, hasRole('BUYER'), wishlistGetProducts); +router.delete( + '/delete/:id', + authMiddleware as RequestHandler, + checkUserStatus, + hasRole('BUYER'), + wishlistRemoveProduct +); +router.delete( + '/clearAll', + authMiddleware as RequestHandler, + checkUserStatus, + hasRole('BUYER'), + wishlistClearAllProducts +); -export default router; \ No newline at end of file +export default router; diff --git a/src/services/adminOrderServices/readOrder.ts b/src/services/adminOrderServices/readOrder.ts new file mode 100644 index 0000000..4bb20f0 --- /dev/null +++ b/src/services/adminOrderServices/readOrder.ts @@ -0,0 +1,158 @@ +import { Request, Response } from 'express'; +import { getRepository } from 'typeorm'; +import { responseSuccess, responseError } from '../../utils/response.utils'; +import { VendorOrderItem } from '../../entities/VendorOrderItem'; +import { VendorOrders } from '../../entities/vendorOrders'; +import { Order } from '../../entities/Order'; + +export const getBuyerVendorOrdersService = async (req: Request, res: Response) => { + try { + const vendorOrderRepository = getRepository(VendorOrders); + const orderRepository = getRepository(Order); + + const orders = await orderRepository.find({ + relations: ['buyer', 'orderItems'], + order: { + createdAt: 'DESC', // Order by creation date, most recent first + }, + }); + + if (!orders.length) { + return responseError(res, 200, `There is no pending orders from buyer`, { orders: [] }); + } + + const sanitizedOrdersResponse = []; + + for (const order of orders) { + const vendorOrders = await vendorOrderRepository.find({ + where: { + order: { + id: order.id, + }, + }, + relations: ['vendor', 'vendorOrderItems', 'vendorOrderItems.product'], + order: { + createdAt: 'DESC', // Order by creation date, most recent first + }, + }); + + sanitizedOrdersResponse.push({ + id: order.id, + totalPrice: order.totalPrice, + totalProducts: order.orderItems.length, + orderStatus: order.orderStatus, + address: order.address, + createdAt: order.createdAt, + updatedAt: order.updatedAt, + buyer: { + id: order.buyer.id, + firstName: order.buyer.firstName, + lastName: order.buyer.lastName, + email: order.buyer.lastName, + gender: order.buyer.gender, + phoneNumber: order.buyer.phoneNumber, + photoUrl: order.buyer.photoUrl, + }, + vendors: vendorOrders.map(vendoOrder => ({ + id: vendoOrder.vendor.id, + firstName: vendoOrder.vendor.firstName, + lastName: vendoOrder.vendor.lastName, + email: vendoOrder.vendor.lastName, + gender: vendoOrder.vendor.gender, + phoneNumber: vendoOrder.vendor.phoneNumber, + photoUrl: vendoOrder.vendor.photoUrl, + order: { + id: vendoOrder.id, + totalPrice: vendoOrder.totalPrice, + orderStatus: vendoOrder.orderStatus, + createdAt: order.createdAt, + updatedAt: order.updatedAt, + orderItems: vendoOrder.vendorOrderItems, + }, + })), + }); + } + + responseSuccess(res, 200, 'Orders retrieved successfully', { + totalOrders: orders.length, + orders: sanitizedOrdersResponse, + }); + } catch (error) { + return responseError(res, 400, (error as Error).message); + } +}; + +// Get single vendor order info +export const getSingleBuyerVendorOrderService = async (req: Request, res: Response) => { + try { + const orderId = req.params.id; + + const vendorOrderRepository = getRepository(VendorOrders); + const orderRepository = getRepository(Order); + + const order = await orderRepository.findOne({ + where: { + id: orderId, + }, + relations: ['buyer', 'orderItems'], + order: { + createdAt: 'DESC', // Order by creation date, most recent first + }, + }); + + if (!order) { + return responseError(res, 404, `Order Not Found.`); + } + + const vendorOrders = await vendorOrderRepository.find({ + where: { + order: { + id: order.id, + }, + }, + relations: ['vendor', 'vendorOrderItems', 'vendorOrderItems.product'], + }); + + const sanitizedOrderResponse = { + id: order.id, + totalPrice: order.totalPrice, + totalProducts: order.orderItems.length, + orderStatus: order.orderStatus, + address: order.address, + createdAt: order.createdAt, + updatedAt: order.updatedAt, + buyer: { + id: order.buyer.id, + firstName: order.buyer.firstName, + lastName: order.buyer.lastName, + email: order.buyer.lastName, + gender: order.buyer.gender, + phoneNumber: order.buyer.phoneNumber, + photoUrl: order.buyer.photoUrl, + }, + vendors: vendorOrders.map(vendoOrder => ({ + id: vendoOrder.vendor.id, + firstName: vendoOrder.vendor.firstName, + lastName: vendoOrder.vendor.lastName, + email: vendoOrder.vendor.lastName, + gender: vendoOrder.vendor.gender, + phoneNumber: vendoOrder.vendor.phoneNumber, + photoUrl: vendoOrder.vendor.photoUrl, + order: { + id: vendoOrder.id, + totalPrice: vendoOrder.totalPrice, + orderStatus: vendoOrder.orderStatus, + createdAt: order.createdAt, + updatedAt: order.updatedAt, + orderItems: vendoOrder.vendorOrderItems, + }, + })), + }; + + responseSuccess(res, 200, 'Order retrieved successfully', { + order: sanitizedOrderResponse, + }); + } catch (error) { + return responseError(res, 400, (error as Error).message); + } +}; diff --git a/src/services/adminOrderServices/updateOrder.ts b/src/services/adminOrderServices/updateOrder.ts new file mode 100644 index 0000000..876160f --- /dev/null +++ b/src/services/adminOrderServices/updateOrder.ts @@ -0,0 +1,107 @@ +import { Request, Response } from 'express'; +import { Not, getRepository } from 'typeorm'; +import { responseSuccess, responseError } from '../../utils/response.utils'; +import { VendorOrderItem } from '../../entities/VendorOrderItem'; +import { VendorOrders } from '../../entities/vendorOrders'; +import { Order } from '../../entities/Order'; +import { getIO } from '../../utils/socket'; + +export const updateBuyerVendorOrderService = async (req: Request, res: Response) => { + try { + const orderId = req.params.id; + + const vendorOrderRepository = getRepository(VendorOrders); + const orderRepository = getRepository(Order); + + const order = await orderRepository.findOne({ + where: { + id: orderId, + }, + relations: ['buyer', 'orderItems'], + }); + + if (!order) { + return responseError(res, 404, `Order Not Found.`); + } + + if (order.orderStatus === 'completed') { + return responseError(res, 409, 'The order has already been completed.'); + } + + if (order.orderStatus !== 'received') { + return responseError(res, 409, 'Order closure failed: The buyer has not received the item yet.'); + } + + const vendorOrders = await vendorOrderRepository.find({ + where: { + order: { + id: order.id, + }, + }, + relations: ['vendor', 'vendorOrderItems', 'vendorOrderItems.product'], + }); + + for (const order of vendorOrders) { + if (order.orderStatus !== 'delivered') { + return responseError(res, 409, 'Order closure failed: Some vendors have not yet delivered items to the buyer.'); + } + } + + // Update Whole Order + + order.orderStatus = 'completed'; + await orderRepository.save(order); + + const updatedVendorOrder = vendorOrders.map(async order => { + order.orderStatus = 'completed'; + await vendorOrderRepository.save(order); + }); + + const sanitizedOrderResponse = { + id: order.id, + totalPrice: order.totalPrice, + totalProducts: order.orderItems.length, + orderStatus: order.orderStatus, + address: order.address, + createdAt: order.createdAt, + updatedAt: order.updatedAt, + buyer: { + id: order.buyer.id, + firstName: order.buyer.firstName, + lastName: order.buyer.lastName, + email: order.buyer.lastName, + gender: order.buyer.gender, + phoneNumber: order.buyer.phoneNumber, + photoUrl: order.buyer.photoUrl, + }, + vendors: vendorOrders.map(vendoOrder => ({ + id: vendoOrder.vendor.id, + firstName: vendoOrder.vendor.firstName, + lastName: vendoOrder.vendor.lastName, + email: vendoOrder.vendor.lastName, + gender: vendoOrder.vendor.gender, + phoneNumber: vendoOrder.vendor.phoneNumber, + photoUrl: vendoOrder.vendor.photoUrl, + order: { + id: vendoOrder.id, + totalPrice: vendoOrder.totalPrice, + orderStatus: vendoOrder.orderStatus, + createdAt: order.createdAt, + updatedAt: order.updatedAt, + orderItems: vendoOrder.vendorOrderItems, + }, + })), + }; + + getIO().emit('orders', { + action: 'admin update', + order: sanitizedOrderResponse, + }); + + responseSuccess(res, 200, 'Order updated successfully', { + order: sanitizedOrderResponse, + }); + } catch (error) { + return responseError(res, 400, (error as Error).message); + } +}; diff --git a/src/services/cartServices/createCart.ts b/src/services/cartServices/createCart.ts index 4fa3646..36232a3 100644 --- a/src/services/cartServices/createCart.ts +++ b/src/services/cartServices/createCart.ts @@ -11,8 +11,7 @@ export const createCartService = async (req: Request, res: Response) => { try { const { error } = validateCartItem(req.body); if (error) { - responseError(res, 400, error.details[0].message); - return; + return responseError(res, 400, error.details[0].message); } if (req.body.quantity < 1) { diff --git a/src/services/couponServices/accessAllCoupon.ts b/src/services/couponServices/accessAllCoupon.ts new file mode 100644 index 0000000..9266a44 --- /dev/null +++ b/src/services/couponServices/accessAllCoupon.ts @@ -0,0 +1,37 @@ +import { Request, Response } from 'express'; +import { responseSuccess, responseError, responseServerError } from '../../utils/response.utils'; +import { getRepository } from 'typeorm'; +import { Coupon } from '../../entities/coupon'; +import { User } from '../../entities/User'; + +export const accessAllCouponService = async (req: Request, res: Response) => { + try { + const { id } = req.params; + + // Retrieve the user by id + const userRepository = getRepository(User); + const user = await userRepository.findOne({ where: { id } }); + + if (!user) { + console.log('User not found with id:', id); + return responseError(res, 404, 'User not found'); + } + + // Retrieve all coupons for the user + const couponRepository = getRepository(Coupon); + const coupons = await couponRepository.find({ + where: { vendor: { id: user.id } }, + relations: ['product'], + }); + + if (!coupons.length) { + console.log('No coupons found for user with id:', id); + return responseError(res, 404, 'No coupons found'); + } + + return responseSuccess(res, 200, 'Coupons retrieved successfully', coupons); + } catch (error: any) { + console.log('Error retrieving all coupons:\n', error); + return responseServerError(res, error); + } +}; diff --git a/src/services/couponServices/buyerApplyCoupon.ts b/src/services/couponServices/buyerApplyCoupon.ts new file mode 100644 index 0000000..85762f6 --- /dev/null +++ b/src/services/couponServices/buyerApplyCoupon.ts @@ -0,0 +1,87 @@ +import { Request, Response } from 'express'; +import { getRepository } from 'typeorm'; +import { Coupon } from '../../entities/coupon'; +import { Cart } from '../../entities/Cart'; +import { CartItem } from '../../entities/CartItem'; + +export const buyerApplyCouponService = async (req: Request, res: Response) => { + try { + const { couponCode } = req.body; + + if (!couponCode) return res.status(400).json({ message: 'Coupon Code is required' }); + + const couponRepository = getRepository(Coupon); + const coupon = await couponRepository.findOne({ + where: { code: couponCode }, + relations: ['product'], + }); + + if (!coupon) return res.status(404).json({ message: 'Invalid Coupon Code' }); + + if (coupon) { + if (coupon.expirationDate && coupon.expirationDate < new Date()) { + return res.status(400).json({ message: 'Coupon is expired' }); + } + + if (coupon.usageTimes == coupon.maxUsageLimit) { + return res.status(400).json({ message: 'Coupon Discount Ended' }); + } + } + const couponProductId = coupon.product.id; + + const cartRepository = getRepository(Cart); + let cart = await cartRepository.findOne({ + where: { user: { id: req.user?.id }, isCheckedOut: false }, + relations: ['items', 'items.product'], + }); + + if (!cart) return res.status(400).json({ message: "You don't have a product in cart" }); + + const cartItemRepository = getRepository(CartItem); + const couponCartItem = await cartItemRepository.findOne({ + where: { + cart: { id: cart.id }, + product: { id: couponProductId }, + }, + relations: ['product'], + }); + + if (!couponCartItem) return res.status(404).json({ message: 'No product in Cart with that coupon code' }); + + let amountReducted; + if (coupon.discountType === 'percentage') { + const reduction = (couponCartItem.product.newPrice * coupon.discountRate) / 100; + amountReducted = reduction; + couponCartItem.newPrice = couponCartItem.product.newPrice - reduction; + + await cartItemRepository.save(couponCartItem); + } else { + amountReducted = coupon.discountRate; + couponCartItem.newPrice = couponCartItem.product.newPrice - amountReducted; + await cartItemRepository.save(couponCartItem); + } + + cart = await cartRepository.findOne({ where: { id: cart.id }, relations: ['items', 'items.product'] }); + if (cart) { + cart.updateTotal(); + await cartRepository.save(cart); + } + + coupon.usageTimes += 1; + + if (req.user?.id) { + coupon.usedBy.push(req.user?.id); + } + + await couponRepository.save(coupon); + + return res + .status(200) + .json({ + message: `Coupon Code successfully activated discount on product: ${couponCartItem.product.name}`, + amountDiscounted: amountReducted, + }); + } catch (error) { + return res.status(500).json({ error: 'Internal server error' }); + } +}; diff --git a/src/services/couponServices/createCouponService.ts b/src/services/couponServices/createCouponService.ts new file mode 100644 index 0000000..a824ddf --- /dev/null +++ b/src/services/couponServices/createCouponService.ts @@ -0,0 +1,55 @@ +import { Request, Response } from 'express'; +import { responseSuccess, responseError, responseServerError } from '../../utils/response.utils'; +import { getRepository } from 'typeorm'; +import { Coupon } from '../../entities/coupon'; +import { validateCoupon } from '../../helper/couponValidator'; +import { User } from '../../entities/User'; +import { Product } from '../../entities/Product'; + +export const createCouponService = async (req: Request, res: Response) => { + try { + const { error } = validateCoupon(req.body); + if (error) { + console.log('Validation Error creating coupon:\n', error); + return res.status(400).json({ status: 'error', error: error?.details[0].message }); + } + + const { code, discountRate, expirationDate, maxUsageLimit, discountType, product: productId } = req.body; + const { id: vendorId } = req.params; + + const userRepository = getRepository(User); + const user = await userRepository.findOne({ where: { id: vendorId } }); + if (!user) { + console.log('Error creating coupon: User not found', user); + return responseError(res, 404, 'User not found'); + } + + const productRepository = getRepository(Product); + const product = await productRepository.findOne({ where: { id: productId } }); + if (!product) { + console.log('Error creating coupon: Product not found', product); + return responseError(res, 403, 'Product not found'); + } + + const couponRepository = getRepository(Coupon); + const existingCoupon = await couponRepository.findOne({ where: { code } }); + if (existingCoupon) { + return responseError(res, 402, 'Coupon code already exists'); + } + + const newCoupon = new Coupon(); + newCoupon.code = code; + newCoupon.discountRate = discountRate; + newCoupon.expirationDate = expirationDate; + newCoupon.maxUsageLimit = maxUsageLimit; + newCoupon.discountType = discountType; + newCoupon.product = product; + newCoupon.vendor = user; + + await couponRepository.save(newCoupon); + responseSuccess(res, 201, 'Coupon created successfully'); + } catch (error: any) { + console.log('Error creating coupon:\n', error); + return responseServerError(res, error); + } +}; diff --git a/src/services/couponServices/deleteCoupon.ts b/src/services/couponServices/deleteCoupon.ts new file mode 100644 index 0000000..c984d9e --- /dev/null +++ b/src/services/couponServices/deleteCoupon.ts @@ -0,0 +1,23 @@ +import { Request, Response } from 'express'; +import { responseSuccess, responseError, responseServerError } from '../../utils/response.utils'; +import { getRepository } from 'typeorm'; +import { Coupon } from '../../entities/coupon'; + +export const deleteCouponService = async (req: Request, res: Response) => { + try { + const couponRepository = getRepository(Coupon); + const coupon = await couponRepository.findOne({ where: { code: req.body.code } }); + + if (!coupon) { + console.log('Invalid coupon.'); + return responseError(res, 404, 'Invalid coupon'); + } + + await couponRepository.remove(coupon); + + return responseSuccess(res, 200, 'Coupon deleted successfully'); + } catch (error: any) { + console.log('Error deleting coupon:\n', error); + return responseServerError(res, error); + } +}; diff --git a/src/services/couponServices/readCoupon.ts b/src/services/couponServices/readCoupon.ts new file mode 100644 index 0000000..47e12ea --- /dev/null +++ b/src/services/couponServices/readCoupon.ts @@ -0,0 +1,23 @@ +import { Request, Response } from 'express'; +import { responseSuccess, responseError, responseServerError } from '../../utils/response.utils'; +import { getRepository } from 'typeorm'; +import { Coupon } from '../../entities/coupon'; + +export const readCouponService = async (req: Request, res: Response) => { + try { + const { code } = req.params; + if (!code) return responseError(res, 400, 'coupon code is required'); + + const couponRepository = getRepository(Coupon); + const coupon = await couponRepository.findOne({ where: { code: code } }); + + if (!coupon) { + return responseError(res, 404, 'Invalid coupon'); + } + + return responseSuccess(res, 200, 'Coupon retrieved successfully', coupon); + } catch (error: any) { + console.log('Error retrieving coupon:\n', error); + return responseServerError(res, error); + } +}; diff --git a/src/services/couponServices/updateService.ts b/src/services/couponServices/updateService.ts new file mode 100644 index 0000000..26aeef6 --- /dev/null +++ b/src/services/couponServices/updateService.ts @@ -0,0 +1,59 @@ +import { Coupon } from '../../entities/coupon'; +import { Request, Response } from 'express'; +import { responseSuccess, responseError, responseServerError } from '../../utils/response.utils'; +import { getRepository } from 'typeorm'; +import { validateCouponUpdate } from '../../helper/couponValidator'; +import { Product } from '../../entities/Product'; + +export const updateCouponService = async (req: Request, res: Response) => { + try { + const { code } = req.params; + const { error } = validateCouponUpdate(req.body); + if (error) { + return res.status(400).json({ status: 'error', error: error?.details[0].message }); + } + + const couponRepository = getRepository(Coupon); + const coupon = await couponRepository.findOne({ where: { code } }); + if (coupon) { + if (req.body.code !== undefined) { + const existtCoupon = await couponRepository.findOne({ where: { code: req.body.code } }); + if (existtCoupon) return responseError(res, 400, 'Coupon code already exists'); + if (req.body.code === coupon.code) return responseError(res, 400, 'Coupon code already up to date'); + coupon.code = req.body.code; + } + if (req.body.discountRate !== undefined) { + coupon.discountRate = req.body.discountRate; + } + if (req.body.expirationDate !== undefined) { + coupon.expirationDate = req.body.expirationDate; + } + if (req.body.maxUsageLimit !== undefined) { + coupon.maxUsageLimit = req.body.maxUsageLimit; + } + if (req.body.discountType !== undefined) { + coupon.discountType = req.body.discountType; + } + if (req.body.product !== undefined) { + const { id } = req.body.product; + const productRepository = getRepository(Product); + const product = await productRepository.findOne({ where: { id } }); + if (!product) { + console.log('Error updating coupon: Product not found', product); + return responseError(res, 404, 'Product not found'); + } + + coupon.product = product; + } + + await couponRepository.save(coupon); + return responseSuccess(res, 200, 'Coupon updated successfully', coupon); + } else { + console.log('Error updating coupon: Coupon not found', coupon); + return responseError(res, 404, 'Coupon not found'); + } + } catch (error: any) { + console.log('Error while updating coupon:\n', error); + return responseServerError(res, error); + } +}; diff --git a/src/services/index.ts b/src/services/index.ts index 8f560c3..08bdbe4 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -21,6 +21,7 @@ export * from './productServices/listAllProductsService'; export * from './productServices/productStatus'; export * from './productServices/viewSingleProduct'; export * from './productServices/searchProduct'; +export * from './productServices/payment' // Buyer wishlist services export * from './wishListServices/addProduct'; @@ -33,3 +34,11 @@ export * from './cartServices/createCart'; export * from './cartServices/readCart'; export * from './cartServices/removeProductInCart'; export * from './cartServices/clearCart'; + +// vendor order management +export * from './vendorOrderServices/readVendorOrder'; +export * from './vendorOrderServices/updateVendorOrder'; + +// vendor order management +export * from './adminOrderServices/readOrder'; +export * from './adminOrderServices/updateOrder'; diff --git a/src/services/orderServices/createOrder.ts b/src/services/orderServices/createOrder.ts new file mode 100644 index 0000000..256bf40 --- /dev/null +++ b/src/services/orderServices/createOrder.ts @@ -0,0 +1,186 @@ +import { Request, Response } from 'express'; +import { getRepository, getManager } from 'typeorm'; +import { Order } from '../../entities/Order'; +import { OrderItem } from '../../entities/OrderItem'; +import { Product } from '../../entities/Product'; +import { User } from '../../entities/User'; +import { Cart } from '../../entities/Cart'; +import { Transaction } from '../../entities/transaction'; +import { responseError, sendErrorResponse, sendSuccessResponse } from '../../utils/response.utils'; +import sendMail from '../../utils/sendOrderMail'; +import { VendorOrders } from '../../entities/vendorOrders'; +import { CartItem } from '../../entities/CartItem'; +import { VendorOrderItem } from '../../entities/VendorOrderItem'; + +export const createOrderService = async (req: Request, res: Response) => { + const { cartId, address } = req.body; + const buyerId = req.user?.id; + + try { + const userRepository = getRepository(User); + const productRepository = getRepository(Product); + const cartRepository = getRepository(Cart); + + const buyer = await userRepository.findOne({ where: { id: buyerId } }); + if (!buyer) { + return responseError(res, 404, 'Buyer not found'); + } + + const cart = await cartRepository.findOne({ + where: { + user: { + id: buyerId, + }, + isCheckedOut: false, + }, + relations: ['items', 'items.product', 'user'], + }); + + if (!cart || cart.items.length === 0) { + return sendErrorResponse(res, 400, 'Cart is empty or already checked out'); + } + + let totalPrice = 0; + const orderItems: OrderItem[] = []; + + for (const item of cart.items) { + const product = item.product; + + if (product.quantity < item.quantity) { + return sendErrorResponse(res, 400, `Not enough ${product.name} in stock`); + } + + totalPrice += product.newPrice * item.quantity; + product.quantity -= item.quantity; + + const orderItem = new OrderItem(); + orderItem.product = product; + orderItem.price = product.newPrice; + orderItem.quantity = item.quantity; + orderItems.push(orderItem); + } + + if (!buyer.accountBalance || buyer.accountBalance < totalPrice) { + return sendErrorResponse(res, 400, 'Not enough funds to perform this transaction'); + } + + const previousBalance = buyer.accountBalance; + buyer.accountBalance -= totalPrice; + const currentBalance = buyer.accountBalance; + + const newOrder = new Order(); + newOrder.buyer = buyer; + newOrder.totalPrice = totalPrice; + newOrder.orderItems = orderItems; + newOrder.quantity = cart.items.reduce((acc, item) => acc + item.quantity, 0); + newOrder.orderDate = new Date(); + newOrder.address = `${address.country}, ${address.city}, ${address.street}`; + + await getManager().transaction(async transactionalEntityManager => { + for (const item of cart.items) { + const product = item.product; + await transactionalEntityManager.save(Product, product); + } + + await transactionalEntityManager.save(User, buyer); + + await transactionalEntityManager.save(Order, newOrder); + for (const orderItem of orderItems) { + orderItem.order = newOrder; + await transactionalEntityManager.save(OrderItem, orderItem); + } + + const orderTransaction = new Transaction(); + orderTransaction.user = buyer; + orderTransaction.order = newOrder; + orderTransaction.amount = totalPrice; + orderTransaction.previousBalance = previousBalance; + orderTransaction.currentBalance = currentBalance; + orderTransaction.type = 'debit'; + orderTransaction.description = 'Purchase of products'; + await transactionalEntityManager.save(Transaction, orderTransaction); + + cart.isCheckedOut = true; + await transactionalEntityManager.save(Cart, cart); + }); + + const orderResponse = { + fullName: `${newOrder.buyer.firstName} ${newOrder.buyer.lastName}`, + email: newOrder.buyer.email, + products: orderItems.map(item => ({ + name: item.product.name, + newPrice: item.price, + quantity: item.quantity, + })), + totalAmount: newOrder.totalPrice, + quantity: newOrder.quantity, + orderDate: newOrder.orderDate, + address: newOrder.address, + }; + + const message = { + subject: 'Order created successfully', + ...orderResponse, + }; + + await sendMail(message); + + // separate order by each vendor getting order related to his products + await saveVendorRelatedOrder(newOrder, cart.items); + + return sendSuccessResponse(res, 201, 'Order created successfully', orderResponse); + } catch (error) { + return sendErrorResponse(res, 500, (error as Error).message); + } +}; + +const saveVendorRelatedOrder = async (order: Order, CartItem: CartItem[]) => { + try { + for (const item of CartItem) { + const productRepository = getRepository(Product); + + const product = await productRepository.findOne({ + where: { + id: item.product.id, + }, + relations: ['vendor'], + }); + + if (!product) return; + + const orderItem = new VendorOrderItem(); + orderItem.product = product; + orderItem['price/unit'] = product.newPrice; + orderItem.quantity = item.quantity; + + const vendorOrdersRepository = getRepository(VendorOrders); + let vendorOrders = await vendorOrdersRepository.findOne({ + where: { + vendor: { + id: product.vendor.id, + }, + order: { + id: order.id, + }, + }, + relations: ['vendorOrderItems'], + }); + + if (vendorOrders) { + vendorOrders.totalPrice = Number(vendorOrders.totalPrice) + +product.newPrice * +item.quantity; + vendorOrders.vendorOrderItems = [...vendorOrders.vendorOrderItems, orderItem]; + } else { + const newVendorOrders = new VendorOrders(); + newVendorOrders.vendor = product.vendor; + newVendorOrders.vendorOrderItems = [orderItem]; + newVendorOrders.order = order; + newVendorOrders.totalPrice = +product.newPrice * item.quantity; + vendorOrders = newVendorOrders; + } + + await vendorOrdersRepository.save(vendorOrders); + } + } catch (error) { + console.log((error as Error).message); + } +}; \ No newline at end of file diff --git a/src/services/orderServices/getOrderService.ts b/src/services/orderServices/getOrderService.ts new file mode 100644 index 0000000..4208123 --- /dev/null +++ b/src/services/orderServices/getOrderService.ts @@ -0,0 +1,63 @@ +import { Request, Response } from 'express'; +import { getRepository } from 'typeorm'; +import { responseSuccess, responseError } from '../../utils/response.utils'; +import { Order } from '../../entities/Order'; +import { OrderItem } from '../../entities/OrderItem'; + +// Example usage: + +export const getOrdersService = async (req: Request, res: Response) => { + try { + const orderRepository = getRepository(Order); + const buyerId = req.user?.id; + + const orders = await orderRepository.find({ + where: { + buyer: { + id: buyerId, + }, + }, + relations: ['buyer', 'orderItems', 'orderItems.product'], + order: { + createdAt: 'DESC', // Order by creation date, most recent first + }, + }); + + if (!orders || orders.length === 0) { + return responseSuccess(res, 404, `You haven't made any orders yet`, { orders: [] }); + } + + const sanitezedResponse = orders.map(order => ({ + id: order.id, + totalPrice: order.totalPrice, + orderStatus: order.orderStatus, + quantity: order.quantity, + address: order.address, + orderDate: order.orderDate, + createdAt: order.createdAt, + updatedAt: order.updatedAt, + buyer: { + id: order.buyer.id, + firstName: order.buyer.firstName, + lastName: order.buyer.lastName, + accountBalance: order.buyer.accountBalance, + }, + orderItems: order.orderItems.map((item: OrderItem) => ({ + id: item.id, + price: item.price, + quantity: item.quantity, + product: { + id: item.product.id, + name: item.product.name, + description: item.product.description, + images: item.product.images, + price: item.product.newPrice, + expirationDate: item.product.expirationDate, + }, + })), + })); + responseSuccess(res, 200, 'Orders retrieved successfully', { orders: sanitezedResponse }); + } catch (error) { + return responseError(res, 400, (error as Error).message); + } +}; diff --git a/src/services/orderServices/getOrderTransactionHistory.ts b/src/services/orderServices/getOrderTransactionHistory.ts new file mode 100644 index 0000000..6bd0b17 --- /dev/null +++ b/src/services/orderServices/getOrderTransactionHistory.ts @@ -0,0 +1,42 @@ +import { Request, Response } from 'express'; +import { getRepository } from 'typeorm'; +import { Transaction } from '../../entities/transaction'; +import { sendErrorResponse, sendSuccessResponse } from '../../utils/response.utils'; +import { OrderItem } from '../../entities/OrderItem'; + +export const getTransactionHistoryService = async (req: Request, res: Response) => { + const userId = req.user?.id; + + try { + const transactionRepository = getRepository(Transaction); + const transactions = await transactionRepository.find({ + where: { user: { id: userId } }, + order: { createdAt: 'DESC' }, + relations: ['order'], + }); + + if (!transactions || transactions.length === 0) { + return sendErrorResponse(res, 404, 'No transaction history found'); + } + + const transactionHistory = transactions.map(transaction => ({ + id: transaction.id, + amount: transaction.amount, + type: transaction.type, + description: transaction.description, + createdAt: transaction.createdAt, + order: transaction.order + ? { + id: transaction.order.id, + totalPrice: transaction.order.totalPrice, + orderDate: transaction.order.orderDate, + address: transaction.order.address, + } + : null, + })); + + return sendSuccessResponse(res, 200, 'Transaction history retrieved successfully', transactionHistory); + } catch (error) { + return sendErrorResponse(res, 500, (error as Error).message); + } +}; diff --git a/src/services/orderServices/updateOrderService.ts b/src/services/orderServices/updateOrderService.ts new file mode 100644 index 0000000..82a043a --- /dev/null +++ b/src/services/orderServices/updateOrderService.ts @@ -0,0 +1,136 @@ +import { Request, Response } from 'express'; +import { getManager, EntityManager, Repository } from 'typeorm'; +import { Order } from '../../entities/Order'; +import { Product } from '../../entities/Product'; +import { User } from '../../entities/User'; +import { OrderItem } from '../../entities/OrderItem'; +import { Transaction } from '../../entities/transaction'; +import { responseError, sendErrorResponse, sendSuccessResponse } from '../../utils/response.utils'; +import sendMail from '../../utils/sendOrderMail'; +interface OrderStatusType { + orderStatus: + | 'order placed' + | 'cancelled' + | 'awaiting shipment' + | 'in transit' + | 'delivered' + | 'received' + | 'returned'; +} +export const updateOrderService = async (req: Request, res: Response) => { + const { orderId } = req.params; + const { orderStatus } = req.body; + + try { + await getManager().transaction(async (transactionalEntityManager: EntityManager) => { + const orderRepository: Repository = transactionalEntityManager.getRepository(Order); + const productRepository: Repository = transactionalEntityManager.getRepository(Product); + const userRepository: Repository = transactionalEntityManager.getRepository(User); + const orderItemRepository: Repository = transactionalEntityManager.getRepository(OrderItem); + const transactionRepository: Repository = transactionalEntityManager.getRepository(Transaction); + + const buyerId = req.user?.id; + if (!buyerId) { + throw new Error('Unauthorized'); + } + + // Fetch order and related entities + const order: Order | null = await orderRepository.findOne({ + where: { id: orderId, buyer: { id: buyerId } }, + relations: ['orderItems', 'orderItems.product', 'buyer'], + }); + + if (!order) { + return sendErrorResponse(res, 404, 'Order not found'); + } + // Check if order can be updated + if (isOrderFinalStatus(order.orderStatus)) { + return sendErrorResponse(res, 401, `Order cannot be updated once it is ${order.orderStatus}`); + } + + // Handle order status transitions + if (orderStatus !== undefined && order.orderStatus !== orderStatus) { + switch (orderStatus) { + case 'cancelled': + case 'returned': + if (order.orderStatus !== 'delivered') { + await processRefund(order, transactionalEntityManager); + } + break; + default: + break; + } + + order.orderStatus = orderStatus; + } + + // Save updated order status + await orderRepository.save(order); + + // Prepare response data + const orderResponse = { + fullName: `${order.buyer.firstName} ${order.buyer.lastName}`, + email: order.buyer.email, + products: order.orderItems.map((item: OrderItem) => ({ + name: item.product.name, + newPrice: item.price, + quantity: item.quantity, + })), + totalAmount: order.totalPrice, + quantity: order.quantity, + orderDate: order.orderDate, + address: order.address, + }; + + // Send email notification + const message = { + subject: 'Order updated successfully', + ...orderResponse, + }; + await sendMail(message); + + // Respond with success + return sendSuccessResponse(res, 200, 'Order updated successfully', orderResponse); + }); + } catch (error) { + console.error('Error updating order:', error); + return sendErrorResponse(res, 500, (error as Error).message); + } +}; + +async function processRefund (order: Order, entityManager: EntityManager) { + const buyer = order.buyer; + + // Refund buyer + const previousBalance = buyer.accountBalance; + buyer.accountBalance += order.totalPrice; + const currentBalance = buyer.accountBalance; + await entityManager.save(buyer); + + // Record refund transaction + const refundTransaction = new Transaction(); + refundTransaction.user = buyer; + refundTransaction.order = order; + refundTransaction.amount = order.totalPrice; + refundTransaction.previousBalance = previousBalance; + refundTransaction.currentBalance = currentBalance; + refundTransaction.type = 'credit'; + refundTransaction.description = 'Refund for cancelled or returned order'; + await entityManager.save(refundTransaction); + + // Return products to store + for (const orderItem of order.orderItems) { + const product = orderItem.product; + product.quantity += orderItem.quantity; + await entityManager.save(product); + } + + // Clear order details + order.orderItems = []; + order.totalPrice = 0; + order.quantity = 0; +} + +function isOrderFinalStatus (status: string): boolean { + return ['cancelled', 'delivered', 'returned', 'completed'].includes(status); +} diff --git a/src/services/productServices/deleteProduct.ts b/src/services/productServices/deleteProduct.ts index 43ec3d1..068c4c9 100644 --- a/src/services/productServices/deleteProduct.ts +++ b/src/services/productServices/deleteProduct.ts @@ -3,30 +3,28 @@ import { Product } from '../../entities/Product'; import { getRepository } from 'typeorm'; import { responseError, responseSuccess } from '../../utils/response.utils'; - export const deleteProductService = async (req: Request, res: Response) => { - try { - const { id } = req.params; - - const productRepository = getRepository(Product); + try { + const { id } = req.params; - const product = await productRepository.findOne({ - where: { - id: id, - vendor: { - id: req.user?.id - } - } - }); + const productRepository = getRepository(Product); - if (product) { - await productRepository.remove(product); - return responseSuccess(res, 200, 'Product successfully deleted'); - } + const product = await productRepository.findOne({ + where: { + id: id, + vendor: { + id: req.user?.id, + }, + }, + }); - return responseError(res, 404, 'Product not found'); - - } catch (error) { - responseError(res, 400, (error as Error).message); + if (product) { + await productRepository.remove(product); + return responseSuccess(res, 200, 'Product successfully deleted'); } + + return responseError(res, 404, 'Product not found'); + } catch (error) { + responseError(res, 400, (error as Error).message); + } }; diff --git a/src/services/productServices/getRecommendedProductsService.ts b/src/services/productServices/getRecommendedProductsService.ts index 19368e1..fde015d 100644 --- a/src/services/productServices/getRecommendedProductsService.ts +++ b/src/services/productServices/getRecommendedProductsService.ts @@ -1,62 +1,63 @@ -import { Request, Response } from "express"; -import { responseError, responseSuccess } from "../../utils/response.utils"; -import { getRepository } from "typeorm"; -import { Product } from "../../entities/Product"; +import { Request, Response } from 'express'; +import { responseError, responseSuccess } from '../../utils/response.utils'; +import { getRepository } from 'typeorm'; +import { Product } from '../../entities/Product'; interface conditionDoc { - categories: any[] | null; - vendor: any | null + categories: any[] | null; + vendor: any | null; } export const getRecommendedProductsService = async (req: Request, res: Response) => { + try { + // Define pagination parameters + const page = req.query.page ? Number(req.query.page) : 1; + const limit = req.query.limit ? Number(req.query.limit) : 10; + const skip = (page - 1) * limit; + const condition: conditionDoc = { + categories: null, + vendor: null, + }; - try { - // Define pagination parameters - const page = req.query.page ? Number(req.query.page) : 1; - const limit = req.query.limit ? Number(req.query.limit) : 10; - const skip = (page - 1) * limit; - const condition: conditionDoc = { - categories: null, - vendor: null - }; - - if (req.query.categories) { - const categoryIds = Array.isArray(req.query.categories) ? req.query.categories : [req.query.categories]; - condition.categories = categoryIds; - }; - if (req.query.vendor) condition.vendor = req.query.vendor; + if (req.query.categories) { + const categoryIds = Array.isArray(req.query.categories) ? req.query.categories : [req.query.categories]; + condition.categories = categoryIds; + } + if (req.query.vendor) condition.vendor = req.query.vendor; - const productRepository = getRepository(Product); - const productsQuery = productRepository.createQueryBuilder("product") - .leftJoinAndSelect("product.categories", "category") - .leftJoinAndSelect("product.vendor", "vendor") - .where("1 = 1"); + const productRepository = getRepository(Product); + const productsQuery = productRepository + .createQueryBuilder('product') + .leftJoinAndSelect('product.categories', 'category') + .leftJoinAndSelect('product.vendor', 'vendor') + .where('1 = 1'); - if (condition.categories && condition.categories.length > 0) { - productsQuery.andWhere("category.id IN (:...categories)", { categories: condition.categories }); - } - if (condition.vendor) { - productsQuery.andWhere("vendor.id = :vendorId", { vendorId: condition.vendor }); - } + if (condition.categories && condition.categories.length > 0) { + productsQuery.andWhere('category.id IN (:...categories)', { categories: condition.categories }); + } + if (condition.vendor) { + productsQuery.andWhere('vendor.id = :vendorId', { vendorId: condition.vendor }); + } - const products = await productsQuery - .skip(skip) - .take(limit) - .getMany(); - if (products.length < 1) { - return responseSuccess(res, 200, `No products found for the specified ${condition.vendor ? 'vendor' : 'category'}`); - } - const sanitizedProducts = products.map(product => ({ - ...product, - vendor: { - firstName: product.vendor.firstName, - lastName: product.vendor.lastName, - phoneNumber: product.vendor.phoneNumber, - photoUrl: product.vendor.photoUrl - } - })); - return responseSuccess(res, 200, 'Products retrieved', { products: sanitizedProducts }); - } catch (error) { - return responseError(res, 400, (error as Error).message); + const products = await productsQuery.skip(skip).take(limit).getMany(); + if (products.length < 1) { + return responseSuccess( + res, + 200, + `No products found for the specified ${condition.vendor ? 'vendor' : 'category'}` + ); } -}; \ No newline at end of file + const sanitizedProducts = products.map(product => ({ + ...product, + vendor: { + firstName: product.vendor.firstName, + lastName: product.vendor.lastName, + phoneNumber: product.vendor.phoneNumber, + photoUrl: product.vendor.photoUrl, + }, + })); + return responseSuccess(res, 200, 'Products retrieved', { products: sanitizedProducts }); + } catch (error) { + return responseError(res, 400, (error as Error).message); + } +}; diff --git a/src/services/productServices/listAllProductsService.ts b/src/services/productServices/listAllProductsService.ts index 8950abd..f39c7bb 100644 --- a/src/services/productServices/listAllProductsService.ts +++ b/src/services/productServices/listAllProductsService.ts @@ -5,38 +5,40 @@ import { responseError, responseSuccess } from '../../utils/response.utils'; import { validate } from 'uuid'; export const listAllProductsService = async (req: Request, res: Response) => { - try { - const page = req.query.page ? Number(req.query.page) : 1; - const limit = req.query.limit ? Number(req.query.limit) : 10; - const skip = (page - 1) * limit; - const category = req.query.category ; + try { + const page = req.query.page ? Number(req.query.page) : 1; + const limit = req.query.limit ? Number(req.query.limit) : 10; + const skip = (page - 1) * limit; + const category = req.query.category; - - const productRepository = getRepository(Product); - const products = await productRepository.find({ - where: { - categories: { - name: category as string - } - }, - skip, - take: limit, - relations: ["categories","vendor"], - select: { - vendor: { - id: true, firstName: true, lastName: true, - email: true, phoneNumber: true, photoUrl: true - } - } - } - ); + const productRepository = getRepository(Product); + const products = await productRepository.find({ + where: { + categories: { + name: category as string, + }, + }, + skip, + take: limit, + relations: ['categories', 'vendor'], + select: { + vendor: { + id: true, + firstName: true, + lastName: true, + email: true, + phoneNumber: true, + photoUrl: true, + }, + }, + }); - if (products.length < 1) { - return responseSuccess(res, 200, 'No products found'); - } - - return responseSuccess(res, 200, 'Products retrieved', { products }); - } catch (error) { - responseError(res, 400, (error as Error).message); + if (products.length < 1) { + return responseSuccess(res, 200, 'No products found'); } + + return responseSuccess(res, 200, 'Products retrieved', { products }); + } catch (error) { + responseError(res, 400, (error as Error).message); + } }; diff --git a/src/services/productServices/payment.ts b/src/services/productServices/payment.ts new file mode 100644 index 0000000..b613296 --- /dev/null +++ b/src/services/productServices/payment.ts @@ -0,0 +1,52 @@ +import { Request, Response } from 'express'; +import { Cart } from '../../entities/Cart'; // Import your Cart entity +import { Order } from '../../entities/Order'; // Import your Order entity +import { getRepository, getTreeRepository } from 'typeorm'; +import dotenv from 'dotenv'; +import Stripe from 'stripe'; +dotenv.config(); +const stripeInstance = new Stripe(process.env.STRIPE_SECRET_KEY as string, { + apiVersion: "2024-04-10", +}); + +export const confirmPayment = async (req: Request, res: Response) => { + try { + const { payment_method } = req.body; + const cartId = req.params.cartId; // Get the cart ID from the params + + const cartRepository = getRepository(Cart); + const orderRepository = getTreeRepository(Order) + const cart = await cartRepository.findOne({where: {id : cartId}}); + if (!cart) { + return res.status(404).json({ error: 'Cart not found.' }); + } + const order = await orderRepository.findOne({ where: { buyer: cart.user } }); + if (!order) { + return res.status(404).json({ error: 'order not found.' }); + } + + const paymentIntent = await stripeInstance.paymentIntents.create({ + amount: cart.totalAmount, // Convert total to cents + currency: 'usd', + description: `Order #${cartId}`, + return_url: 'https://frontend-website.com/success', + confirm: true, + payment_method, + }); + + order.orderStatus = 'awaiting shipment'; + await orderRepository.save(order); + + + if (paymentIntent.status === 'succeeded') { + // Payment succeeded + res.status(200).json({ message: 'Payment successful!' }); + } else { + // Payment failed + res.status(400).json({ error: 'Payment failed.' }); + } + } catch (error) { + console.error('Error confirming payment:', error); + res.status(500).json({ error: 'Something went wrong' }); + } +}; \ No newline at end of file diff --git a/src/services/productServices/readProduct.ts b/src/services/productServices/readProduct.ts index 5c9257c..b3c244d 100644 --- a/src/services/productServices/readProduct.ts +++ b/src/services/productServices/readProduct.ts @@ -4,67 +4,75 @@ import { getRepository } from 'typeorm'; import { responseError, responseSuccess } from '../../utils/response.utils'; export const readProductsService = async (req: Request, res: Response) => { - try { - // Define pagination parameters - const page = req.query.page ? Number(req.query.page) : 1; - const limit = req.query.limit ? Number(req.query.limit) : 10; - const skip = (page - 1) * limit; + try { + // Define pagination parameters + const page = req.query.page ? Number(req.query.page) : 1; + const limit = req.query.limit ? Number(req.query.limit) : 10; + const skip = (page - 1) * limit; - // Retrieve products - const productRepository = getRepository(Product); - const products = await productRepository.find({ - where: { - vendor: { - id: req.user?.id, - }, - }, - skip, - take: limit, - relations: ['categories', 'vendor'], - select: { - vendor: { - id: true, firstName: true, lastName: true, - email: true, phoneNumber: true, photoUrl: true - } - } - }); + // Retrieve products + const productRepository = getRepository(Product); + const products = await productRepository.find({ + where: { + vendor: { + id: req.user?.id, + }, + }, + skip, + take: limit, + relations: ['categories', 'vendor'], + select: { + vendor: { + id: true, + firstName: true, + lastName: true, + email: true, + phoneNumber: true, + photoUrl: true, + }, + }, + }); - if (products.length < 1) { - return responseSuccess(res, 200, 'You have no products yet'); - } - return responseSuccess(res, 200, 'Products retrieved', { products }); - } catch (error) { - responseError(res, 400, (error as Error).message); + if (products.length < 1) { + return responseSuccess(res, 200, 'You have no products yet'); } + return responseSuccess(res, 200, 'Products retrieved', { products }); + } catch (error) { + responseError(res, 400, (error as Error).message); + } }; export const readProductService = async (req: Request, res: Response) => { - try { - const { id } = req.params; + try { + const { id } = req.params; - const productRepository = getRepository(Product); - const product = await productRepository.findOne({ - where: { - id: id, - vendor: { - id: req.user?.id, - }, - }, - relations: ['categories', 'vendor'], - select: { - vendor: { - id: true, firstName: true, lastName: true, - email: true, phoneNumber: true, photoUrl: true - } - } - }); + const productRepository = getRepository(Product); + const product = await productRepository.findOne({ + where: { + id: id, + vendor: { + id: req.user?.id, + }, + }, + relations: ['categories', 'vendor'], + select: { + vendor: { + id: true, + firstName: true, + lastName: true, + email: true, + phoneNumber: true, + photoUrl: true, + }, + }, + }); - if (!product) { - return responseError(res, 404, 'Product not found'); - } - - return responseSuccess(res, 200, 'Product retrieved', { product }); - } catch (error) { - responseError(res, 400, (error as Error).message); + if (!product) { + return responseError(res, 404, 'Product not found'); } + + return responseSuccess(res, 200, 'Product retrieved', { product }); + } catch (error) { + responseError(res, 400, (error as Error).message); + } }; diff --git a/src/services/productServices/removeProductImage.ts b/src/services/productServices/removeProductImage.ts index 2995593..4424676 100644 --- a/src/services/productServices/removeProductImage.ts +++ b/src/services/productServices/removeProductImage.ts @@ -21,7 +21,7 @@ export const removeProductImageService = async (req: Request, res: Response) => const product = await productRepository.findOne({ where: { id, - vendor: { id: req.user?.id } + vendor: { id: req.user?.id }, }, relations: ['vendor'], }); diff --git a/src/services/productServices/searchProduct.ts b/src/services/productServices/searchProduct.ts index 765f431..9f33b5f 100644 --- a/src/services/productServices/searchProduct.ts +++ b/src/services/productServices/searchProduct.ts @@ -1,4 +1,4 @@ -import { Request, Response } from "express"; +import { Request, Response } from 'express'; import { getRepository, Like } from 'typeorm'; import { Product } from '../../entities/Product'; @@ -26,10 +26,7 @@ export const searchProductService = async (params: SearchProductParams) => { const skip = (page - 1) * limit; - const [products, total] = await query - .skip(skip) - .take(limit) - .getManyAndCount(); + const [products, total] = await query.skip(skip).take(limit).getManyAndCount(); const totalPages = Math.ceil(total / limit); diff --git a/src/services/productServices/viewSingleProduct.ts b/src/services/productServices/viewSingleProduct.ts index f956625..29ac167 100644 --- a/src/services/productServices/viewSingleProduct.ts +++ b/src/services/productServices/viewSingleProduct.ts @@ -4,35 +4,28 @@ import { getRepository } from 'typeorm'; import { responseError } from '../../utils/response.utils'; import { validate } from 'uuid'; - - export const viewSingleProduct = async (req: Request, res: Response) => { - try { - const productId = req.params.id; + try { + const productId = req.params.id; + + if (!validate(productId)) { + return res.status(400).json({ status: 'error', message: 'Invalid product ID' }); + } + if (productId) { + const products = getRepository(Product); + const product = await products.findOneBy({ id: productId }); - if (!validate(productId)) { - return res.status(400).json({ status: 'error', message: 'Invalid product ID' }); - + if (!product) { + return res.status(404).send({ status: 'error', message: 'Product not found' }); } - if(productId){ - const products = getRepository(Product); - const product = await products.findOneBy({ id: productId }); - - if (!product) { - return res.status(404).send({status:'error', message: 'Product not found'}); - - } - - if (product.expirationDate && new Date(product.expirationDate) < new Date()) { - return res.status(400).json({ status: 'error', message: 'Product expired' }); - - } - res.status(200).json({ status: 'success', product: product }); + if (product.expirationDate && new Date(product.expirationDate) < new Date()) { + return res.status(400).json({ status: 'error', message: 'Product expired' }); } - - } catch (error) { - console.error('Error handling request:', error); - res.status(500).send('Error fetching product details'); + res.status(200).json({ status: 'success', product: product }); } -} \ No newline at end of file + } catch (error) { + console.error('Error handling request:', error); + res.status(500).send('Error fetching product details'); + } +}; diff --git a/src/services/vendorOrderServices/readVendorOrder.ts b/src/services/vendorOrderServices/readVendorOrder.ts new file mode 100644 index 0000000..feec0c3 --- /dev/null +++ b/src/services/vendorOrderServices/readVendorOrder.ts @@ -0,0 +1,119 @@ +import { Request, Response } from 'express'; +import { getRepository } from 'typeorm'; +import { responseSuccess, responseError } from '../../utils/response.utils'; +import { VendorOrderItem } from '../../entities/VendorOrderItem'; +import { VendorOrders } from '../../entities/vendorOrders'; + +export const getVendorOrdersService = async (req: Request, res: Response) => { + try { + const vendorOrderRepository = getRepository(VendorOrders); + const vendorId = req.user?.id; + + const vendorOrders = await vendorOrderRepository.find({ + where: { + vendor: { + id: vendorId, + }, + }, + relations: ['vendor', 'order.buyer', 'vendorOrderItems', 'vendorOrderItems.product'], + order: { + createdAt: 'DESC', // Order by creation date, most recent first + }, + }); + + if (!vendorOrders.length) { + return responseError(res, 200, `You don't have any pending orders from buyer`, { orders: [] }); + } + + const sanitizedOrderResponse = vendorOrders.map(order => ({ + id: order.id, + totalPrice: order.totalPrice, + orderStatus: order.orderStatus, + address: order.order.address, + createdAt: order.createdAt, + updatedAt: order.updatedAt, + vendor: { + id: order.vendor.id, + firstName: order.vendor.firstName, + lastName: order.vendor.lastName, + email: order.vendor.email, + gender: order.vendor.gender, + phoneNumber: order.vendor.phoneNumber, + photoUrl: order.vendor.photoUrl, + }, + buyer: { + id: order.order.buyer.id, + firstName: order.order.buyer.firstName, + lastName: order.order.buyer.lastName, + email: order.order.buyer.lastName, + gender: order.order.buyer.gender, + phoneNumber: order.order.buyer.phoneNumber, + photoUrl: order.order.buyer.photoUrl, + }, + vendorOrderItems: order.vendorOrderItems, + })); + responseSuccess(res, 200, 'Orders retrieved successfully', { + orders: sanitizedOrderResponse, + }); + } catch (error) { + return responseError(res, 400, (error as Error).message); + } +}; + +// Get single vendor order info +export const getSingleVendorOrderService = async (req: Request, res: Response) => { + try { + const vendorOrderId = req.params.id; + + const vendorOrderRepository = getRepository(VendorOrders); + const vendorId = req.user?.id; + + const vendorOrder = await vendorOrderRepository.findOne({ + where: { + id: vendorOrderId, + vendor: { + id: vendorId, + }, + }, + relations: ['vendor', 'order.buyer', 'vendorOrderItems', 'vendorOrderItems.product'], + }); + + if (!vendorOrder) { + return responseError(res, 404, `Order Not Found.`); + } + + const sanitizedOrderResponse = { + id: vendorOrder.id, + totalPrice: vendorOrder.totalPrice, + orderStatus: vendorOrder.orderStatus, + address: vendorOrder.order.address, + createdAt: vendorOrder.createdAt, + updatedAt: vendorOrder.updatedAt, + vendor: { + id: vendorOrder.vendor.id, + firstName: vendorOrder.vendor.firstName, + lastName: vendorOrder.vendor.lastName, + email: vendorOrder.vendor.email, + gender: vendorOrder.vendor.gender, + phoneNumber: vendorOrder.vendor.phoneNumber, + photoUrl: vendorOrder.vendor.photoUrl, + }, + buyer: { + id: vendorOrder.order.buyer.id, + firstName: vendorOrder.order.buyer.firstName, + lastName: vendorOrder.order.buyer.lastName, + email: vendorOrder.order.buyer.lastName, + gender: vendorOrder.order.buyer.gender, + phoneNumber: vendorOrder.order.buyer.phoneNumber, + photoUrl: vendorOrder.order.buyer.photoUrl, + }, + vendorOrderItems: vendorOrder.vendorOrderItems, + }; + + responseSuccess(res, 200, 'Order retrieved successfully', { + order: sanitizedOrderResponse, + }); + } catch (error) { + return responseError(res, 400, (error as Error).message); + } +}; diff --git a/src/services/vendorOrderServices/updateVendorOrder.ts b/src/services/vendorOrderServices/updateVendorOrder.ts new file mode 100644 index 0000000..cae2c60 --- /dev/null +++ b/src/services/vendorOrderServices/updateVendorOrder.ts @@ -0,0 +1,87 @@ +import { Request, Response } from 'express'; +import { getRepository } from 'typeorm'; +import { OrderItem } from '../../entities/OrderItem'; +import { responseError, responseSuccess } from '../../utils/response.utils'; +import sendMail from '../../utils/sendOrderMail'; +import { VendorOrders } from '../../entities/vendorOrders'; +import { getIO } from '../../utils/socket'; + +export const updateVendorOrderService = async (req: Request, res: Response) => { + try { + const vendorOrderId = req.params.id; + const { orderStatus } = req.body; + if ( + !['pending', 'is-accepted', 'in-transit', 'cancelled', 'delivered'].includes( + (orderStatus as string).toLowerCase() + ) + ) { + return responseError(res, 400, `Please provide one of defined statuses.`); + } + + const vendorOrderRepository = getRepository(VendorOrders); + const vendorId = req.user?.id; + + const vendorOrder = await vendorOrderRepository.findOne({ + where: { + id: vendorOrderId, + vendor: { + id: vendorId, + }, + }, + relations: ['vendor', 'order.buyer', 'vendorOrderItems', 'vendorOrderItems.product'], + }); + + if (!vendorOrder) { + return responseError(res, 404, `Order Not Found.`); + } + + // Check if order can be updated + if (['delivered', 'cancelled', 'completed'].includes(vendorOrder.orderStatus)) { + return responseError(res, 409, `Order cannot be updated once it is marked as ${vendorOrder.orderStatus}`); + } + + vendorOrder.orderStatus = (orderStatus as string).toLowerCase(); + + // Save updated order status + const updatedVendorOrder = await vendorOrderRepository.save(vendorOrder); + + const sanitizedOrderResponse = { + id: updatedVendorOrder.id, + totalPrice: updatedVendorOrder.totalPrice, + orderStatus: updatedVendorOrder.orderStatus, + address: updatedVendorOrder.order.address, + createdAt: updatedVendorOrder.createdAt, + updatedAt: updatedVendorOrder.updatedAt, + vendor: { + id: updatedVendorOrder.vendor.id, + firstName: updatedVendorOrder.vendor.firstName, + lastName: updatedVendorOrder.vendor.lastName, + email: updatedVendorOrder.vendor.email, + gender: updatedVendorOrder.vendor.gender, + phoneNumber: updatedVendorOrder.vendor.phoneNumber, + photoUrl: updatedVendorOrder.vendor.photoUrl, + }, + buyer: { + id: updatedVendorOrder.order.buyer.id, + firstName: updatedVendorOrder.order.buyer.firstName, + lastName: updatedVendorOrder.order.buyer.lastName, + email: updatedVendorOrder.order.buyer.lastName, + gender: updatedVendorOrder.order.buyer.gender, + phoneNumber: updatedVendorOrder.order.buyer.phoneNumber, + photoUrl: updatedVendorOrder.order.buyer.photoUrl, + }, + vendorOrderItems: updatedVendorOrder.vendorOrderItems, + }; + + getIO().emit('orders', { + action: 'vendor update', + order: sanitizedOrderResponse, + }); + + return responseSuccess(res, 200, 'Order updated successfully', { + order: sanitizedOrderResponse, + }); + } catch (error) { + return responseError(res, 400, (error as Error).message); + } +}; diff --git a/src/services/wishListServices/addProduct.ts b/src/services/wishListServices/addProduct.ts index da3db89..79d0a38 100644 --- a/src/services/wishListServices/addProduct.ts +++ b/src/services/wishListServices/addProduct.ts @@ -4,57 +4,54 @@ import { getRepository } from 'typeorm'; import { wishList } from '../../entities/wishList'; import { Product } from '../../entities/Product'; -export const addProductService = async (req:Request,res:Response)=>{ - try { +export const addProductService = async (req: Request, res: Response) => { + try { + const id = req.params.id; + const wishListRepository = getRepository(wishList); + const productRepository = getRepository(Product); - const id = req.params.id; - const wishListRepository = getRepository(wishList); - const productRepository = getRepository(Product); + const product = await productRepository.findOne({ where: { id } }); + if (!product) { + return res.status(404).json({ message: 'Product not found' }); + } - const product = await productRepository.findOne({where: { id }}); - - if(!product){ - return res.status(404).json({message: "Product not found"}); - } - - const productDetails = { - productId: product.id, - name: product.name, - image: product.images, - newPrice: product.newPrice, - vendorId: product.vendor - } - - const alreadyIn = await wishListRepository.findOne({where: {productId: id, buyer:{ id: req.user?.id} }}) - - if(alreadyIn){ - return res.status(401).json({ - data: { - message: 'Product Already in the wish list', - wishlistAdded: alreadyIn, - product: productDetails, - }, - }) - } - - const addNewProduct = new wishList(); - addNewProduct.productId = id; - addNewProduct.buyer = req.user as User; - - await wishListRepository.save(addNewProduct); + const productDetails = { + productId: product.id, + name: product.name, + image: product.images, + newPrice: product.newPrice, + vendorId: product.vendor, + }; - addNewProduct.buyer = { id: addNewProduct.buyer.id } as unknown as User; + const alreadyIn = await wishListRepository.findOne({ where: { productId: id, buyer: { id: req.user?.id } } }); - return res.status(201).json({ + if (alreadyIn) { + return res.status(401).json({ data: { - message: 'Product Added to wish list', - wishlistAdded: addNewProduct, + message: 'Product Already in the wish list', + wishlistAdded: alreadyIn, product: productDetails, - }, - }); - - } catch (error) { - return res.status(500).json({ error: 'Internal server error' }); + }, + }); } -} \ No newline at end of file + + const addNewProduct = new wishList(); + addNewProduct.productId = id; + addNewProduct.buyer = req.user as User; + + await wishListRepository.save(addNewProduct); + + addNewProduct.buyer = { id: addNewProduct.buyer.id } as unknown as User; + + return res.status(201).json({ + data: { + message: 'Product Added to wish list', + wishlistAdded: addNewProduct, + product: productDetails, + }, + }); + } catch (error) { + return res.status(500).json({ error: 'Internal server error' }); + } +}; diff --git a/src/services/wishListServices/clearAll.ts b/src/services/wishListServices/clearAll.ts index 88af3c6..7299454 100644 --- a/src/services/wishListServices/clearAll.ts +++ b/src/services/wishListServices/clearAll.ts @@ -2,19 +2,18 @@ import { Request, Response } from 'express'; import { getRepository } from 'typeorm'; import { wishList } from '../../entities/wishList'; -export const clearAllProductService = async (req:Request,res:Response)=>{ - try { - const wishListRepository = getRepository(wishList); - const productsForBuyer = await wishListRepository.find({where: { buyer:{ id: req.user?.id} }}); +export const clearAllProductService = async (req: Request, res: Response) => { + try { + const wishListRepository = getRepository(wishList); + const productsForBuyer = await wishListRepository.find({ where: { buyer: { id: req.user?.id } } }); - if (productsForBuyer.length === 0) { - return res.status(404).json({ message: 'No products in wish list' }); - } - - await wishListRepository.remove(productsForBuyer); - return res.status(200).json({ message: 'All products removed successfully'}); - - } catch (error) { - return res.status(500).json({ error: 'Internal server error' }); + if (productsForBuyer.length === 0) { + return res.status(404).json({ message: 'No products in wish list' }); } -} \ No newline at end of file + + await wishListRepository.remove(productsForBuyer); + return res.status(200).json({ message: 'All products removed successfully' }); + } catch (error) { + return res.status(500).json({ error: 'Internal server error' }); + } +}; diff --git a/src/services/wishListServices/getProducts.ts b/src/services/wishListServices/getProducts.ts index 107f3aa..98dc434 100644 --- a/src/services/wishListServices/getProducts.ts +++ b/src/services/wishListServices/getProducts.ts @@ -3,36 +3,37 @@ import { getRepository } from 'typeorm'; import { wishList } from '../../entities/wishList'; import { Product } from '../../entities/Product'; -export const getProductsService = async (req:Request,res:Response)=>{ - try { - const wishListRepository = getRepository(wishList); - const productRepository =getRepository(Product); +export const getProductsService = async (req: Request, res: Response) => { + try { + const wishListRepository = getRepository(wishList); + const productRepository = getRepository(Product); - const productsForBuyer = await wishListRepository.find({where: { buyer:{ id: req.user?.id} }}); + const productsForBuyer = await wishListRepository.find({ where: { buyer: { id: req.user?.id } } }); - if (productsForBuyer.length === 0) { - return res.status(404).json({ message: 'No products in wish list', products: productsForBuyer }); - } - - const buyerWishProducts = await Promise.all(productsForBuyer.map(async (product) => { - const productDetails = await productRepository.findOne({ where: { id: product.productId } }); - if(productDetails){ - return { - wishListDetails: product, - productInfo: { - productId: productDetails.id, - name: productDetails.name, - image: productDetails.images, - newPrice: productDetails.newPrice, - vendorId: productDetails.vendor - } - }; - } - })); + if (productsForBuyer.length === 0) { + return res.status(404).json({ message: 'No products in wish list', products: productsForBuyer }); + } - return res.status(200).json({ message: 'Products retrieved', productsForBuyer: buyerWishProducts }); + const buyerWishProducts = await Promise.all( + productsForBuyer.map(async product => { + const productDetails = await productRepository.findOne({ where: { id: product.productId } }); + if (productDetails) { + return { + wishListDetails: product, + productInfo: { + productId: productDetails.id, + name: productDetails.name, + image: productDetails.images, + newPrice: productDetails.newPrice, + vendorId: productDetails.vendor, + }, + }; + } + }) + ); - } catch (error) { - return res.status(500).json({ error: 'Internal server error' }); - } -} \ No newline at end of file + return res.status(200).json({ message: 'Products retrieved', productsForBuyer: buyerWishProducts }); + } catch (error) { + return res.status(500).json({ error: 'Internal server error' }); + } +}; diff --git a/src/services/wishListServices/removeProducts.ts b/src/services/wishListServices/removeProducts.ts index cb99c0f..b42052f 100644 --- a/src/services/wishListServices/removeProducts.ts +++ b/src/services/wishListServices/removeProducts.ts @@ -2,22 +2,20 @@ import { Request, Response } from 'express'; import { getRepository } from 'typeorm'; import { wishList } from '../../entities/wishList'; -export const removeProductService = async (req:Request,res:Response)=>{ - try { +export const removeProductService = async (req: Request, res: Response) => { + try { + const id = parseInt(req.params.id); + const wishListRepository = getRepository(wishList); - const id = parseInt(req.params.id); - const wishListRepository = getRepository(wishList); + const product = await wishListRepository.findOne({ where: { id } }); - const product = await wishListRepository.findOne({where: { id }}); - - if(!product){ - return res.status(404).json({message: "Product not found in wish list"}); - } - - await wishListRepository.remove(product); - return res.status(200).json({ message: "Product removed from wish list" }); - - } catch (error) { - return res.status(500).json({ error: 'Internal server error' }); + if (!product) { + return res.status(404).json({ message: 'Product not found in wish list' }); } -} \ No newline at end of file + + await wishListRepository.remove(product); + return res.status(200).json({ message: 'Product removed from wish list' }); + } catch (error) { + return res.status(500).json({ error: 'Internal server error' }); + } +}; diff --git a/src/startups/getSwaggerServer.ts b/src/startups/getSwaggerServer.ts index efe12fa..6a7416d 100644 --- a/src/startups/getSwaggerServer.ts +++ b/src/startups/getSwaggerServer.ts @@ -7,7 +7,7 @@ function getSwaggerServer (): string { return process.env.SWAGGER_SERVER; } - return `http://localhost:${process.env.PORT}/api/v1`; + return `http://localhost:${process.env.PORT}`; } export { getSwaggerServer }; diff --git a/src/utils/auth.ts b/src/utils/auth.ts index 91874e3..623883f 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -1,72 +1,66 @@ /* eslint-disable camelcase */ import passport from 'passport'; -import { Strategy } from "passport-google-oauth20"; +import { Strategy } from 'passport-google-oauth20'; import { User } from '../entities/User'; import { getRepository } from 'typeorm'; import bcrypt from 'bcrypt'; -import "../utils/auth"; +import '../utils/auth'; passport.use( - new Strategy( - { - clientID: process.env.GOOGLE_CLIENT_ID as string, - clientSecret: process.env.GOOGLE_CLIENT_SECRET as string, - callbackURL: 'http://localhost:6890/user/auth/google/callback/', - scope: ['email', 'profile'], - }, - async (accessToken: any, refreshToken: any, profile: any, cb: any) => { - const userRepository = getRepository(User); - const { family_name, - name, - picture, - email, - email_verified + new Strategy( + { + clientID: process.env.GOOGLE_CLIENT_ID as string, + clientSecret: process.env.GOOGLE_CLIENT_SECRET as string, + callbackURL: 'http://localhost:6890/user/auth/google/callback/', + scope: ['email', 'profile'], + }, + async (accessToken: any, refreshToken: any, profile: any, cb: any) => { + const userRepository = getRepository(User); + const { family_name, name, picture, email, email_verified } = profile._json; + const { familyName, givenName } = profile.name; - } = profile._json; - const { familyName, givenName } = profile.name; + if (email || givenName || family_name || picture) { + try { + // Check for existing user + const existingUser = await userRepository.findOneBy({ email }); - if (email || givenName || family_name || picture) { - try { - // Check for existing user - const existingUser = await userRepository.findOneBy({ email }); + if (existingUser) { + return await cb(null, existingUser); + } + const saltRounds = 10; + const hashedPassword = await bcrypt.hash('password', saltRounds); + const newUser = new User(); + newUser.firstName = givenName; + newUser.lastName = family_name ?? familyName ?? 'undefined'; + newUser.email = email; + newUser.userType = 'Buyer'; + newUser.photoUrl = picture; + newUser.gender = 'Not specified'; + newUser.phoneNumber = 'Not specified'; + newUser.password = hashedPassword; + newUser.verified = email_verified; - if (existingUser) { - return await cb(null, existingUser); - } - const saltRounds = 10; - const hashedPassword = await bcrypt.hash("password", saltRounds); - const newUser = new User(); - newUser.firstName = givenName; - newUser.lastName = family_name ?? familyName ?? "undefined"; - newUser.email = email; - newUser.userType = 'Buyer'; - newUser.photoUrl = picture; - newUser.gender = "Not specified"; - newUser.phoneNumber = "Not specified"; - newUser.password = hashedPassword; - newUser.verified = email_verified; - - await userRepository.save(newUser); - return await cb(null, newUser); - } catch (error) { - console.error(error); - return await cb(error, null); - } - } - return await cb(null, profile, { message: 'Missing required profile information' }); + await userRepository.save(newUser); + return await cb(null, newUser); + } catch (error) { + console.error(error); + return await cb(error, null); } - ) + } + return await cb(null, profile, { message: 'Missing required profile information' }); + } + ) ); passport.serializeUser((user: any, cb) => { - cb(null, user.id); + cb(null, user.id); }); passport.deserializeUser(async (id: any, cb) => { - const userRepository = getRepository(User); - try { - const user = await userRepository.findOneBy({id}); - cb(null, user); - } catch (error) { - cb(error); - } + const userRepository = getRepository(User); + try { + const user = await userRepository.findOneBy({ id }); + cb(null, user); + } catch (error) { + cb(error); + } }); diff --git a/src/utils/index.ts b/src/utils/index.ts index 490375a..fdc7feb 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1 +1,20 @@ // export all utils +/** + * Format a number as a currency string. + * @param amount - The amount to format. + * @param currency - The currency code (e.g., 'USD', 'EUR'). Defaults to 'USD'. + * @returns The formatted currency string. + */ +export function formatMoney (amount: number, currency: string = 'RWF'): string { + return amount.toLocaleString('en-US', { style: 'currency', currency }); +} +/** + * Format a date string into a more readable format. + * @param dateString - The date string to format. + * @returns The formatted date string. + */ +export function formatDate (dateString: Date): string { + const options: Intl.DateTimeFormatOptions = { year: 'numeric', month: 'long', day: 'numeric' }; + const date = new Date(dateString); + return date.toLocaleDateString('en-US', options); +} diff --git a/src/utils/response.utils.ts b/src/utils/response.utils.ts index 3fd1ce5..f0e5513 100644 --- a/src/utils/response.utils.ts +++ b/src/utils/response.utils.ts @@ -45,3 +45,11 @@ export const responseServerError = (res: Response, error: string): Response { + return res.status(statusCode).json({ status: 'success', message, data }); +}; + +export const sendErrorResponse = (res: Response, statusCode: number, message: string) => { + return res.status(statusCode).json({ status: 'error', message }); +}; diff --git a/src/utils/sendOrderMail.ts b/src/utils/sendOrderMail.ts new file mode 100644 index 0000000..72ee5b0 --- /dev/null +++ b/src/utils/sendOrderMail.ts @@ -0,0 +1,215 @@ +import nodemailer from 'nodemailer'; +import { formatMoney, formatDate } from './index'; + +interface Product { + name: string; + newPrice: number; + quantity: number; +} + +interface Message { + subject: string; + fullName: string; + email: string; + products: Product[]; + totalAmount: number; + quantity: number; + orderDate: Date; + address: string; +} + +const sendMail = async (message: Message) => { + const transporter = nodemailer.createTransport({ + host: process.env.HOST, + port: 587, + secure: false, // true for 465, false for other ports + auth: { + user: process.env.AUTH_EMAIL as string, + pass: process.env.AUTH_PASSWORD as string, + }, + }); + + const { subject, fullName, email, products, totalAmount, quantity, orderDate, address } = message; + + const mailOptions = { + to: email, + subject: subject, + html: ` + + + + + + + Order Details + + + + +
+ shoping image +

Order Success

+
+

User information

+ + + + + + + + + + + + + + + +
Full Name:Email:Address:
${fullName}${email}${address}
+ + + + + + + + + + + + + + + +
Order Date:Quantity:Total Amount:
${formatDate(orderDate)}${quantity}${formatMoney(totalAmount)}
+
+
+

Products

+ + + + + + + + ${products + .map( + (product: Product) => ` + + + + + + + ` + ) + .join('')} + + + + +
Product NameProduct PriceQuantityTotal
${product.name}${formatMoney(product.newPrice)}${product.quantity}${product.quantity * product.newPrice}
Total${totalAmount}
+
+ +
+ + + `, + }; + + try { + const info = await transporter.sendMail(mailOptions); + console.log('Email sent: ' + info.response); + } catch (error) { + console.log('Error occurred while sending email', error); + } +}; + +export default sendMail; diff --git a/src/utils/sendOrderMailUpdated.ts b/src/utils/sendOrderMailUpdated.ts new file mode 100644 index 0000000..adddc9a --- /dev/null +++ b/src/utils/sendOrderMailUpdated.ts @@ -0,0 +1,215 @@ +import nodemailer from 'nodemailer'; +import { formatMoney, formatDate } from './index'; + +interface Product { + name: string; + newPrice: number; + quantity: number; +} + +interface Message { + subject: string; + fullName: string; + email: string; + products: Product[]; + totalAmount: number; + quantity: number; + orderDate: Date; + address: string; +} + +const sendMail = async (message: Message) => { + const transporter = nodemailer.createTransport({ + host: process.env.HOST, + port: 587, + secure: false, // true for 465, false for other ports + auth: { + user: process.env.AUTH_EMAIL as string, + pass: process.env.AUTH_PASSWORD as string, + }, + }); + + const { subject, fullName, email, products, totalAmount, quantity, orderDate, address } = message; + + const mailOptions = { + to: email, + subject: subject, + html: ` + + + + + + + Your order details have been updated + + + + +
+ shoping image +

Order Updated

+
+

User information

+ + + + + + + + + + + + + + + +
Full Name:Email:Address:
${fullName}${email}${address}
+ + + + + + + + + + + + + + + +
Order Date:Quantity:Total Amount:
${formatDate(orderDate)}${quantity}${formatMoney(totalAmount)}
+
+
+

Products

+ + + + + + + + ${products + .map( + (product: Product) => ` + + + + + + + ` + ) + .join('')} + + + + +
Product NameProduct PriceQuantityTotal
${product.name}${formatMoney(product.newPrice)}${product.quantity}${product.quantity * product.newPrice}
Total${totalAmount}
+
+ +
+ + + `, + }; + + try { + const info = await transporter.sendMail(mailOptions); + console.log('Email sent: ' + info.response); + } catch (error) { + console.log('Error occurred while sending email', error); + } +}; + +export default sendMail; diff --git a/src/utils/socket.ts b/src/utils/socket.ts new file mode 100644 index 0000000..e32ae19 --- /dev/null +++ b/src/utils/socket.ts @@ -0,0 +1,21 @@ +import { Server as HTTPServer } from 'http'; +import { Server as SocketIOServer } from 'socket.io'; + +let io: SocketIOServer | undefined; + +export const init = (httpServer: HTTPServer): SocketIOServer => { + io = new SocketIOServer(httpServer, { + cors: { + origin: '*', + methods: ['GET', 'POST', 'DELETE', 'PUT'], + }, + }); + return io; +}; + +export const getIO = (): SocketIOServer => { + if (!io) { + throw new Error('Socket.io not initialized!'); + } + return io; +}; diff --git a/tsconfig.json b/tsconfig.json index 4665a6d..d58c75f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,108 +1,108 @@ { - "compilerOptions": { - /* Visit https://aka.ms/tsconfig to read more about this file */ - /* Projects */ - // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ - // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ - // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ - // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ - // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ - // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - /* Language and Environment */ - "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, - // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - // "jsx": "preserve", /* Specify what JSX code is generated. */ - "experimentalDecorators": true /* Enable experimental support for legacy experimental decorators. */, - "emitDecoratorMetadata": true /* Emit design-type metadata for decorated declarations in source files. */, - // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ - // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ - // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ - // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ - // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ - // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ - // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ - /* Modules */ - "module": "commonjs" /* Specify what module code is generated. */, - "rootDir": "./src" /* Specify the root folder within your source files. */, - // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ - // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ - // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ - "types": [ - "node", - "jest", - "express", - "joi" - ] /* Specify type package names to be included without being referenced in a source file. */, - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ - // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ - // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ - // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ - // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ - // "resolveJsonModule": true, /* Enable importing .json files. */ - // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ - // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ - /* JavaScript Support */ - "allowJs": true /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */, - // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ - // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ - /* Emit */ - // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - // "declarationMap": true, /* Create sourcemaps for d.ts files. */ - // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ - // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - "outDir": "./dist" /* Specify an output folder for all emitted files. */, - // "removeComments": true, /* Disable emitting comments. */ - // "noEmit": true, /* Disable emitting files from a compilation. */ - // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ - // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ - // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ - // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ - // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ - // "newLine": "crlf", /* Set the newline character for emitting files. */ - // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ - // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ - // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ - // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ - // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ - // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ - /* Interop Constraints */ - // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ - // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ - // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, - // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, - /* Type Checking */ - "strict": true /* Enable all strict type-checking options. */, - // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ - // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ - // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ - // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ - // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ - // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ - // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ - // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ - // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ - // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ - // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ - // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ - // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ - // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - /* Completeness */ - // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ - }, - "include": ["src/**/*.ts"], - "exclude": ["node_modules"] - } \ No newline at end of file + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + /* Language and Environment */ + "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + "experimentalDecorators": true /* Enable experimental support for legacy experimental decorators. */, + "emitDecoratorMetadata": true /* Emit design-type metadata for decorated declarations in source files. */, + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + /* Modules */ + "module": "commonjs" /* Specify what module code is generated. */, + "rootDir": "./src" /* Specify the root folder within your source files. */, + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + "types": [ + "node", + "jest", + "express", + "joi" + ] /* Specify type package names to be included without being referenced in a source file. */, + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + /* JavaScript Support */ + "allowJs": true /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */, + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./dist" /* Specify an output folder for all emitted files. */, + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules"] +} From f976460b3d83f4316ba66c9ef5ccf2ca9e25fb1f Mon Sep 17 00:00:00 2001 From: Iadivin Date: Fri, 31 May 2024 07:53:48 +0200 Subject: [PATCH 49/51] feat(docker): implement Docker for project - Include Docker file to build image for project and containerize the project - Configure Docker compose for easy setup --- .dockerignore | 5 +++++ Dockerfile | 13 +++++++++++++ README.md | 18 ++++++++++++++++++ docker-compose.yml | 27 +++++++++++++++++++++++++++ 4 files changed, 63 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 docker-compose.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2deef83 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +node_modules +npm-debug.log +Dockerfile +docker-compose.yml +.dockerignore \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5190e01 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM node:20-alpine + +WORKDIR /app + +COPY package.json . + +RUN npm install + +COPY . . + +EXPOSE $PORT + +CMD ["npm", "run", "dev"] diff --git a/README.md b/README.md index 0d6c5f0..453c9d5 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,24 @@ logger.debug('This is a debug message'); npm test ``` +### Setting up docker and using it + +- Download and install docker + ``` + https://www.docker.com/products/docker-desktop/ + ``` +- Download Subsystem for Linux for none linux users +- Set environment varibles like database host to postgresdb + +- Building the image, you must navigate to the project directory in the terminal, then run + ``` + docker-compose up --build + ``` +- Stoping docker-compose container, run + ``` + docker-compose down + ``` + ## Authors - [Maxime Mizero](https://github.com/maxCastro1) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..99ed647 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,27 @@ +version: '3.8' + +services: + postgresdb: + image: postgres + environment: + POSTGRES_USER: $DEV_DB_USER + POSTGRES_PASSWORD: $DEV_DB_PASS + POSTGRES_DB: $DEV_DB_NAME + volumes: + - knights-data:/var/lib/postgresql/data + + node-app: + build: . + volumes: + - .:/app + - /app/node_modules + image: knights-app:1.0 + env_file: + - ./.env + ports: + - $PORT:$PORT + depends_on: + - postgresdb + +volumes: + knights-data: From c5aa42c180c9bd5e7f99e055d60f99f374cfbf5d Mon Sep 17 00:00:00 2001 From: Icyeza Date: Thu, 30 May 2024 13:15:07 +0200 Subject: [PATCH 50/51] Implement buyer able to leave feedback on products ft adding discount coupon feature implementing order tracking and order managment issue implementing order tracking and order managment issue implementing order tracking and order managment issue implementing order tracking and order managment issue adding documentation and tests writing tests Feat-Buyer-coupon-discount-management implementing order tracking and order managment issue implementing order tracking and order managment issue implementing order tracking and order managment issue implementing order tracking and order managment issue adding documentation and tests writing tests implementing stripe payment system implementing stripe payment system Implement buyer able to leave feedback on products Implement buyer able to leave feedback on products --- src/__test__/cart.test.ts | 219 ++++++++++-------- src/__test__/test-assets/DatabaseCleanup.ts | 2 + src/controllers/feedbackController.ts | 21 ++ src/entities/Feedback.ts | 30 +++ src/entities/Order.ts | 5 + src/entities/Product.ts | 5 +- src/entities/User.ts | 3 + src/routes/ProductRoutes.ts | 1 - src/routes/feedbackRoutes.ts | 19 ++ src/routes/index.ts | 2 + .../feedbackServices/adminDeleteFeedback.ts | 25 ++ .../feedbackServices/createFeedback.ts | 44 ++++ .../feedbackServices/deleteFeedback.ts | 27 +++ .../feedbackServices/updateFeedback.ts | 32 +++ src/services/orderServices/createOrder.ts | 14 +- .../getOrderTransactionHistory.ts | 2 - .../orderServices/updateOrderService.ts | 5 - .../getRecommendedProductsService.ts | 1 + .../productServices/listAllProductsService.ts | 2 +- src/services/productServices/readProduct.ts | 2 +- .../productServices/viewSingleProduct.ts | 2 +- 21 files changed, 338 insertions(+), 125 deletions(-) create mode 100644 src/controllers/feedbackController.ts create mode 100644 src/entities/Feedback.ts create mode 100644 src/routes/feedbackRoutes.ts create mode 100644 src/services/feedbackServices/adminDeleteFeedback.ts create mode 100644 src/services/feedbackServices/createFeedback.ts create mode 100644 src/services/feedbackServices/deleteFeedback.ts create mode 100644 src/services/feedbackServices/updateFeedback.ts diff --git a/src/__test__/cart.test.ts b/src/__test__/cart.test.ts index 4d6d1f0..7fe7146 100644 --- a/src/__test__/cart.test.ts +++ b/src/__test__/cart.test.ts @@ -14,6 +14,7 @@ import { cleanDatabase } from './test-assets/DatabaseCleanup'; const vendor1Id = uuid(); const buyer1Id = uuid(); const buyer2Id = uuid(); +const buyer3Id = uuid(); const product1Id = uuid(); const product2Id = uuid(); const catId = uuid(); @@ -52,7 +53,7 @@ const sampleBuyer1: UserInterface = { id: buyer1Id, firstName: 'buyer1', lastName: 'user', - email: 'elijahladdiedv@gmail.com', + email: 'manger@gmail.com', password: 'password', userType: 'Buyer', gender: 'Male', @@ -65,7 +66,7 @@ const sampleBuyer2: UserInterface = { id: buyer2Id, firstName: 'buyer1', lastName: 'user', - email: 'buyer1112@example.com', + email: 'elijahladdiedv@example.com', password: 'password', userType: 'Buyer', gender: 'Male', @@ -73,6 +74,18 @@ const sampleBuyer2: UserInterface = { photoUrl: 'https://example.com/photo.jpg', role: 'BUYER', }; +const sampleBuyer3: UserInterface = { + id: buyer3Id, + firstName: 'buyer1', + lastName: 'user', + email: 'elhladdiedv@example.com', + password: 'password', + userType: 'Admin', + gender: 'Male', + phoneNumber: '121163800', + photoUrl: 'https://example.com/photo.jpg', + role: 'ADMIN', +}; const sampleCat = { id: catId, @@ -175,7 +188,7 @@ afterAll(async () => { server.close(); }); -describe('Cart management for guest/buyer', () => { +describe('Cart| Order management for guest/buyer', () => { describe('Creating new product', () => { it('should create new product', async () => { const response = await request(app) @@ -376,6 +389,108 @@ describe('Cart management for guest/buyer', () => { }); }); + describe('Order management tests', () => { + let orderId: any; + let productId: any; + let feedbackId: any; + let feedback2Id: any; + describe('Create order', () => { + it('should return 400 when user ID is not provided', async () => { + const response = await request(app) + .post('/product/orders') + .send({ + address: { + country: 'Test Country', + city: 'Test City', + street: 'Test Street', + }, + }) + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); + expect(response.status).toBe(201); + }); + + it('should return orders for the buyer', async () => { + const response = await request(app) + .get('/product/client/orders') + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); + expect(response.status).toBe(200); + orderId = response.body.data.orders[0]?.id; + productId = response.body.data.orders[0]?.orderItems[0]?.product?.id; + }); + it('should return 404 if the buyer has no orders', async () => { + const response = await request(app) + .get('/product/client/orders') + .set('Authorization', `Bearer ${getAccessToken(buyer2Id, sampleBuyer2.email)}`); + expect(response.status).toBe(404); + expect(response.body.message).toBeUndefined; + }); + + it('should return transaction history for the buyer', async () => { + const response = await request(app) + .get('/product/orders/history') + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); + expect(response.status).toBe(200); + expect(response.body.message).toBe('Transaction history retrieved successfully'); + }); + + it('should return 400 when user ID is not provided', async () => { + const response = await request(app) + .get('/product/orders/history') + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); + expect(response.status).toBe(200); + }); + }); + + describe('Update order', () => { + it('should update order status successfully', async () => { + const response = await request(app) + .put(`/product/client/orders/${orderId}`) + .send({ orderStatus: 'completed' }) + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); + expect(response.status).toBe(200); + }); + }); + describe('Add feedback to the product with order', () => { + it('should create new feedback to the ordered product', async () => { + const response = await request(app) + .post(`/feedback/${productId}/new`) + .send({ orderId, comment: 'Well this product looks so fantastic' }) + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); + expect(response.status).toBe(201); + feedbackId = response.body.data.id + }); + it('should create new feedback to the ordered product', async () => { + const response = await request(app) + .post(`/feedback/${productId}/new`) + .send({ orderId, comment: 'Murigalike this product looks so fantastic' }) + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); + expect(response.status).toBe(201); + feedback2Id = response.body.data.id + }); + it('should updated existing feedback successfully', async () => { + const response = await request(app) + .put(`/feedback/update/${feedbackId}`,) + .send({ orderId, comment: 'Well this product looks so lovely' }) + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); + expect(response.status).toBe(200); + }); + it('should remove recorded feedback', async () => { + const response = await request(app) + .delete(`/feedback/delete/${feedbackId}`) + .send({ orderId, comment: 'Well this product looks so lovely' }) + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); + expect(response.status).toBe(200); + }); + it('should remove recorder feedback as admin ', async () => { + const response = await request(app) + .delete(`/feedback/admin/delete/${feedback2Id}`) + .send({ orderId, comment: 'Well this product looks so lovely' }) + .set('Authorization', `Bearer ${getAccessToken(buyer3Id, sampleBuyer3.email)}`); + expect(response.status).toBe(401); + }); + }); + }); + describe('Deleting product from cart', () => { it('should return 404 if product does not exist in cart', async () => { const response = await request(app) @@ -511,101 +626,3 @@ describe('Cart management for guest/buyer', () => { }); }); }); - -describe('Order management tests', () => { - let orderId: string | null; - describe('Create order', () => { - it('should return 400 when user ID is not provided', async () => { - const response = await request(app) - .post('/product/orders') - .send({ - address: { - country: 'Test Country', - city: 'Test City', - street: 'Test Street', - }, - }) - .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); - expect(response.status).toBe(400); - }); - - it('should create a new order', async () => { - const response = await request(app) - .post('/product/orders') - .send({ - address: { - country: 'Test Country', - city: 'Test City', - street: 'Test Street', - }, - }) - .set('Authorization', `Bearer ${getAccessToken(buyer2Id, sampleBuyer2.email)}`); - - expect(response.status).toBe(400); - expect(response.body.message).toBeUndefined; - orderId = response.body.data?.orderId; // Assuming orderId is returned in response - }); - - it('should insert a new order', async () => { - const response = await request(app) - .post('/product/orders') - .send({ - address: { - country: 'Test Country', - city: 'Test City', - street: 'Test Street', - }, - }) - .set('Authorization', `Bearer ${getAccessToken(buyer2Id, sampleBuyer2.email)}`); - - expect(response.status).toBe(400); - expect(response.body.message).toBeUndefined; - orderId = response.body.data?.orderId; // Assuming orderId is returned in response - }); - }); - - describe('Get orders', () => { - it('should return orders for the buyer', async () => { - const response = await request(app) - .get('/product/client/orders') - .set('Authorization', `Bearer ${getAccessToken(buyer2Id, sampleBuyer2.email)}`); - expect(response.status).toBe(404); - expect(response.body.message).toBeUndefined; - }); - - it('should return 404 if the buyer has no orders', async () => { - const response = await request(app) - .get('/product/client/orders') - .set('Authorization', `Bearer ${getAccessToken(buyer2Id, sampleBuyer2.email)}`); - expect(response.status).toBe(404); - expect(response.body.message).toBeUndefined; - }); - }); - - describe('Get transaction history', () => { - it('should return transaction history for the buyer', async () => { - const response = await request(app) - .get('/product/orders/history') - .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); - expect(response.status).toBe(404); - expect(response.body.message).toBe('No transaction history found'); - }); - - it('should return 400 when user ID is not provided', async () => { - const response = await request(app) - .get('/product/orders/history') - .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); - expect(response.status).toBe(404); - }); - }); - - describe('Update order', () => { - it('should update order status successfully', async () => { - const response = await request(app) - .put(`/product/client/orders/${orderId}`) - .send({ orderStatus: 'delivered' }) - .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); - expect(response.status).toBe(500); - }); - }); -}); diff --git a/src/__test__/test-assets/DatabaseCleanup.ts b/src/__test__/test-assets/DatabaseCleanup.ts index 3674dfb..b8739d1 100644 --- a/src/__test__/test-assets/DatabaseCleanup.ts +++ b/src/__test__/test-assets/DatabaseCleanup.ts @@ -12,11 +12,13 @@ import { User } from '../../entities/User'; import { server } from '../..'; import { VendorOrderItem } from '../../entities/VendorOrderItem'; import { VendorOrders } from '../../entities/vendorOrders'; +import { Feedback } from '../../entities/Feedback'; export const cleanDatabase = async () => { const connection = getConnection(); // Delete from child tables first + await connection.getRepository(Feedback).delete({}); await connection.getRepository(Transaction).delete({}); await connection.getRepository(Coupon).delete({}); await connection.getRepository(VendorOrderItem).delete({}); diff --git a/src/controllers/feedbackController.ts b/src/controllers/feedbackController.ts new file mode 100644 index 0000000..0cbce14 --- /dev/null +++ b/src/controllers/feedbackController.ts @@ -0,0 +1,21 @@ +import { Request, Response } from 'express'; +import { createFeedbackService } from '../services/feedbackServices/createFeedback'; +import { updateFeedbackService } from '../services/feedbackServices/updateFeedback'; +import { deleteFeedbackService } from '../services/feedbackServices/deleteFeedback'; +import { adminDeleteFeedbackService } from '../services/feedbackServices/adminDeleteFeedback'; + +export const createFeedback = async (req: Request, res: Response) => { + await createFeedbackService(req, res); +}; + +export const updateFeedback = async (req: Request, res: Response) => { + await updateFeedbackService(req, res); +}; + +export const deleteFeedback = async (req: Request, res: Response) => { + await deleteFeedbackService(req, res); +}; + +export const adminDeleteFeedback = async (req: Request, res: Response) => { + await adminDeleteFeedbackService(req, res); +}; diff --git a/src/entities/Feedback.ts b/src/entities/Feedback.ts new file mode 100644 index 0000000..6de9058 --- /dev/null +++ b/src/entities/Feedback.ts @@ -0,0 +1,30 @@ +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, CreateDateColumn, UpdateDateColumn } from 'typeorm'; +import { User } from './User'; +import { Product } from './Product'; +import { IsNotEmpty } from 'class-validator'; +import { Order } from './Order'; + +@Entity() +export class Feedback { + @PrimaryGeneratedColumn('uuid') + @IsNotEmpty() + id!: string; + + @Column('text') + comment!: string; + + @ManyToOne(() => User, user => user.feedbacks) + user!: User; + + @ManyToOne(() => Product, product => product.feedbacks) + product!: Product; + + @ManyToOne(() => Order, order => order.feedbacks) + order!: Order; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; +} diff --git a/src/entities/Order.ts b/src/entities/Order.ts index 47649a7..faa19db 100644 --- a/src/entities/Order.ts +++ b/src/entities/Order.ts @@ -11,6 +11,7 @@ import { IsNotEmpty, IsNumber, IsDate, IsIn } from 'class-validator'; import { User } from './User'; import { OrderItem } from './OrderItem'; import { Transaction } from './transaction'; +import { Feedback } from './Feedback'; @Entity() export class Order { @@ -33,6 +34,10 @@ export class Order { @OneToMany(() => Transaction, transaction => transaction.order) transactions!: Transaction[]; + + @OneToMany(() => Feedback, feedback => feedback.order) + feedbacks!: Feedback[]; + @Column({ default: 'order placed' }) @IsNotEmpty() @IsIn([ diff --git a/src/entities/Product.ts b/src/entities/Product.ts index e144a04..ae027ef 100644 --- a/src/entities/Product.ts +++ b/src/entities/Product.ts @@ -19,11 +19,12 @@ import { Order } from './Order'; import { Coupon } from './coupon'; import { OrderItem } from './OrderItem'; import { VendorOrderItem } from './VendorOrderItem'; +import { Feedback } from './Feedback'; @Entity() @Unique(['id']) export class Product { - static query () { + static query() { throw new Error('Method not implemented.'); } @PrimaryGeneratedColumn('uuid') @@ -39,6 +40,8 @@ export class Product { @OneToMany(() => VendorOrderItem, vendorOrderItems => vendorOrderItems.product) vendorOrderItems!: VendorOrderItem[]; + @OneToMany(() => Feedback, feedback => feedback.product) + feedbacks!: Feedback[]; @OneToOne(() => Coupon, (coupons: any) => coupons.product) @JoinColumn() diff --git a/src/entities/User.ts b/src/entities/User.ts index ef256d7..232ce11 100644 --- a/src/entities/User.ts +++ b/src/entities/User.ts @@ -13,6 +13,7 @@ import { IsEmail, IsNotEmpty, IsString, IsBoolean, IsIn } from 'class-validator' import { roles } from '../utils/roles'; import { Order } from './Order'; import { Transaction } from './transaction'; +import { Feedback } from './Feedback'; export interface UserInterface { id?: string; @@ -111,6 +112,8 @@ export class User { @Column({ type: 'numeric', precision: 24, scale: 2, default: 0 }) accountBalance!: number; + @OneToMany(() => Feedback, feedback => feedback.product) + feedbacks!: Feedback[]; @BeforeInsert() setRole (): void { diff --git a/src/routes/ProductRoutes.ts b/src/routes/ProductRoutes.ts index 4a72b47..2352000 100644 --- a/src/routes/ProductRoutes.ts +++ b/src/routes/ProductRoutes.ts @@ -26,7 +26,6 @@ import { getSingleBuyerVendorOrder, updateBuyerVendorOrder, } from '../controllers'; - const router = Router(); router.get('/all', listAllProducts); router.get('/recommended', authMiddleware as RequestHandler, hasRole('BUYER'), getRecommendedProducts); diff --git a/src/routes/feedbackRoutes.ts b/src/routes/feedbackRoutes.ts new file mode 100644 index 0000000..3ada81b --- /dev/null +++ b/src/routes/feedbackRoutes.ts @@ -0,0 +1,19 @@ +import { RequestHandler, Router } from 'express'; +import { + createFeedback, + updateFeedback, + deleteFeedback, + adminDeleteFeedback +} from '../controllers/feedbackController' +import { authMiddleware } from '../middlewares/verifyToken'; +import { hasRole } from '../middlewares/roleCheck'; + + +const router = Router(); + +router.post('/:productId/new', authMiddleware as RequestHandler, hasRole('BUYER'), createFeedback); +router.put('/update/:feedbackId', authMiddleware as RequestHandler, hasRole('BUYER'), updateFeedback ); +router.delete('/delete/:feedbackId', authMiddleware as RequestHandler, hasRole('BUYER'), deleteFeedback); +router.delete('/admin/delete/:feedbackId', authMiddleware as RequestHandler, hasRole('ADMIN'), adminDeleteFeedback ); + +export default router; diff --git a/src/routes/index.ts b/src/routes/index.ts index 6f632d6..ee8dcdb 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -5,6 +5,7 @@ import productRoutes from './ProductRoutes'; import wishListRoutes from './wishListRoute'; import couponRoute from './couponRoutes'; import cartRoutes from './CartRoutes'; +import feedbackRoute from './feedbackRoutes'; const router = Router(); @@ -17,5 +18,6 @@ router.use('/product', productRoutes); router.use('/wish-list', wishListRoutes); router.use('/cart', cartRoutes); router.use('/coupons', couponRoute); +router.use('/feedback', feedbackRoute); export default router; diff --git a/src/services/feedbackServices/adminDeleteFeedback.ts b/src/services/feedbackServices/adminDeleteFeedback.ts new file mode 100644 index 0000000..7bf6261 --- /dev/null +++ b/src/services/feedbackServices/adminDeleteFeedback.ts @@ -0,0 +1,25 @@ +import { Request, Response } from 'express'; +import { getRepository } from 'typeorm'; +import { Feedback } from '../../entities/Feedback'; +import { responseError, responseSuccess } from '../../utils/response.utils'; + +export const adminDeleteFeedbackService = async (req: Request, res: Response) => { + const { feedbackId } = req.params; + + try { + const feedbackRepository = getRepository(Feedback); + const feedback = await feedbackRepository.findOne({ + where: { id: feedbackId }, + }); + + if (!feedback) { + return responseError(res, 404, 'Feedback not found'); + } + + await feedbackRepository.remove(feedback); + + return responseSuccess(res, 200, 'Feedback successfully removed'); + } catch (error) { + return responseError(res, 500, 'Server error'); + } +}; diff --git a/src/services/feedbackServices/createFeedback.ts b/src/services/feedbackServices/createFeedback.ts new file mode 100644 index 0000000..fa731f3 --- /dev/null +++ b/src/services/feedbackServices/createFeedback.ts @@ -0,0 +1,44 @@ +import { Request, Response } from 'express'; +import { getRepository } from 'typeorm'; +import { Feedback } from '../../entities/Feedback'; +import { Product } from '../../entities/Product'; +import { User } from '../../entities/User'; +import { responseError, responseSuccess } from '../../utils/response.utils'; +import { Order } from '../../entities/Order'; + +interface AuthRequest extends Request { + user?: User; +} + +export const createFeedbackService = async (req: Request, res: Response) => { + const { productId } = req.params; + const { comment, orderId } = req.body; + + try { + const feedbackRepository = getRepository(Feedback); + const productRepository = getRepository(Product); + const orderRepository = getRepository(Order); + if (!orderId) { + return responseError(res, 404, `Your feedback can't be recorded at this time Your order doesn't exist `); + } + const product = await productRepository.findOne({ where: { id: productId } }); + if (!product) { + return responseError(res, 404, `Your feedback can't be recorded at this time product not found`); + } + const order = await orderRepository.findBy({ id: orderId, orderStatus: 'completed', buyer: { id: req.user?.id }, orderItems: { product: { id: productId } } }) + if (!order.length) { + return responseError(res, 404, `Your feedback can't be recorded at this time Your order haven't been completed yet or doesn't contain this product`); + } + + const feedback = new Feedback(); + feedback.comment = comment; + feedback.user = req.user as User; + feedback.product = product; + + await feedbackRepository.save(feedback); + + return responseSuccess(res, 201, 'Feedback created successfully', feedback); + } catch (error) { + return responseError(res, 500, 'Server error'); + } +}; diff --git a/src/services/feedbackServices/deleteFeedback.ts b/src/services/feedbackServices/deleteFeedback.ts new file mode 100644 index 0000000..5de4ea0 --- /dev/null +++ b/src/services/feedbackServices/deleteFeedback.ts @@ -0,0 +1,27 @@ +import { Request, Response } from 'express'; +import { getRepository } from 'typeorm'; +import { Feedback } from '../../entities/Feedback'; +import { responseError, responseSuccess } from '../../utils/response.utils'; + +export const deleteFeedbackService = async (req: Request, res: Response) => { + const { feedbackId } = req.params; + + try { + const feedbackRepository = getRepository(Feedback); + const feedback = await feedbackRepository.findOne({ + where: { id: feedbackId, + user: {id: req?.user?.id }, + } + }); + + if (!feedback) { + return responseError(res, 404, 'Feedback not found'); + } + + await feedbackRepository.remove(feedback); + + return responseSuccess(res, 200, 'Feedback successfully removed'); + } catch (error) { + return responseError(res, 500, 'Server error'); + } +}; diff --git a/src/services/feedbackServices/updateFeedback.ts b/src/services/feedbackServices/updateFeedback.ts new file mode 100644 index 0000000..18258c2 --- /dev/null +++ b/src/services/feedbackServices/updateFeedback.ts @@ -0,0 +1,32 @@ +import { Request, Response } from 'express'; +import { getRepository } from 'typeorm'; +import { Feedback } from '../../entities/Feedback'; +import { responseError, responseSuccess } from '../../utils/response.utils'; +import { User } from '../../entities/User'; + +export const updateFeedbackService = async (req: Request, res: Response) => { + const { feedbackId } = req.params; + const { comment } = req.body; + + try { + const feedbackRepository = getRepository(Feedback); + + const feedback = await feedbackRepository.findOne({ + where: { + id: feedbackId, + user: { id: req?.user?.id }, + }, + }); + + if (!feedback) { + return responseError(res, 404, 'You are not allowed to remove this feedback or you are not allowed to edit this feedback'); + } + + feedback.comment = comment; + await feedbackRepository.save(feedback); + + return responseSuccess(res, 200, 'Feedback updated successfully', feedback); + } catch (error) { + return responseError(res, 500, 'Server error'); + } +}; diff --git a/src/services/orderServices/createOrder.ts b/src/services/orderServices/createOrder.ts index 256bf40..7e1916e 100644 --- a/src/services/orderServices/createOrder.ts +++ b/src/services/orderServices/createOrder.ts @@ -59,15 +59,6 @@ export const createOrderService = async (req: Request, res: Response) => { orderItem.quantity = item.quantity; orderItems.push(orderItem); } - - if (!buyer.accountBalance || buyer.accountBalance < totalPrice) { - return sendErrorResponse(res, 400, 'Not enough funds to perform this transaction'); - } - - const previousBalance = buyer.accountBalance; - buyer.accountBalance -= totalPrice; - const currentBalance = buyer.accountBalance; - const newOrder = new Order(); newOrder.buyer = buyer; newOrder.totalPrice = totalPrice; @@ -94,8 +85,6 @@ export const createOrderService = async (req: Request, res: Response) => { orderTransaction.user = buyer; orderTransaction.order = newOrder; orderTransaction.amount = totalPrice; - orderTransaction.previousBalance = previousBalance; - orderTransaction.currentBalance = currentBalance; orderTransaction.type = 'debit'; orderTransaction.description = 'Purchase of products'; await transactionalEntityManager.save(Transaction, orderTransaction); @@ -105,6 +94,7 @@ export const createOrderService = async (req: Request, res: Response) => { }); const orderResponse = { + id: newOrder.id, fullName: `${newOrder.buyer.firstName} ${newOrder.buyer.lastName}`, email: newOrder.buyer.email, products: orderItems.map(item => ({ @@ -174,7 +164,7 @@ const saveVendorRelatedOrder = async (order: Order, CartItem: CartItem[]) => { newVendorOrders.vendor = product.vendor; newVendorOrders.vendorOrderItems = [orderItem]; newVendorOrders.order = order; - newVendorOrders.totalPrice = +product.newPrice * item.quantity; + newVendorOrders.totalPrice = product.newPrice * item.quantity; vendorOrders = newVendorOrders; } diff --git a/src/services/orderServices/getOrderTransactionHistory.ts b/src/services/orderServices/getOrderTransactionHistory.ts index 74ae473..6bd0b17 100644 --- a/src/services/orderServices/getOrderTransactionHistory.ts +++ b/src/services/orderServices/getOrderTransactionHistory.ts @@ -23,8 +23,6 @@ export const getTransactionHistoryService = async (req: Request, res: Response) id: transaction.id, amount: transaction.amount, type: transaction.type, - previousBalance: transaction.previousBalance, - currentBalance: transaction.currentBalance, description: transaction.description, createdAt: transaction.createdAt, order: transaction.order diff --git a/src/services/orderServices/updateOrderService.ts b/src/services/orderServices/updateOrderService.ts index 6a163fe..bfddf1f 100644 --- a/src/services/orderServices/updateOrderService.ts +++ b/src/services/orderServices/updateOrderService.ts @@ -102,9 +102,6 @@ async function processRefund (order: Order, entityManager: EntityManager) { const buyer = order.buyer; // Refund buyer - const previousBalance = buyer.accountBalance; - buyer.accountBalance += order.totalPrice; - const currentBalance = buyer.accountBalance; await entityManager.save(buyer); // Record refund transaction @@ -112,8 +109,6 @@ async function processRefund (order: Order, entityManager: EntityManager) { refundTransaction.user = buyer; refundTransaction.order = order; refundTransaction.amount = order.totalPrice; - refundTransaction.previousBalance = previousBalance; - refundTransaction.currentBalance = currentBalance; refundTransaction.type = 'credit'; refundTransaction.description = 'Refund for cancelled or returned order'; await entityManager.save(refundTransaction); diff --git a/src/services/productServices/getRecommendedProductsService.ts b/src/services/productServices/getRecommendedProductsService.ts index fde015d..533dcd9 100644 --- a/src/services/productServices/getRecommendedProductsService.ts +++ b/src/services/productServices/getRecommendedProductsService.ts @@ -30,6 +30,7 @@ export const getRecommendedProductsService = async (req: Request, res: Response) .createQueryBuilder('product') .leftJoinAndSelect('product.categories', 'category') .leftJoinAndSelect('product.vendor', 'vendor') + .leftJoinAndSelect('product.feedbacks', 'feedbacks') .where('1 = 1'); if (condition.categories && condition.categories.length > 0) { diff --git a/src/services/productServices/listAllProductsService.ts b/src/services/productServices/listAllProductsService.ts index f39c7bb..4429e89 100644 --- a/src/services/productServices/listAllProductsService.ts +++ b/src/services/productServices/listAllProductsService.ts @@ -20,7 +20,7 @@ export const listAllProductsService = async (req: Request, res: Response) => { }, skip, take: limit, - relations: ['categories', 'vendor'], + relations: ['categories', 'vendor', 'feedbacks'], select: { vendor: { id: true, diff --git a/src/services/productServices/readProduct.ts b/src/services/productServices/readProduct.ts index b3c244d..2836b21 100644 --- a/src/services/productServices/readProduct.ts +++ b/src/services/productServices/readProduct.ts @@ -20,7 +20,7 @@ export const readProductsService = async (req: Request, res: Response) => { }, skip, take: limit, - relations: ['categories', 'vendor'], + relations: ['categories', 'vendor', 'feedbacks'], select: { vendor: { id: true, diff --git a/src/services/productServices/viewSingleProduct.ts b/src/services/productServices/viewSingleProduct.ts index 29ac167..be9764d 100644 --- a/src/services/productServices/viewSingleProduct.ts +++ b/src/services/productServices/viewSingleProduct.ts @@ -13,7 +13,7 @@ export const viewSingleProduct = async (req: Request, res: Response) => { } if (productId) { const products = getRepository(Product); - const product = await products.findOneBy({ id: productId }); + const product = await products.findOne({ where: { id: productId }, relations: ['categories', 'vendor', 'feedbacks'], }); if (!product) { return res.status(404).send({ status: 'error', message: 'Product not found' }); From 8aac64ca6e2e0373895eb120affe699e8de5a70d Mon Sep 17 00:00:00 2001 From: maxCastro1 Date: Tue, 4 Jun 2024 11:31:38 +0200 Subject: [PATCH 51/51] This PR is for the addition of the chatbot --- Intents/cancel.json | 14 + Intents/confirm.json | 14 + Intents/delay.json | 16 + Intents/delivery.json | 15 + Intents/greetings.bye.json | 18 + Intents/greetings.hello.json | 19 + Intents/items.json | 23 + Intents/more.contact.json | 12 + Intents/more.help.json | 19 + Intents/order.json | 12 + Intents/payments.json | 15 + Intents/personal.json | 12 + Intents/profile.info.json | 15 + Intents/reason.json | 13 + Intents/refund.demanding.json | 14 + Intents/refund.status.json | 14 + Intents/thanks.json | 13 + Intents/track.json | 13 + Intents/user.response.json | 14 + Intents/voucher.json | 12 + model.nlp | 5621 +++++++++++++++++++++++ package.json | 3 + src/@types/index.d.ts | 1 + src/__test__/chatBot.test.ts | 53 + src/controllers/chatBotController.ts | 6 + src/controllers/index.ts | 1 + src/node-nlp.d.ts | 1 + src/routes/chatBot.ts | 10 + src/routes/index.ts | 2 + src/services/chatbotServices/chatBot.ts | 30 + src/services/index.ts | 3 + src/train.ts | 43 + tsconfig.json | 4 +- 33 files changed, 6073 insertions(+), 2 deletions(-) create mode 100644 Intents/cancel.json create mode 100644 Intents/confirm.json create mode 100644 Intents/delay.json create mode 100644 Intents/delivery.json create mode 100644 Intents/greetings.bye.json create mode 100644 Intents/greetings.hello.json create mode 100644 Intents/items.json create mode 100644 Intents/more.contact.json create mode 100644 Intents/more.help.json create mode 100644 Intents/order.json create mode 100644 Intents/payments.json create mode 100644 Intents/personal.json create mode 100644 Intents/profile.info.json create mode 100644 Intents/reason.json create mode 100644 Intents/refund.demanding.json create mode 100644 Intents/refund.status.json create mode 100644 Intents/thanks.json create mode 100644 Intents/track.json create mode 100644 Intents/user.response.json create mode 100644 Intents/voucher.json create mode 100644 model.nlp create mode 100644 src/__test__/chatBot.test.ts create mode 100644 src/controllers/chatBotController.ts create mode 100644 src/node-nlp.d.ts create mode 100644 src/routes/chatBot.ts create mode 100644 src/services/chatbotServices/chatBot.ts create mode 100644 src/train.ts diff --git a/Intents/cancel.json b/Intents/cancel.json new file mode 100644 index 0000000..0171b1b --- /dev/null +++ b/Intents/cancel.json @@ -0,0 +1,14 @@ +{ + "intent": "cancel", + "utterances": [ + "Can I cancel my order?", + "Why is my order cancelled?", + "How do I can cancel my order", + "Can I cancel an order" + ], + "responses": [ + "Order can only be cancelled within 7 days of placement. Digital goods do not qualify for refund.", + "Visit my orders page to check status of specific orders." + ] + } + \ No newline at end of file diff --git a/Intents/confirm.json b/Intents/confirm.json new file mode 100644 index 0000000..5c708ec --- /dev/null +++ b/Intents/confirm.json @@ -0,0 +1,14 @@ +{ + "intent": "confirm", + "utterances": [ + "My payment was processed successfully but I didn't get any order confirmation. What should I do?", + "Payment was completed but no confirmation", + "Order was not confirmed", + "Money deducted but order is not confirmed" + ], + "responses": [ + "knight sends you an Email & SMS upon confirmation of your order. If it requires manual confirmation, our team will contact you within 24 hours after order placement. Delay in confirmation SMS may occur due to network error, you may receive it with a delay.", + "It takes upto 24 hours for confirmation, please bear with us! Type Need more help for more assistance. " + ] + } + \ No newline at end of file diff --git a/Intents/delay.json b/Intents/delay.json new file mode 100644 index 0000000..52d0cdf --- /dev/null +++ b/Intents/delay.json @@ -0,0 +1,16 @@ +{ + "intent": "delay", + "utterances": [ + "Why is my order delayed? ", + "Did my order get delayed?", + "Why is my order not delivered yet?", + "When do I get my delivery?" + ], + "responses": [ + "We are really sorry if there has been a delay in your order! If your order is within delivery period, We recommend you to please wait during this period since our Delivery Heroes make 3 attempts to deliver your order!", + "If your order is past up to 3 days after delivery period, There may be logistic issues causing a delay in delivery. Please type 'Common reasons for delivery delay' to know more about this.", + "We appreciate if you could wait for your items as most orders are delivered successfully within this period.", + "If your order is past more than 3 days, Since there may be unexpected issues causing delivery delays, you can click on 'Need more Help' for further assistance." + ] + } + \ No newline at end of file diff --git a/Intents/delivery.json b/Intents/delivery.json new file mode 100644 index 0000000..d3b969b --- /dev/null +++ b/Intents/delivery.json @@ -0,0 +1,15 @@ +{ + "intent": "delivery", + "utterances": [ + "How long does delivery take?", + "How long does shipping take?", + "Please Tell me about my delivery", + "When do I get my delivery?", + "Why is my order not delivered yet" + ], + "responses": [ + "Delivery takes 2-4 days. Please bear with us!", + "Shipping takes 2-4 days. Please bear with us!" + ] + } + \ No newline at end of file diff --git a/Intents/greetings.bye.json b/Intents/greetings.bye.json new file mode 100644 index 0000000..b9aa6f0 --- /dev/null +++ b/Intents/greetings.bye.json @@ -0,0 +1,18 @@ +{ + "intent": "goodbye", + "utterances": [ + "goodbye", + "bye take care", + "see you later", + "bye for now", + "i must go" + ], + "responses": [ + "see you soon!", + "Till next time", + "bye bye", + "have a great day", + "See you later, thanks for visiting. Hope I was able to help!", + "Have a nice day. Hope I was able to help!" + ] +} \ No newline at end of file diff --git a/Intents/greetings.hello.json b/Intents/greetings.hello.json new file mode 100644 index 0000000..7926f49 --- /dev/null +++ b/Intents/greetings.hello.json @@ -0,0 +1,19 @@ +{ + "intent": "greetings", + "utterances": [ + "hello", + "hi", + "howdy", + "Greetings", + "Is anyone there?", + "Hello", + "Good day" + ], + "responses": [ + "Hey :-) My name is knight!", + "Hello, thanks for visiting. My name is knight!", + "Hi there, My name is knight!. What can I do for you?", + "Hi there, My name is knight! How can I help?" + ] + } + \ No newline at end of file diff --git a/Intents/items.json b/Intents/items.json new file mode 100644 index 0000000..3a34861 --- /dev/null +++ b/Intents/items.json @@ -0,0 +1,23 @@ +{ + "intent": "items", + "utterances": [ + "Which items do you have?", + "What kinds of items are there?", + "What do you sell?", + "What do you offer?", + "What can I buy?", + "I'm looking for...", + "Do you have any...", + "I'm interested in...", + "Can I see what you have in...", + "I want to buy a...", + "I'm looking for something like this...", + "What are your most popular items?", + "What are some of your best deals?", + "Do you have any new arrivals?" + ], + "responses": [ + "Search your preference in our flagship store's search bar to see all available products. " + ] + } + \ No newline at end of file diff --git a/Intents/more.contact.json b/Intents/more.contact.json new file mode 100644 index 0000000..df773ac --- /dev/null +++ b/Intents/more.contact.json @@ -0,0 +1,12 @@ +{ + "intent": "more_contact", + "utterances": [ + "Need more help", + "Help me more" + ], + "responses": [ + "Absolutely! How can I help you today? Here are some options based on your inquiry:", + "Sure, let me know what you need help with. Here are a few things I can assist you with:" + ] + } + \ No newline at end of file diff --git a/Intents/more.help.json b/Intents/more.help.json new file mode 100644 index 0000000..3ce18e9 --- /dev/null +++ b/Intents/more.help.json @@ -0,0 +1,19 @@ +{ + "intent": "more_help", + "utterances": [ + "can I talk to an agent", + "can I call customer service", + "customer support number", + "how to contact customer service", + "customer service number", + "contact number for help", + "helpline number", + "How to become a seller", + "How to contact a seller" + ], + "responses": [ + "Contact us for further information here: Phone: +250 780 000 000. Timings are from 09:00 AM to 05:00 PM from Monday to Saturday.", + "For immediate assistance, you can contact our customer service team at +250 780 000 000. Our hours are from 09:00 AM to 05:00 PM, Monday to Saturday." + ] + } + \ No newline at end of file diff --git a/Intents/order.json b/Intents/order.json new file mode 100644 index 0000000..b110674 --- /dev/null +++ b/Intents/order.json @@ -0,0 +1,12 @@ +{ + "intent": "order_status", + "utterances": [ + "What is my order status", + "I want to know my return status", + "How to return status" + ], + "responses": [ + "Please visit the My Orders page for a list of your confirmed orders." + ] + } + \ No newline at end of file diff --git a/Intents/payments.json b/Intents/payments.json new file mode 100644 index 0000000..2243fa3 --- /dev/null +++ b/Intents/payments.json @@ -0,0 +1,15 @@ +{ + "intent": "payments", + "utterances": [ + "Do you take credit cards?", + "Do you accept Mastercard?", + "Can I pay with Cash?", + "Are you cash only?", + "What are your payment methods?", + "How do I pay?" + ], + "responses": [ + "We accept VISA and Mastercard" + ] + } + \ No newline at end of file diff --git a/Intents/personal.json b/Intents/personal.json new file mode 100644 index 0000000..fa32b95 --- /dev/null +++ b/Intents/personal.json @@ -0,0 +1,12 @@ +{ + "intent": "personal", + "utterances": [ + "How are you?", + "How are you doing?", + "How is your day?" + ], + "responses": [ + "I'm good, all's good, thanks. How about you?" + ] + } + \ No newline at end of file diff --git a/Intents/profile.info.json b/Intents/profile.info.json new file mode 100644 index 0000000..229b6ac --- /dev/null +++ b/Intents/profile.info.json @@ -0,0 +1,15 @@ +{ + "intent": "profile_info", + "utterances": [ + "How can I change my profile information", + "I want to change my password", + "I want to change my phone number", + "I want to change my address", + "I want to Reset my password", + "I want to delete my account", + "delete my account" + ], + "responses": [ + "You can easily add or change your account details by following the steps below: Step 1: Click on 'Account', Step 2: Click on 'Manage my account' from the icon, Step 3: You can change or edit your name, address, email address, mobile number, etc., Step 4: Fill in the required details, and click on Save. Note: You can also change your delivery address from the Checkout page before proceeding to pay." + ] + } \ No newline at end of file diff --git a/Intents/reason.json b/Intents/reason.json new file mode 100644 index 0000000..4c7b831 --- /dev/null +++ b/Intents/reason.json @@ -0,0 +1,13 @@ +{ + "intent": "reasons", + "utterances": [ + "Common reasons for delivery delay", + "common reasons for delivery delay", + "reasons for delay", + "delivery delay" + ], + "responses": [ + "Reasons include Seller Sourcing Issues, Courier Issues, Cross Border shipment delay, Wrong Address or Phone Number, and Unavailability of Customer. " + ] + } + \ No newline at end of file diff --git a/Intents/refund.demanding.json b/Intents/refund.demanding.json new file mode 100644 index 0000000..70e004e --- /dev/null +++ b/Intents/refund.demanding.json @@ -0,0 +1,14 @@ +{ + "intent": "demandin_refund", + "utterances": [ + "Can I refund an item.", + "I want to refund an item", + "can I refund my order", + "Are refunds available" + ], + "responses": [ + "Refund can only be issued within 7 days of placement. Digital goods do not qualify for refund.", + "Visit my orders page to check for specific orders." + ] + } + \ No newline at end of file diff --git a/Intents/refund.status.json b/Intents/refund.status.json new file mode 100644 index 0000000..a1f1361 --- /dev/null +++ b/Intents/refund.status.json @@ -0,0 +1,14 @@ +{ + "intent": "refund_status", + "utterances": [ + "Why is the status Refunded when it's not credited?", + "No refund even though status is refunded", + "No refund when status says refunded", + "I did not receive my refund money", + "Refund money not received" + ], + "responses": [ + "Please be patient as refunds take upto 30 days to receive into bank. " + ] + } + diff --git a/Intents/thanks.json b/Intents/thanks.json new file mode 100644 index 0000000..4d0fbb0 --- /dev/null +++ b/Intents/thanks.json @@ -0,0 +1,13 @@ +{ + "intent": "thanks", + "utterances": ["Thanks", + "Thank you", + "That's helpful", + "Thank's a lot!", + "thx", + "thnks"], + "responses": ["Happy to help!", + "Any time!", + "My pleasure"] + } + \ No newline at end of file diff --git a/Intents/track.json b/Intents/track.json new file mode 100644 index 0000000..a130950 --- /dev/null +++ b/Intents/track.json @@ -0,0 +1,13 @@ +{ + "intent": "track", + "utterances": [ + "How can I track my order", + "I want to track my order", + "Can I track my order", + "Track order" + ], + "responses": [ + "Visit the order page, click on the specific order, select 'track my order', and check the status" + ] + } + \ No newline at end of file diff --git a/Intents/user.response.json b/Intents/user.response.json new file mode 100644 index 0000000..998d545 --- /dev/null +++ b/Intents/user.response.json @@ -0,0 +1,14 @@ +{ + "intent": "user_response", + "utterances": [ + "I'm good", + "Im good", + "Im doing good", + "I am good", + "I am okay" + ], + "responses": [ + "Great to hear you are doing good." + ] + } + \ No newline at end of file diff --git a/Intents/voucher.json b/Intents/voucher.json new file mode 100644 index 0000000..cc75c1f --- /dev/null +++ b/Intents/voucher.json @@ -0,0 +1,12 @@ +{ + "intent": "use_voucher", + "utterances": [ + "How to use a voucher?", + "Can I use a voucher?", + "How to use a voucher?" + ], + "responses": [ + "You can add a voucher by clicking on My Cart > Check Out > Enter Voucher Code > APPLY. " + ] + } + \ No newline at end of file diff --git a/model.nlp b/model.nlp new file mode 100644 index 0000000..6aee508 --- /dev/null +++ b/model.nlp @@ -0,0 +1,5621 @@ +{ + "settings": { + "languages": [ + "en" + ], + "tag": "nlp", + "threshold": 0.5, + "autoLoad": true, + "autoSave": true, + "modelFileName": "model.nlp", + "executeActionsBeforeAnswers": false, + "calculateSentiment": true + }, + "nluManager": { + "settings": { + "tag": "nlu-manager" + }, + "locales": [ + "en" + ], + "languageNames": {}, + "domainManagers": { + "en": { + "settings": { + "locale": "en", + "trainByDomain": false, + "tag": "domain-manager-en", + "nluByDomain": { + "default": { + "className": "NeuralNlu", + "settings": {} + } + }, + "useStemDict": true + }, + "stemDict": { + "can,cancel,i,my,order": { + "intent": "cancel", + "domain": "default" + }, + "cancel,is,my,order,whi": { + "intent": "cancel", + "domain": "default" + }, + "can,cancel,do,how,i,my,order": { + "intent": "cancel", + "domain": "default" + }, + "an,can,cancel,i,order": { + "intent": "cancel", + "domain": "default" + }, + "ani,but,confirm,did,do,get,i,my,not,order,payment,process,should,success,was,what": { + "intent": "confirm", + "domain": "default" + }, + "but,complet,confirm,no,payment,was": { + "intent": "confirm", + "domain": "default" + }, + "confirm,not,order,was": { + "intent": "confirm", + "domain": "default" + }, + "but,confirm,deduct,is,money,not,order": { + "intent": "confirm", + "domain": "default" + }, + "delay,is,my,order,whi": { + "intent": "delay", + "domain": "default" + }, + "delay,did,get,my,order": { + "intent": "delay", + "domain": "default" + }, + "deliv,is,my,not,order,whi,yet": { + "intent": "delivery", + "domain": "default" + }, + "deliveri,do,get,i,my,when": { + "intent": "delivery", + "domain": "default" + }, + "deliveri,doe,how,long,take": { + "intent": "delivery", + "domain": "default" + }, + "doe,how,long,ship,take": { + "intent": "delivery", + "domain": "default" + }, + "about,deliveri,me,my,pleas,tell": { + "intent": "delivery", + "domain": "default" + }, + "goodby": { + "intent": "greetings.bye", + "domain": "default" + }, + "bye,care,take": { + "intent": "greetings.bye", + "domain": "default" + }, + "later,see,you": { + "intent": "greetings.bye", + "domain": "default" + }, + "bye,for,now": { + "intent": "greetings.bye", + "domain": "default" + }, + "go,i,must": { + "intent": "greetings.bye", + "domain": "default" + }, + "hello": { + "intent": "greetings.hello", + "domain": "default" + }, + "hi": { + "intent": "greetings.hello", + "domain": "default" + }, + "howdi": { + "intent": "greetings.hello", + "domain": "default" + }, + "greet": { + "intent": "greetings.hello", + "domain": "default" + }, + "anyon,is,there": { + "intent": "greetings.hello", + "domain": "default" + }, + "day,good": { + "intent": "greetings.hello", + "domain": "default" + }, + "do,have,item,which,you": { + "intent": "items", + "domain": "default" + }, + "are,item,kind,of,there,what": { + "intent": "items", + "domain": "default" + }, + "do,sell,what,you": { + "intent": "items", + "domain": "default" + }, + "do,offer,what,you": { + "intent": "items", + "domain": "default" + }, + "buy,can,i,what": { + "intent": "items", + "domain": "default" + }, + "am,for,i,look": { + "intent": "items", + "domain": "default" + }, + "ani,do,have,you": { + "intent": "items", + "domain": "default" + }, + "am,i,in,interest": { + "intent": "items", + "domain": "default" + }, + "can,have,i,in,see,what,you": { + "intent": "items", + "domain": "default" + }, + "a,buy,i,to,want": { + "intent": "items", + "domain": "default" + }, + "am,for,i,like,look,someth,this": { + "intent": "items", + "domain": "default" + }, + "are,item,most,popular,what,your": { + "intent": "items", + "domain": "default" + }, + "are,best,deal,of,some,what,your": { + "intent": "items", + "domain": "default" + }, + "ani,arriv,do,have,new,you": { + "intent": "items", + "domain": "default" + }, + "help,more,need": { + "intent": "more.contact", + "domain": "default" + }, + "help,me,more": { + "intent": "more.contact", + "domain": "default" + }, + "agent,an,can,i,talk,to": { + "intent": "more.help", + "domain": "default" + }, + "call,can,custom,i,servic": { + "intent": "more.help", + "domain": "default" + }, + "custom,number,support": { + "intent": "more.help", + "domain": "default" + }, + "contact,custom,how,servic,to": { + "intent": "more.help", + "domain": "default" + }, + "custom,number,servic": { + "intent": "more.help", + "domain": "default" + }, + "contact,for,help,number": { + "intent": "more.help", + "domain": "default" + }, + "helplin,number": { + "intent": "more.help", + "domain": "default" + }, + "a,becom,how,seller,to": { + "intent": "more.help", + "domain": "default" + }, + "a,contact,how,seller,to": { + "intent": "more.help", + "domain": "default" + }, + "is,my,order,status,what": { + "intent": "order", + "domain": "default" + }, + "i,know,my,return,status,to,want": { + "intent": "order", + "domain": "default" + }, + "how,return,status,to": { + "intent": "order", + "domain": "default" + }, + "card,credit,do,take,you": { + "intent": "payments", + "domain": "default" + }, + "accept,do,mastercard,you": { + "intent": "payments", + "domain": "default" + }, + "can,cash,i,pay,with": { + "intent": "payments", + "domain": "default" + }, + "are,cash,onli,you": { + "intent": "payments", + "domain": "default" + }, + "are,method,payment,what,your": { + "intent": "payments", + "domain": "default" + }, + "do,how,i,pay": { + "intent": "payments", + "domain": "default" + }, + "are,how,you": { + "intent": "personal", + "domain": "default" + }, + "are,do,how,you": { + "intent": "personal", + "domain": "default" + }, + "day,how,is,your": { + "intent": "personal", + "domain": "default" + }, + "can,chang,how,i,inform,my,profil": { + "intent": "profile.info", + "domain": "default" + }, + "chang,i,my,password,to,want": { + "intent": "profile.info", + "domain": "default" + }, + "chang,i,my,number,phone,to,want": { + "intent": "profile.info", + "domain": "default" + }, + "address,chang,i,my,to,want": { + "intent": "profile.info", + "domain": "default" + }, + "i,my,password,reset,to,want": { + "intent": "profile.info", + "domain": "default" + }, + "account,delet,i,my,to,want": { + "intent": "profile.info", + "domain": "default" + }, + "account,delet,my": { + "intent": "profile.info", + "domain": "default" + }, + "common,delay,deliveri,for,reason": { + "intent": "reason", + "domain": "default" + }, + "delay,for,reason": { + "intent": "reason", + "domain": "default" + }, + "delay,deliveri": { + "intent": "reason", + "domain": "default" + }, + "an,can,i,item,refund": { + "intent": "refund.demanding", + "domain": "default" + }, + "an,i,item,refund,to,want": { + "intent": "refund.demanding", + "domain": "default" + }, + "can,i,my,order,refund": { + "intent": "refund.demanding", + "domain": "default" + }, + "are,avail,refund": { + "intent": "refund.demanding", + "domain": "default" + }, + "credit,is,it,not,refund,status,the,when,whi": { + "intent": "refund.status", + "domain": "default" + }, + "even,is,no,refund,status,though": { + "intent": "refund.status", + "domain": "default" + }, + "no,refund,say,status,when": { + "intent": "refund.status", + "domain": "default" + }, + "did,i,money,my,not,receiv,refund": { + "intent": "refund.status", + "domain": "default" + }, + "money,not,receiv,refund": { + "intent": "refund.status", + "domain": "default" + }, + "thank": { + "intent": "thanks", + "domain": "default" + }, + "thank,you": { + "intent": "thanks", + "domain": "default" + }, + "help,is,that": { + "intent": "thanks", + "domain": "default" + }, + "a,is,lot,thank": { + "intent": "thanks", + "domain": "default" + }, + "thx": { + "intent": "thanks", + "domain": "default" + }, + "thnks": { + "intent": "thanks", + "domain": "default" + }, + "can,how,i,my,order,track": { + "intent": "track", + "domain": "default" + }, + "i,my,order,to,track,want": { + "intent": "track", + "domain": "default" + }, + "can,i,my,order,track": { + "intent": "track", + "domain": "default" + }, + "order,track": { + "intent": "track", + "domain": "default" + }, + "am,good,i": { + "intent": "user.response", + "domain": "default" + }, + "good,im": { + "intent": "user.response", + "domain": "default" + }, + "do,good,im": { + "intent": "user.response", + "domain": "default" + }, + "am,i,okay": { + "intent": "user.response", + "domain": "default" + }, + "a,how,to,use,voucher": { + "intent": "voucher", + "domain": "default" + }, + "a,can,i,use,voucher": { + "intent": "voucher", + "domain": "default" + } + }, + "intentDict": { + "cancel": "default", + "confirm": "default", + "delay": "default", + "delivery": "default", + "greetings.bye": "default", + "greetings.hello": "default", + "items": "default", + "more.contact": "default", + "more.help": "default", + "order": "default", + "payments": "default", + "personal": "default", + "profile.info": "default", + "reason": "default", + "refund.demanding": "default", + "refund.status": "default", + "thanks": "default", + "track": "default", + "user.response": "default", + "voucher": "default" + }, + "sentences": [ + { + "domain": "default", + "utterance": "Can I cancel my order?", + "intent": "cancel" + }, + { + "domain": "default", + "utterance": "Why is my order cancelled?", + "intent": "cancel" + }, + { + "domain": "default", + "utterance": "How do I can cancel my order", + "intent": "cancel" + }, + { + "domain": "default", + "utterance": "Can I cancel an order", + "intent": "cancel" + }, + { + "domain": "default", + "utterance": "My payment was processed successfully but I didn't get any order confirmation. What should I do?", + "intent": "confirm" + }, + { + "domain": "default", + "utterance": "Payment was completed but no confirmation", + "intent": "confirm" + }, + { + "domain": "default", + "utterance": "Order was not confirmed", + "intent": "confirm" + }, + { + "domain": "default", + "utterance": "Money deducted but order is not confirmed", + "intent": "confirm" + }, + { + "domain": "default", + "utterance": "Why is my order delayed? ", + "intent": "delay" + }, + { + "domain": "default", + "utterance": "Did my order get delayed?", + "intent": "delay" + }, + { + "domain": "default", + "utterance": "Why is my order not delivered yet?", + "intent": "delay" + }, + { + "domain": "default", + "utterance": "When do I get my delivery?", + "intent": "delay" + }, + { + "domain": "default", + "utterance": "How long does delivery take?", + "intent": "delivery" + }, + { + "domain": "default", + "utterance": "How long does shipping take?", + "intent": "delivery" + }, + { + "domain": "default", + "utterance": "Please Tell me about my delivery", + "intent": "delivery" + }, + { + "domain": "default", + "utterance": "When do I get my delivery?", + "intent": "delivery" + }, + { + "domain": "default", + "utterance": "Why is my order not delivered yet", + "intent": "delivery" + }, + { + "domain": "default", + "utterance": "goodbye", + "intent": "greetings.bye" + }, + { + "domain": "default", + "utterance": "bye take care", + "intent": "greetings.bye" + }, + { + "domain": "default", + "utterance": "see you later", + "intent": "greetings.bye" + }, + { + "domain": "default", + "utterance": "bye for now", + "intent": "greetings.bye" + }, + { + "domain": "default", + "utterance": "i must go", + "intent": "greetings.bye" + }, + { + "domain": "default", + "utterance": "hello", + "intent": "greetings.hello" + }, + { + "domain": "default", + "utterance": "hi", + "intent": "greetings.hello" + }, + { + "domain": "default", + "utterance": "howdy", + "intent": "greetings.hello" + }, + { + "domain": "default", + "utterance": "Greetings", + "intent": "greetings.hello" + }, + { + "domain": "default", + "utterance": "Is anyone there?", + "intent": "greetings.hello" + }, + { + "domain": "default", + "utterance": "Hello", + "intent": "greetings.hello" + }, + { + "domain": "default", + "utterance": "Good day", + "intent": "greetings.hello" + }, + { + "domain": "default", + "utterance": "Which items do you have?", + "intent": "items" + }, + { + "domain": "default", + "utterance": "What kinds of items are there?", + "intent": "items" + }, + { + "domain": "default", + "utterance": "What do you sell?", + "intent": "items" + }, + { + "domain": "default", + "utterance": "What do you offer?", + "intent": "items" + }, + { + "domain": "default", + "utterance": "What can I buy?", + "intent": "items" + }, + { + "domain": "default", + "utterance": "I'm looking for...", + "intent": "items" + }, + { + "domain": "default", + "utterance": "Do you have any...", + "intent": "items" + }, + { + "domain": "default", + "utterance": "I'm interested in...", + "intent": "items" + }, + { + "domain": "default", + "utterance": "Can I see what you have in...", + "intent": "items" + }, + { + "domain": "default", + "utterance": "I want to buy a...", + "intent": "items" + }, + { + "domain": "default", + "utterance": "I'm looking for something like this...", + "intent": "items" + }, + { + "domain": "default", + "utterance": "What are your most popular items?", + "intent": "items" + }, + { + "domain": "default", + "utterance": "What are some of your best deals?", + "intent": "items" + }, + { + "domain": "default", + "utterance": "Do you have any new arrivals?", + "intent": "items" + }, + { + "domain": "default", + "utterance": "Need more help", + "intent": "more.contact" + }, + { + "domain": "default", + "utterance": "Help me more", + "intent": "more.contact" + }, + { + "domain": "default", + "utterance": "can I talk to an agent", + "intent": "more.help" + }, + { + "domain": "default", + "utterance": "can I call customer service", + "intent": "more.help" + }, + { + "domain": "default", + "utterance": "customer support number", + "intent": "more.help" + }, + { + "domain": "default", + "utterance": "how to contact customer service", + "intent": "more.help" + }, + { + "domain": "default", + "utterance": "customer service number", + "intent": "more.help" + }, + { + "domain": "default", + "utterance": "contact number for help", + "intent": "more.help" + }, + { + "domain": "default", + "utterance": "helpline number", + "intent": "more.help" + }, + { + "domain": "default", + "utterance": "How to become a seller", + "intent": "more.help" + }, + { + "domain": "default", + "utterance": "How to contact a seller", + "intent": "more.help" + }, + { + "domain": "default", + "utterance": "What is my order status", + "intent": "order" + }, + { + "domain": "default", + "utterance": "I want to know my return status", + "intent": "order" + }, + { + "domain": "default", + "utterance": "How to return status", + "intent": "order" + }, + { + "domain": "default", + "utterance": "Do you take credit cards?", + "intent": "payments" + }, + { + "domain": "default", + "utterance": "Do you accept Mastercard?", + "intent": "payments" + }, + { + "domain": "default", + "utterance": "Can I pay with Cash?", + "intent": "payments" + }, + { + "domain": "default", + "utterance": "Are you cash only?", + "intent": "payments" + }, + { + "domain": "default", + "utterance": "What are your payment methods?", + "intent": "payments" + }, + { + "domain": "default", + "utterance": "How do I pay?", + "intent": "payments" + }, + { + "domain": "default", + "utterance": "How are you?", + "intent": "personal" + }, + { + "domain": "default", + "utterance": "How are you doing?", + "intent": "personal" + }, + { + "domain": "default", + "utterance": "How is your day?", + "intent": "personal" + }, + { + "domain": "default", + "utterance": "How can I change my profile information", + "intent": "profile.info" + }, + { + "domain": "default", + "utterance": "I want to change my password", + "intent": "profile.info" + }, + { + "domain": "default", + "utterance": "I want to change my phone number", + "intent": "profile.info" + }, + { + "domain": "default", + "utterance": "I want to change my address", + "intent": "profile.info" + }, + { + "domain": "default", + "utterance": "I want to Reset my password", + "intent": "profile.info" + }, + { + "domain": "default", + "utterance": "I want to delete my account", + "intent": "profile.info" + }, + { + "domain": "default", + "utterance": "delete my account", + "intent": "profile.info" + }, + { + "domain": "default", + "utterance": "Common reasons for delivery delay", + "intent": "reason" + }, + { + "domain": "default", + "utterance": "common reasons for delivery delay", + "intent": "reason" + }, + { + "domain": "default", + "utterance": "reasons for delay", + "intent": "reason" + }, + { + "domain": "default", + "utterance": "delivery delay", + "intent": "reason" + }, + { + "domain": "default", + "utterance": "Can I refund an item.", + "intent": "refund.demanding" + }, + { + "domain": "default", + "utterance": "I want to refund an item", + "intent": "refund.demanding" + }, + { + "domain": "default", + "utterance": "can I refund my order", + "intent": "refund.demanding" + }, + { + "domain": "default", + "utterance": "Are refunds available", + "intent": "refund.demanding" + }, + { + "domain": "default", + "utterance": "Why is the status Refunded when it's not credited?", + "intent": "refund.status" + }, + { + "domain": "default", + "utterance": "No refund even though status is refunded", + "intent": "refund.status" + }, + { + "domain": "default", + "utterance": "No refund when status says refunded", + "intent": "refund.status" + }, + { + "domain": "default", + "utterance": "I did not receive my refund money", + "intent": "refund.status" + }, + { + "domain": "default", + "utterance": "Refund money not received", + "intent": "refund.status" + }, + { + "domain": "default", + "utterance": "Thanks", + "intent": "thanks" + }, + { + "domain": "default", + "utterance": "Thank you", + "intent": "thanks" + }, + { + "domain": "default", + "utterance": "That's helpful", + "intent": "thanks" + }, + { + "domain": "default", + "utterance": "Thank's a lot!", + "intent": "thanks" + }, + { + "domain": "default", + "utterance": "thx", + "intent": "thanks" + }, + { + "domain": "default", + "utterance": "thnks", + "intent": "thanks" + }, + { + "domain": "default", + "utterance": "How can I track my order", + "intent": "track" + }, + { + "domain": "default", + "utterance": "I want to track my order", + "intent": "track" + }, + { + "domain": "default", + "utterance": "Can I track my order", + "intent": "track" + }, + { + "domain": "default", + "utterance": "Track order", + "intent": "track" + }, + { + "domain": "default", + "utterance": "I'm good", + "intent": "user.response" + }, + { + "domain": "default", + "utterance": "Im good", + "intent": "user.response" + }, + { + "domain": "default", + "utterance": "Im doing good", + "intent": "user.response" + }, + { + "domain": "default", + "utterance": "I am good", + "intent": "user.response" + }, + { + "domain": "default", + "utterance": "I am okay", + "intent": "user.response" + }, + { + "domain": "default", + "utterance": "How to use a voucher?", + "intent": "voucher" + }, + { + "domain": "default", + "utterance": "Can I use a voucher?", + "intent": "voucher" + }, + { + "domain": "default", + "utterance": "How to use a voucher?", + "intent": "voucher" + } + ], + "domains": { + "master_domain": { + "settings": { + "locale": "en", + "tag": "nlu-en", + "keepStopwords": true, + "nonefeatureValue": 1, + "nonedeltaMultiplier": 1.2, + "spellCheck": false, + "spellCheckDistance": 1, + "filterZeros": true, + "log": true + }, + "features": { + "can": 1, + "i": 1, + "cancel": 1, + "my": 1, + "order": 1, + "whi": 1, + "is": 1, + "how": 1, + "do": 1, + "an": 1, + "payment": 1, + "was": 1, + "process": 1, + "success": 1, + "but": 1, + "did": 1, + "not": 1, + "get": 1, + "ani": 1, + "confirm": 1, + "what": 1, + "should": 1, + "complet": 1, + "no": 1, + "money": 1, + "deduct": 1, + "delay": 1, + "deliv": 1, + "yet": 1, + "when": 1, + "deliveri": 1, + "long": 1, + "doe": 1, + "take": 1, + "ship": 1, + "pleas": 1, + "tell": 1, + "me": 1, + "about": 1, + "goodby": 1, + "bye": 1, + "care": 1, + "see": 1, + "you": 1, + "later": 1, + "for": 1, + "now": 1, + "must": 1, + "go": 1, + "hello": 1, + "hi": 1, + "howdi": 1, + "greet": 1, + "anyon": 1, + "there": 1, + "good": 1, + "day": 1, + "which": 1, + "item": 1, + "have": 1, + "kind": 1, + "of": 1, + "are": 1, + "sell": 1, + "offer": 1, + "buy": 1, + "am": 1, + "look": 1, + "interest": 1, + "in": 1, + "want": 1, + "to": 1, + "a": 1, + "someth": 1, + "like": 1, + "this": 1, + "your": 1, + "most": 1, + "popular": 1, + "some": 1, + "best": 1, + "deal": 1, + "new": 1, + "arriv": 1, + "need": 1, + "more": 1, + "help": 1, + "talk": 1, + "agent": 1, + "call": 1, + "custom": 1, + "servic": 1, + "support": 1, + "number": 1, + "contact": 1, + "helplin": 1, + "becom": 1, + "seller": 1, + "status": 1, + "know": 1, + "return": 1, + "credit": 1, + "card": 1, + "accept": 1, + "mastercard": 1, + "pay": 1, + "with": 1, + "cash": 1, + "onli": 1, + "method": 1, + "chang": 1, + "profil": 1, + "inform": 1, + "password": 1, + "phone": 1, + "address": 1, + "reset": 1, + "delet": 1, + "account": 1, + "common": 1, + "reason": 1, + "refund": 1, + "avail": 1, + "the": 1, + "it": 1, + "even": 1, + "though": 1, + "say": 1, + "receiv": 1, + "thank": 1, + "that": 1, + "lot": 1, + "thx": 1, + "thnks": 1, + "track": 1, + "im": 1, + "okay": 1, + "use": 1, + "voucher": 1 + }, + "intents": { + "cancel": 1, + "confirm": 1, + "delay": 1, + "delivery": 1, + "greetings.bye": 1, + "greetings.hello": 1, + "items": 1, + "more.contact": 1, + "more.help": 1, + "order": 1, + "payments": 1, + "personal": 1, + "profile.info": 1, + "reason": 1, + "refund.demanding": 1, + "refund.status": 1, + "thanks": 1, + "track": 1, + "user.response": 1, + "voucher": 1 + }, + "intentFeatures": { + "cancel": { + "can": 1, + "i": 1, + "cancel": 1, + "my": 1, + "order": 1, + "whi": 1, + "is": 1, + "how": 1, + "do": 1, + "an": 1 + }, + "confirm": { + "my": 1, + "payment": 1, + "was": 1, + "process": 1, + "success": 1, + "but": 1, + "i": 1, + "did": 1, + "not": 1, + "get": 1, + "ani": 1, + "order": 1, + "confirm": 1, + "what": 1, + "should": 1, + "do": 1, + "complet": 1, + "no": 1, + "money": 1, + "deduct": 1, + "is": 1 + }, + "delay": { + "whi": 1, + "is": 1, + "my": 1, + "order": 1, + "delay": 1, + "did": 1, + "get": 1, + "not": 1, + "deliv": 1, + "yet": 1, + "when": 1, + "do": 1, + "i": 1, + "deliveri": 1 + }, + "delivery": { + "how": 1, + "long": 1, + "doe": 1, + "deliveri": 1, + "take": 1, + "ship": 1, + "pleas": 1, + "tell": 1, + "me": 1, + "about": 1, + "my": 1, + "when": 1, + "do": 1, + "i": 1, + "get": 1, + "whi": 1, + "is": 1, + "order": 1, + "not": 1, + "deliv": 1, + "yet": 1 + }, + "greetings.bye": { + "goodby": 1, + "bye": 1, + "take": 1, + "care": 1, + "see": 1, + "you": 1, + "later": 1, + "for": 1, + "now": 1, + "i": 1, + "must": 1, + "go": 1 + }, + "greetings.hello": { + "hello": 1, + "hi": 1, + "howdi": 1, + "greet": 1, + "is": 1, + "anyon": 1, + "there": 1, + "good": 1, + "day": 1 + }, + "items": { + "which": 1, + "item": 1, + "do": 1, + "you": 1, + "have": 1, + "what": 1, + "kind": 1, + "of": 1, + "are": 1, + "there": 1, + "sell": 1, + "offer": 1, + "can": 1, + "i": 1, + "buy": 1, + "am": 1, + "look": 1, + "for": 1, + "ani": 1, + "interest": 1, + "in": 1, + "see": 1, + "want": 1, + "to": 1, + "a": 1, + "someth": 1, + "like": 1, + "this": 1, + "your": 1, + "most": 1, + "popular": 1, + "some": 1, + "best": 1, + "deal": 1, + "new": 1, + "arriv": 1 + }, + "more.contact": { + "need": 1, + "more": 1, + "help": 1, + "me": 1 + }, + "more.help": { + "can": 1, + "i": 1, + "talk": 1, + "to": 1, + "an": 1, + "agent": 1, + "call": 1, + "custom": 1, + "servic": 1, + "support": 1, + "number": 1, + "how": 1, + "contact": 1, + "for": 1, + "help": 1, + "helplin": 1, + "becom": 1, + "a": 1, + "seller": 1 + }, + "order": { + "what": 1, + "is": 1, + "my": 1, + "order": 1, + "status": 1, + "i": 1, + "want": 1, + "to": 1, + "know": 1, + "return": 1, + "how": 1 + }, + "payments": { + "do": 1, + "you": 1, + "take": 1, + "credit": 1, + "card": 1, + "accept": 1, + "mastercard": 1, + "can": 1, + "i": 1, + "pay": 1, + "with": 1, + "cash": 1, + "are": 1, + "onli": 1, + "what": 1, + "your": 1, + "payment": 1, + "method": 1, + "how": 1 + }, + "personal": { + "how": 1, + "are": 1, + "you": 1, + "do": 1, + "is": 1, + "your": 1, + "day": 1 + }, + "profile.info": { + "how": 1, + "can": 1, + "i": 1, + "chang": 1, + "my": 1, + "profil": 1, + "inform": 1, + "want": 1, + "to": 1, + "password": 1, + "phone": 1, + "number": 1, + "address": 1, + "reset": 1, + "delet": 1, + "account": 1 + }, + "reason": { + "common": 1, + "reason": 1, + "for": 1, + "deliveri": 1, + "delay": 1 + }, + "refund.demanding": { + "can": 1, + "i": 1, + "refund": 1, + "an": 1, + "item": 1, + "want": 1, + "to": 1, + "my": 1, + "order": 1, + "are": 1, + "avail": 1 + }, + "refund.status": { + "whi": 1, + "is": 1, + "the": 1, + "status": 1, + "refund": 1, + "when": 1, + "it": 1, + "not": 1, + "credit": 1, + "no": 1, + "even": 1, + "though": 1, + "say": 1, + "i": 1, + "did": 1, + "receiv": 1, + "my": 1, + "money": 1 + }, + "thanks": { + "thank": 1, + "you": 1, + "that": 1, + "is": 1, + "help": 1, + "a": 1, + "lot": 1, + "thx": 1, + "thnks": 1 + }, + "track": { + "how": 1, + "can": 1, + "i": 1, + "track": 1, + "my": 1, + "order": 1, + "want": 1, + "to": 1 + }, + "user.response": { + "i": 1, + "am": 1, + "good": 1, + "im": 1, + "do": 1, + "okay": 1 + }, + "voucher": { + "how": 1, + "to": 1, + "use": 1, + "a": 1, + "voucher": 1, + "can": 1, + "i": 1 + } + }, + "featuresToIntent": { + "can": [ + "cancel", + "items", + "more.help", + "payments", + "profile.info", + "refund.demanding", + "track", + "voucher" + ], + "i": [ + "cancel", + "confirm", + "delay", + "delivery", + "greetings.bye", + "items", + "more.help", + "order", + "payments", + "profile.info", + "refund.demanding", + "refund.status", + "track", + "user.response", + "voucher" + ], + "cancel": [ + "cancel" + ], + "my": [ + "cancel", + "confirm", + "delay", + "delivery", + "order", + "profile.info", + "refund.demanding", + "refund.status", + "track" + ], + "order": [ + "cancel", + "confirm", + "delay", + "delivery", + "order", + "refund.demanding", + "track" + ], + "whi": [ + "cancel", + "delay", + "delivery", + "refund.status" + ], + "is": [ + "cancel", + "confirm", + "delay", + "delivery", + "greetings.hello", + "order", + "personal", + "refund.status", + "thanks" + ], + "how": [ + "cancel", + "delivery", + "more.help", + "order", + "payments", + "personal", + "profile.info", + "track", + "voucher" + ], + "do": [ + "cancel", + "confirm", + "delay", + "delivery", + "items", + "payments", + "personal", + "user.response" + ], + "an": [ + "cancel", + "more.help", + "refund.demanding" + ], + "payment": [ + "confirm", + "payments" + ], + "was": [ + "confirm" + ], + "process": [ + "confirm" + ], + "success": [ + "confirm" + ], + "but": [ + "confirm" + ], + "did": [ + "confirm", + "delay", + "refund.status" + ], + "not": [ + "confirm", + "delay", + "delivery", + "refund.status" + ], + "get": [ + "confirm", + "delay", + "delivery" + ], + "ani": [ + "confirm", + "items" + ], + "confirm": [ + "confirm" + ], + "what": [ + "confirm", + "items", + "order", + "payments" + ], + "should": [ + "confirm" + ], + "complet": [ + "confirm" + ], + "no": [ + "confirm", + "refund.status" + ], + "money": [ + "confirm", + "refund.status" + ], + "deduct": [ + "confirm" + ], + "delay": [ + "delay", + "reason" + ], + "deliv": [ + "delay", + "delivery" + ], + "yet": [ + "delay", + "delivery" + ], + "when": [ + "delay", + "delivery", + "refund.status" + ], + "deliveri": [ + "delay", + "delivery", + "reason" + ], + "long": [ + "delivery" + ], + "doe": [ + "delivery" + ], + "take": [ + "delivery", + "greetings.bye", + "payments" + ], + "ship": [ + "delivery" + ], + "pleas": [ + "delivery" + ], + "tell": [ + "delivery" + ], + "me": [ + "delivery", + "more.contact" + ], + "about": [ + "delivery" + ], + "goodby": [ + "greetings.bye" + ], + "bye": [ + "greetings.bye" + ], + "care": [ + "greetings.bye" + ], + "see": [ + "greetings.bye", + "items" + ], + "you": [ + "greetings.bye", + "items", + "payments", + "personal", + "thanks" + ], + "later": [ + "greetings.bye" + ], + "for": [ + "greetings.bye", + "items", + "more.help", + "reason" + ], + "now": [ + "greetings.bye" + ], + "must": [ + "greetings.bye" + ], + "go": [ + "greetings.bye" + ], + "hello": [ + "greetings.hello" + ], + "hi": [ + "greetings.hello" + ], + "howdi": [ + "greetings.hello" + ], + "greet": [ + "greetings.hello" + ], + "anyon": [ + "greetings.hello" + ], + "there": [ + "greetings.hello", + "items" + ], + "good": [ + "greetings.hello", + "user.response" + ], + "day": [ + "greetings.hello", + "personal" + ], + "which": [ + "items" + ], + "item": [ + "items", + "refund.demanding" + ], + "have": [ + "items" + ], + "kind": [ + "items" + ], + "of": [ + "items" + ], + "are": [ + "items", + "payments", + "personal", + "refund.demanding" + ], + "sell": [ + "items" + ], + "offer": [ + "items" + ], + "buy": [ + "items" + ], + "am": [ + "items", + "user.response" + ], + "look": [ + "items" + ], + "interest": [ + "items" + ], + "in": [ + "items" + ], + "want": [ + "items", + "order", + "profile.info", + "refund.demanding", + "track" + ], + "to": [ + "items", + "more.help", + "order", + "profile.info", + "refund.demanding", + "track", + "voucher" + ], + "a": [ + "items", + "more.help", + "thanks", + "voucher" + ], + "someth": [ + "items" + ], + "like": [ + "items" + ], + "this": [ + "items" + ], + "your": [ + "items", + "payments", + "personal" + ], + "most": [ + "items" + ], + "popular": [ + "items" + ], + "some": [ + "items" + ], + "best": [ + "items" + ], + "deal": [ + "items" + ], + "new": [ + "items" + ], + "arriv": [ + "items" + ], + "need": [ + "more.contact" + ], + "more": [ + "more.contact" + ], + "help": [ + "more.contact", + "more.help", + "thanks" + ], + "talk": [ + "more.help" + ], + "agent": [ + "more.help" + ], + "call": [ + "more.help" + ], + "custom": [ + "more.help" + ], + "servic": [ + "more.help" + ], + "support": [ + "more.help" + ], + "number": [ + "more.help", + "profile.info" + ], + "contact": [ + "more.help" + ], + "helplin": [ + "more.help" + ], + "becom": [ + "more.help" + ], + "seller": [ + "more.help" + ], + "status": [ + "order", + "refund.status" + ], + "know": [ + "order" + ], + "return": [ + "order" + ], + "credit": [ + "payments", + "refund.status" + ], + "card": [ + "payments" + ], + "accept": [ + "payments" + ], + "mastercard": [ + "payments" + ], + "pay": [ + "payments" + ], + "with": [ + "payments" + ], + "cash": [ + "payments" + ], + "onli": [ + "payments" + ], + "method": [ + "payments" + ], + "chang": [ + "profile.info" + ], + "profil": [ + "profile.info" + ], + "inform": [ + "profile.info" + ], + "password": [ + "profile.info" + ], + "phone": [ + "profile.info" + ], + "address": [ + "profile.info" + ], + "reset": [ + "profile.info" + ], + "delet": [ + "profile.info" + ], + "account": [ + "profile.info" + ], + "common": [ + "reason" + ], + "reason": [ + "reason" + ], + "refund": [ + "refund.demanding", + "refund.status" + ], + "avail": [ + "refund.demanding" + ], + "the": [ + "refund.status" + ], + "it": [ + "refund.status" + ], + "even": [ + "refund.status" + ], + "though": [ + "refund.status" + ], + "say": [ + "refund.status" + ], + "receiv": [ + "refund.status" + ], + "thank": [ + "thanks" + ], + "that": [ + "thanks" + ], + "lot": [ + "thanks" + ], + "thx": [ + "thanks" + ], + "thnks": [ + "thanks" + ], + "track": [ + "track" + ], + "im": [ + "user.response" + ], + "okay": [ + "user.response" + ], + "use": [ + "voucher" + ], + "voucher": [ + "voucher" + ] + }, + "neuralNetwork": { + "settings": { + "locale": "en", + "tag": "nlu-en", + "keepStopwords": true, + "nonefeatureValue": 1, + "nonedeltaMultiplier": 1.2, + "spellCheck": false, + "spellCheckDistance": 1, + "filterZeros": true, + "log": true + }, + "features": [ + "can", + "i", + "cancel", + "my", + "order", + "whi", + "is", + "how", + "do", + "an", + "payment", + "was", + "process", + "success", + "but", + "did", + "not", + "get", + "ani", + "confirm", + "what", + "should", + "complet", + "no", + "money", + "deduct", + "delay", + "deliv", + "yet", + "when", + "deliveri", + "long", + "doe", + "take", + "ship", + "pleas", + "tell", + "me", + "about", + "goodby", + "bye", + "care", + "see", + "you", + "later", + "for", + "now", + "must", + "go", + "hello", + "hi", + "howdi", + "greet", + "anyon", + "there", + "good", + "day", + "which", + "item", + "have", + "kind", + "of", + "are", + "sell", + "offer", + "buy", + "am", + "look", + "interest", + "in", + "want", + "to", + "a", + "someth", + "like", + "this", + "your", + "most", + "popular", + "some", + "best", + "deal", + "new", + "arriv", + "need", + "more", + "help", + "talk", + "agent", + "call", + "custom", + "servic", + "support", + "number", + "contact", + "helplin", + "becom", + "seller", + "status", + "know", + "return", + "credit", + "card", + "accept", + "mastercard", + "pay", + "with", + "cash", + "onli", + "method", + "chang", + "profil", + "inform", + "password", + "phone", + "address", + "reset", + "delet", + "account", + "common", + "reason", + "refund", + "avail", + "the", + "it", + "even", + "though", + "say", + "receiv", + "thank", + "that", + "lot", + "thx", + "thnks", + "track", + "im", + "okay", + "use", + "voucher" + ], + "intents": [ + "cancel", + "confirm", + "delay", + "delivery", + "greetings.bye", + "greetings.hello", + "items", + "more.contact", + "more.help", + "order", + "payments", + "personal", + "profile.info", + "reason", + "refund.demanding", + "refund.status", + "thanks", + "track", + "user.response", + "voucher" + ], + "perceptrons": [ + [ + 1.0137546062469482, + -0.5474245548248291, + 11.848198890686035, + 0.4021919369697571, + 2.41121768951416, + 0.7352370619773865, + -0.11675096303224564, + -0.531909167766571, + 0.6789054870605469, + 0.5686358213424683, + -0.43452420830726624, + -0.7262206673622131, + -0.287818044424057, + -0.287818044424057, + -0.7318246364593506, + -0.9134994149208069, + -1.9920411109924316, + -1.386992335319519, + -0.287818044424057, + -1.0195549726486206, + -1.6521244049072266, + -0.287818044424057, + -0.002797139110043645, + -0.002797139110043645, + -0.18727074563503265, + -0.18727074563503265, + -2.59305739402771, + -0.8779183626174927, + -0.8779183626174927, + -0.33857086300849915, + -0.5329172015190125, + -0.14780335128307343, + -0.14780335128307343, + -0.1599469631910324, + -0.0766788199543953, + -0.0694824680685997, + -0.0694824680685997, + -0.0694824680685997, + -0.0694824680685997, + -0.0069275954738259315, + -0.010616364888846874, + -0.003003086894750595, + -0.16376909613609314, + -0.31498128175735474, + -0.006510923616588116, + -0.13119056820869446, + -0.006111734081059694, + -0.11673133820295334, + -0.11673133820295334, + -0.008423068560659885, + -0.0037213850300759077, + -0.0035652429796755314, + -0.0034156523179262877, + -0.012222626246511936, + -0.012222626246511936, + -0.02360852062702179, + -0.015696197748184204, + -0.0112577760592103, + -0.2867816090583801, + -0.17207609117031097, + 0, + 0, + -0.050361473113298416, + 0, + 0, + -0.3207390010356903, + -0.21179741621017456, + -0.11934908479452133, + -0.060127075761556625, + -0.24743472039699554, + -0.027676893398165703, + -0.5639355182647705, + -0.049367498606443405, + -0.015957951545715332, + -0.015957951545715332, + -0.015957951545715332, + -0.011720363982021809, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + -0.4059183895587921, + -0.4059183895587921, + -0.2879806160926819, + -0.3107443153858185, + -0.3107443153858185, + 0, + 0, + -0.020473212003707886, + 0, + -0.011515682563185692, + -0.025052422657608986, + -0.5015026330947876, + -0.0068354010581970215, + -0.017309928312897682, + 0, + 0, + 0, + 0, + -0.38837194442749023, + -0.22747227549552917, + -0.22747227549552917, + 0, + 0, + -0.48755571246147156, + -0.48755571246147156, + -0.48755571246147156, + 0, + 0, + 0, + 0, + -0.005079725757241249, + -0.005079725757241249, + 0, + 0, + -2.282877206802368, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + -2.269744634628296, + -0.018390238285064697, + 0, + 0, + 0, + -0.9896306948005769 + ], + [ + -0.3291322588920593, + -0.7270809412002563, + -0.16681955754756927, + -1.7737600803375244, + 1.892374873161316, + -1.1385151147842407, + -0.004768540151417255, + -0.19638314843177795, + -0.29744264483451843, + -0.18516650795936584, + 0.7383124828338623, + 4.340796947479248, + 0.2160470336675644, + 0.2160470336675644, + 2.533582925796509, + -0.36327800154685974, + 2.891287326812744, + -0.25634461641311646, + 0.09664863348007202, + 5.821985244750977, + -0.5967516303062439, + 0.2160470336675644, + 0.8364499807357788, + 0.6777428388595581, + 0.332731157541275, + 1.4811530113220215, + -0.5748860836029053, + -0.7142410278320312, + -0.7142410278320312, + -0.22726833820343018, + -0.18083734810352325, + -0.10491679608821869, + -0.10491679608821869, + -0.2062942087650299, + -0.036959417164325714, + -0.01146351732313633, + -0.01146351732313633, + -0.029890920966863632, + -0.01146351732313633, + -0.0765857994556427, + -0.1386977881193161, + -0.056668512523174286, + -0.061468705534935, + -0.4443903863430023, + -0.061468705534935, + -0.15229962766170502, + -0.057634614408016205, + -0.07866756618022919, + -0.07866756618022919, + -0.1323222666978836, + -0.0604797825217247, + -0.05799480155110359, + -0.055611882358789444, + -0.05049731209874153, + -0.10385537892580032, + -0.04838823154568672, + -0.04838823154568672, + -0.05117446184158325, + -0.1468285769224167, + -0.1599263697862625, + -0.03525151312351227, + -0.06675173342227936, + -0.3849853277206421, + -0.06461493670940399, + -0.05490032583475113, + -0.11226759105920792, + -0.11075426638126373, + -0.059033848345279694, + -0.03388472646474838, + -0.03388472646474838, + -0.03679174557328224, + -0.08090842515230179, + -0.03815352916717529, + -0.01093367300927639, + -0.01093367300927639, + -0.01093367300927639, + -0.32491371035575867, + -0.021362071856856346, + -0.021362071856856346, + -0.01930716447532177, + -0.01930716447532177, + -0.01930716447532177, + -0.02836797386407852, + -0.02836797386407852, + -0.023965725675225258, + -0.04677771031856537, + -0.06024671345949173, + -0.012021305970847607, + -0.012021305970847607, + -0.009854178875684738, + -0.06307084113359451, + -0.03795885667204857, + -0.013223793357610703, + -0.0717272013425827, + -0.0205734483897686, + -0.017550142481923103, + -0.0036168168298900127, + -0.006382063962519169, + -0.2861216962337494, + 0, + -0.0005231029936112463, + -0.038572512567043304, + 0, + 0, + 0, + -0.019087273627519608, + -0.00501098670065403, + -0.00501098670065403, + 0, + -0.26335811614990234, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + -1.4128700494766235, + -0.005007071420550346, + -0.038572512567043304, + -0.038572512567043304, + -0.04391763359308243, + -0.04391763359308243, + -0.062341995537281036, + -1.1483964920043945, + -0.0012407215544953942, + 0, + 0, + -0.001188663300126791, + -0.0011387893464416265, + -1.2280935049057007, + 0, + 0, + 0, + 0, + -0.6626121500146844 + ], + [ + -1.2319387197494507, + -0.21801558136940002, + -7.187047004699707, + 3.2004880905151367, + 1.9386931657791138, + 3.482285976409912, + 1.7419606447219849, + -0.49997925758361816, + 2.0536139011383057, + -0.010287845507264137, + -0.7569913864135742, + -0.7583239674568176, + -0.7569913864135742, + -0.7569913864135742, + -0.7569913864135742, + 1.2748510837554932, + -1.373896598815918, + 4.428487300872803, + -0.7913482785224915, + -0.7583239674568176, + -2.4707539081573486, + -0.7569913864135742, + 0, + 0, + -0.19782285392284393, + 0, + 6.592715263366699, + -0.01434242632240057, + -0.01434242632240057, + 2.600883960723877, + -3.6173911094665527, + -0.23327256739139557, + -0.23327256739139557, + -0.2857179045677185, + -0.04939230531454086, + -0.6831585168838501, + -0.6831585168838501, + -0.6831585168838501, + -0.6831585168838501, + -0.04469833895564079, + -0.05349443107843399, + -0.019467800855636597, + -0.02620924822986126, + -0.21258683502674103, + -0.02620924822986126, + -1.9369317293167114, + -0.024292727932333946, + -0.04075866937637329, + -0.04075866937637329, + -0.06484147161245346, + -0.03467138111591339, + -0.03325444832444191, + -0.031895387917757034, + -0.13987219333648682, + -0.16497263312339783, + -0.02490754798054695, + -0.02490754798054695, + -0.031334955245256424, + -0.07237178832292557, + -0.07801632583141327, + -0.011419232003390789, + -0.01838001422584057, + -0.021844979375600815, + -0.024473845958709717, + -0.017832733690738678, + -0.04915530979633331, + -0.07258743047714233, + -0.040489088743925095, + -0.017853913828730583, + -0.017853913828730583, + -0.7192970514297485, + -0.7398727536201477, + -0.015225691720843315, + -0.008584280498325825, + -0.008584280498325825, + -0.008584280498325825, + -0.0018613251158967614, + -0.0004067723057232797, + -0.0004067723057232797, + -0.0012511666864156723, + -0.0012511666864156723, + -0.0012511666864156723, + -0.005174988880753517, + -0.005174988880753517, + -0.012096227146685123, + -0.012096227146685123, + -0.016754278913140297, + -0.010287845507264137, + -0.010287845507264137, + -0.008589562959969044, + -0.029535546898841858, + -0.017882922664284706, + -0.006336822174489498, + -0.046847932040691376, + -0.0018024882301688194, + -0.005290467292070389, + 0, + 0, + -1.9490653276443481, + -0.061430059373378754, + -0.061430059373378754, + -0.2684548497200012, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + -0.20504964888095856, + -0.07669688761234283, + -0.07669688761234283, + -0.08714839071035385, + -0.014107950031757355, + -0.003805781714618206, + -0.012716507539153099, + -0.5159547328948975, + -0.5159547328948975, + -0.6215592622756958, + -1.8250330686569214, + -1.2078466415405273, + 0, + -0.2684548497200012, + -0.2684548497200012, + 0, + 0, + 0, + -0.19782285392284393, + 0, + 0, + 0, + 0, + 0, + -0.9307127594947815, + 0, + 0, + 0, + 0, + -2.8474070106071645 + ], + [ + -1.4451781511306763, + -1.0388836860656738, + -1.4264543056488037, + 2.5314676761627197, + -0.6839140057563782, + 0.7223742604255676, + -0.3522108495235443, + 1.6454493999481201, + 0.5822571516036987, + -0.03301115334033966, + -0.31011736392974854, + -0.31032857298851013, + -0.31011736392974854, + -0.31011736392974854, + -0.32225391268730164, + -0.8661167025566101, + 1.6880625486373901, + 1.8911114931106567, + -0.43638893961906433, + -0.32240062952041626, + -1.270650029182434, + -0.31011736392974854, + 0, + 0, + -0.354684978723526, + -0.006027175113558769, + -2.6551761627197266, + 2.678882122039795, + 2.678882122039795, + 2.1090545654296875, + 3.617849349975586, + 4.080210208892822, + 4.080210208892822, + 2.783381223678589, + 3.421110153198242, + 2.7887074947357178, + 2.7887074947357178, + 2.250455617904663, + 2.7887074947357178, + -0.17000629007816315, + -1.0011149644851685, + -0.852727472782135, + -0.12743763625621796, + -1.3179848194122314, + -0.08253855258226395, + -1.0258104801177979, + -0.07425964623689651, + -0.09686865657567978, + -0.09686865657567978, + -0.28232651948928833, + -0.13063865900039673, + -0.12527960538864136, + -0.12014026939868927, + -0.14789435267448425, + -0.23303095996379852, + -0.09936640411615372, + -0.4535828232765198, + -0.07006469368934631, + -0.19244204461574554, + -0.23298411071300507, + -0.04336852952837944, + -0.07129500061273575, + -0.3177240490913391, + -0.0663289874792099, + -0.05570872128009796, + -0.14094312489032745, + -0.23260855674743652, + -0.1286415457725525, + -0.05846906080842018, + -0.09133340418338776, + -0.5046393275260925, + -1.0630557537078857, + -0.34214919805526733, + -0.031061027199029922, + -0.031061027199029922, + -0.031061027199029922, + -0.3316175937652588, + -0.004776031244546175, + -0.004776031244546175, + -0.006242210045456886, + -0.006242210045456886, + -0.006242210045456886, + -0.019280698150396347, + -0.019280698150396347, + -0.05666345730423927, + -0.6110260486602783, + -0.6137350797653198, + -0.03301115334033966, + -0.03301115334033966, + -0.02883484587073326, + -0.23428231477737427, + -0.16950009763240814, + -0.0407194085419178, + -0.15757475793361664, + -0.21978570520877838, + -0.03889086842536926, + -0.11133536696434021, + -0.24443987011909485, + -1.0306564569473267, + -0.10143721848726273, + -0.20791855454444885, + -0.7177991271018982, + -0.3758620321750641, + 0, + 0, + -0.23245687782764435, + 0, + 0, + 0, + 0, + -0.5588673949241638, + -0.39160844683647156, + -0.39160844683647156, + -0.11280962079763412, + -0.01937008835375309, + -0.006116317119449377, + -0.02907666377723217, + -0.7320131063461304, + -0.7320131063461304, + -0.6825774312019348, + -0.6825774312019348, + -0.803561806678772, + 0, + -0.29712578654289246, + -0.29712578654289246, + 0, + 0, + 0, + -0.3424622714519501, + 0, + 0, + 0, + 0, + 0, + -0.40630239248275757, + 0, + 0, + 0, + 0, + -1.0805806744340831 + ], + [ + -2.1148478984832764, + 0.7997274994850159, + 0, + -0.8561583161354065, + -0.9215051531791687, + -0.006411668844521046, + -0.6620265245437622, + -1.2655911445617676, + -2.195646286010742, + -0.1171831414103508, + -0.22510455548763275, + -0.32071661949157715, + 0, + 0, + -0.22510455548763275, + -0.1251218467950821, + -0.439662367105484, + 0, + -0.23576650023460388, + -0.32071661949157715, + -2.3462860584259033, + 0, + -0.22510455548763275, + -0.4320996403694153, + -0.3351232707500458, + 0, + -1.3317266702651978, + 0, + 0, + -0.18428704142570496, + -0.7287682294845581, + -0.8036311864852905, + -0.8036311864852905, + 1.5954744815826416, + -0.4747324287891388, + 0, + 0, + -0.22864001989364624, + 0, + 12.440009117126465, + 7.522450923919678, + 3.414806365966797, + 5.261117458343506, + 0.5175203084945679, + 6.749449253082275, + 0.9009185433387756, + 4.107618808746338, + 5.865237712860107, + 5.865237712860107, + -1.7676278352737427, + -1.747532844543457, + -1.7450001239776611, + -1.7422480583190918, + -0.5406187176704407, + -0.6789042353630066, + -1.2395411729812622, + -0.739136278629303, + -0.18670904636383057, + -0.3578399419784546, + -2.0377581119537354, + -0.09841568022966385, + -0.12081167101860046, + -1.1265017986297607, + -0.22349581122398376, + -0.1958358734846115, + -0.5817500352859497, + -2.1600258350372314, + -1.4215301275253296, + -0.296112596988678, + -1.8322443962097168, + -0.6588131785392761, + -0.9206690192222595, + -0.4332333505153656, + -0.3052285313606262, + -0.3052285313606262, + -0.3052285313606262, + -0.02176925726234913, + 0, + 0, + -0.0050658793188631535, + -0.0050658793188631535, + -0.0050658793188631535, + -0.029020555317401886, + -0.029020555317401886, + -0.27671024203300476, + -0.533606231212616, + -1.04434072971344, + -0.1171831414103508, + -0.1171831414103508, + -0.13155850768089294, + -0.5340460538864136, + -0.3047577440738678, + -0.2095731496810913, + -1.2781013250350952, + -0.493476003408432, + -0.4744429588317871, + -0.0272076353430748, + -0.03713247552514076, + -0.35035839676856995, + -0.09408554434776306, + -0.12777751684188843, + -1.0055477619171143, + -0.9965164065361023, + -0.12271378934383392, + -0.12271378934383392, + -0.12974151968955994, + -0.12974151968955994, + -0.7104417681694031, + -0.5687134265899658, + 0, + -0.1271994262933731, + 0, + 0, + -0.11704851686954498, + 0, + -0.05229974165558815, + -0.04225518926978111, + -0.4482121765613556, + -0.4482121765613556, + -0.39033257961273193, + -1.3211712837219238, + -0.6426398158073425, + -0.08353639394044876, + -0.006411668844521046, + -0.006411668844521046, + -0.02102518081665039, + -0.02102518081665039, + -0.17535850405693054, + -0.3351232707500458, + -2.2644107341766357, + -0.04576003551483154, + 0, + -1.7263158559799194, + -1.72139310836792, + -0.8258929252624512, + -0.507616400718689, + -0.38756808638572693, + -0.00036949245259165764, + -0.00036949245259165764, + 1.7494741355394363 + ], + [ + -0.7732609510421753, + -4.034710884094238, + -0.16621102392673492, + -1.6832696199417114, + -2.2922849655151367, + -0.43388643860816956, + -1.0391435623168945, + -5.290063381195068, + -2.185589551925659, + -0.12314745038747787, + -0.5387074947357178, + -0.6298438906669617, + 0, + 0, + -0.5978250503540039, + 0, + -1.0414392948150635, + 0, + -0.31424424052238464, + -0.6889615654945374, + -2.4489927291870117, + 0, + -0.5387074947357178, + -1.0135843753814697, + -0.6875489354133606, + -0.04868263751268387, + -2.267845630645752, + 0, + 0, + -0.3452916443347931, + -1.842872977256775, + -0.07328616827726364, + -0.07328616827726364, + -1.2708898782730103, + -0.07328616827726364, + -0.01949692890048027, + -0.01949692890048027, + -0.638847827911377, + -0.01949692890048027, + -3.9341750144958496, + -1.613313913345337, + -1.1190882921218872, + -0.738145649433136, + -2.546537160873413, + -0.717369019985199, + -1.8951687812805176, + -0.4939974844455719, + -0.11324970424175262, + -0.11324970424175262, + 10.278204917907715, + 10.237865447998047, + 10.222160339355469, + 10.205157279968262, + 6.274806022644043, + 4.996850490570068, + 2.905292510986328, + 7.114895343780518, + -0.2540985643863678, + -1.758578896522522, + -0.7043660879135132, + -1.2804059982299805, + -1.3918099403381348, + -2.1373608112335205, + -0.1865386962890625, + -0.13705794513225555, + -0.44377002120018005, + -2.8761284351348877, + -0.4335267245769501, + -0.3696480691432953, + -0.3948061168193817, + -0.19673971831798553, + -1.0657670497894287, + -1.130800485610962, + -0.09097345173358917, + -0.09097345173358917, + -0.09097345173358917, + -4.737697601318359, + -0.07488127052783966, + -0.07488127052783966, + -0.07511845231056213, + -0.07511845231056213, + -0.07511845231056213, + -0.07152976095676422, + -0.07152976095676422, + -0.7411147356033325, + -1.3895823955535889, + -2.6020333766937256, + -0.12314745038747787, + -0.12314745038747787, + -0.10397472232580185, + -1.4408961534500122, + -0.725871205329895, + -0.6670722365379333, + -2.7231180667877197, + -0.3968248665332794, + -1.4507756233215332, + -0.20117171108722687, + -0.30739134550094604, + -1.123271107673645, + 0, + -0.1443576067686081, + -0.3079591393470764, + -0.05154714360833168, + -0.1468522697687149, + -0.1468522697687149, + -0.2189943641424179, + -0.2189943641424179, + -0.4288059175014496, + -0.17068028450012207, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + -1.2483890056610107, + -1.2483890056610107, + -0.32805249094963074, + -0.7630698680877686, + -1.8030940294265747, + -0.4283504784107208, + -0.21113920211791992, + -0.21113920211791992, + -0.34558773040771484, + -0.34558773040771484, + -0.08313897997140884, + -0.6284115314483643, + -4.005587577819824, + -1.0675207376480103, + -0.5681915879249573, + -3.989133358001709, + -3.9799017906188965, + -1.730439305305481, + -6.760979175567627, + 0, + 0, + 0, + 4.00835038226357 + ], + [ + -2.0729150772094727, + 1.2105480432510376, + 0, + -3.075517416000366, + -2.1280879974365234, + 0, + -1.4291672706604004, + -3.7731683254241943, + 3.750875234603882, + -3.4637320041656494, + -3.808359146118164, + -0.7863858938217163, + -0.7863858938217163, + -0.7863858938217163, + -0.7863858938217163, + -0.7872745394706726, + -0.7872745394706726, + -0.8203457593917847, + 4.417398929595947, + -0.7863858938217163, + 7.2096943855285645, + -0.7863858938217163, + 0, + 0, + -0.0004443310899659991, + 0, + -1.4016138315200806, + 0, + 0, + -0.0339190810918808, + -0.5746974349021912, + 0, + 0, + -1.8838376998901367, + 0, + 0, + 0, + -0.29276347160339355, + 0, + 0, + -1.3986157178878784, + 0, + -2.1145453453063965, + 1.3252555131912231, + -0.3257300853729248, + 3.266697406768799, + -1.3986157178878784, + -0.37427330017089844, + -0.37427330017089844, + 0, + 0, + 0, + 0, + 0, + 1.3228527307510376, + -4.476612567901611, + 0, + 1.6962932348251343, + 2.854417085647583, + 5.147310733795166, + 1.3228527307510376, + 2.871696710586548, + -0.8401740193367004, + 2.4716386795043945, + 2.4736406803131104, + 8.386700630187988, + 3.6803982257843018, + 6.546629428863525, + 5.792974948883057, + 4.042235851287842, + 1.9097124338150024, + 0.2351648062467575, + 2.9972054958343506, + 0.014523402787744999, + 0.014523402787744999, + 0.014523402787744999, + 0.8547791838645935, + 2.3277828693389893, + 2.3277828693389893, + 1.5485361814498901, + 1.5485361814498901, + 1.5485361814498901, + 0.06981951743364334, + 0.06981951743364334, + -0.3465847671031952, + -0.7826536297798157, + -1.29860520362854, + -0.6727563142776489, + -0.6727563142776489, + -0.7746207118034363, + -1.5045040845870972, + -1.1596300601959229, + -0.21376065909862518, + -1.2060894966125488, + -0.7638314962387085, + -0.2846128046512604, + -0.21201957762241364, + -0.380899578332901, + -1.7838538885116577, + -0.3531716465950012, + -0.4074578583240509, + -1.8838376998901367, + -1.8838376998901367, + -2.419515371322632, + -2.419515371322632, + -1.6732022762298584, + -0.5018000602722168, + -1.5228439569473267, + -0.8719638586044312, + -3.0196237564086914, + -0.23078517615795135, + 0, + 0, + -0.19459597766399384, + 0, + -0.06361047178506851, + -0.032615043222904205, + -0.06359861046075821, + -0.06359861046075821, + -0.5170090794563293, + -1.4016138315200806, + -2.525960922241211, + -0.01885991171002388, + 0, + 0, + 0, + 0, + 0, + -0.0004443310899659991, + -1.074087142944336, + 0, + -0.08746566623449326, + -0.0025967436376959085, + -0.002491012681275606, + 0, + -0.1930888444185257, + -4.403278350830078, + -0.8553791642189026, + -0.8553791642189026, + -0.4586347245196692 + ], + [ + -0.22140660881996155, + -0.2870965600013733, + 0, + -0.7846165299415588, + -0.15873953700065613, + -0.011281087063252926, + -2.0644774436950684, + -0.23955339193344116, + -0.19472968578338623, + -0.08523216098546982, + -0.10342646390199661, + -0.05132650211453438, + 0, + 0, + -0.025844065472483635, + -0.00017023066175170243, + -0.11176835745573044, + 0, + 0, + -0.05132650211453438, + -0.12461326271295547, + 0, + -0.025844065472483635, + -0.08902999758720398, + -0.06705322861671448, + 0, + -0.15028099715709686, + 0, + 0, + -0.0490955151617527, + -0.745243489742279, + -0.0033512338995933533, + -0.0033512338995933533, + -0.12648093700408936, + -0.0033512338995933533, + -0.6030545830726624, + -0.6030545830726624, + 3.2248520851135254, + -0.6030545830726624, + -0.12146465480327606, + -0.03870086744427681, + -0.03870086744427681, + 0, + -0.30469033122062683, + 0, + -1.3063645362854004, + 0, + 0, + 0, + -0.137643501162529, + -0.11902635544538498, + -0.1179807186126709, + -0.11715350300073624, + 0, + 0, + -0.1290770322084427, + -0.05281056836247444, + 0, + -0.002628645393997431, + 0, + 0, + 0, + -0.23906093835830688, + 0, + 0, + 0, + -0.0207328200340271, + 0, + 0, + 0, + -0.014595159329473972, + -0.27468565106391907, + -0.07625328004360199, + 0, + 0, + 0, + -0.095185287296772, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 3.235283613204956, + 7.06290864944458, + 3.877683401107788, + -0.06153886765241623, + -0.06153886765241623, + -0.05395161360502243, + -0.2852313220500946, + -0.17201417684555054, + -0.08172166347503662, + -1.455437421798706, + -1.2979955673217773, + -0.053663939237594604, + -0.051299452781677246, + -0.07426265627145767, + -0.18414506316184998, + -0.014595159329473972, + -0.038418881595134735, + -0.08742590248584747, + -0.06441590934991837, + -0.0632362961769104, + -0.0632362961769104, + -0.052749164402484894, + -0.03576807305216789, + -0.10305000096559525, + -0.0530945248901844, + -0.06034179404377937, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + -0.0791553482413292, + -0.0791553482413292, + -0.04793284460902214, + -0.06975246965885162, + -0.23104454576969147, + -0.0613517090678215, + -0.011281087063252926, + -0.011281087063252926, + -0.011696902103722095, + -0.011696902103722095, + -0.03297434374690056, + -0.06705322861671448, + -0.21499337255954742, + -1.9282985925674438, + -0.0005038426024839282, + -0.14002913236618042, + -0.13676577806472778, + -0.0662107840180397, + -0.08920516818761826, + -0.009708352386951447, + 0, + 0, + 0.11517137685731398 + ], + [ + 3.200519561767578, + -0.7589099407196045, + -1.5275399684906006, + -3.0995192527770996, + -2.00679612159729, + 0, + -1.429031491279602, + -0.012760818004608154, + -1.0567845106124878, + 0.7500213384628296, + -0.299394428730011, + -0.07060258835554123, + 0, + 0, + -0.0594392791390419, + 0, + -0.12525728344917297, + 0, + 0, + -0.07060258835554123, + -1.4175167083740234, + 0, + -0.0594392791390419, + -0.0651191994547844, + -0.11094554513692856, + 0, + -0.9104652404785156, + 0, + 0, + -0.00293157109990716, + -0.5573859810829163, + -0.08501628041267395, + -0.08501628041267395, + -0.3801807165145874, + -0.05955946817994118, + 0, + 0, + -0.552058219909668, + 0, + -0.3559640347957611, + -0.7074311971664429, + -0.023821957409381866, + -0.042731598019599915, + -1.234588623046875, + 0, + 1.0346851348876953, + -0.6757804155349731, + 0, + 0, + -0.38050055503845215, + -0.3475361168384552, + -0.3460211753845215, + -0.3452502191066742, + 0, + 0, + -0.3270740211009979, + -0.35466599464416504, + 0, + -1.6785608530044556, + -0.042731598019599915, + 0, + 0, + -0.8738346695899963, + 0, + 0, + -1.3493611812591553, + -0.33822640776634216, + -0.2678961753845215, + 0, + -0.042731598019599915, + -3.1001944541931152, + 2.860347270965576, + 1.7957031726837158, + -0.018584920093417168, + -0.018584920093417168, + -0.018584920093417168, + -0.5774023532867432, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + -0.5536002516746521, + -1.1061232089996338, + 1.3213735818862915, + 3.957881212234497, + 3.957881212234497, + 4.638659954071045, + 4.42684268951416, + 2.3681249618530273, + 2.0528454780578613, + 7.3816752433776855, + 4.17827033996582, + 6.468132495880127, + 4.017130374908447, + 5.217040538787842, + -1.9796524047851562, + -0.3908320665359497, + -1.6709413528442383, + -0.19154834747314453, + -0.19154834747314453, + -0.17416070401668549, + -0.17416070401668549, + -1.2428146600723267, + -0.7238667011260986, + -0.8973953723907471, + -0.0867948979139328, + -0.16916145384311676, + -2.2424511909484863, + -0.32075998187065125, + -0.32075998187065125, + -0.2476712465286255, + -1.478501558303833, + 0, + -0.006954232230782509, + 0, + 0, + -0.3725646734237671, + -0.7989145517349243, + -1.854297399520874, + -0.016981689259409904, + 0, + 0, + 0, + 0, + -0.00293157109990716, + -0.11094554513692856, + -0.6883361339569092, + -0.3473883867263794, + -0.3317181169986725, + -0.4416748285293579, + -0.43391868472099304, + -0.1418841928243637, + -0.27213436365127563, + -0.021505234763026237, + -2.5270771980285645, + -2.5270771980285645, + 0.3450876493306147 + ], + [ + -0.6420600414276123, + -1.9181957244873047, + -0.9622277617454529, + 1.3733851909637451, + 1.7130494117736816, + -2.1014411449432373, + 0.9902098774909973, + 0.909596860408783, + -1.1616764068603516, + -0.0607258602976799, + -0.4026883542537689, + -0.10765376687049866, + -0.0967993512749672, + -0.0967993512749672, + -0.2516874670982361, + -0.5531376600265503, + -0.9255282282829285, + -0.5531376600265503, + -0.0967993512749672, + -0.2632655203342438, + 3.4905331134796143, + -0.0967993512749672, + 0, + -2.0475926399230957, + -0.12525439262390137, + -0.12525439262390137, + -0.9677612781524658, + -0.021905967965722084, + -0.021905967965722084, + -1.255740761756897, + -0.11220162361860275, + 0, + 0, + -0.04746983200311661, + 0, + -0.07886837422847748, + -0.07886837422847748, + -0.07886837422847748, + -0.07886837422847748, + 0, + 0, + 0, + 0, + -1.0360527038574219, + 0, + -0.02445678412914276, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + -0.28641000390052795, + 0, + -0.21070648729801178, + 0, + -0.47402000427246094, + 0, + -0.28641000390052795, + -0.3845341205596924, + -1.04691481590271, + -0.2858976721763611, + -0.28032997250556946, + 0, + 0, + 0, + 0, + 0, + -0.9096251130104065, + 1.1884770393371582, + -0.6546885967254639, + 0, + 0, + 0, + -0.7524798512458801, + -0.10243398696184158, + -0.10243398696184158, + -0.09811604022979736, + -0.09811604022979736, + -0.09811604022979736, + 0, + 0, + 0, + 0, + -0.015519799664616585, + 0, + 0, + 0, + -0.3532436788082123, + -0.3532436788082123, + 0, + -0.3316343426704407, + -0.3924787938594818, + 0, + -0.2033385932445526, + -0.24257385730743408, + 7.729468822479248, + 2.36399245262146, + 5.470393657684326, + -0.6171549558639526, + -0.04746983200311661, + -0.06629005819559097, + -0.06629005819559097, + -0.2993519902229309, + -0.10318173468112946, + -0.1877218633890152, + -0.05038710683584213, + -0.2762553095817566, + -1.657719612121582, + -0.20799502730369568, + -0.20799502730369568, + -0.8902863264083862, + -0.3316343426704407, + -0.32283616065979004, + -0.3706023395061493, + -0.5664393901824951, + -0.5664393901824951, + -0.015637066215276718, + -0.02445678412914276, + -2.798739433288574, + 0, + -0.5251821875572205, + -0.5251821875572205, + -1.4305318593978882, + -1.4305318593978882, + -0.6157680153846741, + 0, + 0, + -0.015519799664616585, + 0, + 0, + 0, + -1.0123201608657837, + 0, + 0, + -0.39987918734550476, + -0.39987918734550476, + -1.012125551662553 + ], + [ + -3.8618507385253906, + 2.661921739578247, + -0.324486643075943, + -2.681408166885376, + -1.070864200592041, + -0.1429082453250885, + -0.9401305913925171, + -2.0808780193328857, + 3.4362409114837646, + -0.5510810613632202, + 5.012321949005127, + -1.0682185888290405, + -0.24958382546901703, + -0.24958382546901703, + -1.0682185888290405, + -0.24958382546901703, + -0.5058361291885376, + -1.057460904121399, + -1.2256693840026855, + -1.0682185888290405, + -0.5766453146934509, + -0.24958382546901703, + -0.7393555045127869, + -0.7923861742019653, + -0.02366284839808941, + 0, + -0.5207908749580383, + 0, + 0, + -0.9092050790786743, + -1.1664316654205322, + -0.23604610562324524, + -0.23604610562324524, + 2.949445962905884, + -0.22940532863140106, + 0, + 0, + 0, + 0, + -0.10126614570617676, + -1.332739233970642, + -1.332739233970642, + -0.5901528000831604, + -0.7608097195625305, + -0.5048365592956543, + -0.5548068881034851, + 0, + -1.1728324890136719, + -1.1728324890136719, + -0.06950733810663223, + -0.04147882014513016, + -0.03993690013885498, + -0.03845171630382538, + 0, + -0.10618571937084198, + -1.829228401184082, + -0.6239013671875, + -0.7514669299125671, + -2.0999088287353516, + -1.7867448329925537, + -0.10618571937084198, + -0.9889819622039795, + 0.08560609817504883, + -2.1254465579986572, + -1.9958056211471558, + -0.3938402533531189, + -1.6643561124801636, + -0.16029290854930878, + -0.31657809019088745, + -0.39398059248924255, + -0.7145650386810303, + -0.7145650386810303, + -0.6206731796264648, + -0.007626057136803865, + -0.007626057136803865, + -0.007626057136803865, + 3.9068644046783447, + -0.7097881436347961, + -0.7097881436347961, + -0.8330930471420288, + -0.8330930471420288, + -0.8330930471420288, + -0.13839125633239746, + -0.13839125633239746, + 0, + 0, + -0.08971934020519257, + 0, + 0, + -0.2813188433647156, + -0.2813188433647156, + -0.2813188433647156, + 0, + -0.09263147413730621, + 0, + 0, + 0, + 0, + -0.19597065448760986, + 0, + 0, + 4.378785133361816, + 4.526092529296875, + 5.922995090484619, + 5.922995090484619, + 10.107356071472168, + -1.0512547492980957, + 6.942415714263916, + 7.998384952545166, + 6.079689025878906, + -0.7038584351539612, + -0.15375125408172607, + -0.15375125408172607, + -0.23844508826732635, + -0.09263147413730621, + -0.05494779720902443, + -0.048433784395456314, + -0.09314513951539993, + -0.09314513951539993, + -0.23382462561130524, + -0.35334011912345886, + -1.2957956790924072, + -0.409479022026062, + -0.1429082453250885, + -0.1429082453250885, + -0.009866238571703434, + -0.009866238571703434, + -0.023210253566503525, + -0.02366284839808941, + -1.1075395345687866, + -0.08971934020519257, + -0.022661320865154266, + -0.2660955786705017, + -0.2567301392555237, + -0.18965840339660645, + -1.3401103019714355, + -0.6953500509262085, + -0.5650429129600525, + -0.5650429129600525, + -0.2684726967987179 + ], + [ + -1.0223896503448486, + -3.3254759311676025, + -0.3594515919685364, + -1.2034928798675537, + -0.7094810009002686, + -0.11835822463035583, + 3.11519455909729, + 7.990487098693848, + 0.09484227001667023, + -0.03216062858700752, + -0.9383915066719055, + 0, + 0, + 0, + -0.043270353227853775, + 0, + -0.18485020101070404, + 0, + -0.6258415579795837, + -0.043270353227853775, + -2.782179832458496, + 0, + 0, + -0.12773528695106506, + -0.06305236369371414, + -0.043270353227853775, + -0.24601295590400696, + 0, + 0, + -0.10957992821931839, + -0.8636319041252136, + -1.4621272087097168, + -1.4621272087097168, + -1.591508150100708, + -0.7966815233230591, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + -0.6140099167823792, + 3.904226064682007, + -0.6140099167823792, + -0.17317022383213043, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + -0.2878693640232086, + -0.3986928164958954, + -0.9750335216522217, + 3.525062322616577, + -0.35377830266952515, + -1.0733240842819214, + -1.0409214496612549, + -0.06699375808238983, + -0.6057316064834595, + 5.004830837249756, + -0.3178902864456177, + -0.25369635224342346, + 0, + 0, + 0, + 0, + 0, + -0.13478565216064453, + -3.095573663711548, + -1.419830322265625, + 0, + 0, + 0, + 2.3318679332733154, + -0.4985043406486511, + -0.4985043406486511, + -0.4953354299068451, + -0.4953354299068451, + -0.4953354299068451, + -0.16272452473640442, + -0.16272452473640442, + 0, + 0, + -0.37520232796669006, + 0, + 0, + 0, + -0.7256875038146973, + -0.7256875038146973, + 0, + -0.015781344845891, + -0.7694862484931946, + 0, + -0.377000093460083, + -0.4205278754234314, + -1.2554208040237427, + 0, + -0.9600287079811096, + -0.24299727380275726, + -0.11906661838293076, + -0.7379854321479797, + -0.7379854321479797, + -2.122434139251709, + 0, + -3.112035036087036, + -3.112035036087036, + -0.9383915066719055, + -0.4391774535179138, + -0.31684646010398865, + -0.31684646010398865, + -0.05564532428979874, + -0.015781344845891, + -0.005053800996392965, + -0.009785249829292297, + -0.03709046170115471, + -0.03709046170115471, + -0.09168491512537003, + -0.17317022383213043, + -1.3464040756225586, + -0.9806076288223267, + -0.08754246681928635, + -0.08754246681928635, + -0.10532320290803909, + -0.10532320290803909, + -0.006247076205909252, + -0.011304006911814213, + -1.222589373588562, + -0.37520232796669006, + -0.10687950998544693, + -0.0387471504509449, + -0.03715534880757332, + -0.22339007258415222, + -0.1271572709083557, + 0, + -0.8067957758903503, + -0.8067957758903503, + -2.6753750922540664 + ], + [ + -0.12718820571899414, + -0.9853842854499817, + -0.5445895195007324, + 5.641761779785156, + -4.2250847816467285, + -0.280537873506546, + -0.4256134331226349, + 0.3338262736797333, + -0.918182909488678, + -1.3661450147628784, + 0, + 0, + 0, + 0, + 0, + -0.6540312767028809, + -0.6540312767028809, + -0.8484894037246704, + 0, + 0, + 0, + 0, + 0, + 0, + -0.6540312767028809, + 0, + -0.5487508773803711, + 0, + 0, + -0.8484894037246704, + -2.1415629386901855, + -0.0575675331056118, + -0.0575675331056118, + -0.0575675331056118, + -0.0575675331056118, + -0.8333489298820496, + -0.8333489298820496, + -0.8333489298820496, + -0.8333489298820496, + 0, + 0, + 0, + 0, + -0.1419188380241394, + 0, + -0.4246017634868622, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + -0.15350814163684845, + 0, + 0, + -1.3661450147628784, + 0, + 0, + 0, + -0.037509985268116, + 0, + 0, + -0.844822108745575, + -0.22552567720413208, + 0, + 0, + 0, + 1.1137510538101196, + 0.27898016571998596, + -1.4447450637817383, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + -0.11407973617315292, + 0, + 0, + 0, + -0.5878565311431885, + -0.3544137477874756, + -0.21363283693790436, + 0.967481255531311, + -0.3053710162639618, + -0.7675119638442993, + -0.007795386016368866, + -0.007795386016368866, + -2.1931285858154297, + -2.1931285858154297, + -2.1931285858154297, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 5.353912830352783, + 2.1121280193328857, + 2.1121280193328857, + 3.23943829536438, + 2.038325786590576, + 2.9767329692840576, + 5.013604164123535, + 4.2997145652771, + 4.2997145652771, + -0.19218771159648895, + -0.3588467240333557, + -3.047213554382324, + 0, + 0, + 0, + 0, + 0, + 0, + -0.6540312767028809, + -0.250901460647583, + -0.08072638511657715, + -0.021549042314291, + -0.09487134218215942, + -0.09106424450874329, + -2.8172950744628906, + -0.0021756167989224195, + -0.04602548107504845, + -0.4669470489025116, + -0.4669470489025116, + -0.14674373041027708 + ], + [ + -0.23209355771541595, + -1.1615275144577026, + -0.005190588533878326, + -3.6277239322662354, + -3.169827938079834, + -1.4822739362716675, + -1.7275220155715942, + -1.2139179706573486, + -0.2021854668855667, + -0.1523972898721695, + -0.11314810812473297, + -0.08779977262020111, + 0, + 0, + -0.059583138674497604, + -1.5408085584640503, + -0.15550094842910767, + -1.6674295663833618, + 0, + -0.08779977262020111, + -0.05608239024877548, + 0, + -0.059583138674497604, + -0.16862013936042786, + -0.045018356293439865, + 0, + 9.606298446655273, + 0, + 0, + -0.24552878737449646, + 4.0383477210998535, + -1.0773080587387085, + -1.0773080587387085, + -1.092512607574463, + 0, + -0.3511105179786682, + -0.3511105179786682, + -0.3511105179786682, + -0.3511105179786682, + -0.4085167646408081, + -0.9077059030532837, + -0.007602308411151171, + -0.09288404881954193, + -0.23860332369804382, + -0.09288404881954193, + 1.0059305429458618, + -0.8925086259841919, + 0, + 0, + -0.41859158873558044, + -0.4048844575881958, + -0.4027630090713501, + -0.4005860686302185, + 0, + -0.0000060307379499136005, + -0.3426108956336975, + -0.0742335394024849, + 0, + -0.19533050060272217, + 0, + -0.0000060307379499136005, + -0.03409332409501076, + -0.1738254725933075, + 0, + 0, + 0, + -0.5821152925491333, + -0.4939543306827545, + 0, + 0, + -0.09052671492099762, + -0.20556989312171936, + -0.14422670006752014, + -0.15840737521648407, + -0.15840737521648407, + -0.15840737521648407, + -0.05607032775878906, + 0, + 0, + -0.03408125042915344, + -0.03408125042915344, + -0.03408125042915344, + 0, + 0, + -0.00014428634312935174, + -0.00014428634312935174, + -0.7335526943206787, + 0, + 0, + 0, + -0.004638887010514736, + -0.001708140131086111, + -0.001953831408172846, + -0.6299360990524292, + -0.6226332187652588, + 0, + 0, + 0, + -0.1530466079711914, + 0, + 0, + -0.04347405955195427, + 0, + 0, + 0, + 0, + 0, + -0.0043099685572087765, + -0.0043099685572087765, + -0.02191714569926262, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + -3.760802745819092, + 3.084120035171509, + -0.6946653127670288, + -0.08140380680561066, + -0.04347405955195427, + -0.04347405955195427, + -0.034206993877887726, + -0.034206993877887726, + -0.02565651200711727, + -0.045018356293439865, + -0.42496785521507263, + -0.07680274546146393, + -0.033271674066782, + -0.4210048317909241, + -0.41981253027915955, + -0.11387768387794495, + -0.16938365995883942, + -0.012891450896859169, + -0.08184230327606201, + -0.08184230327606201, + 0.42938872349061835 + ], + [ + 1.292214274406433, + 0.26029807329177856, + -3.265094041824341, + 1.323584794998169, + 2.0901131629943848, + -0.3446880578994751, + -1.5694751739501953, + -0.8493215441703796, + -0.5331605672836304, + 1.0180702209472656, + -0.0004536763299256563, + 0, + 0, + 0, + 0, + -1.6404364109039307, + -3.4016611576080322, + -0.037170104682445526, + 0, + 0, + -1.677952527999878, + 0, + 0, + -2.535099506378174, + -2.8807036876678467, + 0, + -0.037170104682445526, + 0, + 0, + -2.04948091506958, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + -0.0057027386501431465, + -0.5997958779335022, + 0, + 0, + 0, + -0.05450645834207535, + -0.05450645834207535, + 0, + 0, + 0, + 0, + 0, + -0.7775706052780151, + -0.4098859131336212, + 0, + -0.4394400715827942, + 2.708770275115967, + -0.4485231637954712, + -0.7775706052780151, + -0.785632848739624, + 1.7545942068099976, + 0, + 0, + -0.398097425699234, + -0.5929336547851562, + 0, + 0, + -0.0057027386501431465, + 1.2487276792526245, + -0.1948939859867096, + -0.8948578238487244, + 0, + 0, + 0, + -0.7649872899055481, + -0.7560442090034485, + -0.7560442090034485, + -0.003941982984542847, + -0.003941982984542847, + -0.003941982984542847, + 0, + 0, + 0, + 0, + -0.015651213005185127, + -1.3101164102554321, + -1.3101164102554321, + -0.26000744104385376, + -0.26000744104385376, + -0.26000744104385376, + 0, + -0.13279704749584198, + 0, + 0, + 0, + 0, + -3.0560569763183594, + 0, + 0, + -0.3446880578994751, + 0, + 0, + 0, + -0.21462611854076385, + -0.21462611854076385, + -0.26094186305999756, + -0.039530932903289795, + -0.0004536763299256563, + -0.6876921057701111, + -0.10246691852807999, + -0.10246691852807999, + -0.5645003914833069, + -0.13279704749584198, + -0.25327029824256897, + -0.37001025676727295, + -0.4633078873157501, + -0.4633078873157501, + 0, + 0, + 10.846235275268555, + 3.3832929134368896, + -0.3446880578994751, + -0.3446880578994751, + -1.0060052871704102, + -1.0060052871704102, + -1.5285238027572632, + -2.8807036876678467, + -0.17504888772964478, + -0.015651213005185127, + -0.0056052375584840775, + -0.0669519379734993, + -0.06419989466667175, + -3.2434446811676025, + -0.012034664861857891, + -0.15987606346607208, + -0.5193120241165161, + -0.5193120241165161, + -1.6741036713965793 + ], + [ + -1.2913458347320557, + -1.1410175561904907, + -0.1504516303539276, + -0.4215856194496155, + -2.5707266330718994, + -0.003674748819321394, + 0.13974682986736298, + -0.6987276077270508, + -0.39745214581489563, + -1.0970606803894043, + -0.3989872634410858, + -0.5193899273872375, + -0.08799205720424652, + -0.08799205720424652, + -1.1870545148849487, + 1.5699361562728882, + 3.095979928970337, + -0.3248724043369293, + -0.08799205720424652, + -1.2793129682540894, + -0.37088605761528015, + -0.08799205720424652, + -0.23400215804576874, + 3.2401087284088135, + 3.209850788116455, + -0.7191513776779175, + -0.016282057389616966, + -0.31694793701171875, + -0.31694793701171875, + 2.076730966567993, + -0.18918496370315552, + -0.013376947492361069, + -0.013376947492361069, + -0.04278998076915741, + -0.004762548021972179, + -0.011217543855309486, + -0.011217543855309486, + -0.017532918602228165, + -0.011217543855309486, + -0.028222307562828064, + -0.0557534284889698, + -0.02368752472102642, + -0.014544656500220299, + -0.17036139965057373, + -0.014544656500220299, + -0.02665693685412407, + -0.02205268293619156, + -0.0021749138832092285, + -0.0021749138832092285, + -0.049194373190402985, + -0.023485533893108368, + -0.02252129279077053, + -0.021596623584628105, + -0.10593338310718536, + -0.11573706567287445, + -0.3629792332649231, + -0.0001258983975276351, + 0, + -1.1090625524520874, + 0, + -0.0036658381577581167, + -0.006373447831720114, + -2.0295188426971436, + 0, + 0, + 0, + -0.2419360876083374, + 0, + 0, + 0, + -0.722599983215332, + -1.3000450134277344, + -0.33351635932922363, + 0, + 0, + 0, + -0.00553872948512435, + -0.0023433116730302572, + -0.0023433116730302572, + -0.0008746905950829387, + -0.0008746905950829387, + -0.0008746905950829387, + 0, + 0, + -0.0038404290098696947, + -0.006467245519161224, + -0.29648056626319885, + 0, + 0, + 0, + -0.03231830894947052, + -0.012083077803254128, + -0.014325657859444618, + -0.050506096333265305, + 0, + -0.012288431636989117, + 0, + 0, + 3.2137577533721924, + -0.13101829588413239, + -0.6130020618438721, + 0.572363018989563, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + -0.001087351469323039, + 4.734946250915527, + -2.0157506465911865, + 0.572363018989563, + 0.572363018989563, + 1.8228611946105957, + 1.8228611946105957, + 1.7091647386550903, + 3.9290292263031006, + -0.5727516412734985, + -0.26853373646736145, + -0.18373097479343414, + -0.12631776928901672, + -0.12113834172487259, + -0.5061092376708984, + -0.13984650373458862, + -0.0468229278922081, + -0.09297265112400055, + -0.09297265112400055, + -0.6886503062546867 + ], + [ + -0.5077318549156189, + -1.8987878561019897, + -0.35498031973838806, + -1.8763723373413086, + -2.0653798580169678, + -0.9951695799827576, + 1.7244404554367065, + -2.168811082839966, + -2.1415905952453613, + 0, + -0.23866921663284302, + -0.3075999617576599, + 0, + 0, + -0.6796745657920837, + 0, + -0.891762912273407, + 0, + -0.40114548802375793, + -0.7481383681297302, + -0.7970276474952698, + 0, + -0.23866921663284302, + -1.0001347064971924, + -0.5179142355918884, + -0.36288952827453613, + -1.3573659658432007, + -0.09471361339092255, + -0.09471361339092255, + -0.158460795879364, + -1.3037259578704834, + -0.24867312610149384, + -0.24867312610149384, + -0.9490104913711548, + -0.09194017201662064, + -0.10955855250358582, + -0.10955855250358582, + -1.783765196800232, + -0.10955855250358582, + -2.646313428878784, + -1.0704121589660645, + -0.6563863158226013, + -1.646806240081787, + 0.34065505862236023, + -1.646806240081787, + -1.748129963874817, + -0.3809452950954437, + -0.3920825123786926, + -0.3920825123786926, + -2.667757511138916, + -2.6342594623565674, + -2.626725673675537, + -2.6188108921051025, + -2.1725428104400635, + -2.231743335723877, + -1.5861741304397583, + -1.1299805641174316, + -0.2543991804122925, + -0.36038318276405334, + -0.6977182626724243, + -0.04082721844315529, + -0.0990259125828743, + -1.3207709789276123, + -0.32789477705955505, + -0.3065217435359955, + -0.04539057984948158, + -0.698974609375, + 0, + -0.035125356167554855, + -0.035125356167554855, + -0.22145676612854004, + -0.8647521138191223, + -1.1884366273880005, + 0, + 0, + 0, + -1.1018884181976318, + -0.02143898233771324, + -0.02143898233771324, + -0.04176821559667587, + -0.04176821559667587, + -0.04176821559667587, + -0.08753964304924011, + -0.08753964304924011, + -1.7882267236709595, + -3.4442903995513916, + 2.565450668334961, + 0, + 0, + 0, + -0.4173498749732971, + -0.18671193718910217, + -0.22115515172481537, + -2.18947696685791, + -1.294816017150879, + -0.47465768456459045, + -0.033020079135894775, + -0.033020079135894775, + -0.8756087422370911, + 0, + 0, + -0.1833024024963379, + -0.016651319339871407, + -0.5369012951850891, + -0.5369012951850891, + 0, + 0, + -0.9610477089881897, + -0.9610477089881897, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + -0.3945724070072174, + -0.3945724070072174, + 0, + -0.03544429689645767, + -1.1593499183654785, + -0.18549960851669312, + -0.158460795879364, + -0.158460795879364, + -0.6906583905220032, + -0.6906583905220032, + 0, + -0.097763292491436, + 11.378676414489746, + 7.304425239562988, + -0.2728983163833618, + 11.550247192382812, + 11.530162811279297, + -0.7071039080619812, + -1.0914838314056396, + -0.2598259449005127, + -0.7921883463859558, + -0.7921883463859558, + 2.6784531290209324 + ], + [ + 0.380982369184494, + -0.3258844316005707, + -3.4917116165161133, + -0.27739769220352173, + 3.301130533218384, + -1.073818564414978, + -1.505022644996643, + 0.27774229645729065, + -1.3573098182678223, + -0.5270810127258301, + -0.24048076570034027, + -0.8727750778198242, + -0.24048076570034027, + -0.24048076570034027, + -0.3888031840324402, + -0.9459995627403259, + -1.035170555114746, + -0.9459995627403259, + -0.24048076570034027, + -0.9452674388885498, + -0.6623518466949463, + -0.24048076570034027, + 0, + 0, + -0.05696911737322807, + -0.05696911737322807, + -1.082931637763977, + -0.062191810458898544, + -0.062191810458898544, + 0, + -0.09859281778335571, + -0.08082381635904312, + -0.08082381635904312, + -0.08082381635904312, + -0.04772651940584183, + -0.053415942937135696, + -0.053415942937135696, + -0.053415942937135696, + -0.053415942937135696, + 0, + 0, + 0, + -0.005526010878384113, + -0.12121487408876419, + 0, + 0, + 0, + -0.007201648782938719, + -0.007201648782938719, + 0, + 0, + 0, + 0, + 0, + 0, + -0.7524583339691162, + 0, + 0, + 0, + -0.005526010878384113, + 0, + 0, + -0.11378240585327148, + 0, + 0, + -0.2490343451499939, + -0.8400887846946716, + 0, + 0, + -0.005526010878384113, + 0.6075114607810974, + -0.014191977679729462, + -0.8784146904945374, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + -0.12548333406448364, + -0.12548333406448364, + -0.020310726016759872, + -0.2117539495229721, + -0.2117539495229721, + 0, + -0.20880654454231262, + -0.1780262440443039, + 0, + 0, + 0, + -0.756860613822937, + -0.37837353348731995, + -0.37837353348731995, + 0, + 0, + 0, + 0, + -0.026584208011627197, + -0.026584208011627197, + -0.026584208011627197, + 0, + 0, + -0.8632755875587463, + -0.09813888370990753, + -0.09813888370990753, + -0.5334068536758423, + -0.20880654454231262, + -0.141290083527565, + -0.18400461971759796, + -0.24847225844860077, + -0.24847225844860077, + 0, + 0, + -2.349536180496216, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 11.706945419311523, + -0.16522370278835297, + -0.22235870361328125, + -0.5889762043952942, + -0.5889762043952942, + -0.669537763085042 + ], + [ + -1.257106065750122, + 1.9722994565963745, + -0.7403843998908997, + -1.3834630250930786, + -0.97077476978302, + -0.05542337894439697, + -0.2643361985683441, + -1.355240821838379, + 0.2562214136123657, + -0.35549765825271606, + -0.2758129835128784, + -0.3064761757850647, + -0.10345030575990677, + -0.10345030575990677, + -0.2758129835128784, + -0.10345030575990677, + -0.16006582975387573, + -0.7584336996078491, + -0.2175266444683075, + -0.3064761757850647, + -0.5171506404876709, + -0.10345030575990677, + -0.09477495402097702, + -0.09477495402097702, + 0, + 0, + 0, + 0, + 0, + -0.5531495213508606, + -0.6761674284934998, + -0.16182109713554382, + -0.16182109713554382, + -0.36729753017425537, + -0.06748000532388687, + -0.008540223352611065, + -0.008540223352611065, + -0.026633432134985924, + -0.008540223352611065, + -0.3734872341156006, + -0.313742458820343, + -0.1641637533903122, + -0.1652340143918991, + -0.5971291661262512, + -0.1652340143918991, + -3.7121341228485107, + -0.09692328423261642, + -0.8412911891937256, + -0.8412911891937256, + -0.47956082224845886, + -0.3054158389568329, + -0.29459866881370544, + -0.2841568887233734, + -0.1532236486673355, + -0.19646738469600677, + 7.023808479309082, + -6.51149845123291, + -0.12688302993774414, + -0.3361934423446655, + -0.17926666140556335, + -0.026485782116651535, + -0.026485782116651535, + -0.0460079051554203, + -0.07561150193214417, + -0.05101285129785538, + -0.18610434234142303, + 5.773743629455566, + -3.558574914932251, + -3.6353445053100586, + -3.6353445053100586, + -0.19886760413646698, + -0.45692336559295654, + -0.6680894494056702, + -0.5418877601623535, + -0.5418877601623535, + -0.5418877601623535, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + -0.014895623549818993, + -0.03276222199201584, + -0.03418026119470596, + 0, + 0, + -0.06170174479484558, + -0.09910255670547485, + -0.07873842865228653, + -0.013664050959050655, + -0.05212222784757614, + 0, + -0.014957047067582607, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + -0.02163555845618248, + -0.02163555845618248, + -0.6897953748703003, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + -0.14927265048027039, + -0.010437005199491978, + 0, + 0, + 0, + 0, + 0, + 0, + -0.017030984163284302, + -0.00009560387843521312, + 0, + -0.01634802483022213, + -0.015698639675974846, + 0, + 7.572530269622803, + 7.000411510467529, + -0.5314501523971558, + -0.5314501523971558, + -0.4734956401909367 + ], + [ + 1.0063420534133911, + -0.30277952551841736, + -0.6083040237426758, + -0.6710527539253235, + -0.6855297684669495, + -0.030787145718932152, + -0.7419338226318359, + 0.5955686569213867, + -0.34317153692245483, + -0.37119558453559875, + -0.09519360959529877, + -0.10691992193460464, + -0.025333672761917114, + -0.025333672761917114, + -0.09519360959529877, + -0.025333672761917114, + -0.038938913494348526, + -0.09744702279567719, + -0.025333672761917114, + -0.10691992193460464, + -0.3539024293422699, + -0.025333672761917114, + -0.05085968226194382, + -0.05085968226194382, + 0, + 0, + 0, + 0, + 0, + -0.05311308801174164, + -0.21816998720169067, + -0.2906172275543213, + -0.2906172275543213, + -0.3700330853462219, + -0.1103484034538269, + -0.00015175093722064048, + -0.00015175093722064048, + -0.0064401342533528805, + -0.00015175093722064048, + -0.05705874040722847, + -0.08894956856966019, + -0.030272455886006355, + -0.15075212717056274, + -0.1544485092163086, + -0.04713387414813042, + -0.12149176001548767, + -0.04507482424378395, + -0.09408246725797653, + -0.09408246725797653, + -0.09745470434427261, + -0.04398646950721741, + -0.04217682406306267, + -0.0404415987432003, + -0.029789570719003677, + -0.06194312870502472, + -0.03551335260272026, + -0.03551335260272026, + -0.0011829889845103025, + -0.021876968443393707, + -0.0818881243467331, + -0.019806738942861557, + -0.019806738942861557, + -0.019806738942861557, + 0, + 0, + -1.0367865562438965, + -0.12502995133399963, + -0.05542667210102081, + -0.04631301388144493, + -0.1500464528799057, + -0.8274975419044495, + 0.1079937145113945, + 2.482558488845825, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + -0.007324249017983675, + -0.0165301151573658, + -0.0165301151573658, + -0.20215901732444763, + -0.20215901732444763, + -0.038466840982437134, + -0.26354047656059265, + -0.26284584403038025, + -0.00009888518980005756, + -0.00009888518980005756, + -0.9729569554328918, + 0, + -0.9938164949417114, + -1.7601009607315063, + -0.11758595705032349, + 0, + -0.11758595705032349, + 0, + 0, + 0, + 0, + -0.07950513064861298, + -0.07950513064861298, + -0.07950513064861298, + 0, + 0, + -0.05576881393790245, + -0.05576881393790245, + -0.05576881393790245, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + -0.6361966729164124, + 0, + -0.6361966729164124, + 0, + 0, + 0, + 0, + 0, + 5.785028457641602, + 5.785028457641602, + -0.4704635307696706 + ] + ] + } + } + } + } + }, + "intentDomains": {}, + "extraSentences": [ + [ + "en", + "Can I cancel my order?" + ], + [ + "en", + "Why is my order cancelled?" + ], + [ + "en", + "How do I can cancel my order" + ], + [ + "en", + "Can I cancel an order" + ], + [ + "en", + "My payment was processed successfully but I didn't get any order confirmation. What should I do?" + ], + [ + "en", + "Payment was completed but no confirmation" + ], + [ + "en", + "Order was not confirmed" + ], + [ + "en", + "Money deducted but order is not confirmed" + ], + [ + "en", + "Why is my order delayed? " + ], + [ + "en", + "Did my order get delayed?" + ], + [ + "en", + "Why is my order not delivered yet?" + ], + [ + "en", + "When do I get my delivery?" + ], + [ + "en", + "How long does delivery take?" + ], + [ + "en", + "How long does shipping take?" + ], + [ + "en", + "Please Tell me about my delivery" + ], + [ + "en", + "When do I get my delivery?" + ], + [ + "en", + "Why is my order not delivered yet" + ], + [ + "en", + "goodbye" + ], + [ + "en", + "bye take care" + ], + [ + "en", + "see you later" + ], + [ + "en", + "bye for now" + ], + [ + "en", + "i must go" + ], + [ + "en", + "hello" + ], + [ + "en", + "hi" + ], + [ + "en", + "howdy" + ], + [ + "en", + "Greetings" + ], + [ + "en", + "Is anyone there?" + ], + [ + "en", + "Hello" + ], + [ + "en", + "Good day" + ], + [ + "en", + "Which items do you have?" + ], + [ + "en", + "What kinds of items are there?" + ], + [ + "en", + "What do you sell?" + ], + [ + "en", + "What do you offer?" + ], + [ + "en", + "What can I buy?" + ], + [ + "en", + "I'm looking for..." + ], + [ + "en", + "Do you have any..." + ], + [ + "en", + "I'm interested in..." + ], + [ + "en", + "Can I see what you have in..." + ], + [ + "en", + "I want to buy a..." + ], + [ + "en", + "I'm looking for something like this..." + ], + [ + "en", + "What are your most popular items?" + ], + [ + "en", + "What are some of your best deals?" + ], + [ + "en", + "Do you have any new arrivals?" + ], + [ + "en", + "Need more help" + ], + [ + "en", + "Help me more" + ], + [ + "en", + "can I talk to an agent" + ], + [ + "en", + "can I call customer service" + ], + [ + "en", + "customer support number" + ], + [ + "en", + "how to contact customer service" + ], + [ + "en", + "customer service number" + ], + [ + "en", + "contact number for help" + ], + [ + "en", + "helpline number" + ], + [ + "en", + "How to become a seller" + ], + [ + "en", + "How to contact a seller" + ], + [ + "en", + "What is my order status" + ], + [ + "en", + "I want to know my return status" + ], + [ + "en", + "How to return status" + ], + [ + "en", + "Do you take credit cards?" + ], + [ + "en", + "Do you accept Mastercard?" + ], + [ + "en", + "Can I pay with Cash?" + ], + [ + "en", + "Are you cash only?" + ], + [ + "en", + "What are your payment methods?" + ], + [ + "en", + "How do I pay?" + ], + [ + "en", + "How are you?" + ], + [ + "en", + "How are you doing?" + ], + [ + "en", + "How is your day?" + ], + [ + "en", + "How can I change my profile information" + ], + [ + "en", + "I want to change my password" + ], + [ + "en", + "I want to change my phone number" + ], + [ + "en", + "I want to change my address" + ], + [ + "en", + "I want to Reset my password" + ], + [ + "en", + "I want to delete my account" + ], + [ + "en", + "delete my account" + ], + [ + "en", + "Common reasons for delivery delay" + ], + [ + "en", + "common reasons for delivery delay" + ], + [ + "en", + "reasons for delay" + ], + [ + "en", + "delivery delay" + ], + [ + "en", + "Can I refund an item." + ], + [ + "en", + "I want to refund an item" + ], + [ + "en", + "can I refund my order" + ], + [ + "en", + "Are refunds available" + ], + [ + "en", + "Why is the status Refunded when it's not credited?" + ], + [ + "en", + "No refund even though status is refunded" + ], + [ + "en", + "No refund when status says refunded" + ], + [ + "en", + "I did not receive my refund money" + ], + [ + "en", + "Refund money not received" + ], + [ + "en", + "Thanks" + ], + [ + "en", + "Thank you" + ], + [ + "en", + "That's helpful" + ], + [ + "en", + "Thank's a lot!" + ], + [ + "en", + "thx" + ], + [ + "en", + "thnks" + ], + [ + "en", + "How can I track my order" + ], + [ + "en", + "I want to track my order" + ], + [ + "en", + "Can I track my order" + ], + [ + "en", + "Track order" + ], + [ + "en", + "I'm good" + ], + [ + "en", + "Im good" + ], + [ + "en", + "Im doing good" + ], + [ + "en", + "I am good" + ], + [ + "en", + "I am okay" + ], + [ + "en", + "How to use a voucher?" + ], + [ + "en", + "Can I use a voucher?" + ], + [ + "en", + "How to use a voucher?" + ] + ] + }, + "ner": { + "settings": { + "tag": "ner", + "entityPreffix": "%", + "entitySuffix": "%" + }, + "rules": {} + }, + "nlgManager": { + "settings": { + "tag": "nlg-manager" + }, + "responses": { + "en": { + "cancel": [ + { + "answer": "Order can only be cancelled within 7 days of placement. Digital goods do not qualify for refund." + }, + { + "answer": "Visit my orders page to check status of specific orders." + } + ], + "confirm": [ + { + "answer": "knight sends you an Email & SMS upon confirmation of your order. If it requires manual confirmation, our team will contact you within 24 hours after order placement. Delay in confirmation SMS may occur due to network error, you may receive it with a delay." + }, + { + "answer": "It takes upto 24 hours for confirmation, please bear with us! Type Need more help for more assistance. " + } + ], + "delay": [ + { + "answer": "We are really sorry if there has been a delay in your order! If your order is within delivery period, We recommend you to please wait during this period since our Delivery Heroes make 3 attempts to deliver your order!" + }, + { + "answer": "If your order is past up to 3 days after delivery period, There may be logistic issues causing a delay in delivery. Please type 'Common reasons for delivery delay' to know more about this." + }, + { + "answer": "We appreciate if you could wait for your items as most orders are delivered successfully within this period." + }, + { + "answer": "If your order is past more than 3 days, Since there may be unexpected issues causing delivery delays, you can click on 'Need more Help' for further assistance." + } + ], + "delivery": [ + { + "answer": "Delivery takes 2-4 days. Please bear with us!" + }, + { + "answer": "Shipping takes 2-4 days. Please bear with us!" + } + ], + "greetings.bye": [ + { + "answer": "see you soon!" + }, + { + "answer": "Till next time" + }, + { + "answer": "bye bye" + }, + { + "answer": "have a great day" + }, + { + "answer": "See you later, thanks for visiting. Hope I was able to help!" + }, + { + "answer": "Have a nice day. Hope I was able to help!" + } + ], + "greetings.hello": [ + { + "answer": "Hey :-) My name is knight!" + }, + { + "answer": "Hello, thanks for visiting. My name is knight!" + }, + { + "answer": "Hi there, My name is knight!. What can I do for you?" + }, + { + "answer": "Hi there, My name is knight! How can I help?" + } + ], + "items": [ + { + "answer": "Search your preference in our flagship store's search bar to see all available products. " + } + ], + "more.contact": [ + { + "answer": "Absolutely! How can I help you today? Here are some options based on your inquiry:" + }, + { + "answer": "Sure, let me know what you need help with. Here are a few things I can assist you with:" + } + ], + "more.help": [ + { + "answer": "Contact us for further information here: Phone: +250 780 000 000. Timings are from 09:00 AM to 05:00 PM from Monday to Saturday." + }, + { + "answer": "For immediate assistance, you can contact our customer service team at +250 780 000 000. Our hours are from 09:00 AM to 05:00 PM, Monday to Saturday." + } + ], + "order": [ + { + "answer": "Please visit the My Orders page for a list of your confirmed orders." + } + ], + "payments": [ + { + "answer": "We accept VISA and Mastercard" + } + ], + "personal": [ + { + "answer": "I'm good, all's good, thanks. How about you?" + } + ], + "profile.info": [ + { + "answer": "You can easily add or change your account details by following the steps below: Step 1: Click on 'Account', Step 2: Click on 'Manage my account' from the icon, Step 3: You can change or edit your name, address, email address, mobile number, etc., Step 4: Fill in the required details, and click on Save. Note: You can also change your delivery address from the Checkout page before proceeding to pay." + } + ], + "reason": [ + { + "answer": "Reasons include Seller Sourcing Issues, Courier Issues, Cross Border shipment delay, Wrong Address or Phone Number, and Unavailability of Customer. " + } + ], + "refund.demanding": [ + { + "answer": "Refund can only be issued within 7 days of placement. Digital goods do not qualify for refund." + }, + { + "answer": "Visit my orders page to check for specific orders." + } + ], + "refund.status": [ + { + "answer": "Please be patient as refunds take upto 30 days to receive into bank. " + } + ], + "thanks": [ + { + "answer": "Happy to help!" + }, + { + "answer": "Any time!" + }, + { + "answer": "My pleasure" + } + ], + "track": [ + { + "answer": "Visit the order page, click on the specific order, select 'track my order', and check the status" + } + ], + "user.response": [ + { + "answer": "Great to hear you are doing good." + } + ], + "voucher": [ + { + "answer": "You can add a voucher by clicking on My Cart > Check Out > Enter Voucher Code > APPLY. " + } + ] + } + } + }, + "actionManager": { + "settings": { + "tag": "action-manager" + }, + "actions": {} + }, + "slotManager": {} +} \ No newline at end of file diff --git a/package.json b/package.json index 53cc5a7..06430f2 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "mailgen": "^2.0.28", "morgan": "^1.10.0", "multer": "^1.4.5-lts.1", + "node-nlp": "^4.27.0", "nodemailer": "^6.9.13", "nodemon": "^3.1.0", "passport": "^0.7.0", @@ -73,8 +74,10 @@ "@types/express": "^4.17.21", "@types/express-session": "^1.18.0", "@types/jest": "^29.5.12", + "@types/joi": "^17.2.3", "@types/jsend": "^1.0.32", "@types/jsonwebtoken": "^9.0.6", + "@types/mocha": "^10.0.6", "@types/morgan": "^1.9.9", "@types/node": "^20.12.7", "@types/nodemailer": "^6.4.15", diff --git a/src/@types/index.d.ts b/src/@types/index.d.ts index e69de29..8228fa6 100644 --- a/src/@types/index.d.ts +++ b/src/@types/index.d.ts @@ -0,0 +1 @@ +declare module 'node-nlp'; diff --git a/src/__test__/chatBot.test.ts b/src/__test__/chatBot.test.ts new file mode 100644 index 0000000..e588ff6 --- /dev/null +++ b/src/__test__/chatBot.test.ts @@ -0,0 +1,53 @@ +import request from 'supertest'; +import { app, server } from '../index'; +import { createConnection} from 'typeorm'; +import { cleanDatabase } from './test-assets/DatabaseCleanup'; + +beforeAll(async () => { + await createConnection(); +}); + +jest.setTimeout(20000); +afterAll(async () => { + await cleanDatabase(); + server.close(); +}); + + +describe('POST /chat', () => { + it('should respond with a successful message for a valid user query', async () => { + const userMessage = 'What kind of items do you have?'; + const response = await request(app) + .post('/chat') + .send({ message: userMessage }) + .expect(200) + .expect('Content-Type', /json/); + + expect(response.body).toHaveProperty('message'); + expect(response.status).toBe(200); + expect(response.body.status).toBe('success'); + }); + it('should respond with an error for an empty user message', async () => { + const emptyMessageReq = { body: { message: '' } }; + const response = await request(app) + .post('/chat') + .send(emptyMessageReq) + + expect(response.status).toBe(400); + expect(response.body.status).toBe('error'); + expect(response.body.message).toBe('No user message'); + + }); + it('should respond with an error for an empty user message', async () => { + const userMessage = 'dojdojdodjojoqdj'; + const response = await request(app) + .post('/chat') + .send({ message: userMessage }) + expect(response.status).toBe(200); + expect(response.body.status).toBe('success'); + expect(response.body.message).toBe('Sorry, I am not sure what you mean. Can you rephrase?'); + + + }); + +}); diff --git a/src/controllers/chatBotController.ts b/src/controllers/chatBotController.ts new file mode 100644 index 0000000..5c3d366 --- /dev/null +++ b/src/controllers/chatBotController.ts @@ -0,0 +1,6 @@ +import { Request, Response } from 'express'; +import { chatBot } from '../services'; + +export const chatBotController = async (req: Request, res: Response) => { + await chatBot(req, res); +}; diff --git a/src/controllers/index.ts b/src/controllers/index.ts index 70dea3b..adaf1d1 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -3,3 +3,4 @@ export * from './productController'; export * from './orderController'; export * from './vendorOrderController'; export * from './adminOrdercontroller'; +export * from './chatBotController'; \ No newline at end of file diff --git a/src/node-nlp.d.ts b/src/node-nlp.d.ts new file mode 100644 index 0000000..8228fa6 --- /dev/null +++ b/src/node-nlp.d.ts @@ -0,0 +1 @@ +declare module 'node-nlp'; diff --git a/src/routes/chatBot.ts b/src/routes/chatBot.ts new file mode 100644 index 0000000..f54638a --- /dev/null +++ b/src/routes/chatBot.ts @@ -0,0 +1,10 @@ +import { RequestHandler, Router } from 'express'; +import { chatBotController } from '../controllers/chatBotController'; +import { optinalAuthMiddleware } from '../middlewares/optionalAuthorization'; + +const router = Router(); + +router.post('/', optinalAuthMiddleware as RequestHandler, chatBotController); + + +export default router; diff --git a/src/routes/index.ts b/src/routes/index.ts index 0d4c5fe..af462b4 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -7,6 +7,7 @@ import couponRoute from './couponRoutes'; import cartRoutes from './CartRoutes'; import feedbackRoute from './feedbackRoutes'; import notificationRoute from './NoficationRoutes' +import chatBot from './chatBot'; const router = Router(); @@ -21,5 +22,6 @@ router.use('/cart', cartRoutes); router.use('/coupons', couponRoute); router.use('/feedback', feedbackRoute); router.use('/notification', notificationRoute); +router.use('/chat', chatBot); export default router; diff --git a/src/services/chatbotServices/chatBot.ts b/src/services/chatbotServices/chatBot.ts new file mode 100644 index 0000000..44b2e37 --- /dev/null +++ b/src/services/chatbotServices/chatBot.ts @@ -0,0 +1,30 @@ +import { configDotenv } from 'dotenv'; +import { Request, Response } from 'express'; +import { sendSuccessResponse, sendErrorResponse } from '../../utils/response.utils'; +import { manager } from '../../train'; + + +export const chatBot = async (req: Request, res: Response) => { + const userMessage = req.body.message; + + try { + if(!userMessage){ + return sendErrorResponse(res, 400, 'No user message'); + } + const result = await manager.process('en', userMessage); + const intent = result.intent; + + if (result.answer || intent !== 'None') { + return sendSuccessResponse(res, 200, "", result.answer) + } + + else { + return sendSuccessResponse(res, 200, "Sorry, I am not sure what you mean. Can you rephrase?", result.answer) + } + + } catch (error) { + console.error(error); + return sendErrorResponse(res, 500, (error as Error).message); + + } + }; \ No newline at end of file diff --git a/src/services/index.ts b/src/services/index.ts index 4f808ed..f31e750 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -47,3 +47,6 @@ export * from './adminOrderServices/updateOrder'; export * from './notificationServices/getNotifications'; export * from './notificationServices/deleteNotification'; export * from './notificationServices/updateNotification'; + +// chatbot +export * from './chatbotServices/chatBot'; \ No newline at end of file diff --git a/src/train.ts b/src/train.ts new file mode 100644 index 0000000..732324e --- /dev/null +++ b/src/train.ts @@ -0,0 +1,43 @@ + +import fs from 'fs' + +import { NlpManager } from 'node-nlp'; + +export const manager = new NlpManager({ languages: ["en"] }); + +async function trainManager() { + const intentFiles = fs.readdirSync('./intents'); + + for (const file of intentFiles) { + try { + const filePath = `./intents/${file}`; + const data = await fs.promises.readFile(filePath, 'utf8'); + const jsonData = JSON.parse(data); + + const intent = file.replace('.json', ''); + + for (const utterances of jsonData.utterances) { + manager.addDocument('en', utterances, intent); + } + + for (const responses of jsonData.responses) { + manager.addAnswer('en', intent, responses); + } + } catch (error) { + console.error(`Error processing intent file ${file}:`, error); + } + } + + await manager.train(); +} + +trainManager() + .then(async () => { + manager.save(); + }) + .catch((error) => console.error('Error training NLP manager:', error)); + + module.exports = { + manager, + trainManager + }; diff --git a/tsconfig.json b/tsconfig.json index d58c75f..7e48aed 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -28,12 +28,12 @@ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + "typeRoots": ["node_modules/@types", "./src"], /* Specify multiple folders that act like './node_modules/@types'. */ "types": [ "node", "jest", "express", - "joi" + "node-nlp" ] /* Specify type package names to be included without being referenced in a source file. */, // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */