`;
+ }).join('');
+ }
+};
+
+// THis function clears the cart and reloads the page
+const clearCart = () => {
+ localStorage.clear();
+ return window.location.reload();
+};
+
+// Populate shopping cart
+let totalcartitem = JSON.parse(localStorage.getItem('product'));
+if (totalcartitem) {
+ document.getElementById('shoppingcartlabel').innerHTML = totalcartitem.length;
+}
+
+showCart();
+
+// Get the modal
+const modal = document.getElementById('myModal');
+
+// Get the button that opens the modal
+const btn = document.getElementById("shoppingcart");
+
+// Get the element that closes the modal
+const span = document.getElementsByClassName("close")[0];
+
+// When the user clicks the button, open the modal
+btn.onclick = function () {
+ modal.style.display = "block";
+};
+
+// When the user clicks on (x), close the modal
+span.onclick = function () {
+ modal.style.display = "none";
+};
+
+// When the user clicks anywhere outside of the modal, close it
+window.onclick = function (event) {
+ if (event.target == modal) {
+ modal.style.display = "none";
+ }
+};
diff --git a/UI/sales_new.html b/UI/sales_new.html
new file mode 100644
index 0000000..1ea1579
--- /dev/null
+++ b/UI/sales_new.html
@@ -0,0 +1,203 @@
+
+
+
+
+
+
+
+
+ Store Manager | Add Products
+
+
+
+
+
+
+
+
+
Description: The Google Pixel 2 is powered by 1.9GHz octa-core processor and it comes with 4GB of RAM. The phone packs 64GB of internal storage that cannot be expanded.
+
Price: $649
+
Quantity: 3
+
+
+
+
+
+
+
+
+
+
+
+
+ ×
+
+
+
PRODUCT
+
AMOUNT
+
QUANTITY
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..9dc5fed
--- /dev/null
+++ b/package.json
@@ -0,0 +1,63 @@
+{
+ "name": "storemanager",
+ "version": "1.0.0",
+ "description": "API backend for store manager application.",
+ "main": "server/app.js",
+ "scripts": {
+ "dev": "nodemon server/app",
+ "start": "npm run clean-dev && babel server -d server/build/dev && node server/build/dev/app",
+ "test": "mocha server/test --compilers js:babel-core/register --exit || true",
+ "test-dev": "npm run clean-dev && npm run clean && babel server -d server/build/dev && mocha --recursive server/build/dev --exit || true",
+ "lint": "eslint ./server",
+ "clean": "rm -rf server/build",
+ "clean-dev": "rm -rf server/build/dev",
+ "build": "npm run clean && babel server -d server/build && node server/build/app",
+ "coveralls": "nyc npm test && nyc report --reporter=text-lcov | coveralls"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/Easybuoy/storemanager.git"
+ },
+ "engines": {
+ "npm": "5.5.1",
+ "node": "8.9.1"
+ },
+ "nyc": {
+ "exclude": [
+ "server/app.js",
+ "server/config/keys.js"
+ ]
+ },
+ "author": "Ekunola Ezekiel",
+ "license": "ISC",
+ "bugs": {
+ "url": "https://github.com/Easybuoy/storemanager/issues"
+ },
+ "homepage": "https://github.com/Easybuoy/storemanager#readme",
+ "dependencies": {
+ "babel-preset-stage-2": "^6.24.1",
+ "bcryptjs": "^2.4.3",
+ "body-parser": "^1.18.3",
+ "chai": "^4.2.0",
+ "chai-http": "^4.2.0",
+ "cors": "^2.8.4",
+ "coveralls": "^3.0.2",
+ "dotenv": "^6.1.0",
+ "eslint-config-airbnb-base": "^13.1.0",
+ "eslint-plugin-import": "^2.14.0",
+ "express": "^4.16.4",
+ "jsonwebtoken": "^8.3.0",
+ "mocha": "^5.2.0",
+ "morgan": "^1.9.1",
+ "multer": "^1.4.1",
+ "nodemon": "^1.18.4",
+ "nyc": "^13.0.1",
+ "pg": "^7.5.0",
+ "validator": "^10.8.0"
+ },
+ "devDependencies": {
+ "babel-cli": "^6.26.0",
+ "babel-preset-env": "^1.7.0",
+ "eslint": "^4.19.1"
+ }
+}
diff --git a/server/app.js b/server/app.js
new file mode 100644
index 0000000..a1954d8
--- /dev/null
+++ b/server/app.js
@@ -0,0 +1,55 @@
+import express from 'express';
+import bodyParser from 'body-parser';
+import cors from 'cors';
+import morgan from 'morgan';
+
+import products from './routes/api/v1/products';
+import sales from './routes/api/v1/sales';
+import users from './routes/api/v1/users';
+
+const app = express();
+
+// Body Parser Middleware
+app.use(bodyParser.urlencoded({ extended: false }));
+app.use(bodyParser.json());
+
+app.use(cors());
+
+// Make uploads folder available publicly
+app.use('/uploads', express.static('uploads'));
+
+
+app.get('/', (req, res) => {
+ res.json({ message: 'Welcome To Store Manager API' });
+});
+
+// Use morgan to log requests.
+app.use(morgan('dev'));
+
+// using routes
+app.use('/api/v1/products', products);
+app.use('/api/v1/sales', sales);
+app.use('/api/v1/users', users);
+
+app.use((req, res, next) => {
+ const error = new Error('Not found');
+ error.status = 404;
+ next(error);
+});
+
+app.use((error, req, res, next) => {
+ res.status(error.status || 500);
+ res.json({
+ error: {
+ message: error.message,
+ },
+ });
+ next();
+});
+
+const port = process.env.PORT || 3000;
+
+// eslint-disable-next-line no-console
+app.listen(port, () => console.log(`sever listening on port ${port}`));
+
+module.exports = app;
diff --git a/server/controllers/productController.js b/server/controllers/productController.js
new file mode 100644
index 0000000..d975749
--- /dev/null
+++ b/server/controllers/productController.js
@@ -0,0 +1,99 @@
+import db from '../models/mockdb';
+
+import productsValidation from '../validation/products';
+
+class productController {
+ // @route POST api/v1/products
+ // @desc This function implements the logic for creating new product.
+ // @access Private
+ static createProduct(req, res) {
+ const { errors, isValid } = productsValidation.validateProductInput(req.body);
+ // Check validation
+ if (!isValid) {
+ return res.status(400).json(errors);
+ }
+
+ let productImage = 'uploads\\products\\default.png';
+ if (req.file) {
+ productImage = req.file.path;
+ }
+
+ let id = db.products.length;
+ id += 1;
+ const host = req.get('host');
+ const data = {
+ id,
+ name: req.body.name,
+ description: req.body.description,
+ quantity: req.body.quantity,
+ price: {
+ currency: '$',
+ amount: req.body.price,
+ },
+ productImage,
+ };
+
+ db.products.push(data);
+
+ data.request = {
+ method: 'GET',
+ url: `${host}/api/v1/products/${id}`,
+ };
+
+ return res.status(201).json({ message: 'Product added successfully', data });
+ }
+
+
+ // @route GET api/v1/products
+ // @desc This function implements the logic for getting all products.
+ // @access Private
+ static getProducts(req, res) {
+ res.json(db.products);
+ }
+
+ // @route GET api/v1/products/
+ // @desc This function implements the logic for getting a product detail by Id.
+ // @access Private
+ static getProductById(req, res) {
+ const { id } = req.params;
+
+ const product = db.products[id - 1];
+ if (!product) {
+ return res.status(400).json({ message: `Product with id ${id} not found.` });
+ }
+
+ return res.json(product);
+ }
+
+ // @route DELETE api/v1/products/
+ // @desc This function implements the logic for deleting a product by Id.
+ // @access Private
+ static deleteProductById(req, res) {
+ // Checks if user making the request is the store owner / admin
+ if (!(Number(req.user.type) === 1)) {
+ return res.status(401).json({ message: 'Unauthorized' });
+ }
+
+ const { id } = req.params;
+
+ const dbidtoberemoved = id - 1;
+
+ const product = db.products[dbidtoberemoved];
+
+ if (!product) {
+ return res.status(400).json({ message: `Product with id ${id} not found.` });
+ }
+
+ if (product.id !== Number(id)) {
+ return res.status(400).json({ message: `Product with id ${id} not found.` });
+ }
+
+ if (db.products.splice(dbidtoberemoved, 1)) {
+ return res.json({ message: `Product with id ${id} deleted successfully.` });
+ }
+
+ return res.json({ message: 'Unable to delete product.' });
+ }
+}
+
+export default productController;
diff --git a/server/controllers/saleController.js b/server/controllers/saleController.js
new file mode 100644
index 0000000..dbd046a
--- /dev/null
+++ b/server/controllers/saleController.js
@@ -0,0 +1,118 @@
+import db from '../models/mockdb';
+import salesValidation from '../validation/sales';
+
+class salesControler {
+ // @route POST api/v1/sales
+ // @desc This function implements the logic for creating a new sale.
+ // @access Private
+ static createSale(req, res) {
+ const processSale = (order) => {
+ let isMoreThanStock = false;
+ let isNotProductAvailable = false;
+ let totalSalesAmount = 0;
+ order.map((ordertobeprocessed) => {
+ const { quantity } = ordertobeprocessed;
+ const productId = ordertobeprocessed.product_id;
+
+ const product = db.products[productId - 1];
+
+ if (!product) {
+ isNotProductAvailable = true;
+ return false;
+ }
+
+ const productPrice = Number(product.price.amount);
+ const productQuantity = Number(product.quantity);
+ // Check if quantity in stock for product is more than quantity requested
+ if (quantity > product.quantity) {
+ isMoreThanStock = true;
+ return false;
+ }
+
+ const totalamount = quantity * productPrice;
+
+ // Update 'product table' by reducing quantity left in store from what user just bought
+ product.quantity = productQuantity - quantity;
+ totalSalesAmount += totalamount;
+ return { status: 200, totalamount };
+ });
+
+ if (isNotProductAvailable) {
+ return { status: 400, message: 'One Of Product Requested Is Not Available' };
+ }
+
+ if (isMoreThanStock) {
+ return { status: 400, message: 'One Of Product Requested Is More Than In Stock' };
+ }
+
+ const response = { amount: totalSalesAmount, currency: '$' };
+ return response;
+ };
+
+ const { errors, isValid } = salesValidation.validateSalesInput(req.body);
+
+ // Check validation
+ if (!isValid) {
+ return res.status(400).json(errors);
+ }
+
+ const processedSale = processSale(req.body.order);
+ // Check if there is error processing sale
+ if (processedSale.status === 400) {
+ return res.status(400).json({ message: processedSale.message });
+ }
+
+ let id = db.sales.length;
+ id += 1;
+
+ const data = {
+ id,
+ store_attendant_user_id: req.user.id,
+ order: req.body.order,
+ totalSaleAmount: processedSale,
+ date_time: new Date(),
+ };
+ db.sales.push(data);
+
+ return res.status(201).json({ message: 'Sale added successfully', data });
+ }
+
+ // @route GET api/v1/sales
+ // @desc This function implements the logic for getting all sale records.
+ // @access Private
+ static getSales(req, res) {
+ res.json(db.sales);
+ }
+
+ // @route GET api/v1/sales/
+ // @desc This function implements the logic for getting a sale details by Id.
+ // @access Private
+ static getSaleById(req, res) {
+ // check if user making the request is the Store Owner / Admin
+
+ if (!Number(req.user.type) === 1 || !Number(req.user.type) === 3) {
+ return res.status(401).json({ message: 'Unauthorized' });
+ }
+
+ const { id } = req.params;
+
+ const sales = db.sales[id - 1];
+
+ if (sales) {
+ if (Number(req.user.type) !== 1) {
+ // check if user making the request is the store attendant that made the sale
+ if (req.user.id !== sales.store_attendant_user_id) {
+ return res.status(401).json({ message: 'Unauthorized' });
+ }
+ }
+ }
+
+ if (!sales) {
+ return res.status(400).json({ message: `Sales with id ${id} not found.` });
+ }
+
+ return res.json(sales);
+ }
+}
+
+export default salesControler;
diff --git a/server/controllers/userController.js b/server/controllers/userController.js
new file mode 100644
index 0000000..bb7e732
--- /dev/null
+++ b/server/controllers/userController.js
@@ -0,0 +1,125 @@
+import bcrypt from 'bcryptjs';
+import jwt from 'jsonwebtoken';
+/* eslint-disable import/first */
+import dotenv from 'dotenv';
+
+dotenv.config();
+import db from '../models/mockdb';
+// Load Input validation
+import usersValidation from '../validation/users';
+
+const { SECRET_OR_KEY } = process.env;
+class usersController {
+ // @route POST api/users/register
+ // @desc This function implements the logic for registering a new user.
+ // @access Private
+ static signup(req, res) {
+ const { errors, isValid } = usersValidation.validateSignupInput(req.body);
+
+ // Check validation
+ if (!isValid) {
+ return res.status(400).json(errors);
+ }
+
+ const {
+ email, password, name, type,
+ } = req.body;
+ let exist = false;
+ db.user.map((user) => {
+ if (user.email === email) {
+ exist = true;
+ return exist;
+ }
+ });
+ if (exist) {
+ return res.status(409).json({ email: 'Email Already Exist' });
+ }
+
+ let id = db.user.length;
+ id += 1;
+
+ const data = {
+ id,
+ email,
+ password,
+ name,
+ status: 1,
+ type,
+ date_time: new Date(),
+ };
+
+ bcrypt.genSalt(10, (err, salt) => {
+ // Check if there is error generating salt
+ if (err) {
+ return res.status(500).json({ message: 'Error Creating User, Try again ' });
+ }
+
+ bcrypt.hash(data.password, salt, (error, hash) => {
+ if (error) throw error;
+ data.password = hash;
+ db.user.push(data);
+ res.status(201).json({ message: 'User Created Successfully', data });
+ });
+ });
+ }
+
+ // @route POST api/users/login
+ // @desc This function implements the logic to loggin a user.
+ // @access Public
+ static login(req, res) {
+ const { errors, isValid } = usersValidation.validateLoginInput(req.body);
+
+ // Check validation
+ if (!isValid) {
+ return res.status(400).json(errors);
+ }
+
+ let exist = false;
+ let userData = {};
+ const { email, password } = req.body;
+ db.user.map((user) => {
+ if (user.email === email) {
+ exist = true;
+ userData = user;
+ return user;
+ }
+ });
+
+ if (exist === false) {
+ return res.status(404).json({ email: 'User Not Found' });
+ }
+
+ bcrypt.compare(password, userData.password)
+ .then((isMatch) => {
+ if (isMatch) {
+ // User Matched
+ const payload = {
+ id: userData.id,
+ email: userData.email,
+ name: userData.name,
+ type: userData.type,
+ };
+ // Sign Token
+ jwt.sign(payload, SECRET_OR_KEY, { expiresIn: 3600 }, (err, token) => {
+ res.json({ success: true, token: `Bearer ${token}` });
+ });
+ } else {
+ return res.status(401).json({ password: 'Incorrect Password' });
+ }
+ });
+ }
+
+ // @route POST api/users/current
+ // @desc This function implements the logic for getting the current user
+ // details with token parsed.
+ // @access Private
+ static getCurrentUser(req, res) {
+ res.json({
+ id: req.user.id,
+ name: req.user.name,
+ email: req.user.email,
+ });
+ }
+}
+
+export default usersController;
diff --git a/server/middleware/authenticate.js b/server/middleware/authenticate.js
new file mode 100644
index 0000000..34533c4
--- /dev/null
+++ b/server/middleware/authenticate.js
@@ -0,0 +1,48 @@
+import jwt from 'jsonwebtoken';
+/* eslint-disable import/first */
+import dotenv from 'dotenv';
+
+dotenv.config();
+
+const { SECRET_OR_KEY } = process.env;
+
+class Authenticate {
+
+ static isLoggedIn(req, res, next) {
+ try {
+ const token = req.headers.authorization.split(' ')[1];
+ const decoded = jwt.verify(token, SECRET_OR_KEY);
+ req.user = decoded;
+ next();
+ } catch (e) {
+ return res.status(401).json({ message: 'Unauthorized' });
+ }
+ }
+
+ // Checks if user making the request is the store owner / admin
+ static isAdmin(req, res, next) {
+ if (req.user.type !== 1) {
+ return res.status(401).json({ message: 'Unauthorized' });
+ }
+ next();
+ }
+
+ // Checks if user making the request is the store attendant admin
+ // static isStoreAttendantAdmin(req, res, next) {
+ // if (req.user.type !== 2) {
+ // return res.status(401).json({ message: 'Unauthorized' });
+ // }
+ // next();
+ // }
+
+
+ // Checks if user making the request is the store attendant
+ static isStoreAttendant(req, res, next) {
+ if (req.user.type !== 3) {
+ return res.status(401).json({ message: 'Unauthorized' });
+ }
+ next();
+ }
+}
+
+export default Authenticate;
diff --git a/server/models/mockdb.js b/server/models/mockdb.js
new file mode 100644
index 0000000..ac406af
--- /dev/null
+++ b/server/models/mockdb.js
@@ -0,0 +1,100 @@
+module.exports = {
+ products: [
+ {
+ id: 1,
+ name: 'Google Pixel 2',
+ description: 'The Google Pixel 2 is powered by 1.9GHz octa-core processor and it comes with 4GB of RAM. The phone packs 64GB of internal storage that cannot be expanded.',
+ quantity: 50,
+ price: {
+ currency: '$',
+ amount: '649',
+ },
+ productImage: 'uploads\\products\\default.png',
+ },
+ {
+ id: 2,
+ name: 'Google Pixel 3',
+ description: 'The Pixel 3 is the latest causality. Wireless charging is a new feature for the Pixel phones, and a welcome change now that Google is launching the Pixel Stand wireless charger alongside its new devices.',
+ quantity: 75,
+ price: {
+ currency: '$',
+ amount: '649',
+ },
+ productImage: 'uploads\\products\\default.png',
+ },
+ {
+ id: 3,
+ name: 'iPhone 7 Plus',
+ description: 'The phone comes with a 5.50-inch touchscreen display with a resolution of 1080 pixels by 1920 pixels at a PPI of 401 pixels per inch.',
+ quantity: 125,
+ price: {
+ currency: '$',
+ amount: '649',
+ },
+ productImage: 'uploads\\products\\default.png',
+ },
+ ],
+ sales: [
+ {
+ id: 1,
+ store_attendant_user_id: 2,
+ order: [{
+ product_id: '2',
+ quantity: '5',
+ },
+ {
+ product_id: '2',
+ quantity: '10',
+ }],
+ totalSaleAmount: '50000',
+ date_time: '2018-10-12T13:04:51.884Z',
+
+ },
+ {
+ id: 2,
+ store_attendant_user_id: 1,
+ order: [{
+ product_id: '2',
+ quantity: '5',
+ },
+ {
+ product_id: 3,
+ quantity: 4,
+ }],
+ totalSaleAmount: '30000',
+ date_time: '2018-10-12T13:04:51.884Z',
+ },
+ ],
+ user: [
+ {
+ id: 1,
+ email: 'example@gmail.com',
+ password: '$2a$10$/SwT..8jeE8Bo0sOUZ7mc.yR1EXFrKA/5pa9RWj8LbsctDzJsG4EO',
+ name: 'Ezekiel Example',
+ status: 1,
+ type: 1,
+ userImage: 'uploads\\user\\default.png',
+ date_time: '2018-10-12T13:04:51.884Z',
+ },
+ {
+ id: 2,
+ email: 'example2@gmail.com',
+ password: '$2a$10$/SwT..8jeE8Bo0sOUZ7mc.yR1EXFrKA/5pa9RWj8LbsctDzJsG4EO',
+ name: 'Example Example',
+ status: 1,
+ type: 3,
+ userImage: 'uploads\\user\\default.png',
+ date_time: '2018-10-12T13:04:51.884Z',
+ },
+ {
+ id: 3,
+ email: 'example3@gmail.com',
+ password: '$2a$10$/SwT..8jeE8Bo0sOUZ7mc.yR1EXFrKA/5pa9RWj8LbsctDzJsG4EO',
+ name: 'Example Example',
+ status: 1,
+ type: 2,
+ userImage: 'uploads\\user\\default.png',
+ date_time: '2018-10-12T13:04:51.884Z',
+ },
+ ],
+};
diff --git a/server/routes/api/v1/products.js b/server/routes/api/v1/products.js
new file mode 100644
index 0000000..296b174
--- /dev/null
+++ b/server/routes/api/v1/products.js
@@ -0,0 +1,65 @@
+import express from 'express';
+// import multer from 'multer';
+
+import authenticate from '../../../middleware/authenticate';
+import productController from '../../../controllers/productController';
+
+const { isLoggedIn, isAdmin } = authenticate;
+const {
+ createProduct, getProducts, getProductById, deleteProductById,
+} = productController;
+
+// const fileFilter = (req, file, cb) => {
+// // reject a file
+// if (file.mimetype === 'image/jpeg' || file.mimetype === 'image/png') {
+// cb(null, true);
+// } else {
+// cb(null, false);
+// }
+// };
+// const storage = multer.diskStorage({
+// destination: (req, file, cb) => {
+// cb(null, 'uploads/products/');
+// },
+// filename: (req, file, cb) => {
+// cb(null, new Date().getTime() + file.originalname);
+// },
+// });
+
+// const upload = multer(
+// {
+// storage,
+// limits: {
+// fileSize: 1024 * 1024 * 5,
+
+// },
+// fileFilter,
+// },
+// );
+
+const router = express.Router();
+
+// @route GET api/v1/products
+// @desc Get/Fetch all products
+// @access Private
+router.get('/', isLoggedIn, getProducts);
+
+
+// @route GET api/v1/products/
+// @desc Get/Fetch a single product record
+// @access Private
+router.get('/:id', isLoggedIn, getProductById);
+
+// @route POST api/v1/products/
+// @desc Create a product
+// @access Private
+// router.post('/', authenticate, upload.single('productImage'), createProduct);
+router.post('/', isLoggedIn, isAdmin, createProduct);
+
+
+// @route DELETE api/v1/products/
+// @desc Delete a single product record
+// @access Private
+router.delete('/:id', isLoggedIn, isAdmin, deleteProductById);
+
+module.exports = router;
diff --git a/server/routes/api/v1/sales.js b/server/routes/api/v1/sales.js
new file mode 100644
index 0000000..fe97984
--- /dev/null
+++ b/server/routes/api/v1/sales.js
@@ -0,0 +1,27 @@
+import express from 'express';
+
+import authenticate from '../../../middleware/authenticate';
+// import db from '../../../models/db';
+import saleController from '../../../controllers/saleController';
+
+const { isLoggedIn, isAdmin, isStoreAttendant } = authenticate;
+const { createSale, getSales, getSaleById } = saleController;
+const router = express.Router();
+
+// @route GET api/v1/sales
+// @desc Get/Fetch all sale records
+// @access Private
+router.get('/', isLoggedIn, isAdmin, getSales);
+
+
+// @route GET api/v1/sales/
+// @desc Get/Fetch a single sale record
+// @access Private
+router.get('/:id', isLoggedIn, getSaleById);
+
+// @route POST api/v1/sales
+// @desc Create a sale order
+// @access Private
+router.post('/', isLoggedIn, isStoreAttendant, createSale);
+
+module.exports = router;
diff --git a/server/routes/api/v1/users.js b/server/routes/api/v1/users.js
new file mode 100644
index 0000000..d2d43bc
--- /dev/null
+++ b/server/routes/api/v1/users.js
@@ -0,0 +1,29 @@
+import express from 'express';
+
+import authenticate from '../../../middleware/authenticate';
+import usersController from '../../../controllers/userController';
+
+const { login, signup, getCurrentUser } = usersController;
+
+const { isLoggedIn, isAdmin } = authenticate;
+const router = express.Router();
+
+// @route POST api/v1/users/register
+// @desc Register user
+// @access Private
+router.post('/signup', isLoggedIn, isAdmin, signup);
+
+
+// @route POST api/v1/users/register
+// @desc Login user / Returning JWT Token
+// @access Public
+router.post('/login', login);
+
+
+// @route GET api/v1/users/current
+// @desc Return current user
+// @access Private
+router.get('/current', isLoggedIn, getCurrentUser);
+
+
+module.exports = router;
diff --git a/server/test/.eslintrc.json b/server/test/.eslintrc.json
new file mode 100644
index 0000000..f22b7c0
--- /dev/null
+++ b/server/test/.eslintrc.json
@@ -0,0 +1,6 @@
+{
+ "env": {
+ "node": true,
+ "mocha": true
+ }
+}
\ No newline at end of file
diff --git a/server/test/appTest.js b/server/test/appTest.js
new file mode 100644
index 0000000..4aad728
--- /dev/null
+++ b/server/test/appTest.js
@@ -0,0 +1,28 @@
+import chai from 'chai';
+import chaiHttp from 'chai-http';
+
+import app from '../app';
+
+const { expect } = chai;
+
+chai.use(chaiHttp);
+
+describe('App', () => {
+ it('returns welcome to API message', (done) => {
+ chai.request(app).get('/')
+ .end((err, res) => {
+ expect(res).to.have.status(200);
+ expect(res.body.message).to.equal('Welcome To Store Manager API');
+ });
+ done();
+ });
+
+ it('returns 404 because route does not exist', (done) => {
+ chai.request(app).post('/')
+ .end((err, res) => {
+ expect(res).to.have.status(404);
+ expect(res.body.error.message).to.equal('Not found');
+ });
+ done();
+ });
+});
diff --git a/server/test/productsTest.js b/server/test/productsTest.js
new file mode 100644
index 0000000..6f83cc2
--- /dev/null
+++ b/server/test/productsTest.js
@@ -0,0 +1,267 @@
+import chai from 'chai';
+import chaiHttp from 'chai-http';
+import app from '../app';
+
+const { expect } = chai;
+
+chai.use(chaiHttp);
+
+describe('Get Products', () => {
+ it('returns array of all products', (done) => {
+ chai.request(app).post('/api/v1/users/login')
+ .send({
+ email: 'example@gmail.com', password: '123456',
+ })
+ .end((err, res) => {
+ const { token } = res.body;
+ expect(res).to.have.status(200);
+ expect(res.body).to.be.an('object');
+ expect(res.body.success).to.equal(true);
+ chai.request(app).get('/api/v1/products')
+ .set('Authorization', token)
+ .end((error, data) => {
+ expect(data).to.have.status(200);
+ expect(data.body).to.be.an('array');
+ done();
+ });
+ });
+ });
+
+ it('returns unauthorized because user is not logged in', (done) => {
+ chai.request(app).get('/api/v1/products')
+ .end((error, res) => {
+ expect(res).to.have.status(401);
+ done();
+ });
+ });
+});
+
+describe('Get A Product', () => {
+ it('returns details of a product', (done) => {
+ chai.request(app).post('/api/v1/users/login')
+ .send({
+ email: 'example@gmail.com', password: '123456',
+ })
+ .end((err, res) => {
+ const { token } = res.body;
+ expect(res).to.have.status(200);
+ expect(res.body).to.be.an('object');
+ expect(res.body.success).to.equal(true);
+ const id = 2;
+ chai.request(app).get(`/api/v1/products/${id}`)
+ .set('Authorization', token)
+ .end((error, data) => {
+ expect(data).to.have.status(200);
+ expect(id).to.equal(data.body.id);
+ done();
+ });
+ });
+ });
+
+ it('return product not found error', (done) => {
+ chai.request(app).post('/api/v1/users/login')
+ .send({
+ email: 'example@gmail.com', password: '123456',
+ })
+ .end((err, res) => {
+ const { token } = res.body;
+ expect(res).to.have.status(200);
+ expect(res.body).to.be.an('object');
+ expect(res.body.success).to.equal(true);
+ const id = 89;
+ chai.request(app).get(`/api/v1/products/${id}`)
+ .set('Authorization', token)
+ .end((error, data) => {
+ expect(data).to.have.status(400);
+ expect(res.body).to.be.an('object');
+ expect(data.body.message).to.equal(`Product with id ${id} not found.`);
+ done();
+ });
+ });
+ });
+
+ it('returns unauthorized because user is not logged in', (done) => {
+ const id = 2;
+ chai.request(app).get(`/api/v1/products/${id}`)
+ .end((error, res) => {
+ expect(res).to.have.status(401);
+ done();
+ });
+ });
+});
+
+describe('Create New Product', () => {
+ it('return unauthorized because user is not admin', (done) => {
+ chai.request(app).post('/api/v1/users/login')
+ .send({
+ email: 'example2@gmail.com', password: '123456',
+ })
+ .end((err, res) => {
+ const { token } = res.body;
+ expect(res).to.have.status(200);
+ expect(res.body).to.be.an('object');
+ expect(res.body.success).to.equal(true);
+ chai.request(app).post('/api/v1/products')
+ .set('Authorization', token)
+ .end((error, data) => {
+ expect(data).to.have.status(401);
+ expect(data.body.message).to.equal('Unauthorized');
+ done();
+ });
+ });
+ });
+
+ it('return validation error if no data is sent', (done) => {
+ chai.request(app).post('/api/v1/users/login')
+ .send({
+ email: 'example@gmail.com', password: '123456',
+ })
+ .end((err, res) => {
+ const { token } = res.body;
+ expect(res).to.have.status(200);
+ expect(res.body).to.be.an('object');
+ expect(res.body.success).to.equal(true);
+ chai.request(app).post('/api/v1/products')
+ .set('Authorization', token)
+ .send({
+ name: 'iPhone',
+ })
+ .end((error, data) => {
+ expect(data).to.have.status(400);
+ expect(data.body).to.be.an('object');
+ // expect(data.body.name).to.equal('Name field is required');
+ expect(data.body.description).to.equal('Description field is required');
+ expect(data.body.price).to.equal('Price field is required');
+ expect(data.body.quantity).to.equal('Quantity field is required');
+ done();
+ });
+ });
+ });
+
+
+ it('create a new product', (done) => {
+ chai.request(app).post('/api/v1/users/login')
+ .send({
+ email: 'example@gmail.com', password: '123456',
+ })
+ .end((err, res) => {
+ const { token } = res.body;
+ expect(res).to.have.status(200);
+ expect(res.body).to.be.an('object');
+ expect(res.body.success).to.equal(true);
+ chai.request(app).post('/api/v1/products')
+ .send({
+ name: 'Tecno', description: 'Tecno Phone', quantity: '2', price: '$200',
+ })
+ .set('Authorization', token)
+ .end((error, data) => {
+ expect(data).to.have.status(201);
+ expect(data.body).to.be.an('object');
+ expect(data.body.message).to.equal('Product added successfully');
+ done();
+ });
+ });
+ });
+
+ it('returns unauthorized because user is not logged in', (done) => {
+ chai.request(app).post('/api/v1/products')
+ .send({
+ name: 'Tecno', description: 'Tecno Phone', quantity: '2', price: '$200',
+ })
+ .end((error, res) => {
+ expect(res).to.have.status(401);
+ done();
+ });
+ });
+});
+
+
+describe('Delete A Product', () => {
+ it('should delete a product', (done) => {
+ chai.request(app).post('/api/v1/users/login')
+ .send({
+ email: 'example@gmail.com', password: '123456',
+ })
+ .end((err, res) => {
+ const { token } = res.body;
+ expect(res).to.have.status(200);
+ expect(res.body).to.be.an('object');
+ expect(res.body.success).to.equal(true);
+ chai.request(app).del('/api/v1/products/3')
+ .set('Authorization', token)
+ .end((error, data) => {
+ expect(data).to.have.status(200);
+ expect(data.body).to.be.an('object');
+ expect(data.body.message).to.equal('Product with id 3 deleted successfully.');
+ done();
+ });
+ });
+ });
+
+ it('should return unauthorized because user does not have right access', (done) => {
+ chai.request(app).post('/api/v1/users/login')
+ .send({
+ email: 'example2@gmail.com', password: '123456',
+ })
+ .end((err, res) => {
+ const { token } = res.body;
+ expect(res).to.have.status(200);
+ expect(res.body).to.be.an('object');
+ expect(res.body.success).to.equal(true);
+ chai.request(app).del('/api/v1/products/3')
+ .set('Authorization', token)
+ .end((error, data) => {
+ expect(data).to.have.status(401);
+ done();
+ });
+ });
+ });
+
+ it('should return product not found', (done) => {
+ chai.request(app).post('/api/v1/users/login')
+ .send({
+ email: 'example@gmail.com', password: '123456',
+ })
+ .end((err, res) => {
+ const { token } = res.body;
+ expect(res).to.have.status(200);
+ expect(res.body).to.be.an('object');
+ expect(res.body.success).to.equal(true);
+ chai.request(app).del('/api/v1/products/59')
+ .set('Authorization', token)
+ .end((error, data) => {
+ expect(data).to.have.status(400);
+ expect(data.body).to.be.an('object');
+ expect(data.body.message).to.equal('Product with id 59 not found.');
+ done();
+ });
+ });
+ });
+
+ it('should return product not found because the product has just been deleted', (done) => {
+ chai.request(app).post('/api/v1/users/login')
+ .send({
+ email: 'example@gmail.com', password: '123456',
+ })
+ .end((err, res) => {
+ const { token } = res.body;
+ expect(res).to.have.status(200);
+ expect(res.body).to.be.an('object');
+ expect(res.body.success).to.equal(true);
+ chai.request(app).del('/api/v1/products/2')
+ .set('Authorization', token)
+ .end((error, data) => {
+ expect(data).to.have.status(200);
+ expect(data.body).to.be.an('object');
+ chai.request(app).del('/api/v1/products/2')
+ .set('Authorization', token)
+ .end((error2, data2) => {
+ expect(data2).to.have.status(400);
+ expect(data2.body).to.be.an('object');
+ expect(data2.body.message).to.equal('Product with id 2 not found.');
+ done();
+ });
+ });
+ });
+ });
+});
diff --git a/server/test/salesTest.js b/server/test/salesTest.js
new file mode 100644
index 0000000..6178f0c
--- /dev/null
+++ b/server/test/salesTest.js
@@ -0,0 +1,254 @@
+import chai from 'chai';
+import chaiHttp from 'chai-http';
+import app from '../app';
+
+const { expect } = chai;
+
+chai.use(chaiHttp);
+
+describe('Get All Sale Records', () => {
+ it('returns array of all sale records', (done) => {
+ chai.request(app).post('/api/v1/users/login')
+ .send({
+ email: 'example@gmail.com', password: '123456',
+ })
+ .end((err, res) => {
+ const { token } = res.body;
+ expect(res).to.have.status(200);
+ expect(res.body).to.be.an('object');
+ expect(res.body.success).to.equal(true);
+ chai.request(app).get('/api/v1/sales')
+ .set('Authorization', token)
+ .end((error, data) => {
+ expect(data).to.have.status(200);
+ expect(data.body).to.be.an('array');
+ done();
+ });
+ });
+ });
+
+ it('returns error because only store owner / admin has access to view all sales', (done) => {
+ chai.request(app).post('/api/v1/users/login')
+ .send({
+ email: 'example2@gmail.com', password: '123456',
+ })
+ .end((err, res) => {
+ const { token } = res.body;
+ expect(res).to.have.status(200);
+ expect(res.body).to.be.an('object');
+ expect(res.body.success).to.equal(true);
+ chai.request(app).get('/api/v1/sales')
+ .set('Authorization', token)
+ .end((error, data) => {
+ expect(data).to.have.status(401);
+ done();
+ });
+ });
+ });
+
+ it('returns unauthorized because user is not logged in', (done) => {
+ chai.request(app).get('/api/v1/sales')
+ .end((error, res) => {
+ expect(res).to.have.status(401);
+ done();
+ });
+ });
+});
+
+describe('Get A Sale Record', () => {
+ it('returns details of a sale record', (done) => {
+ chai.request(app).post('/api/v1/users/login')
+ .send({
+ email: 'example@gmail.com', password: '123456',
+ })
+ .end((err, res) => {
+ const { token } = res.body;
+ expect(res).to.have.status(200);
+ expect(res.body).to.be.an('object');
+ expect(res.body.success).to.equal(true);
+ const id = 2;
+ chai.request(app).get(`/api/v1/sales/${id}`)
+ .set('Authorization', token)
+ .end((error, data) => {
+ expect(data).to.have.status(200);
+ expect(id).to.equal(data.body.id);
+ done();
+ });
+ });
+ });
+
+ it('returns unauthorized because he/she did not create the sale || is not store owner / admin', (done) => {
+ chai.request(app).post('/api/v1/users/login')
+ .send({
+ email: 'example3@gmail.com', password: '123456',
+ })
+ .end((err, res) => {
+ const { token } = res.body;
+ expect(res).to.have.status(200);
+ expect(res.body).to.be.an('object');
+ expect(res.body.success).to.equal(true);
+ const id = 2;
+ chai.request(app).get(`/api/v1/sales/${id}`)
+ .set('Authorization', token)
+ .end((error, data) => {
+ expect(data).to.have.status(401);
+ done();
+ });
+ });
+ });
+
+ it('return sale not found error', (done) => {
+ chai.request(app).post('/api/v1/users/login')
+ .send({
+ email: 'example@gmail.com', password: '123456',
+ })
+ .end((err, res) => {
+ const { token } = res.body;
+ expect(res).to.have.status(200);
+ expect(res.body).to.be.an('object');
+ expect(res.body.success).to.equal(true);
+ const id = 299;
+ chai.request(app).get(`/api/v1/sales/${id}`)
+ .set('Authorization', token)
+ .end((error, data) => {
+ expect(data).to.have.status(400);
+ expect(data.body).to.be.an('object');
+ expect(data.body.message).to.equal(`Sales with id ${id} not found.`);
+ done();
+ });
+ });
+ });
+
+ it('returns unauthorized because user is not logged in', (done) => {
+ const id = 2;
+ chai.request(app).get(`/api/v1/sales/${id}`)
+ .end((error, res) => {
+ expect(res).to.have.status(401);
+ done();
+ });
+ });
+});
+
+describe('Create New Sale Record', () => {
+ it('create a new sale', (done) => {
+ chai.request(app).post('/api/v1/users/login')
+ .send({
+ email: 'example2@gmail.com', password: '123456',
+ })
+ .end((err, res) => {
+ const { token } = res.body;
+ expect(res).to.have.status(200);
+ expect(res.body).to.be.an('object');
+ expect(res.body.success).to.equal(true);
+ chai.request(app).post('/api/v1/sales')
+ .send({
+ order: [{ quantity: 2, product_id: 2 }, { quantity: 8, product_id: 1 }],
+ })
+ .set('Authorization', token)
+ .end((error, data) => {
+ expect(data).to.have.status(201);
+ expect(data.body).to.be.an('object');
+ expect(data.body.message).to.equal('Sale added successfully');
+ done();
+ });
+ });
+ });
+
+ it('return validation error if no data is sent', (done) => {
+ chai.request(app).post('/api/v1/users/login')
+ .send({
+ email: 'example2@gmail.com', password: '123456',
+ })
+ .end((err, res) => {
+ const { token } = res.body;
+ expect(res).to.have.status(200);
+ expect(res.body).to.be.an('object');
+ expect(res.body.success).to.equal(true);
+ chai.request(app).post('/api/v1/sales')
+ .set('Authorization', token)
+ .end((error, data) => {
+ expect(data).to.have.status(400);
+ expect(data.body).to.be.an('object');
+ done();
+ });
+ });
+ });
+
+ it('return error because quantity of product requested is more than quantity in store', (done) => {
+ chai.request(app).post('/api/v1/users/login')
+ .send({
+ email: 'example2@gmail.com', password: '123456',
+ })
+ .end((err, res) => {
+ const { token } = res.body;
+ expect(res).to.have.status(200);
+ expect(res.body).to.be.an('object');
+ expect(res.body.success).to.equal(true);
+ chai.request(app).post('/api/v1/sales')
+ .set('Authorization', token)
+ .send({
+ order: [{ quantity: 200, product_id: 2 }, { quantity: 8, product_id: 1 }],
+ })
+ .end((error, data) => {
+ expect(data).to.have.status(400);
+ expect(data.body).to.be.an('object');
+ expect(data.body.message).to.equal('One Of Product Requested Is More Than In Stock');
+ done();
+ });
+ });
+ });
+
+ it('return error because one of product requested is not available in store', (done) => {
+ chai.request(app).post('/api/v1/users/login')
+ .send({
+ email: 'example2@gmail.com', password: '123456',
+ })
+ .end((err, res) => {
+ const { token } = res.body;
+ expect(res).to.have.status(200);
+ expect(res.body).to.be.an('object');
+ expect(res.body.success).to.equal(true);
+ chai.request(app).post('/api/v1/sales')
+ .set('Authorization', token)
+ .send({
+ order: [{ quantity: 200, product_id: 299 }, { quantity: 8, product_id: 3 }],
+ })
+ .end((error, data) => {
+ expect(data).to.have.status(400);
+ expect(data.body).to.be.an('object');
+ expect(data.body.message).to.equal('One Of Product Requested Is Not Available');
+ done();
+ });
+ });
+ });
+
+ it('return unauthorized because only store attendant can create a sale record', (done) => {
+ chai.request(app).post('/api/v1/users/login')
+ .send({
+ email: 'example@gmail.com', password: '123456',
+ })
+ .end((err, res) => {
+ const { token } = res.body;
+ expect(res).to.have.status(200);
+ expect(res.body).to.be.an('object');
+ expect(res.body.success).to.equal(true);
+ chai.request(app).post('/api/v1/sales')
+ .set('Authorization', token)
+ .send({
+ order: [{ quantity: 2, product_id: 2 }, { quantity: 8, product_id: 3 }],
+ })
+ .end((error, data) => {
+ expect(data).to.have.status(401);
+ done();
+ });
+ });
+ });
+
+ it('returns unauthorized because user is not logged in', (done) => {
+ chai.request(app).post('/api/v1/sales')
+ .end((error, res) => {
+ expect(res).to.have.status(401);
+ done();
+ });
+ });
+});
diff --git a/server/test/userTest.js b/server/test/userTest.js
new file mode 100644
index 0000000..e5c4045
--- /dev/null
+++ b/server/test/userTest.js
@@ -0,0 +1,202 @@
+import chai from 'chai';
+import chaiHttp from 'chai-http';
+import app from '../app';
+
+const { expect } = chai;
+
+chai.use(chaiHttp);
+
+describe('Signup Route', () => {
+ it('create a user and return user details', (done) => {
+ chai.request(app).post('/api/v1/users/login')
+ .send({
+ email: 'example@gmail.com',
+ password: '123456',
+ })
+ .end((loginerr, loginres) => {
+ const { token } = loginres.body;
+ expect(loginres).to.have.status(200);
+ expect(loginres.body).to.be.an('object');
+ expect(loginres.body.success).to.equal(true);
+ expect(loginres.body.token).to.include('Bearer');
+
+ chai.request(app).post('/api/v1/users/signup')
+ .set('Authorization', token)
+ .send({
+ email: 'a@gmail.com',
+ name: 'John Example',
+ password: '123456',
+ type: '1',
+ })
+ .end((err, res) => {
+ expect(res).to.have.status(201);
+ expect(res.body).to.be.an('object');
+ expect(res.body.message).to.equal('User Created Successfully');
+ done();
+ });
+ });
+ });
+
+ it('return validation error if no data is sent', (done) => {
+ chai.request(app).post('/api/v1/users/login')
+ .send({
+ email: 'example@gmail.com',
+ password: '123456',
+ })
+ .end((loginerr, loginres) => {
+ const { token } = loginres.body;
+ expect(loginres).to.have.status(200);
+ expect(loginres.body).to.be.an('object');
+ expect(loginres.body.success).to.equal(true);
+ expect(loginres.body.token).to.include('Bearer');
+ chai.request(app).post('/api/v1/users/signup')
+ .set('Authorization', token)
+ .end((err, res) => {
+ expect(res).to.have.status(400);
+ expect(res.body).to.be.an('object');
+ expect(res.body.email).to.equal('Email field is required');
+ expect(res.body.password).to.equal('Password field is required');
+ expect(res.body.name).to.equal('Name field is required');
+ expect(res.body.type).to.equal('Type field is required');
+ done();
+ });
+ });
+ });
+
+ it('return email already exist', (done) => {
+ chai.request(app).post('/api/v1/users/login')
+ .send({
+ email: 'example@gmail.com',
+ password: '123456',
+ })
+ .end((loginerr, loginres) => {
+ const { token } = loginres.body;
+ expect(loginres).to.have.status(200);
+ expect(loginres.body).to.be.an('object');
+ expect(loginres.body.success).to.equal(true);
+ expect(loginres.body.token).to.include('Bearer');
+ chai.request(app).post('/api/v1/users/signup')
+ .set('Authorization', token)
+ .send({
+ email: 'example@gmail.com',
+ name: 'John Example',
+ password: '123456',
+ type: '1',
+ })
+ .end((err, res) => {
+ expect(res).to.have.status(409);
+ expect(res.body).to.be.an('object');
+ expect(res.body.email).to.equal('Email Already Exist');
+ done();
+ });
+ });
+ });
+});
+
+
+describe('Login Route', () => {
+ it('return token', (done) => {
+ chai.request(app).post('/api/v1/users/login')
+ .send({
+ email: 'example@gmail.com',
+ password: '123456',
+ })
+ .end((err, res) => {
+ expect(res).to.have.status(200);
+ expect(res.body).to.be.an('object');
+ expect(res.body.success).to.equal(true);
+ expect(res.body.token).to.include('Bearer');
+
+ done();
+ });
+ });
+
+ it('return validation error if no data is sent', (done) => {
+ chai.request(app).post('/api/v1/users/login')
+ .end((err, res) => {
+ expect(res).to.have.status(400);
+ expect(res.body).to.be.an('object');
+ expect(res.body.email).to.equal('Email field is required');
+ expect(res.body.password).to.equal('Password field is required');
+
+ done();
+ });
+ });
+
+ it('return user not found', (done) => {
+ chai.request(app).post('/api/v1/users/login')
+ .send({
+ email: 'example232@gmail.com',
+ password: '123456',
+ })
+ .end((err, res) => {
+ expect(res).to.have.status(404);
+ expect(res.body).to.be.an('object');
+ expect(res.body.email).to.equal('User Not Found');
+ done();
+ });
+ });
+
+ it('return incorrect password', (done) => {
+ chai.request(app).post('/api/v1/users/login')
+ .send({
+ email: 'example@gmail.com',
+ password: '1234',
+ })
+ .end((err, res) => {
+ expect(res).to.have.status(401);
+ expect(res.body).to.be.an('object');
+ expect(res.body.password).to.equal('Incorrect Password');
+ done();
+ });
+ });
+});
+
+describe('Get Current user', () => {
+ it('returns details of current user', (done) => {
+ chai.request(app).post('/api/v1/users/login')
+ .send({
+ email: 'example@gmail.com', password: '123456',
+ })
+ .end((err, res) => {
+ const { token } = res.body;
+ expect(res).to.have.status(200);
+ expect(res.body).to.be.an('object');
+ expect(res.body.success).to.equal(true);
+ chai.request(app).get('/api/v1/users/current')
+ .set('Authorization', token)
+ .end((error, data) => {
+ expect(data).to.have.status(200);
+ expect(data.body).to.be.an('object');
+ done();
+ });
+ });
+ });
+
+ it('returns unauthorized because user is not logged in', (done) => {
+ chai.request(app).get('/api/v1/users/current')
+ .end((error, data) => {
+ expect(data).to.have.status(401);
+ done();
+ });
+ });
+
+ it('returns 404 error because post method is not allowed', (done) => {
+ chai.request(app).post('/api/v1/users/login')
+ .send({
+ email: 'example@gmail.com', password: '123456',
+ })
+ .end((err, res) => {
+ const { token } = res.body;
+ expect(res).to.have.status(200);
+ expect(res.body).to.be.an('object');
+ expect(res.body.success).to.equal(true);
+ chai.request(app).post('/api/v1/users/current')
+ .set('Authorization', token)
+ .end((error, data) => {
+ expect(data).to.have.status(404);
+ done();
+ });
+ });
+ });
+});
diff --git a/server/test/usersValidationTest.js b/server/test/usersValidationTest.js
new file mode 100644
index 0000000..b95853c
--- /dev/null
+++ b/server/test/usersValidationTest.js
@@ -0,0 +1,51 @@
+import chai from 'chai';
+import chaiHttp from 'chai-http';
+
+import usersValidation from '../validation/users';
+
+const { expect } = chai;
+
+chai.use(chaiHttp);
+
+describe('Login Validation', () => {
+ it('returns empty object because all validation is passed', (done) => {
+ const result = usersValidation.validateLoginInput({ email: 'example@gmail.com', password: '123456' });
+ expect(result.isValid).to.equal(true);
+ expect(Object.keys(result.errors).length).to.equal(0);
+ done();
+ });
+
+ it('returns object of validation required', (done) => {
+ const result = usersValidation.validateLoginInput({});
+ expect(result.isValid).to.equal(false);
+ expect(Object.keys(result.errors).length).to.be.greaterThan(0);
+ expect(result.errors.email).to.equal('Email field is required');
+ expect(result.errors.password).to.equal('Password field is required');
+ expect(result.errors).to.be.an('object');
+ done();
+ });
+});
+
+
+describe('Login Validation', () => {
+ it('returns empty object because all validation is passed', (done) => {
+ const result = usersValidation.validateLoginInput(
+ {
+ email: 'example@gmail.com', password: '123456', name: 'Example', type: '2',
+ },
+ );
+ expect(result.isValid).to.equal(true);
+ expect(Object.keys(result.errors).length).to.equal(0);
+ done();
+ });
+
+ it('returns object of validation required', (done) => {
+ const result = usersValidation.validateLoginInput({});
+ expect(result.isValid).to.equal(false);
+ expect(Object.keys(result.errors).length).to.be.greaterThan(0);
+ expect(result.errors.email).to.equal('Email field is required');
+ expect(result.errors.password).to.equal('Password field is required');
+ expect(result.errors).to.be.an('object');
+ done();
+ });
+});
diff --git a/server/test/validationTest.js b/server/test/validationTest.js
new file mode 100644
index 0000000..3b799a9
--- /dev/null
+++ b/server/test/validationTest.js
@@ -0,0 +1,120 @@
+import chai from 'chai';
+import chaiHttp from 'chai-http';
+
+import usersValidation from '../validation/users';
+import productsValidation from '../validation/products';
+import salesValidation from '../validation/sales';
+import isEmpty from '../validation/isEmpty';
+
+const { expect } = chai;
+
+chai.use(chaiHttp);
+
+describe('Login Validation', () => {
+ it('returns empty object because all validation is passed', (done) => {
+ const result = usersValidation.validateLoginInput({ email: 'example@gmail.com', password: '123456' });
+ expect(result.isValid).to.equal(true);
+ expect(Object.keys(result.errors).length).to.equal(0);
+ done();
+ });
+
+ it('returns object of validation required', (done) => {
+ const result = usersValidation.validateLoginInput({});
+ expect(result.isValid).to.equal(false);
+ expect(Object.keys(result.errors).length).to.be.greaterThan(0);
+ expect(result.errors.email).to.equal('Email field is required');
+ expect(result.errors.password).to.equal('Password field is required');
+ expect(result.errors).to.be.an('object');
+ done();
+ });
+});
+
+
+describe('Signup Validation', () => {
+ it('returns empty object because all validation is passed', (done) => {
+ const result = usersValidation.validateSignupInput(
+ {
+ email: 'example@gmail.com', password: '123456', name: 'Example', type: '2',
+ },
+ );
+ expect(result.isValid).to.equal(true);
+ expect(Object.keys(result.errors).length).to.equal(0);
+ done();
+ });
+
+ it('returns object of validation required', (done) => {
+ const result = usersValidation.validateSignupInput({});
+ expect(result.isValid).to.equal(false);
+ expect(Object.keys(result.errors).length).to.be.greaterThan(0);
+ expect(result.errors.email).to.equal('Email field is required');
+ expect(result.errors.password).to.equal('Password field is required');
+ expect(result.errors.name).to.equal('Name field is required');
+ expect(result.errors.type).to.equal('Type field is required');
+ expect(result.errors).to.be.an('object');
+ done();
+ });
+});
+
+
+describe('Product Validation', () => {
+ it('returns empty object because all validation is passed', (done) => {
+ const result = productsValidation.validateProductInput(
+ {
+ name: 'Samsung Galaxy', description: 'Good Phone', price: '300', quantity: '29',
+ },
+ );
+ expect(result.isValid).to.equal(true);
+ expect(Object.keys(result.errors).length).to.equal(0);
+ done();
+ });
+
+ it('returns object of validation required', (done) => {
+ const result = productsValidation.validateProductInput({});
+ expect(result.isValid).to.equal(false);
+ expect(Object.keys(result.errors).length).to.be.greaterThan(0);
+ expect(result.errors.name).to.equal('Name field is required');
+ expect(result.errors.description).to.equal('Description field is required');
+ expect(result.errors.price).to.equal('Price field is required');
+ expect(result.errors.quantity).to.equal('Quantity field is required');
+ expect(result.errors).to.be.an('object');
+ done();
+ });
+});
+
+
+describe('Sales Validation', () => {
+ it('returns empty object because all validation is passed', (done) => {
+ const result = salesValidation.validateSalesInput(
+ {
+ order: [{ product_id: '2', quantity: '5' }],
+ },
+ );
+ expect(result.isValid).to.equal(true);
+ expect(Object.keys(result.errors).length).to.equal(0);
+ done();
+ });
+
+ it('returns object of validation required', (done) => {
+ const result = salesValidation.validateSalesInput({});
+ expect(result.isValid).to.equal(false);
+ expect(Object.keys(result.errors).length).to.be.greaterThan(0);
+ expect(result.errors.order).to.equal('Order must be an array of object(s)');
+ expect(result.errors).to.be.an('object');
+ done();
+ });
+});
+
+
+describe('isEmpty Function', () => {
+ it('returns false because input passed is not empty', (done) => {
+ const result = isEmpty('example@gmail.com');
+ expect(result).to.equal(false);
+ done();
+ });
+
+ it('returns true because input passed is empty', (done) => {
+ const result = isEmpty('');
+ expect(result).to.equal(true);
+ done();
+ });
+});
diff --git a/server/validation/isEmpty.js b/server/validation/isEmpty.js
new file mode 100644
index 0000000..34b1339
--- /dev/null
+++ b/server/validation/isEmpty.js
@@ -0,0 +1,8 @@
+const isEmpty = (value) => {
+ if (value === undefined || value === null || (typeof value === 'object' && Object.keys(value).length === 0) || (typeof value === 'string' && value.trim().length === 0)) {
+ return true;
+ }
+ return false;
+};
+
+module.exports = isEmpty;
diff --git a/server/validation/products.js b/server/validation/products.js
new file mode 100644
index 0000000..1327fcb
--- /dev/null
+++ b/server/validation/products.js
@@ -0,0 +1,36 @@
+import Validator from 'validator';
+import isEmpty from './isEmpty';
+
+const validateProductInput = (input) => {
+ const errors = {};
+ const data = input;
+ data.name = !isEmpty(data.name) ? data.name : '';
+ data.description = !isEmpty(data.description) ? data.description : '';
+ data.quantity = !isEmpty(data.quantity) ? data.quantity : '';
+ data.price = !isEmpty(data.price) ? data.price : '';
+
+ if (Validator.isEmpty(data.name)) {
+ errors.name = 'Name field is required';
+ }
+
+ if (Validator.isEmpty(data.description)) {
+ errors.description = 'Description field is required';
+ }
+
+ if (Validator.isEmpty(data.price)) {
+ errors.price = 'Price field is required';
+ }
+
+ if (Validator.isEmpty(data.quantity)) {
+ errors.quantity = 'Quantity field is required';
+ }
+
+ return {
+ errors,
+ isValid: isEmpty(errors),
+ };
+};
+
+module.exports = {
+ validateProductInput,
+};
diff --git a/server/validation/sales.js b/server/validation/sales.js
new file mode 100644
index 0000000..f0b5b74
--- /dev/null
+++ b/server/validation/sales.js
@@ -0,0 +1,22 @@
+import isEmpty from './isEmpty';
+
+const validateSalesInput = (input) => {
+ const errors = {};
+ const data = input;
+ data.quantity = !isEmpty(data.quantity) ? data.quantity : '';
+ data.product_id = !isEmpty(data.product_id) ? data.product_id : '';
+
+ if (!Array.isArray(data.order)) {
+ errors.order = 'Order must be an array of object(s)';
+ }
+
+
+ return {
+ errors,
+ isValid: isEmpty(errors),
+ };
+};
+
+module.exports = {
+ validateSalesInput,
+};
diff --git a/server/validation/users.js b/server/validation/users.js
new file mode 100644
index 0000000..10afc7a
--- /dev/null
+++ b/server/validation/users.js
@@ -0,0 +1,78 @@
+import Validator from 'validator';
+import isEmpty from './isEmpty';
+
+const validateSignupInput = (input) => {
+ const errors = {};
+ const data = input;
+ data.name = !isEmpty(data.name) ? data.name : '';
+ data.email = !isEmpty(data.email) ? data.email : '';
+ data.password = !isEmpty(data.password) ? data.password : '';
+ data.type = !isEmpty(data.type) ? data.type : '';
+
+
+ if (!Validator.isLength(data.name, { min: 2, max: 30 })) {
+ errors.name = 'Name must be between 2 and 30 characters';
+ }
+
+ if (Validator.isEmpty(data.name)) {
+ errors.name = 'Name field is required';
+ }
+
+ if (Validator.isEmpty(data.email)) {
+ errors.email = 'Email field is required';
+ }
+
+ if (!errors.email) {
+ if (!Validator.isEmail(data.email)) {
+ errors.email = 'Email is invalid';
+ }
+ }
+
+ if (Validator.isEmpty(data.password)) {
+ errors.password = 'Password field is required';
+ }
+
+ if (!errors.password) {
+ if (!Validator.isLength(data.password, { min: 6, max: 30 })) {
+ errors.password = 'Password must be at least 6 characters';
+ }
+ }
+
+ if (Validator.isEmpty(data.type)) {
+ errors.type = 'Type field is required';
+ }
+
+ return {
+ errors,
+ isValid: isEmpty(errors),
+ };
+};
+
+const validateLoginInput = (input) => {
+ const errors = {};
+ const data = input;
+ data.email = !isEmpty(data.email) ? data.email : '';
+ data.password = !isEmpty(data.password) ? data.password : '';
+
+ if (!Validator.isEmail(data.email)) {
+ errors.email = 'Email is invalid';
+ }
+
+ if (Validator.isEmpty(data.email)) {
+ errors.email = 'Email field is required';
+ }
+
+ if (Validator.isEmpty(data.password)) {
+ errors.password = 'Password field is required';
+ }
+
+ return {
+ errors,
+ isValid: isEmpty(errors),
+ };
+};
+
+module.exports = {
+ validateSignupInput,
+ validateLoginInput,
+};
diff --git a/uploads/products/default.png b/uploads/products/default.png
new file mode 100644
index 0000000..4cf4597
Binary files /dev/null and b/uploads/products/default.png differ
diff --git a/uploads/users/default-avatar.png b/uploads/users/default-avatar.png
new file mode 100644
index 0000000..e7596cc
Binary files /dev/null and b/uploads/users/default-avatar.png differ
diff --git a/uploads/users/default.png b/uploads/users/default.png
new file mode 100644
index 0000000..ddcb797
Binary files /dev/null and b/uploads/users/default.png differ