-
Notifications
You must be signed in to change notification settings - Fork 117
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #955 from IkkiOcean/backend-schema-11
Added Comprehensive Invoice Management System with Routes, Controllers, and Bulk Operations
- Loading branch information
Showing
3 changed files
with
303 additions
and
0 deletions.
There are no files selected for viewing
217 changes: 217 additions & 0 deletions
217
backend/controllers/shop/sub-controllers/invoiceController.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,217 @@ | ||
const Invoice = require("../models/invoice"); | ||
const Joi = require("joi"); | ||
|
||
// Validation Schemas | ||
const invoiceSchema = Joi.object({ | ||
orderId: Joi.string().required(), | ||
userId: Joi.string().required(), | ||
amount: Joi.number().min(0).required(), | ||
dueDate: Joi.date().required(), | ||
paymentId: Joi.string().optional(), | ||
}); | ||
|
||
const statusUpdateSchema = Joi.object({ | ||
status: Joi.string() | ||
.valid("Paid", "Unpaid", "Overdue", "Cancelled") | ||
.required(), | ||
}); | ||
|
||
// Generate invoice after payment confirmation | ||
const generateInvoice = async (req, res) => { | ||
const { error, value } = invoiceSchema.validate(req.body); | ||
if (error) return res.status(400).json({ message: error.message }); | ||
|
||
const { orderId, userId, amount, dueDate, paymentId } = value; | ||
|
||
const newInvoice = new Invoice({ | ||
invoiceId: `INV${Date.now()}`, | ||
orderId, | ||
userId, | ||
amount, | ||
dueDate, | ||
paymentId, | ||
}); | ||
|
||
try { | ||
const invoice = await newInvoice.save(); | ||
res.status(201).json(invoice); | ||
} catch (error) { | ||
console.error(`Error generating invoice: ${error.message}`); | ||
res.status(500).json({ message: "Internal server error" }); | ||
} | ||
}; | ||
|
||
// Retrieve invoices by user, order, status, or date range with pagination | ||
const getInvoices = async (req, res) => { | ||
const { | ||
userId, | ||
orderId, | ||
status, | ||
startDate, | ||
endDate, | ||
page = 1, | ||
limit = 10, | ||
sortBy = "invoiceDate", | ||
order = "desc", | ||
} = req.query; | ||
|
||
try { | ||
const filters = {}; | ||
if (userId) filters.userId = userId; | ||
if (orderId) filters.orderId = orderId; | ||
if (status) filters.status = status; | ||
|
||
if (startDate || endDate) { | ||
filters.invoiceDate = {}; | ||
if (startDate) filters.invoiceDate.$gte = new Date(startDate); | ||
if (endDate) filters.invoiceDate.$lte = new Date(endDate); | ||
} | ||
|
||
const invoices = await Invoice.find(filters) | ||
.sort({ [sortBy]: order === "asc" ? 1 : -1 }) | ||
.skip((page - 1) * limit) | ||
.limit(Number(limit)); | ||
|
||
res.status(200).json(invoices); | ||
} catch (error) { | ||
console.error(`Error fetching invoices: ${error.message}`); | ||
res.status(500).json({ message: "Internal server error" }); | ||
} | ||
}; | ||
|
||
// Retrieve a single invoice by ID | ||
const getInvoiceById = async (req, res) => { | ||
try { | ||
const invoice = await Invoice.findById(req.params.id); | ||
if (!invoice) return res.status(404).json({ message: "Invoice not found" }); | ||
res.status(200).json(invoice); | ||
} catch (error) { | ||
console.error(`Error fetching invoice by ID: ${error.message}`); | ||
res.status(500).json({ message: "Internal server error" }); | ||
} | ||
}; | ||
|
||
// Update invoice status with validation | ||
const updateInvoiceStatus = async (req, res) => { | ||
const { error, value } = statusUpdateSchema.validate(req.body); | ||
if (error) return res.status(400).json({ message: error.message }); | ||
|
||
try { | ||
const invoice = await Invoice.findByIdAndUpdate( | ||
req.params.id, | ||
{ status: value.status }, | ||
{ new: true } | ||
); | ||
if (!invoice) return res.status(404).json({ message: "Invoice not found" }); | ||
res.status(200).json(invoice); | ||
} catch (error) { | ||
console.error(`Error updating invoice status: ${error.message}`); | ||
res.status(500).json({ message: "Internal server error" }); | ||
} | ||
}; | ||
|
||
// Delete an invoice by ID | ||
const deleteInvoice = async (req, res) => { | ||
try { | ||
const invoice = await Invoice.findByIdAndDelete(req.params.id); | ||
if (!invoice) return res.status(404).json({ message: "Invoice not found" }); | ||
res.status(200).json({ message: "Invoice deleted successfully" }); | ||
} catch (error) { | ||
console.error(`Error deleting invoice: ${error.message}`); | ||
res.status(500).json({ message: "Internal server error" }); | ||
} | ||
}; | ||
|
||
// Mark overdue invoices | ||
const markOverdueInvoices = async (req, res) => { | ||
try { | ||
const overdueInvoices = await Invoice.updateMany( | ||
{ dueDate: { $lt: new Date() }, status: "Unpaid" }, | ||
{ status: "Overdue" } | ||
); | ||
res | ||
.status(200) | ||
.json({ | ||
message: `${overdueInvoices.nModified} invoices marked as overdue`, | ||
}); | ||
} catch (error) { | ||
console.error(`Error marking overdue invoices: ${error.message}`); | ||
res.status(500).json({ message: "Internal server error" }); | ||
} | ||
}; | ||
|
||
// Summary of invoices by status | ||
const invoiceSummary = async (req, res) => { | ||
const { userId } = req.query; | ||
|
||
try { | ||
const match = userId ? { userId } : {}; | ||
|
||
const summary = await Invoice.aggregate([ | ||
{ $match: match }, | ||
{ | ||
$group: { | ||
_id: "$status", | ||
totalAmount: { $sum: "$amount" }, | ||
count: { $sum: 1 }, | ||
}, | ||
}, | ||
]); | ||
|
||
const summaryData = summary.reduce((acc, item) => { | ||
acc[item._id] = { totalAmount: item.totalAmount, count: item.count }; | ||
return acc; | ||
}, {}); | ||
|
||
res.status(200).json(summaryData); | ||
} catch (error) { | ||
console.error(`Error generating invoice summary: ${error.message}`); | ||
res.status(500).json({ message: "Internal server error" }); | ||
} | ||
}; | ||
|
||
// Bulk invoice generation | ||
const generateBulkInvoices = async (req, res) => { | ||
const { invoices } = req.body; | ||
|
||
if (!Array.isArray(invoices) || invoices.length === 0) { | ||
return res.status(400).json({ message: "Invalid invoices data" }); | ||
} | ||
|
||
const validationErrors = invoices | ||
.map((invoiceData, index) => { | ||
const { error } = invoiceSchema.validate(invoiceData); | ||
return error ? { index, message: error.message } : null; | ||
}) | ||
.filter(Boolean); | ||
|
||
if (validationErrors.length > 0) { | ||
return res | ||
.status(400) | ||
.json({ message: "Validation errors", errors: validationErrors }); | ||
} | ||
|
||
try { | ||
const invoiceDocuments = invoices.map((invoiceData) => ({ | ||
...invoiceData, | ||
invoiceId: `INV${Date.now() + Math.floor(Math.random() * 1000)}`, | ||
})); | ||
|
||
const createdInvoices = await Invoice.insertMany(invoiceDocuments); | ||
res.status(201).json(createdInvoices); | ||
} catch (error) { | ||
console.error(`Error generating bulk invoices: ${error.message}`); | ||
res.status(500).json({ message: "Internal server error" }); | ||
} | ||
}; | ||
|
||
module.exports = { | ||
generateInvoice, | ||
getInvoices, | ||
getInvoiceById, | ||
updateInvoiceStatus, | ||
deleteInvoice, | ||
markOverdueInvoices, | ||
invoiceSummary, | ||
generateBulkInvoices, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
const mongoose = require("mongoose"); | ||
|
||
|
||
const invoiceSchema = new mongoose.Schema( | ||
{ | ||
invoiceId: { type: String, required: true, unique: true }, | ||
orderId: { | ||
type: mongoose.Schema.Types.ObjectId, | ||
ref: "Order", | ||
required: true, | ||
}, | ||
userId: { | ||
type: mongoose.Schema.Types.ObjectId, | ||
ref: "User", | ||
required: true, | ||
index: true, // Adds index for faster query on user invoices | ||
}, | ||
amount: { type: Number, required: true, min: 0 }, | ||
invoiceDate: { type: Date, default: Date.now }, | ||
dueDate: { | ||
type: Date, | ||
required: true, | ||
default: function () { | ||
return new Date(+new Date() + 7 * 24 * 60 * 60 * 1000); // Default due date is 7 days from invoice date | ||
}, | ||
}, | ||
status: { | ||
type: String, | ||
enum: ["Paid", "Unpaid", "Overdue", "Cancelled"], | ||
default: "Unpaid", | ||
}, | ||
paymentId: { | ||
type: mongoose.Schema.Types.ObjectId, | ||
ref: "Payment", | ||
sparse: true, // Allows null value indexing for unpaid invoices | ||
}, | ||
}, | ||
{ timestamps: true } | ||
); | ||
|
||
// Virtual field to check if invoice is overdue | ||
invoiceSchema.virtual("isOverdue").get(function () { | ||
return this.status === "Unpaid" && this.dueDate < new Date(); | ||
}); | ||
|
||
const Invoice = mongoose.model("Invoice", invoiceSchema); | ||
module.exports = Invoice; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
const express = require("express"); | ||
const router = express.Router(); | ||
|
||
const { | ||
generateInvoice, | ||
getInvoices, | ||
getInvoiceById, | ||
updateInvoiceStatus, | ||
deleteInvoice, | ||
markOverdueInvoices, | ||
invoiceSummary, | ||
generateBulkInvoices, | ||
} = require("../controllers/invoiceController"); | ||
|
||
// Route to generate a new invoice after payment confirmation | ||
router.post("/generate", generateInvoice); | ||
|
||
// Route to retrieve invoices by user, order, status, or date range with pagination and sorting | ||
router.get("/", getInvoices); | ||
|
||
// Route to retrieve a specific invoice by its ID | ||
router.get("/:id", getInvoiceById); | ||
|
||
// Route to update an invoice's status | ||
router.patch("/:id/status", updateInvoiceStatus); | ||
|
||
// Route to delete an invoice by its ID | ||
router.delete("/:id", deleteInvoice); | ||
|
||
// Route to mark overdue invoices (based on the current date) | ||
router.patch("/mark-overdue", markOverdueInvoices); | ||
|
||
// Route to get an invoice summary (by status, optionally by user) | ||
router.get("/summary", invoiceSummary); | ||
|
||
// Route to generate bulk invoices at once | ||
router.post("/generate-bulk", generateBulkInvoices); | ||
|
||
module.exports = router; |