diff --git a/src/config/constants.js b/src/config/constants.js index 68c3d9b..a7151a4 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -59,6 +59,17 @@ const AUTHORIZATION_FAILURE_ERROR_MESSAGE = const SubLevelTypes = ["POSITION", "BIN", "PALLET"]; +const ItemTransactionTypes = [ + "PUT", + "PICK", + "RESERVE", + "CHECK-IN", + "CHECK-OUT", + "RESERVE" +]; + +const ReportItemForTypes = ["LOCATION", "ISSUE", "INCIDENT"]; + module.exports = { UserActions, InventoryScopes, @@ -75,4 +86,6 @@ module.exports = { AREA_ADMIN_ROLE: "area-admin", AUTHENTICATION_FAILURE_ERROR_MESSAGE, AUTHORIZATION_FAILURE_ERROR_MESSAGE, + ItemTransactionTypes, + ReportItemForTypes, }; diff --git a/src/controller/dashboard.controller.js b/src/controller/dashboard.controller.js index 9b67ce4..0f5c3b6 100644 --- a/src/controller/dashboard.controller.js +++ b/src/controller/dashboard.controller.js @@ -163,14 +163,34 @@ const createSublevels = async (subLevels, level, parent = undefined, depth = 0) return sub_levels_list; }; -const createInventory = async ({ name, type }) => { +const createInventory = async ({ name, type, policies }) => { if (!(name && type)) { return; } + const preferredLocations = []; + if (policies.preferredLocations && Array.isArray(policies.preferredLocations)) { + for (const preferredLocation of policies.preferredLocations) { + preferredLocations.push({ id: preferredLocation.id, type: preferredLocation.type }); + } + } + + const verifiedPolicies = { + orderTracking: policies.orderTracking || {}, + alerting: { + lowestStockLevel: policies.alerting && policies.alerting.lowestStockLevel ? policies.alerting.lowestStockLevel : false, + highestStockLevel: policies.alerting && policies.alerting.highestStockLevel ? policies.alerting.highestStockLevel : false, + alertStockLevel: policies.alerting && policies.alerting.alertStockLevel ? policies.alerting.alertStockLevel : false, + reOrderLevel: policies.alerting && policies.alerting.reOrderLevel ? policies.alerting.reOrderLevel : false, + }, + replenishment: policies.replenishment || {}, + preferredLocations: preferredLocations, + }; + return await Inventory.create({ name, type, + policies: verifiedPolicies, }); }; diff --git a/src/controller/inventory.controller.js b/src/controller/inventory.controller.js index 93b6977..b17bac4 100644 --- a/src/controller/inventory.controller.js +++ b/src/controller/inventory.controller.js @@ -29,17 +29,36 @@ module.exports = { * Create a Inventory */ createInventory: async (req, res, next) => { - const { name, type } = req.body; + const { name, type, policies } = req.body; if (!(name && type)) { res.status(400).send("Missing params param"); return; } + const preferredLocations = []; + if (policies.preferredLocations && Array.isArray(policies.preferredLocations)) { + for (const preferredLocation of policies.preferredLocations) { + preferredLocations.push({ id: preferredLocation.id, type: preferredLocation.type }); + } + } + + const verifiedPolicies = { + orderTracking: policies.orderTracking || {}, + alerting: { + lowestStockLevel: policies.alerting && policies.alerting.lowestStockLevel ? policies.alerting.lowestStockLevel : false, + highestStockLevel: policies.alerting && policies.alerting.highestStockLevel ? policies.alerting.highestStockLevel : false, + alertStockLevel: policies.alerting && policies.alerting.alertStockLevel ? policies.alerting.alertStockLevel : false, + reOrderLevel: policies.alerting && policies.alerting.reOrderLevel ? policies.alerting.reOrderLevel : false, + }, + replenishment: policies.replenishment || {}, + preferredLocations: preferredLocations, + }; try { const inventoryData = new Inventory({ name, type, + policies: verifiedPolicies, }); await inventoryData.save(); @@ -64,7 +83,7 @@ module.exports = { return; } - const { name, type } = req.body; + const { name, type, policies } = req.body; if (!(name || type)) { res.status(400).send("Missing data in body"); @@ -81,6 +100,37 @@ module.exports = { if (name) inventoryData.name = name; if (type) inventoryData.type = type; + if (policies) { + const preferredLocations = []; + if (policies.preferredLocations && Array.isArray(policies.preferredLocations)) { + for (const preferredLocation of policies.preferredLocations) { + preferredLocations.push({ id: preferredLocation.id, type: preferredLocation.type }); + } + } + + inventoryData.policies = { + orderTracking: policies.orderTracking || inventoryData.policies.orderTracking, + alerting: { + lowestStockLevel: + policies.alerting && policies.alerting.lowestStockLevel + ? policies.alerting.lowestStockLevel + : inventoryData.policies.alerting.lowestStockLevel, + highestStockLevel: + policies.alerting && policies.alerting.highestStockLevel + ? policies.alerting.highestStockLevel + : inventoryData.policies.alerting.highestStockLevel, + alertStockLevel: + policies.alerting && policies.alerting.alertStockLevel + ? policies.alerting.alertStockLevel + : inventoryData.policies.alerting.alertStockLevel, + reOrderLevel: + policies.alerting && policies.alerting.reOrderLevel ? policies.alerting.reOrderLevel : inventoryData.policies.alerting.reOrderLevel, + }, + replenishment: policies.replenishment || inventoryData.policies.replenishment, + preferredLocations: preferredLocations, + }; + } + await inventoryData.save(); res.send({ success: true, data: inventoryData }); } catch (error) { diff --git a/src/controller/item.controller.js b/src/controller/item.controller.js index e7e6523..950239d 100644 --- a/src/controller/item.controller.js +++ b/src/controller/item.controller.js @@ -2,7 +2,19 @@ const mongoose = require("mongoose"); const Item = require("../models/Item"); const WidgetFamily = require("../models/WidgetFamily"); const Inventory = require("../models/Inventory"); -const { InventoryTypes } = require("../config/constants"); +const { + PickItemTransaction, + PutItemTransaction, + ReserveItemTransaction, + CheckInItemTransaction, + CheckOutItemTransaction, + ReportItemTransaction, + AdjustItemTransaction, +} = require("../models/ItemTransaction"); + +const ItemAssociation = require("../models/ItemAssociation"); +const Sublevel = require("../models/Sublevel"); +const { InventoryTypes, ReportItemForTypes } = require("../config/constants"); module.exports = { /** @@ -194,4 +206,260 @@ module.exports = { next(error); } }, + putItem: async (req, res, next) => { + try { + const { id } = req.params; + if (!id || mongoose.isValidObjectId(id)) { + res.status(400).send("Missing/Invalid id param"); + return; + } + + const item = await Item.findById(id); + if (!item) { + res.status(404).send("item not found"); + return; + } + + const { putQuantity, subLevel } = req.body; + if (!(putQuantity && putQuantity > 0) || !(subLevel && mongoose.isValidObjectId(subLevel))) { + res.status(400).send("Invalid value for putQuantity/subLevel"); + return; + } + + const subLevelObj = await Sublevel.findById(subLevel); + const itemAssociation = await ItemAssociation.findOne({ item_id: item._id, sub_level_id: subLevelObj._id }); + itemAssociation.totalQuantity = itemAssociation.totalQuantity + putQuantity; + itemAssociation.availableQuantity = itemAssociation.availableQuantity + putQuantity; + await itemAssociation.save(); + + await PutItemTransaction.create({ + type: "PUT", + performedOn: item, + performedBy: res.locals.user, + putQuantity: putQuantity, + subLevel: subLevelObj, + }); + } catch (error) { + next(error); + } + }, + + pickItem: async (req, res, next) => { + try { + const { id } = req.params; + if (!id || mongoose.isValidObjectId(id)) { + res.status(400).send("Missing/Invalid id param"); + return; + } + + const item = await Item.findById(id); + if (!item) { + res.status(404).send("item not found"); + return; + } + + const { pickupQuantity, subLevel } = req.body; + if (!(pickupQuantity && pickupQuantity > 0) || !(subLevel && mongoose.isValidObjectId(subLevel))) { + res.status(400).send("Invalid value for pickupQuantity/subLevel"); + return; + } + + const subLevelObj = await Sublevel.findById(subLevel); + const itemAssociation = await ItemAssociation.findOne({ item_id: item._id, sub_level_id: subLevelObj._id }); + itemAssociation.totalQuantity = itemAssociation.totalQuantity - pickupQuantity; + itemAssociation.availableQuantity = itemAssociation.availableQuantity - pickupQuantity; + await itemAssociation.save(); + + await PickItemTransaction.create({ + type: "PICK", + performedOn: item, + performedBy: res.locals.user, + pickupQuantity: pickupQuantity, + subLevel: subLevelObj, + }); + } catch (error) { + next(error); + } + }, + + reserveItem: async (req, res, next) => { + try { + const { id } = req.params; + if (!id || mongoose.isValidObjectId(id)) { + res.status(400).send("Missing/Invalid id param"); + return; + } + + const item = await Item.findById(id); + if (!item) { + res.status(404).send("item not found"); + return; + } + + const { reserveQuantity, job, pickupDate } = req.body; + if (!(reserveQuantity && reserveQuantity > 0) || !(job && mongoose.isValidObjectId(job)) || !(pickupDate && Date.parse(pickupDate))) { + res.status(400).send("Invalid value for reserveQuantity/job/pickupDate"); + return; + } + + const itemAssociation = await ItemAssociation.findOne({ item_id: item._id, availableQuantity: { $gte: reserveQuantity } }); + itemAssociation.reservedQuantity = itemAssociation.reservedQuantity + reserveQuantity; + itemAssociation.availableQuantity = itemAssociation.availableQuantity - reserveQuantity; + await itemAssociation.save(); + + await ReserveItemTransaction.create({ + type: "RESERVE", + performedOn: item, + performedBy: res.locals.user, + reserveQuantity: reserveQuantity, + job: job, + pickupDate: Date.parse(pickupDate), + }); + } catch (error) { + next(error); + } + }, + + checkInItem: async (req, res, next) => { + try { + const { id } = req.params; + if (!id || mongoose.isValidObjectId(id)) { + res.status(400).send("Missing/Invalid id param"); + return; + } + + const item = await Item.findById(id); + if (!item) { + res.status(404).send("item not found"); + return; + } + + const { checkInMeterReading, hasIssue, issueDescription } = req.body; + if (!(checkInMeterReading && checkInMeterReading > 0)) { + res.status(400).send("Invalid value for checkInMeterReading"); + return; + } + + await CheckInItemTransaction.create({ + type: "CHECK-IN", + performedOn: item, + performedBy: res.locals.user, + checkInMeterReading: checkInMeterReading, + hasIssue: hasIssue, + issueDescription: hasIssue ? issueDescription : "", + }); + } catch (error) { + next(error); + } + }, + + checkOutItem: async (req, res, next) => { + try { + const { id } = req.params; + if (!id || mongoose.isValidObjectId(id)) { + res.status(400).send("Missing/Invalid id param"); + return; + } + + const item = await Item.findById(id); + if (!item) { + res.status(404).send("item not found"); + return; + } + + const { checkOutMeterReading, job, usageReason } = req.body; + if (!(checkOutMeterReading && checkOutMeterReading > 0) || !(job && mongoose.isValidObjectId(job))) { + res.status(400).send("Invalid value for checkOutMeterReading/job"); + return; + } + + await CheckOutItemTransaction.create({ + type: "CHECK-OUT", + performedOn: item, + performedBy: res.locals.user, + checkOutMeterReading: checkOutMeterReading, + job: job, + usageReason: usageReason ? usageReason : "", + }); + } catch (error) { + next(error); + } + }, + + reportItem: async (req, res, next) => { + try { + const { id } = req.params; + if (!id || mongoose.isValidObjectId(id)) { + res.status(400).send("Missing/Invalid id param"); + return; + } + + const item = await Item.findById(id); + if (!item) { + res.status(404).send("item not found"); + return; + } + + const { reportingFor, details } = req.body; + if (!(reportingFor && ReportItemForTypes.includes(reportingFor))) { + res.status(400).send("Invalid value for checkOutMeterReading/job"); + return; + } + + await ReportItemTransaction.create({ + type: "REPORT", + performedOn: item, + performedBy: res.locals.user, + reportingFor: reportingFor, + details: details ? details : "", + }); + } catch (error) { + next(error); + } + }, + + adjustItem: async (req, res, next) => { + try { + const { id } = req.params; + if (!id || mongoose.isValidObjectId(id)) { + res.status(400).send("Missing/Invalid id param"); + return; + } + + const item = await Item.findById(id); + if (!item) { + res.status(404).send("item not found"); + return; + } + + const { recountedQuantity, damagedQuantity, subLevel } = req.body; + if (!(recountedQuantity && recountedQuantity > 0) || !(subLevel && mongoose.isValidObjectId(subLevel))) { + res.status(400).send("Invalid value for pickupQuantity/subLevel"); + return; + } + + const subLevelObj = await Sublevel.findById(subLevel); + const itemAssociation = await ItemAssociation.findOne({ item_id: item._id, sub_level_id: subLevelObj._id }); + const lastRecordedQuantity = itemAssociation.totalQuantity; + const varianceRecordedInQuantity = itemAssociation.totalQuantity - recountedQuantity; + const totalAdjustment = varianceRecordedInQuantity + damagedQuantity; + itemAssociation.totalQuantity = itemAssociation.totalQuantity - totalAdjustment; + itemAssociation.availableQuantity = itemAssociation.availableQuantity - totalAdjustment; + await itemAssociation.save(); + + await AdjustItemTransaction.create({ + type: "ADJUST", + performedOn: item, + performedBy: res.locals.user, + lastRecordedQuantity, + recountedQuantity, + varianceRecordedInQuantity, + damagedQuantity, + totalAdjustment, + newAdjustedQuantity: itemAssociation.totalQuantity, + }); + } catch (error) { + next(error); + } + }, }; diff --git a/src/controller/item.router.js b/src/controller/item.router.js index ec27913..83e4b94 100644 --- a/src/controller/item.router.js +++ b/src/controller/item.router.js @@ -1,6 +1,6 @@ const router = require("express").Router(); const controller = require("./item.controller"); - +const { AuthenticateMiddleware, ItemTransactionCheck } = require("./utils/authorize"); /** * @route /item/ */ @@ -21,4 +21,39 @@ router.get("/filter", controller.getItemsByFilter); */ router.get("/:id", controller.getItemByID); +/** + * @route /item/:id/pick + */ +router.post("/:id/pick", AuthenticateMiddleware, ItemTransactionCheck, controller.pickItem); + +/** + * @route /item/:id/put + */ +router.post("/:id/put", AuthenticateMiddleware, ItemTransactionCheck, controller.putItem); + +/** + * @route /item/:id/reserve + */ +router.post("/:id/reserve", AuthenticateMiddleware, ItemTransactionCheck, controller.reserveItem); + +/** + * @route /item/:id/check-in + */ +router.post("/:id/check-in", AuthenticateMiddleware, ItemTransactionCheck, controller.checkInItem); + +/** + * @route /item/:id/check-out + */ +router.post("/:id/check-out", AuthenticateMiddleware, ItemTransactionCheck, controller.checkOutItem); + +/** + * @route /item/:id/report + */ +router.post("/:id/report", AuthenticateMiddleware, ItemTransactionCheck, controller.reportItem); + +/** + * @route /item/:id/adjust + */ +router.post("/:id/adjust", AuthenticateMiddleware, ItemTransactionCheck, controller.adjustItem); + module.exports = router; diff --git a/src/controller/utils/authorize.js b/src/controller/utils/authorize.js index 286810b..8c98129 100644 --- a/src/controller/utils/authorize.js +++ b/src/controller/utils/authorize.js @@ -1,38 +1,22 @@ const jwt = require("jsonwebtoken"); const User = require("../../models/User"); const UserRole = require("../../models/UserRole"); -const { - SUPER_ADMIN_ROLE, - AUTHORIZATION_FAILURE_ERROR_MESSAGE, -} = require("../../config/constants"); +const { SUPER_ADMIN_ROLE, AUTHORIZATION_FAILURE_ERROR_MESSAGE } = require("../../config/constants"); const { JWT_SECRET } = require("../../config/env"); const constants = require("../../config/constants"); const authenticate = async (token) => { const decodedToken = jwt.verify(token, JWT_SECRET); if (decodedToken) { - return await User.findById(decodedToken.id) - .populate({ path: "roles", populate: "permissions" }) - .populate("permissions"); + return await User.findById(decodedToken.id).populate({ path: "roles", populate: "permissions" }).populate("permissions"); } }; -const authorize = async ( - user, - requiredRoles = [], - requiredPermissions = [] -) => { +const authorize = async (user, requiredRoles = [], requiredPermissions = []) => { const userRoles = user.roles.map((_) => _._id); - const userPermissions = [ - ...user.permissions.map((_) => _._id), - ...userRoles.map((_) => _.permissions).flat(), - ]; + const userPermissions = [...user.permissions.map((_) => _._id), ...userRoles.map((_) => _.permissions).flat()]; - return ( - user != undefined && - requiredRoles.every((_) => userRoles.includes(_)) && - requiredPermissions.every((_) => userPermissions.includes(_)) - ); + return user != undefined && requiredRoles.every((_) => userRoles.includes(_)) && requiredPermissions.every((_) => userPermissions.includes(_)); }; module.exports = { @@ -41,11 +25,13 @@ module.exports = { if (authorize(res.locals.user, [SuperAdmin.id])) { next(); } else { - res - .status(403) - .send({ success: false, error: AUTHORIZATION_FAILURE_ERROR_MESSAGE }); + res.status(403).send({ success: false, error: AUTHORIZATION_FAILURE_ERROR_MESSAGE }); } }, + ItemTransactionCheck: async (req, res, next) => { + // WIP + next(); + }, AuthenticateMiddleware: async (req, res, next) => { try { const token = req.headers.authorization || ""; diff --git a/src/models/Inventory.js b/src/models/Inventory.js index 180ba24..4ee1fb6 100644 --- a/src/models/Inventory.js +++ b/src/models/Inventory.js @@ -1,5 +1,5 @@ const mongoose = require("mongoose"); -const { InventoryTypes } = require("./../config/constants"); +const { InventoryTypes, WarehouseScopes } = require("./../config/constants"); const schema = new mongoose.Schema( { @@ -15,15 +15,42 @@ const schema = new mongoose.Schema( enum: InventoryTypes, }, policies: { - tracking: { + orderTracking: { type: Object, // Create a different model and reference it here once more details available }, alerting: { - type: Object, // Create a different model and reference it here once more details available + lowestStockLevel: { + type: Boolean, + required: true, + }, + highestStockLevel: { + type: Boolean, + required: true, + }, + alertStockLevel: { + type: Boolean, + required: true, + }, + reOrderLevel: { + type: Boolean, + required: true, + }, }, replenishment: { type: Object, // Create a different model and reference it here once more details available }, + preferredLocations: [ + { + id: { + type: mongoose.Schema.Types.ObjectId, + refPath: "type", + }, + type: { + type: String, + enum: WarehouseScopes, + }, + }, + ], }, }, { diff --git a/src/models/ItemAssociation.js b/src/models/ItemAssociation.js index e84cc94..448e465 100644 --- a/src/models/ItemAssociation.js +++ b/src/models/ItemAssociation.js @@ -10,7 +10,15 @@ const schema = new mongoose.Schema( type: mongoose.Schema.Types.ObjectId, ref: "Sublevel", }, - quantity: { + totalQuantity: { + type: Number, + default: 0, + }, + reservedQuantity: { + type: Number, + default: 0, + }, + availableQuantity: { type: Number, default: 0, }, diff --git a/src/models/ItemTransaction.js b/src/models/ItemTransaction.js new file mode 100644 index 0000000..d670589 --- /dev/null +++ b/src/models/ItemTransaction.js @@ -0,0 +1,165 @@ +const mongoose = require("mongoose"); +const { ItemTransactionTypes, ReportItemForTypes } = require("../config/constants"); + +const schema = new mongoose.Schema( + { + type: { + type: String, + required: true, + trim: true, + enum: ItemTransactionTypes, + }, + performedOn: { + type: mongoose.Schema.Types.ObjectId, + ref: "Item", + }, + performedBy: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + }, + }, + { + timestamps: true, + discriminatorKey: "kind", + } +); + +const ItemTransaction = mongoose.model("ItemTransaction", schema); + +const PutItemTransaction = ItemTransaction.discriminator( + "Put", + new mongoose.Schema({ + putQuantity: { + type: Number, + required: true, + }, + subLevel: { + type: mongoose.Schema.Types.ObjectId, + ref: "Sublevel" + }, + }) +); + +const PickItemTransaction = ItemTransaction.discriminator( + "Pick", + new mongoose.Schema({ + pickupQuantity: { + type: Number, + required: true, + }, + subLevel: { + type: mongoose.Schema.Types.ObjectId, + ref: "Sublevel", + }, + }) +); + +const ReserveItemTransaction = ItemTransaction.discriminator( + "Reserve", + new mongoose.Schema({ + reserveQuantity: { + type: Number, + required: true, + }, + job: { + type: mongoose.Schema.Types.ObjectId, + required: true, + }, + pickupDate: { + type: Date, + required: true, + }, + }) +); + +const CheckInItemTransaction = ItemTransaction.discriminator( + "CheckIn", + new mongoose.Schema({ + checkInMeterReading: { + type: Number, + required: true, + }, + hasIssue: { + type: Boolean, + required: true, + }, + issueDescription: { + type: String, + trim: true, + }, + }) +); + +const CheckOutItemTransaction = ItemTransaction.discriminator( + "CheckOut", + new mongoose.Schema({ + checkOutMeterReading: { + type: Number, + required: true, + }, + job: { + type: mongoose.Schema.Types.ObjectId, + required: true, + }, + usageReason: { + type: String, + trim: true, + }, + }) +); + +const ReportItemTransaction = ItemTransaction.discriminator( + "Report", + new mongoose.Schema({ + reportingFor: { + type: String, + required: true, + enum: ReportItemForTypes, + }, + details: { + type: String, + trim: true, + }, + }) +); + +const AdjustItemTransaction = ItemTransaction.discriminator( + "Adjust", + new mongoose.Schema({ + lastRecordedQuantity: { + type: Number, + required: true, + }, + recountedQuantity: { + type: Number, + required: true, + }, + varianceRecordedInQuantity: { + type: Number, + required: true, + }, + damagedQuantity: { + type: Number, + required: true, + }, + totalAdjustment: { + type: Number, + required: true, + }, + newAdjustedQuantity: { + type: Number, + required: true, + }, + }) +); + +module.exports = { + ItemTransaction, + PutItemTransaction, + PickItemTransaction, + ReserveItemTransaction, + CheckInItemTransaction, + CheckOutItemTransaction, + ReportItemTransaction, + AdjustItemTransaction, +};