Spaces:
Runtime error
Runtime error
Bansari Akhani
commited on
Commit
·
1c7eff7
1
Parent(s):
7484a11
w.i.p.- invoice approval
Browse files- src/controllers/invoice/invoice.controller.ts +102 -0
- src/db/migrations/20240808130755-create-invoice-approval.js +56 -0
- src/models/invoice.ts +1 -1
- src/models/invoiceApproval.ts +59 -0
- src/models/roles.ts +0 -1
- src/routes/errorLog.routes.ts +1 -1
- src/routes/invoice.routes.ts +25 -0
- src/shared/interfaces/invoiceApproval.interface.ts +8 -0
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?:
|
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 |
+
}
|