Spaces:
Runtime error
Runtime error
Commit
·
2041c17
1
Parent(s):
6e8e156
Refactor invoice handling:
Browse files- Updated `getAllInvoices` to disable paranoid mode for fetching all invoices.
- Enhanced `deleteInvoice` to prevent modification of already synced invoices and set status to archived before deletion.
- Created migration to remove unique constraint from `reference_number` in invoices.
- Added validation hooks in the Invoice model to ensure unique reference numbers for non-archived invoices.
src/controllers/invoice/invoice.controller.ts
CHANGED
@@ -317,9 +317,10 @@ export const getAllInvoices = async (req: AuthenticatedRequest, res: Response):
|
|
317 |
},
|
318 |
{
|
319 |
model: InvoiceDetail,
|
320 |
-
as:
|
321 |
-
}
|
322 |
-
]
|
|
|
323 |
});
|
324 |
|
325 |
if (roleData?.name === "Property Manager") {
|
@@ -664,6 +665,16 @@ export const deleteInvoice = async (req: Request, res: Response): Promise<Respon
|
|
664 |
return res.status(404).json({ error: "Invoice not found" });
|
665 |
}
|
666 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
667 |
await invoice.destroy();
|
668 |
|
669 |
return res.status(204).send();
|
|
|
317 |
},
|
318 |
{
|
319 |
model: InvoiceDetail,
|
320 |
+
as: "InvoiceDetails",
|
321 |
+
},
|
322 |
+
],
|
323 |
+
paranoid: false,
|
324 |
});
|
325 |
|
326 |
if (roleData?.name === "Property Manager") {
|
|
|
665 |
return res.status(404).json({ error: "Invoice not found" });
|
666 |
}
|
667 |
|
668 |
+
if (invoice.status === "sync success") {
|
669 |
+
return res
|
670 |
+
.status(400)
|
671 |
+
.json({ error: "Invoice has already been synced and cannot be modified" });
|
672 |
+
}
|
673 |
+
|
674 |
+
invoice.status = "archived";
|
675 |
+
|
676 |
+
await invoice.save();
|
677 |
+
|
678 |
await invoice.destroy();
|
679 |
|
680 |
return res.status(204).send();
|
src/db/migrations/20250123074217-remove-unique-constraint-from-invoice-reference-number.js
ADDED
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use strict";
|
2 |
+
|
3 |
+
/** @type {import('sequelize-cli').Migration} */
|
4 |
+
module.exports = {
|
5 |
+
async up(queryInterface, Sequelize) {
|
6 |
+
/**
|
7 |
+
* Add altering commands here.
|
8 |
+
*
|
9 |
+
* Example:
|
10 |
+
* await queryInterface.createTable('users', { id: Sequelize.INTEGER });
|
11 |
+
*/
|
12 |
+
|
13 |
+
await queryInterface.changeColumn("invoices", "reference_number", {
|
14 |
+
type: Sequelize.STRING(100),
|
15 |
+
unique: false,
|
16 |
+
});
|
17 |
+
},
|
18 |
+
|
19 |
+
async down(queryInterface, Sequelize) {
|
20 |
+
/**
|
21 |
+
* Add reverting commands here.
|
22 |
+
*
|
23 |
+
* Example:
|
24 |
+
* await queryInterface.dropTable('users');
|
25 |
+
*/
|
26 |
+
|
27 |
+
await queryInterface.changeColumn("invoices", "reference_number", {
|
28 |
+
type: Sequelize.STRING(100),
|
29 |
+
unique: true,
|
30 |
+
});
|
31 |
+
},
|
32 |
+
};
|
src/models/invoice.ts
CHANGED
@@ -2,11 +2,14 @@ import {
|
|
2 |
DataTypes,
|
3 |
Model,
|
4 |
CreationOptional,
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
import
|
|
|
|
|
|
|
10 |
|
11 |
class Invoice extends Model<InvoiceInterface> implements InvoiceInterface {
|
12 |
declare id?: number;
|
@@ -15,12 +18,12 @@ class Invoice extends Model<InvoiceInterface> implements InvoiceInterface {
|
|
15 |
declare vendor_name: string;
|
16 |
declare invoice_date: Date;
|
17 |
declare total: number;
|
18 |
-
declare amount_paid: number;
|
19 |
declare due_date?: Date;
|
20 |
declare term?: string;
|
21 |
declare description?: string;
|
22 |
declare payment_status?: string;
|
23 |
-
declare pw_work_order_id?: number;
|
24 |
declare pw_vendor_id?: number;
|
25 |
declare filename?: string;
|
26 |
declare pdf_url?: string;
|
@@ -38,7 +41,6 @@ Invoice.init(
|
|
38 |
},
|
39 |
reference_number: {
|
40 |
type: DataTypes.STRING(100),
|
41 |
-
unique: true,
|
42 |
},
|
43 |
invoice_number: {
|
44 |
type: DataTypes.STRING(100),
|
@@ -52,7 +54,7 @@ Invoice.init(
|
|
52 |
total: {
|
53 |
type: DataTypes.DECIMAL(10, 2),
|
54 |
},
|
55 |
-
amount_paid: {
|
56 |
type: DataTypes.DECIMAL(10, 2),
|
57 |
},
|
58 |
due_date: {
|
@@ -69,17 +71,17 @@ Invoice.init(
|
|
69 |
},
|
70 |
payment_status: {
|
71 |
type: DataTypes.STRING(50),
|
72 |
-
defaultValue:
|
73 |
},
|
74 |
pw_work_order_id: {
|
75 |
type: DataTypes.INTEGER,
|
76 |
allowNull: true,
|
77 |
-
},
|
78 |
pw_vendor_id: {
|
79 |
type: DataTypes.INTEGER,
|
80 |
allowNull: true,
|
81 |
},
|
82 |
-
filename:{
|
83 |
type: DataTypes.STRING(255),
|
84 |
allowNull: false,
|
85 |
},
|
@@ -91,26 +93,60 @@ Invoice.init(
|
|
91 |
type: DataTypes.STRING(50),
|
92 |
allowNull: false,
|
93 |
},
|
94 |
-
uploaded_by: {
|
95 |
type: DataTypes.INTEGER,
|
96 |
allowNull: false,
|
97 |
},
|
98 |
},
|
99 |
{
|
100 |
sequelize,
|
101 |
-
tableName:
|
102 |
underscored: true,
|
103 |
freezeTableName: true,
|
104 |
timestamps: true,
|
105 |
-
createdAt:
|
106 |
-
updatedAt:
|
107 |
paranoid: true,
|
108 |
-
deletedAt:
|
109 |
}
|
110 |
);
|
111 |
|
112 |
-
Invoice.belongsTo(User, { foreignKey:
|
113 |
-
Invoice.hasMany(InvoiceDetail, { foreignKey:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
114 |
|
|
|
|
|
|
|
115 |
|
116 |
export default Invoice;
|
|
|
2 |
DataTypes,
|
3 |
Model,
|
4 |
CreationOptional,
|
5 |
+
ValidationErrorItem,
|
6 |
+
Op,
|
7 |
+
ValidationError,
|
8 |
+
} from "sequelize";
|
9 |
+
import { sequelize } from "./index";
|
10 |
+
import { InvoiceInterface } from "../shared/interfaces/invoice.interface";
|
11 |
+
import User from "./users";
|
12 |
+
import InvoiceDetail from "./invoicedetail";
|
13 |
|
14 |
class Invoice extends Model<InvoiceInterface> implements InvoiceInterface {
|
15 |
declare id?: number;
|
|
|
18 |
declare vendor_name: string;
|
19 |
declare invoice_date: Date;
|
20 |
declare total: number;
|
21 |
+
declare amount_paid: number;
|
22 |
declare due_date?: Date;
|
23 |
declare term?: string;
|
24 |
declare description?: string;
|
25 |
declare payment_status?: string;
|
26 |
+
declare pw_work_order_id?: number;
|
27 |
declare pw_vendor_id?: number;
|
28 |
declare filename?: string;
|
29 |
declare pdf_url?: string;
|
|
|
41 |
},
|
42 |
reference_number: {
|
43 |
type: DataTypes.STRING(100),
|
|
|
44 |
},
|
45 |
invoice_number: {
|
46 |
type: DataTypes.STRING(100),
|
|
|
54 |
total: {
|
55 |
type: DataTypes.DECIMAL(10, 2),
|
56 |
},
|
57 |
+
amount_paid: {
|
58 |
type: DataTypes.DECIMAL(10, 2),
|
59 |
},
|
60 |
due_date: {
|
|
|
71 |
},
|
72 |
payment_status: {
|
73 |
type: DataTypes.STRING(50),
|
74 |
+
defaultValue: "pending",
|
75 |
},
|
76 |
pw_work_order_id: {
|
77 |
type: DataTypes.INTEGER,
|
78 |
allowNull: true,
|
79 |
+
},
|
80 |
pw_vendor_id: {
|
81 |
type: DataTypes.INTEGER,
|
82 |
allowNull: true,
|
83 |
},
|
84 |
+
filename: {
|
85 |
type: DataTypes.STRING(255),
|
86 |
allowNull: false,
|
87 |
},
|
|
|
93 |
type: DataTypes.STRING(50),
|
94 |
allowNull: false,
|
95 |
},
|
96 |
+
uploaded_by: {
|
97 |
type: DataTypes.INTEGER,
|
98 |
allowNull: false,
|
99 |
},
|
100 |
},
|
101 |
{
|
102 |
sequelize,
|
103 |
+
tableName: "invoices",
|
104 |
underscored: true,
|
105 |
freezeTableName: true,
|
106 |
timestamps: true,
|
107 |
+
createdAt: "created_at",
|
108 |
+
updatedAt: "updated_at",
|
109 |
paranoid: true,
|
110 |
+
deletedAt: "deleted_at",
|
111 |
}
|
112 |
);
|
113 |
|
114 |
+
Invoice.belongsTo(User, { foreignKey: "uploaded_by", as: "uploadedBy" });
|
115 |
+
Invoice.hasMany(InvoiceDetail, { foreignKey: "invoice_id" });
|
116 |
+
|
117 |
+
const validateInvoice = async (invoice: Invoice) => {
|
118 |
+
const existingInvoice = await Invoice.findOne({
|
119 |
+
where: {
|
120 |
+
reference_number: invoice.reference_number,
|
121 |
+
status: { [Op.ne]: "archived" },
|
122 |
+
id: { [Op.ne]: invoice.id },
|
123 |
+
},
|
124 |
+
});
|
125 |
+
|
126 |
+
const validationErrorItems: ValidationErrorItem[] = [];
|
127 |
+
|
128 |
+
if (existingInvoice) {
|
129 |
+
validationErrorItems.push(
|
130 |
+
new ValidationErrorItem(
|
131 |
+
"Invoice with the same reference number already exists; archive it before adding another.",
|
132 |
+
"unique violation",
|
133 |
+
"reference_number",
|
134 |
+
invoice.reference_number,
|
135 |
+
invoice,
|
136 |
+
"isUnique",
|
137 |
+
"reference_number",
|
138 |
+
[]
|
139 |
+
)
|
140 |
+
);
|
141 |
+
}
|
142 |
+
|
143 |
+
if (validationErrorItems.length > 0) {
|
144 |
+
throw new ValidationError("Validation Error", validationErrorItems);
|
145 |
+
}
|
146 |
+
};
|
147 |
|
148 |
+
Invoice.addHook("beforeCreate", validateInvoice);
|
149 |
+
Invoice.addHook("beforeUpdate", validateInvoice);
|
150 |
+
Invoice.addHook("beforeSave", validateInvoice);
|
151 |
|
152 |
export default Invoice;
|