diff --git a/src/controllers/categories.controller.js b/src/controllers/categories.controller.js new file mode 100644 index 000000000..0b4543bdf --- /dev/null +++ b/src/controllers/categories.controller.js @@ -0,0 +1,81 @@ +const categoriesService = require('../services/categories.service'); + +const getAll = async (req, res) => { + const categories = await categoriesService.getAll(); + + res.send(categories); +}; + +const getOne = async (req, res) => { + const id = Number(req.params.id); + + if (!Number.isInteger(id) || id < 1) { + return res.sendStatus(400); + } + + const category = await categoriesService.getById(id); + + if (!category) { + return res.sendStatus(404); + } + + res.send(category); +}; + +const create = async (req, res) => { + const name = req.body.name; + + if (!name || typeof name !== 'string') { + return res.sendStatus(400); + } + + const category = await categoriesService.create(name); + + res.status(201).json(category); +}; + +const deleteOne = async (req, res) => { + const id = Number(req.params.id); + + if (!Number.isInteger(id) || id < 1) { + return res.sendStatus(400); + } + + const category = await categoriesService.deleteById(id); + + if (!category) { + return res.sendStatus(404); + } + + res.sendStatus(204); +}; + +async function update(req, res) { + const id = Number(req.params.id); + + if (!Number.isInteger(id) || id < 1) { + return res.sendStatus(400); + } + + const { name } = req.body; + + if (!name || typeof name !== 'string') { + return res.sendStatus(400); + } + + const category = await categoriesService.update(id, name); + + if (!category) { + return res.sendStatus(404); + } + + res.send(category); +} + +module.exports = { + getAll, + getOne, + create, + update, + deleteOne, +}; diff --git a/src/controllers/expenses.controller.js b/src/controllers/expenses.controller.js new file mode 100644 index 000000000..fc761e557 --- /dev/null +++ b/src/controllers/expenses.controller.js @@ -0,0 +1,197 @@ +const expensesService = require('../services/expenses.service'); +const usersService = require('../services/users.service'); + +const getAll = async (req, res) => { + const expenses = await expensesService.getAll(req.query); + + res + .status(200) + .send(expenses.map((expense) => expensesService.normalize(expense))); +}; + +const getOne = async (req, res) => { + const id = Number(req.params.id); + + if (!Number.isInteger(id) || id < 1) { + return res.sendStatus(400); + } + + const expense = await expensesService.getById(id); + + if (!expense) { + return res.sendStatus(404); + } + + res.status(200).send(expensesService.normalize(expense)); +}; + +const create = async (req, res) => { + const required = ['userId', 'spentAt', 'title', 'amount']; + + const hasMissingField = required.some( + (field) => req.body[field] === undefined, + ); + + if (hasMissingField) { + return res.sendStatus(400); + } + + const { userId, spentAt, title, amount, category, note } = req.body; + const parsedUserId = Number(userId); + + if (!Number.isInteger(parsedUserId) || parsedUserId < 1) { + return res.sendStatus(400); + } + + const user = await usersService.getById(parsedUserId); + + if (!user) { + return res.sendStatus(400); + } + + if ( + typeof spentAt !== 'string' || + Number.isNaN(new Date(spentAt).getTime()) + ) { + return res.sendStatus(400); + } + + if (typeof title !== 'string' || title.trim() === '') { + return res.sendStatus(400); + } + + const parsedAmount = Number(amount); + + if (!Number.isInteger(parsedAmount) || parsedAmount < 1) { + return res.sendStatus(400); + } + + if ( + category != null && + (typeof category !== 'string' || category.trim() === '') + ) { + return res.sendStatus(400); + } + + if (note != null && typeof note !== 'string') { + return res.sendStatus(400); + } + + const newExpense = await expensesService.create({ + userId: parsedUserId, + spentAt: spentAt, + title: title.trim(), + amount: parsedAmount, + category: category?.trim() || null, + note: typeof note === 'string' ? note : null, + }); + + return res.status(201).send(newExpense); +}; + +const deleteOne = async (req, res) => { + const id = Number(req.params.id); + + if (!Number.isInteger(id) || id < 1) { + return res.sendStatus(400); + } + + const expense = await expensesService.deleteById(id); + + if (!expense) { + return res.sendStatus(404); + } + + res.sendStatus(204); +}; + +const update = async (req, res) => { + const id = Number(req.params.id); + + if (!Number.isInteger(id) || id < 1) { + return res.sendStatus(400); + } + + const expense = await expensesService.getById(id); + + if (!expense) { + return res.sendStatus(404); + } + + const { userId, spentAt, title, amount, category, note } = req.body; + + const updateData = {}; + + if (userId !== undefined) { + const parsedUserId = Number(userId); + + if (!Number.isInteger(parsedUserId) || parsedUserId < 1) { + return res.sendStatus(400); + } + + updateData.userId = parsedUserId; + } + + if (spentAt !== undefined) { + if (typeof spentAt !== 'string') { + return res.sendStatus(400); + } + + const parsedDate = new Date(spentAt); + + if (Number.isNaN(parsedDate.getTime())) { + return res.sendStatus(400); + } + + updateData.spentAt = parsedDate; + } + + if (title !== undefined) { + if (typeof title !== 'string' || title.trim() === '') { + return res.sendStatus(400); + } + + updateData.title = title.trim(); + } + + if (amount !== undefined) { + const parsedAmount = Number(amount); + + if (!Number.isInteger(parsedAmount) || parsedAmount < 1) { + return res.sendStatus(400); + } + + updateData.amount = parsedAmount; + } + + if (category !== undefined) { + if ( + category !== null && + (typeof category !== 'string' || category.trim() === '') + ) { + return res.sendStatus(400); + } + + updateData.category = category?.trim() || null; + } + + if (note !== undefined) { + if (note !== null && typeof note !== 'string') { + return res.sendStatus(400); + } + + updateData.note = note ?? null; + } + + const updatedExpense = await expensesService.update(id, updateData); + + return res.status(200).send(expensesService.normalize(updatedExpense)); +}; + +module.exports = { + getAll, + getOne, + create, + update, + deleteOne, +}; diff --git a/src/controllers/users.controller.js b/src/controllers/users.controller.js new file mode 100644 index 000000000..c5e9e0cce --- /dev/null +++ b/src/controllers/users.controller.js @@ -0,0 +1,81 @@ +const usersService = require('../services/users.service'); + +const getAll = async (req, res) => { + const users = await usersService.getAll(); + + res.send(users); +}; + +const getOne = async (req, res) => { + const id = Number(req.params.id); + + if (!Number.isInteger(id) || id < 1) { + return res.sendStatus(400); + } + + const user = await usersService.getById(id); + + if (!user) { + return res.sendStatus(404); + } + + res.send(user); +}; + +const create = async (req, res) => { + const name = req.body.name; + + if (!name || typeof name !== 'string') { + return res.sendStatus(400); + } + + const user = await usersService.create(name); + + res.status(201).json(user); +}; + +const deleteOne = async (req, res) => { + const id = Number(req.params.id); + + if (!Number.isInteger(id) || id < 1) { + return res.sendStatus(400); + } + + const user = await usersService.deleteById(id); + + if (!user) { + return res.sendStatus(404); + } + + res.sendStatus(204); +}; + +async function update(req, res) { + const id = Number(req.params.id); + + if (!Number.isInteger(id) || id < 1) { + return res.sendStatus(400); + } + + const { name } = req.body; + + if (!name || typeof name !== 'string') { + return res.sendStatus(400); + } + + const user = await usersService.update(id, name); + + if (!user) { + return res.sendStatus(404); + } + + res.send(user); +} + +module.exports = { + getAll, + getOne, + create, + update, + deleteOne, +}; diff --git a/src/createServer.js b/src/createServer.js index 1ea5542d6..0d5bdc172 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -1,8 +1,21 @@ 'use strict'; -const createServer = () => { - // your code goes here -}; +const express = require('express'); +const { usersRouter } = require('./routes/users.route'); +const { expensesRouter } = require('./routes/expenses.route'); +const { categoriesRouter } = require('./routes/categories.route'); + +function createServer() { + const app = express(); + + app.use(express.json()); + + app.use('/users', usersRouter); + app.use('/expenses', expensesRouter); + app.use('/categories', categoriesRouter); + + return app; +} module.exports = { createServer, diff --git a/src/models/Category.model.js b/src/models/Category.model.js new file mode 100644 index 000000000..3cc491012 --- /dev/null +++ b/src/models/Category.model.js @@ -0,0 +1,20 @@ +'use strict'; + +const { DataTypes } = require('sequelize'); +const { sequelize } = require('../db.js'); + +const Category = sequelize.define('Category', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + }, + name: { + type: DataTypes.STRING, + allowNull: false, + }, +}); + +module.exports = { + Category, +}; diff --git a/src/models/Expense.model.js b/src/models/Expense.model.js index 567e1c3e7..62b45363b 100644 --- a/src/models/Expense.model.js +++ b/src/models/Expense.model.js @@ -1,10 +1,39 @@ 'use strict'; +const { DataTypes } = require('sequelize'); const { sequelize } = require('../db.js'); -const Expense = sequelize.define( - // your code goes here -); +const Expense = sequelize.define('Expense', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + }, + userId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + spentAt: { + type: DataTypes.STRING, + allowNull: false, + }, + title: { + type: DataTypes.STRING, + allowNull: false, + }, + amount: { + type: DataTypes.INTEGER, + allowNull: false, + }, + category: { + type: DataTypes.STRING, + allowNull: true, + }, + note: { + type: DataTypes.STRING, + allowNull: true, + }, +}); module.exports = { Expense, diff --git a/src/models/User.model.js b/src/models/User.model.js index 61861c9e4..148d6b912 100644 --- a/src/models/User.model.js +++ b/src/models/User.model.js @@ -1,10 +1,19 @@ 'use strict'; +const { DataTypes } = require('sequelize'); const { sequelize } = require('../db.js'); -const User = sequelize.define( - // your code goes here -); +const User = sequelize.define('User', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + }, + name: { + type: DataTypes.STRING, + allowNull: false, + }, +}); module.exports = { User, diff --git a/src/models/models.js b/src/models/models.js index b43b55752..544e0a26a 100644 --- a/src/models/models.js +++ b/src/models/models.js @@ -2,10 +2,12 @@ const { User } = require('./User.model'); const { Expense } = require('./Expense.model'); +const { Category } = require('./Category.model'); module.exports = { models: { User, Expense, + Category, }, }; diff --git a/src/routes/categories.route.js b/src/routes/categories.route.js new file mode 100644 index 000000000..3b2ff7353 --- /dev/null +++ b/src/routes/categories.route.js @@ -0,0 +1,14 @@ +const { Router } = require('express'); +const categoriesController = require('../controllers/categories.controller'); + +const categoriesRouter = Router(); + +categoriesRouter.get('/', categoriesController.getAll); +categoriesRouter.get('/:id', categoriesController.getOne); +categoriesRouter.post('/', categoriesController.create); +categoriesRouter.patch('/:id', categoriesController.update); +categoriesRouter.delete('/:id', categoriesController.deleteOne); + +module.exports = { + categoriesRouter, +}; diff --git a/src/routes/expenses.route.js b/src/routes/expenses.route.js new file mode 100644 index 000000000..c7c91f901 --- /dev/null +++ b/src/routes/expenses.route.js @@ -0,0 +1,14 @@ +const { Router } = require('express'); +const expensesController = require('../controllers/expenses.controller'); + +const expensesRouter = Router(); + +expensesRouter.get('/', expensesController.getAll); +expensesRouter.get('/:id', expensesController.getOne); +expensesRouter.post('/', expensesController.create); +expensesRouter.patch('/:id', expensesController.update); +expensesRouter.delete('/:id', expensesController.deleteOne); + +module.exports = { + expensesRouter, +}; diff --git a/src/routes/users.route.js b/src/routes/users.route.js new file mode 100644 index 000000000..c3e59ec15 --- /dev/null +++ b/src/routes/users.route.js @@ -0,0 +1,14 @@ +const { Router } = require('express'); +const usersController = require('../controllers/users.controller'); + +const usersRouter = Router(); + +usersRouter.get('/', usersController.getAll); +usersRouter.get('/:id', usersController.getOne); +usersRouter.post('/', usersController.create); +usersRouter.delete('/:id', usersController.deleteOne); +usersRouter.patch('/:id', usersController.update); + +module.exports = { + usersRouter, +}; diff --git a/src/services/categories.service.js b/src/services/categories.service.js new file mode 100644 index 000000000..1557a2336 --- /dev/null +++ b/src/services/categories.service.js @@ -0,0 +1,47 @@ +const { Category } = require('../models/Category.model'); + +async function getAll() { + return Category.findAll(); +} + +async function getById(id) { + return Category.findByPk(id); +} + +async function create(name) { + return Category.create({ name }); +} + +async function deleteById(id) { + return Category.destroy({ where: { id } }); +} + +async function update(id, name) { + await Category.update( + { name: name }, + { + where: { id }, + silent: true, // Save without updating the updatedAt timestamp + }, + ); + + return Category.findByPk(id); +} + +function normalize({ id, name, createdAt, updatedAt }) { + return { + id, + name, + createdAt, + updatedAt, + }; +} + +module.exports = { + getAll, + getById, + create, + deleteById, + update, + normalize, +}; diff --git a/src/services/expenses.service.js b/src/services/expenses.service.js new file mode 100644 index 000000000..097acba71 --- /dev/null +++ b/src/services/expenses.service.js @@ -0,0 +1,89 @@ +const { Expense } = require('../models/Expense.model'); + +async function getAll(query = {}) { + const { userId, from, to, categories } = query; + const parsedUserId = userId == null ? undefined : Number(userId); + const expenses = await Expense.findAll(); + + return expenses.filter((expense) => { + if (parsedUserId && expense.userId !== parsedUserId) { + return false; + } + + const expenseDate = new Date(expense.spentAt); + + if (from && expenseDate < new Date(from)) { + return false; + } + + if (to && expenseDate > new Date(to)) { + return false; + } + + if (categories) { + const categoriesArray = Array.isArray(categories) + ? categories + : categories.split(','); + + if (!categoriesArray.includes(expense.category)) { + return false; + } + } + + return true; + }); +} + +async function getById(id) { + return Expense.findByPk(id); +} + +async function create({ userId, spentAt, title, amount, category, note }) { + return Expense.create({ + userId, + spentAt, + title, + amount, + category, + note, + }); +} + +async function deleteById(id) { + return Expense.destroy({ where: { id } }); +} + +async function update(id, data) { + const expense = await Expense.findByPk(id); + + if (!expense) { + return null; + } + + Object.assign(expense, data); + + await expense.save(); + + return expense; +} + +function normalize({ id, userId, spentAt, title, amount, category, note }) { + return { + id, + userId, + spentAt, + title, + amount, + category, + note, + }; +} + +module.exports = { + getAll, + getById, + create, + deleteById, + update, + normalize, +}; diff --git a/src/services/users.service.js b/src/services/users.service.js new file mode 100644 index 000000000..afa51e98a --- /dev/null +++ b/src/services/users.service.js @@ -0,0 +1,47 @@ +const { User } = require('../models/User.model'); + +async function getAll() { + return User.findAll(); +} + +async function getById(id) { + return User.findByPk(id); +} + +async function create(name) { + return User.create({ name }); +} + +async function deleteById(id) { + return User.destroy({ where: { id } }); +} + +async function update(id, name) { + await User.update( + { name: name }, + { + where: { id }, + silent: true, // Save without updating the updatedAt timestamp + }, + ); + + return User.findByPk(id); +} + +function normalize({ id, name, createdAt, updatedAt }) { + return { + id, + name, + createdAt, + updatedAt, + }; +} + +module.exports = { + getAll, + getById, + create, + deleteById, + update, + normalize, +};