diff --git a/.gitignore b/.gitignore index 25c8fdb..bb4fb40 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,13 @@ node_modules -package-lock.json \ No newline at end of file +package-lock.json +.env + +# Environment variables in day projects +day*/.env +day*/.env.* +day*/**/.env + +# Node modules in day projects +day*/node_modules/ + +day*/package-lock.json \ No newline at end of file diff --git a/day1/.env b/day1/.env deleted file mode 100644 index bf61c44..0000000 --- a/day1/.env +++ /dev/null @@ -1,2 +0,0 @@ -DB_PASSWORD=ayobamidavid -PORT=8000 \ No newline at end of file diff --git a/day1/routes/shippingDock.js b/day1/routes/shippingDock.js index 43fcf5b..a982460 100644 --- a/day1/routes/shippingDock.js +++ b/day1/routes/shippingDock.js @@ -38,7 +38,7 @@ router.post("/", async (req, res) => { return handleError("Name is required and cannot be empty", 400, res); } await ShippingDock.create(req.body); - handleSuccess(res, dock, 201); + handleSuccess(res, null, 201); } catch (err) { handleError(err.message, 500, res); } @@ -85,7 +85,7 @@ router.delete("/:id", async (req, res) => { where: { id: req.params.id }, }); if (deleted === 0) return handleError("Dock not found", 404, res); - handleSuccess(res, null, 204); // 204 No Content + handleSuccess(res, null); } catch (err) { handleSequelizeError(err, res); } diff --git a/day2/models/order.js b/day2/models/order.js new file mode 100644 index 0000000..9ec86e3 --- /dev/null +++ b/day2/models/order.js @@ -0,0 +1,43 @@ +module.exports = (sequelize, DataTypes) => { + const order = sequelize.define( + "order", + { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + }, + user_id: { + type: DataTypes.INTEGER, + allowNull: false, + }, + shipping_dock_id: { + type: DataTypes.INTEGER, + allowNull: false, + }, + amount: { + type: DataTypes.DECIMAL(10, 2), + allowNull: false, + }, + status: { + type: DataTypes.STRING, + defaultValue: "pending", + }, + created_at: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + }, + updated_at: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + }, + }, + { + timestamps: true, + freezeTableName: true, + tableName: "order", + } + ); + + return order; +}; diff --git a/day2/routes/orders.js b/day2/routes/orders.js new file mode 100644 index 0000000..271a117 --- /dev/null +++ b/day2/routes/orders.js @@ -0,0 +1,55 @@ +const express = require("express"); +const router = express.Router(); +const db = require("../models"); +const PaginationService = require("../services/PaginationService"); +const ReportService = require("../services/ReportService"); + +// GET /api/v1/order/odd - Return all odd order_id rows +router.get("/odd", async (req, res) => { + try { + const oddOrders = await PaginationService.getOddOrders(db.order); + res.json(oddOrders); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// GET /api/v1/order - Paginate orders with optional sorting +router.get("/", async (req, res) => { + try { + const { page = 1, limit = 10, sort = "id", direction = "ASC" } = req.query; + + const result = await PaginationService.paginateWithSort(db.order, { + page, + limit, + sort, + direction, + }); + + res.json(result); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// GET /api/v1/order/cursor - Cursor-based pagination +router.get("/cursor", async (req, res) => { + try { + const { id, limit = 10 } = req.query; + + if (!id) { + return res.status(400).json({ error: "id parameter is required" }); + } + + const result = await PaginationService.paginateWithCursor(db.order, { + id, + limit, + }); + + res.json(result); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +module.exports = router; diff --git a/day2/routes/reports.js b/day2/routes/reports.js new file mode 100644 index 0000000..c15dd77 --- /dev/null +++ b/day2/routes/reports.js @@ -0,0 +1,93 @@ +const express = require("express"); +const router = express.Router(); +const db = require("../models"); +const ReportService = require("../services/ReportService"); + +// GET /api/v1/report/sale?month=1&year=2022 +router.get("/sale", async (req, res) => { + try { + const { month, year, from_date, to_date } = req.query; + if (month && year) { + const result = await ReportService.getMonthlySales(db.order, month, year); + return res.json(result); + } else if (from_date && to_date) { + const result = await ReportService.getSalesByDateRange( + db.order, + from_date, + to_date + ); + return res.json(result); + } else { + return res.status(400).json({ error: "Missing required parameters" }); + } + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// GET /api/v1/report/monthly?year=2022 +router.get("/monthly", async (req, res) => { + try { + const { year } = req.query; + if (!year) return res.status(400).json({ error: "year is required" }); + const result = await ReportService.getMonthlySalesByYear(db.order, year); + res.json(result); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// GET /api/v1/report/user?year=2022&user_id=1 +router.get("/user", async (req, res) => { + try { + const { year, user_id } = req.query; + if (!year || !user_id) + return res.status(400).json({ error: "year and user_id are required" }); + const result = await ReportService.getMonthlySalesByUser( + db.order, + year, + user_id + ); + res.json(result); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// GET /api/v1/report/shipping_dock?year=2022&shipping_dock_id=1 +router.get("/shipping_dock", async (req, res) => { + try { + const { year, shipping_dock_id } = req.query; + if (!year || !shipping_dock_id) + return res + .status(400) + .json({ error: "year and shipping_dock_id are required" }); + const result = await ReportService.getMonthlySalesByShippingDock( + db.order, + year, + shipping_dock_id + ); + res.json(result); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// GET /api/v1/report/user/count?year=2022&user_id=1 +router.get("/user/count", async (req, res) => { + try { + const { year, user_id } = req.query; + if (!year || !user_id) + return res.status(400).json({ error: "year and user_id are required" }); + const result = await ReportService.getOrderCountByUser( + db.order, + year, + user_id + ); + res.json(result); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +module.exports = router; diff --git a/day2/services/PaginationService.js b/day2/services/PaginationService.js new file mode 100644 index 0000000..4f33d4b --- /dev/null +++ b/day2/services/PaginationService.js @@ -0,0 +1,82 @@ +const { Op } = require("sequelize"); + +class PaginationService { + /** + * Handle offset-based pagination with sorting + */ + static async paginateWithSort(model, options = {}) { + const { + page = 1, + limit = 10, + sort = "id", + direction = "ASC", + where = {}, + } = options; + + const offset = (page - 1) * limit; + const order = [[sort, direction.toUpperCase()]]; + + const { count, rows } = await model.findAndCountAll({ + where, + order, + limit: parseInt(limit), + offset: parseInt(offset), + }); + + const totalPages = Math.ceil(count / limit); + + return { + total: count, + page: parseInt(page), + totalPages, + list: rows, + }; + } + + /** + * Handle cursor-based pagination + */ + static async paginateWithCursor(model, options = {}) { + const { + id, + limit = 10, + sort = "id", + direction = "ASC", + where = {}, + } = options; + + const order = [[sort, direction.toUpperCase()]]; + const cursorWhere = { + ...where, + [sort]: { + [direction.toUpperCase() === "ASC" ? Op.gt : Op.lt]: id, + }, + }; + + const rows = await model.findAll({ + where: cursorWhere, + order, + limit: parseInt(limit), + }); + + return { + id: parseInt(id), + list: rows, + }; + } + + /** + * Get odd order IDs + */ + static async getOddOrders(model) { + return await model.findAll({ + where: { + id: { + [Op.mod]: [2, 1], // id % 2 = 1 (odd numbers) + }, + }, + }); + } +} + +module.exports = PaginationService; diff --git a/day2/services/ReportService.js b/day2/services/ReportService.js new file mode 100644 index 0000000..5c77b46 --- /dev/null +++ b/day2/services/ReportService.js @@ -0,0 +1,169 @@ +const { Op, fn, col, literal } = require("sequelize"); + +class ReportService { + /** + * Get total sales for a specific month and year + */ + static async getMonthlySales(model, month, year) { + const result = await model.findOne({ + attributes: [[fn("SUM", col("amount")), "total_amount"]], + where: { + created_at: { + [Op.and]: [ + literal(`MONTH(created_at) = ${month}`), + literal(`YEAR(created_at) = ${year}`), + ], + }, + }, + }); + + return { + month: parseInt(month), + year: parseInt(year), + total_amount: parseFloat(result?.dataValues?.total_amount || 0), + }; + } + + /** + * Get total sales for a date range + */ + static async getSalesByDateRange(model, fromDate, toDate) { + // Ensure dates are in correct order + const startDate = new Date(fromDate) < new Date(toDate) ? fromDate : toDate; + const endDate = new Date(fromDate) < new Date(toDate) ? toDate : fromDate; + + const result = await model.findOne({ + attributes: [[fn("SUM", col("amount")), "total_amount"]], + where: { + created_at: { + [Op.between]: [startDate, endDate], + }, + }, + }); + + return { + from_date: startDate, + to_date: endDate, + total_amount: parseFloat(result?.dataValues?.total_amount || 0), + }; + } + + /** + * Get monthly sales for a year (only months with sales > 0) + */ + static async getMonthlySalesByYear(model, year) { + const results = await model.findAll({ + attributes: [ + [fn("MONTH", col("created_at")), "month"], + [fn("SUM", col("amount")), "total_amount"], + ], + where: { + created_at: { + [Op.and]: [literal(`YEAR(created_at) = ${year}`)], + }, + }, + group: [fn("MONTH", col("created_at"))], + having: literal("SUM(amount) > 0"), + order: [[fn("MONTH", col("created_at")), "ASC"]], + }); + + return results.map((result) => ({ + month: parseInt(result.dataValues.month), + total_amount: parseFloat(result.dataValues.total_amount), + })); + } + + /** + * Get monthly sales for a year by user_id (only months with sales > 0) + */ + static async getMonthlySalesByUser(model, year, userId) { + const results = await model.findAll({ + attributes: [ + [fn("MONTH", col("created_at")), "month"], + [fn("SUM", col("amount")), "total_amount"], + ], + where: { + created_at: { + [Op.and]: [literal(`YEAR(created_at) = ${year}`)], + }, + user_id: userId, + }, + group: [fn("MONTH", col("created_at"))], + having: literal("SUM(amount) > 0"), + order: [[fn("MONTH", col("created_at")), "ASC"]], + }); + + return results.map((result) => ({ + month: parseInt(result.dataValues.month), + total_amount: parseFloat(result.dataValues.total_amount), + })); + } + + /** + * Get monthly sales for a year by shipping_dock_id (only months with sales > 0) + */ + static async getMonthlySalesByShippingDock(model, year, shippingDockId) { + const results = await model.findAll({ + attributes: [ + [fn("MONTH", col("created_at")), "month"], + [fn("SUM", col("amount")), "total_amount"], + ], + where: { + created_at: { + [Op.and]: [literal(`YEAR(created_at) = ${year}`)], + }, + shipping_dock_id: shippingDockId, + }, + group: [fn("MONTH", col("created_at"))], + having: literal("SUM(amount) > 0"), + order: [[fn("MONTH", col("created_at")), "ASC"]], + }); + + return results.map((result) => ({ + month: parseInt(result.dataValues.month), + total_amount: parseFloat(result.dataValues.total_amount), + })); + } + + /** + * Get order count per month for a user in a year (include months with 0 orders) + */ + static async getOrderCountByUser(model, year, userId) { + // First get all months with orders + const orderMonths = await model.findAll({ + attributes: [ + [fn("MONTH", col("created_at")), "month"], + [fn("COUNT", col("id")), "order_count"], + ], + where: { + created_at: { + [Op.and]: [literal(`YEAR(created_at) = ${year}`)], + }, + user_id: userId, + }, + group: [fn("MONTH", col("created_at"))], + order: [[fn("MONTH", col("created_at")), "ASC"]], + }); + + // Create a map of months with orders + const monthMap = {}; + orderMonths.forEach((result) => { + monthMap[parseInt(result.dataValues.month)] = parseInt( + result.dataValues.order_count + ); + }); + + // Create result array with all 12 months + const result = []; + for (let month = 1; month <= 12; month++) { + result.push({ + month, + order_count: monthMap[month] || 0, + }); + } + + return result; + } +} + +module.exports = ReportService;