gopalswami commited on
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: 'InvoiceDetails',
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
- } from 'sequelize';
6
- import { sequelize } from './index';
7
- import { InvoiceInterface } from '../shared/interfaces/invoice.interface';
8
- import User from './users';
9
- import InvoiceDetail from './invoicedetail';
 
 
 
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: 'pending',
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: 'invoices',
102
  underscored: true,
103
  freezeTableName: true,
104
  timestamps: true,
105
- createdAt: 'created_at',
106
- updatedAt: 'updated_at',
107
  paranoid: true,
108
- deletedAt: 'deleted_at',
109
  }
110
  );
111
 
112
- Invoice.belongsTo(User, { foreignKey: 'uploaded_by', as: 'uploadedBy' });
113
- Invoice.hasMany(InvoiceDetail, { foreignKey: 'invoice_id' });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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;