Bansari Akhani commited on
Commit
1c7eff7
·
1 Parent(s): 7484a11

w.i.p.- invoice approval

Browse files
src/controllers/invoice/invoice.controller.ts CHANGED
@@ -14,6 +14,8 @@ import { logger } from '../../utils/logger';
14
  import ErrorLog from "../../models/errorLog";
15
  import { fetchWorkorderById } from "../../shared/services/propertyware.service";
16
  import { logInvoiceAction } from "../invoiceActivityLogs.controller";
 
 
17
 
18
  export const createInvoice = async (req: Request, res: Response) => {
19
  const files = req.files as Express.Multer.File[];
@@ -476,3 +478,103 @@ export const deleteInvoice = async (req: Request, res: Response): Promise<Respon
476
  return res.status(500).json({ error: "Internal server error" });
477
  }
478
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  import ErrorLog from "../../models/errorLog";
15
  import { fetchWorkorderById } from "../../shared/services/propertyware.service";
16
  import { logInvoiceAction } from "../invoiceActivityLogs.controller";
17
+ import InvoiceApproval from "../../models/invoiceApproval";
18
+ import Role from "../../models/roles";
19
 
20
  export const createInvoice = async (req: Request, res: Response) => {
21
  const files = req.files as Express.Multer.File[];
 
478
  return res.status(500).json({ error: "Internal server error" });
479
  }
480
  };
481
+
482
+ // Interface for approval data
483
+ interface ApprovalData {
484
+ invoiceId: number;
485
+ userId: number;
486
+ comment?: string;
487
+ }
488
+
489
+ // Function to approve invoice
490
+ export const approveInvoice = async (req: Request, res: Response): Promise<void> => {
491
+ const { invoiceId, userId = 1, comment = '' }: ApprovalData = req.body;
492
+
493
+ try {
494
+ const invoice = await Invoice.findOne({
495
+ where: { id: invoiceId },
496
+ include: [{ model: InvoiceDetail }]
497
+ });
498
+
499
+ if (!invoice) {
500
+ res.status(404).json({ error: 'Invoice not found' });
501
+ return;
502
+ }
503
+
504
+ const missingPwPortfolioId = invoice.InvoiceDetails.some(
505
+ (detail: InvoiceDetail) => !detail.pw_portfolio_id
506
+ );
507
+
508
+ if (missingPwPortfolioId) {
509
+ res.status(400).json({ error: 'Invoice cannot be approved without property address' });
510
+ return;
511
+ }
512
+
513
+ const user = await User.findByPk(1, {
514
+ include: [{ model: Role }],
515
+ });
516
+ if (!user) {
517
+ res.status(400).json({ error: 'Invalid User' });
518
+ return;
519
+ }
520
+
521
+ const role = await Role.findByPk(user.id);
522
+ if (!role) {
523
+ res.status(400).json({ error: 'Invalid approval role ID' });
524
+ return;
525
+ }
526
+ const approvalRoleId = role.id;
527
+
528
+ if (invoice.total < 1500) {
529
+ if (role.name === 'PM' || role.name === 'Accounting Supervisor') {
530
+ await approveAndCreateRecord(invoice.id as number, userId, approvalRoleId, comment);
531
+
532
+ invoice.status = 'Approved';
533
+ await invoice.save();
534
+
535
+ res.status(200).json({ message: 'Invoice approved' });
536
+ } else {
537
+ res.status(403).json({ error: 'Only Property Manager or Accounting Supervisor can approve this invoice' });
538
+ }
539
+ } else {
540
+ if (role.name === 'PM') {
541
+ await approveAndCreateRecord(invoice.id as number, userId, approvalRoleId, comment);
542
+
543
+ invoice.status = 'PM Approved';
544
+ await invoice.save();
545
+
546
+ res.status(200).json({ message: 'Invoice approved by PM' });
547
+
548
+ } else if (role.name === 'Accounting Supervisor' && invoice.status === 'PM Approved') {
549
+ await approveAndCreateRecord(invoice.id as number, userId, approvalRoleId, comment);
550
+
551
+ invoice.status = 'Approved';
552
+ await invoice.save();
553
+
554
+ res.status(200).json({ message: 'Invoice approved by Accounting Supervisor' });
555
+
556
+ } else {
557
+ res.status(403).json({ error: 'Invoice needs to be approved by Property Manager first' });
558
+ }
559
+ }
560
+ } catch (error) {
561
+ res.status(500).json({ error: error });
562
+ }
563
+ };
564
+
565
+ const approveAndCreateRecord = async (
566
+ invoiceId: number,
567
+ userId: number,
568
+ approvalRoleId: number,
569
+ comment: string
570
+ ): Promise<void> => {
571
+ await InvoiceApproval.create({
572
+ invoiceId: invoiceId,
573
+ approvedBy: userId,
574
+ approvalRoleId: approvalRoleId,
575
+ comment,
576
+ createdAt: new Date()
577
+ });
578
+ };
579
+
580
+
src/db/migrations/20240808130755-create-invoice-approval.js ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use strict';
2
+
3
+ /** @type {import('sequelize-cli').Migration} */
4
+ module.exports = {
5
+ async up (queryInterface, Sequelize) {
6
+ await queryInterface.createTable('invoice_approval', {
7
+ id: {
8
+ type: Sequelize.INTEGER,
9
+ allowNull: false,
10
+ autoIncrement: true,
11
+ primaryKey: true,
12
+ },
13
+ invoice_id: {
14
+ type: Sequelize.INTEGER,
15
+ allowNull: false,
16
+ references: {
17
+ model: 'invoices',
18
+ key: 'id',
19
+ },
20
+ onUpdate: 'CASCADE',
21
+ onDelete: 'CASCADE',
22
+ },
23
+ approved_by: {
24
+ type: Sequelize.INTEGER,
25
+ allowNull: false,
26
+ references: {
27
+ model: 'users',
28
+ key: 'id',
29
+ },
30
+ onDelete: 'CASCADE',
31
+ },
32
+ approval_role_id: {
33
+ type: Sequelize.INTEGER,
34
+ allowNull: false,
35
+ references: {
36
+ model: 'roles',
37
+ key: 'id',
38
+ },
39
+ onDelete: 'CASCADE',
40
+ },
41
+ comment: {
42
+ type: Sequelize.TEXT,
43
+ allowNull: true,
44
+ },
45
+ created_at: {
46
+ type: Sequelize.DATE,
47
+ allowNull: false,
48
+ defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
49
+ },
50
+ });
51
+ },
52
+
53
+ async down (queryInterface, Sequelize) {
54
+ await queryInterface.dropTable('invoice_approval');
55
+ }
56
+ };
src/models/invoice.ts CHANGED
@@ -9,7 +9,7 @@ import User from './users';
9
  import InvoiceDetail from './invoicedetail';
10
 
11
  class Invoice extends Model<InvoiceInterface> implements InvoiceInterface {
12
- declare id?: CreationOptional<number>;
13
  declare reference_number: string;
14
  declare invoice_number: string;
15
  declare vendor_name: string;
 
9
  import InvoiceDetail from './invoicedetail';
10
 
11
  class Invoice extends Model<InvoiceInterface> implements InvoiceInterface {
12
+ declare id?: number;
13
  declare reference_number: string;
14
  declare invoice_number: string;
15
  declare vendor_name: string;
src/models/invoiceApproval.ts ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Model, DataTypes } from 'sequelize';
2
+ import { InvoiceApprovalAttributes } from 'shared/interfaces/invoiceApproval.interface';
3
+ import { sequelize } from './index';
4
+
5
+
6
+ class InvoiceApproval extends Model<InvoiceApprovalAttributes>
7
+ implements InvoiceApprovalAttributes {
8
+ public id?: number;
9
+ public invoiceId!: number;
10
+ public approvedBy!: number;
11
+ public approvalRoleId!: number;
12
+ public comment?: string;
13
+ public createdAt?: Date;
14
+
15
+ }
16
+
17
+ InvoiceApproval.init(
18
+ {
19
+ id: {
20
+ type: DataTypes.INTEGER,
21
+ autoIncrement: true,
22
+ primaryKey: true,
23
+ },
24
+ invoiceId: {
25
+ type: DataTypes.INTEGER,
26
+ allowNull: false,
27
+ references: {
28
+ model: 'invoices',
29
+ key: 'id',
30
+ },
31
+ onUpdate: 'CASCADE',
32
+ onDelete: 'CASCADE',
33
+ },
34
+ approvedBy: {
35
+ type: DataTypes.INTEGER,
36
+ allowNull: false,
37
+ },
38
+ approvalRoleId: {
39
+ type: DataTypes.INTEGER,
40
+ allowNull: false,
41
+ },
42
+ comment: {
43
+ type: DataTypes.TEXT,
44
+ allowNull: true,
45
+ },
46
+ createdAt: {
47
+ type: DataTypes.DATE,
48
+ defaultValue: DataTypes.NOW,
49
+ },
50
+ },
51
+ {
52
+ sequelize,
53
+ modelName: 'InvoiceApproval',
54
+ tableName: 'invoice_approval',
55
+ timestamps: false,
56
+ }
57
+ );
58
+
59
+ export default InvoiceApproval;
src/models/roles.ts CHANGED
@@ -28,7 +28,6 @@ Role.init(
28
  allowNull: false,
29
  }
30
  },
31
-
32
  {
33
  sequelize,
34
  tableName: 'roles',
 
28
  allowNull: false,
29
  }
30
  },
 
31
  {
32
  sequelize,
33
  tableName: 'roles',
src/routes/errorLog.routes.ts CHANGED
@@ -137,4 +137,4 @@ errorLogRouter.get("/", getErrorLogs);
137
  */
138
  errorLogRouter.get("/:id", getErrorLogById);
139
 
140
- export default errorLogRouter;
 
137
  */
138
  errorLogRouter.get("/:id", getErrorLogById);
139
 
140
+ export default errorLogRouter;
src/routes/invoice.routes.ts CHANGED
@@ -8,9 +8,34 @@ import {
8
  getInvoiceById,
9
  deleteInvoice,
10
  updateInvoice,
 
11
  } from '../controllers/invoice/invoice.controller';
12
 
13
  const invoiceRouter = express.Router();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  invoiceRouter.use(jwtMiddleware);
15
 
16
  /**
 
8
  getInvoiceById,
9
  deleteInvoice,
10
  updateInvoice,
11
+ approveInvoice,
12
  } from '../controllers/invoice/invoice.controller';
13
 
14
  const invoiceRouter = express.Router();
15
+
16
+ /**
17
+ * @swagger
18
+ * /api/invoices/{id}/approve:
19
+ * post:
20
+ * summary: Approve Invoice
21
+ * tags: [Invoices]
22
+ * parameters:
23
+ * - in: path
24
+ * name: id
25
+ * schema:
26
+ * type: integer
27
+ * required: true
28
+ * description: Invoice ID
29
+ * responses:
30
+ * 200:
31
+ * description: Invoices approved successfully
32
+ * 400:
33
+ * description: Bad Request
34
+ * 500:
35
+ * description: Error approving invoices
36
+ */
37
+ invoiceRouter.post("/:id/approve", [], approveInvoice);
38
+
39
  invoiceRouter.use(jwtMiddleware);
40
 
41
  /**
src/shared/interfaces/invoiceApproval.interface.ts ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ export interface InvoiceApprovalAttributes {
2
+ id?: number;
3
+ invoiceId: number;
4
+ approvedBy: number;
5
+ approvalRoleId: number;
6
+ comment?: string;
7
+ createdAt?: Date;
8
+ }