Spaces:
Runtime error
Runtime error
import { Request, Response } from "express"; | |
import { uploadInvoice } from "../../shared/services/invoice.service"; | |
import { parseInvoice } from "../../shared/services/ai.service"; | |
import FormData from "form-data"; | |
import Invoice from "../../models/invoice"; | |
import InvoiceDetail from "../../models/invoicedetail"; | |
import User from "../../models/users"; | |
import { FindOptions, Op } from "sequelize"; | |
import { logger } from '../../utils/logger'; | |
import ErrorLog from "../../models/errorLog"; | |
import { fetchBuildingsById, fetchPortfolioById, fetchUnitsById, fetchBuildingPropertyManager, fetchGLAccountById } from "../../shared/services/propertyware.service"; | |
import { logInvoiceAction } from "../invoiceActivityLogs.controller"; | |
import InvoiceApproval from "../../models/invoiceApproval"; | |
import Role from "../../models/roles"; | |
import { AuthenticatedRequest } from "shared/interfaces/user.interface"; | |
import { isVendorHasDefaultBillsplitAccount } from "../../controllers/propertyware/vendors.controller"; | |
import InvoiceActivityLog from "../../models/invoiceActivityLogs"; | |
import PwWorkOrders from "../../models/pwWorkOrders"; | |
export const createInvoice = async (req: AuthenticatedRequest, res: Response) => { | |
const files = req.files as Express.Multer.File[]; | |
if (!files?.length) { | |
return res.status(400).json({ message: "No files uploaded" }); | |
} | |
try { | |
const uploadResults = await uploadInvoice(files); | |
res.status(201).json({ message: "Files uploaded successfully, processing invoices in the background" }); | |
// Process the rest of the operations in the background | |
const createInvoicePromises = files.map(async (file, index) => { | |
try { | |
const formData = new FormData(); | |
formData.append("file", file.buffer, file.originalname); | |
// Send the file URLs to the AI service API for parsing | |
const aiServiceResponse = await parseInvoice(formData); | |
const aiServiceData = aiServiceResponse[0]; | |
console.log("file name ", file.originalname); | |
console.log("aiServiceData ", aiServiceData); | |
// validate if parsed workorderID exists in propertyware workorders | |
let portfolioId: number | null = null; | |
let buildingId: number | null = null; | |
let unitId: number | null = null; | |
if (aiServiceData.workOrderID) { | |
try { | |
const PWWorkorderDetails = await PwWorkOrders.findOne({ | |
where: { | |
number: aiServiceData.workOrderID, | |
}, | |
}); | |
if (PWWorkorderDetails) { | |
portfolioId = PWWorkorderDetails.portfolio_id; | |
buildingId = PWWorkorderDetails.building_id; | |
unitId = PWWorkorderDetails.unit_id; | |
aiServiceData.workOrderID = PWWorkorderDetails.pw_id; | |
} else { | |
aiServiceData.workOrderID = null; | |
} | |
} catch (error) { | |
logger.error(error); | |
aiServiceData.workOrderID = null; | |
} | |
} else { | |
aiServiceData.workOrderID = null; | |
} | |
const totalAmount = aiServiceData.billSplits.reduce( | |
(total: number, split: { amount: number }) => total + split.amount, | |
0 | |
); | |
const invoiceDate = new Date(aiServiceData.billDate); | |
const dueDate = new Date(aiServiceData.dueDate); | |
// check if vendor has default bill split ID defined in Propertyware | |
let isDefaultBillsplitAccountSet = null; | |
if (aiServiceData.vendor_id) { | |
try { | |
isDefaultBillsplitAccountSet = await isVendorHasDefaultBillsplitAccount(aiServiceData.vendor_id); | |
} catch (error) { | |
logger.error(error); | |
} | |
} | |
let invoiceRecord = { | |
reference_number: aiServiceData.refNo, | |
invoice_number: aiServiceData.refNo, | |
vendor_name: aiServiceData.vendor_name, | |
pw_vendor_id: aiServiceData.vendor_id, | |
invoice_date: invoiceDate, | |
due_date: dueDate, | |
total: totalAmount, | |
description: '', | |
status: "Pending", | |
amount_paid: 0, | |
term: aiServiceData.terms, | |
pw_work_order_id: aiServiceData.workOrderID, | |
filename: file.originalname, | |
pdf_url: (await uploadResults[index]).url, | |
uploaded_by: req.baseUrl === "/private" ? 1 : req.user?.id as number | |
}; | |
const invoice = await Invoice.create(invoiceRecord); | |
const invoiceDetailsPromises = aiServiceData.billSplits.map(async (details: { portfolio: any; building: any; unitID: any; glAccount: any; amount: any; description: any; }) => { | |
return { | |
invoice_id: invoice.id, | |
pw_portfolio_id: portfolioId ? portfolioId : (details.portfolio ? details.portfolio : null), | |
pw_building_id: buildingId ? buildingId : (details.building ? details.building : null), | |
pw_unit_id: unitId ? unitId : (details.unitID ? details.unitID : null), | |
pw_gl_account_id: isDefaultBillsplitAccountSet ? isDefaultBillsplitAccountSet : (details.glAccount ? details.glAccount : null), | |
amount: details.amount, | |
description: details.description, | |
}; | |
}); | |
const invoiceDetails = await Promise.all(invoiceDetailsPromises); | |
await InvoiceDetail.bulkCreate(invoiceDetails); | |
await logInvoiceAction({ invoice_id: invoice.id as number, user_id: invoice.uploaded_by, activity_type: 'create', field_name: 'invoice', old_value: '', new_value: JSON.stringify(invoice) }); | |
} catch (error) { | |
if (error instanceof Error) { | |
let errorMessage = error.message; | |
if ((error as any)?.parent?.sqlMessage) { | |
errorMessage = (error as any).parent.sqlMessage; | |
} | |
logger.error(`Error creating invoice for file ${file.originalname}`); | |
logger.error(error); | |
// Add entry to error log | |
const errorLogData = { error_type: 'Invoice Create Failed', error_details: `Error creating invoice for file ${file.originalname}: ${errorMessage}` }; | |
await ErrorLog.create(errorLogData); | |
} else { | |
logger.error(`Unknown error creating invoice for file ${file.originalname}`); | |
logger.error(error); | |
const errorLogData = { error_type: 'Invoice Create Failed', error_details: `Unknown error creating invoice for file ${file.originalname}: ${JSON.stringify(error)}` }; | |
await ErrorLog.create(errorLogData); | |
} | |
} | |
}); | |
try { | |
await Promise.all(createInvoicePromises); | |
logger.info("Invoices processed successfully in the background"); | |
} catch (error) { | |
if (error instanceof Error) { | |
logger.error(`Error processing invoices in the background`); | |
logger.error(error); | |
const errorLogData = { error_type: 'Invoice Process Failed', error_details: `Error processing invoices in the background: ${error.message}` }; | |
await ErrorLog.create(errorLogData); | |
} else { | |
logger.error('Unknown error processing invoices in the background'); | |
logger.error(error); | |
const errorLogData = { error_type: 'Invoice Process Failed', error_details: `Unknown error processing invoices in the background: ${JSON.stringify(error)}` }; | |
await ErrorLog.create(errorLogData); | |
} | |
} | |
} catch (error) { | |
if (error instanceof Error) { | |
logger.error(`Error uploading invoices: ${error.message}`); | |
logger.error(error); | |
const errorLogData = { error_type: 'Invoice Upload Failed', error_details: `Error uploading invoices: ${error.message}` }; | |
await ErrorLog.create(errorLogData); | |
} else { | |
logger.error('Unknown error uploading invoices', error); | |
logger.error(error); | |
const errorLogData = { error_type: 'Invoice Upload Failed', error_details: `Unknown error uploading invoices: ${JSON.stringify(error)}` }; | |
await ErrorLog.create(errorLogData); | |
} | |
} | |
}; | |
const buildInvoiceWhereClause = (filter: Record<string, any>): any => { | |
const whereClause: any = {}; | |
if (filter) { | |
if (filter.date) { | |
const date = new Date(filter.date); | |
if (!isNaN(date.getTime())) { | |
const startOfDay = new Date(date); | |
startOfDay.setHours(0, 0, 0, 0); | |
const endOfDay = new Date(date); | |
endOfDay.setHours(23, 59, 59, 999); | |
whereClause.created_at = { | |
[Op.gte]: startOfDay, | |
[Op.lte]: endOfDay | |
}; | |
} | |
} | |
if (filter.id) { | |
whereClause.id = { [Op.eq]: filter.id }; | |
} | |
if (filter.filename) { | |
whereClause.filename = { [Op.like]: `%${filter.filename}%` }; | |
} | |
if (filter.id) { | |
whereClause.id = { [Op.eq]: filter.id }; | |
} | |
if (filter.vendor_name) { | |
whereClause.vendor_name = { [Op.like]: `%${filter.vendor_name}%` }; | |
} | |
if (filter.reference_number) { | |
whereClause.reference_number = { [Op.like]: `%${filter.reference_number}%` }; | |
} | |
if (filter.invoice_date) { | |
const date = new Date(filter.invoice_date); | |
if (!isNaN(date.getTime())) { | |
whereClause.invoice_date = { [Op.eq]: date }; | |
} | |
} | |
if (filter.uploaded_by) { | |
whereClause.uploaded_by = { [Op.eq]: filter.uploaded_by }; | |
} | |
if (filter.due_date) { | |
const date = new Date(filter.due_date); | |
if (!isNaN(date.getTime())) { | |
whereClause.due_date = { [Op.eq]: date }; | |
} | |
} | |
if (filter.payment_status) { | |
whereClause.payment_status = { [Op.eq]: filter.payment_status }; | |
} | |
if (filter.status && filter.status != '*') { | |
whereClause.status = { [Op.eq]: filter.status }; | |
} | |
if (filter.pw_work_order_id) { | |
whereClause.pw_work_order_id = { [Op.eq]: filter.pw_work_order_id }; | |
} | |
if (filter.created_before) { | |
const beforeDate = new Date(filter.created_before); | |
if (!isNaN(beforeDate.getTime())) { | |
whereClause.created_at = { ...whereClause.created_at, [Op.lte]: beforeDate }; | |
} | |
} | |
if (filter.created_after) { | |
const afterDate = new Date(filter.created_after); | |
if (!isNaN(afterDate.getTime())) { | |
whereClause.created_at = { ...whereClause.created_at, [Op.gte]: afterDate }; | |
} | |
} | |
if (filter.total) { | |
whereClause.total = { [Op.eq]: filter.total }; | |
} | |
} | |
return whereClause; | |
}; | |
// Get All Invoices | |
export const getAllInvoices = async (req: AuthenticatedRequest, res: Response): Promise<Response> => { | |
try { | |
const { sort_by, sort_order, page, limit } = req.query; | |
const filter = req.query.filter as Record<string, any>; | |
const roleData = await Role.findByPk(req.user?.role_id); | |
const allowedSortColumns = [ | |
'id', | |
'invoice_date', | |
'total', | |
'amount_paid', | |
'due_date', | |
'pw_work_order_id', | |
'pw_vendor_id', | |
'uploaded_by', | |
'created_at', | |
'reference_number', | |
'filename', | |
'status' | |
]; | |
const whereClause = buildInvoiceWhereClause(filter); | |
const currentPage = parseInt(page as string) || 1; | |
const pageSize = parseInt(limit as string) || 10; | |
const options: FindOptions = { | |
where: whereClause, | |
order: [] | |
}; | |
if (sort_by && allowedSortColumns.includes(sort_by as string)) { | |
options.order = [[sort_by as string, sort_order === 'desc' ? 'DESC' : 'ASC']]; | |
} else { | |
options.order = [['id', 'DESC']]; | |
} | |
let invoices: any = await Invoice.findAll({ | |
...options, | |
include: [ | |
{ | |
model: User, | |
as: 'uploadedBy', | |
attributes: { exclude: ['password'] } | |
}, | |
{ | |
model: InvoiceDetail, | |
as: "InvoiceDetails", | |
}, | |
], | |
paranoid: false, | |
}); | |
if (roleData?.name === "Property Manager") { | |
const propertyManagerEmail = req?.user?.email; | |
invoices = await Promise.all( | |
invoices.map(async (invoice: any) => { | |
const invoiceDetails = invoice.InvoiceDetails || []; | |
for (const detail of invoiceDetails) { | |
if (detail.pw_building_id) { | |
const buildingManagers = await fetchBuildingPropertyManager(detail.pw_building_id); | |
if (buildingManagers.some((manager: any) => manager.email === propertyManagerEmail)) { | |
return invoice; | |
} | |
} | |
// TODO: Check with client if there is any possibility of having different Building in same invoice. and if so, will they have same PM assigned ? | |
// For now assuming it will be same, checking associated PM for first bill split item location only. | |
break; | |
} | |
return null; | |
}) | |
); | |
invoices = invoices.filter((invoice: any) => invoice !== null); | |
} else if (roleData?.name === "Accountant") { | |
const accountantId = req.user?.id; | |
let filteredInvoices = invoices.filter((invoice: any) => { | |
const invoiceDetails = invoice.InvoiceDetails || []; | |
return !invoiceDetails.some((detail: any) => { | |
return detail.pw_building_id || detail.pw_unit_id || detail.pw_portfolio_id; | |
}); | |
}); | |
const updatedInvoices = await InvoiceActivityLog.findAll({ | |
where: { | |
user_id: accountantId | |
}, | |
attributes: ['invoice_id'] | |
}); | |
const updatedInvoiceIds = updatedInvoices.map(log => log.invoice_id); | |
const invoicesUpdatedByAccountant = invoices.filter((invoice: any) => | |
updatedInvoiceIds.includes(invoice.id) | |
); | |
invoices = [...filteredInvoices, ...invoicesUpdatedByAccountant]; | |
} | |
const totalInvoices = invoices.length; | |
let paginatedInvoices; | |
if (currentPage === -1) { | |
paginatedInvoices = invoices; | |
} else { | |
paginatedInvoices = invoices.slice((currentPage - 1) * pageSize, currentPage * pageSize); | |
} | |
const responseData = { | |
page: currentPage === -1 ? 1 : currentPage, | |
limit: pageSize, | |
total: totalInvoices, | |
data: paginatedInvoices | |
}; | |
return res.status(200).json(responseData); | |
} catch (error) { | |
logger.error("Error fetching invoices:"); | |
logger.error(error); | |
return res.status(500).json({ error: "Error fetching invoices" }); | |
} | |
}; | |
export const getInvoiceById = async (req: AuthenticatedRequest, res: Response): Promise<Response> => { | |
try { | |
const { id } = req.params; | |
const invoice = await Invoice.findOne({ | |
where: { id: id }, include: [ | |
{ model: InvoiceDetail } | |
] | |
}); | |
if (!invoice) { | |
return res.status(404).json({ error: "Invoice not found" }); | |
} | |
const invoiceDetailsPromises = invoice.InvoiceDetails.map(async (invoiceDetails) => { | |
return { | |
amount: invoiceDetails.amount, | |
expenseAccount: invoiceDetails.pw_gl_account_id, | |
location: { portfolioId: invoiceDetails.pw_portfolio_id, buildingId: invoiceDetails.pw_building_id, unitId: invoiceDetails.pw_unit_id }, | |
description: invoiceDetails.description || '' | |
}; | |
}); | |
const billSplit = await Promise.all(invoiceDetailsPromises); | |
let showApprove = true; | |
if (invoice.status === 'Approved') { | |
showApprove = false; | |
} else { | |
const user = req.user; | |
const roleData = await Role.findByPk(user?.role_id); | |
if (roleData) { | |
if (roleData.name === 'Property Manager') { | |
if (invoice.status === 'PM Approved') { | |
showApprove = false; | |
} | |
} | |
if (roleData.name === "Accountant") { | |
showApprove = false; | |
} | |
} | |
} | |
return res.status(200).json({ invoice: invoice.dataValues, billSplit: billSplit, showApprove: showApprove }); | |
} catch (error) { | |
logger.error("Error fetching invoice:"); | |
logger.error(error); | |
return res.status(500).json({ error: "Error fetching invoice details" }); | |
} | |
}; | |
const updateInvoiceDetails = async (invoiceId: number, billSplit: any[], userId: number) => { | |
const existingInvoiceDetails = await InvoiceDetail.findAll({ where: { invoice_id: invoiceId } }); | |
// Delete existing InvoiceDetails | |
await InvoiceDetail.destroy({ where: { invoice_id: invoiceId } }); | |
// Create new InvoiceDetails | |
const newInvoiceDetails = billSplit.map((detail: any) => ({ | |
invoice_id: invoiceId, | |
pw_portfolio_id: detail.location.portfolioId, | |
pw_building_id: detail.location.buildingId, | |
pw_unit_id: detail.location.unitId, | |
pw_gl_account_id: detail.expenseAccount, | |
amount: detail.amount, | |
description: detail.description, | |
})); | |
const invoiceDetails = await InvoiceDetail.bulkCreate(newInvoiceDetails); | |
for (let i = 0; i < newInvoiceDetails.length; i++) { | |
const newDetail: any = newInvoiceDetails[i]; | |
const existingDetail: any = existingInvoiceDetails[i] || {}; | |
if (!existingDetail.id) { | |
const newPortfolioName = newDetail.pw_portfolio_id | |
? (await fetchPortfolioById(newDetail.pw_portfolio_id))?.name || '' | |
: 'null'; | |
const newBuildingName = newDetail.pw_building_id | |
? (await fetchBuildingsById(newDetail.pw_building_id))?.name || '' | |
: 'null'; | |
const newUnitName = newDetail.pw_unit_id | |
? (await fetchUnitsById(newDetail.pw_unit_id))?.name || '' | |
: 'null'; | |
const newValue = `${newPortfolioName}, ${newBuildingName}, ${newUnitName}`; | |
await logInvoiceAction({ | |
invoice_id: invoiceId, | |
user_id: userId, | |
activity_type: 'create', | |
field_name: 'bills split', | |
old_value: "null", | |
new_value: newValue, | |
}); | |
} else { | |
if (existingDetail.pw_portfolio_id !== newDetail.pw_portfolio_id) { | |
const oldPortfolioName = existingDetail.pw_portfolio_id | |
? (await fetchPortfolioById(existingDetail.pw_portfolio_id))?.name || 'Unknown Portfolio' | |
: 'null'; | |
const newPortfolioName = newDetail.pw_portfolio_id | |
? (await fetchPortfolioById(newDetail.pw_portfolio_id))?.name || '' | |
: 'null'; | |
if (oldPortfolioName !== newPortfolioName) { | |
await logInvoiceAction({ | |
invoice_id: invoiceId, | |
user_id: userId, | |
activity_type: 'update bill split', | |
field_name: 'portfolio', | |
old_value: oldPortfolioName, | |
new_value: newPortfolioName, | |
}); | |
} | |
} | |
if (existingDetail.pw_building_id !== newDetail.pw_building_id) { | |
const oldBuildingName = existingDetail.pw_building_id | |
? (await fetchBuildingsById(existingDetail.pw_building_id))?.name || 'Unknown Building' | |
: 'null'; | |
const newBuildingName = newDetail.pw_building_id | |
? (await fetchBuildingsById(newDetail.pw_building_id))?.name || '' | |
: 'null'; | |
if (oldBuildingName !== newBuildingName) { | |
await logInvoiceAction({ | |
invoice_id: invoiceId, | |
user_id: userId, | |
activity_type: 'update bill split', | |
field_name: 'building', | |
old_value: oldBuildingName, | |
new_value: newBuildingName, | |
}); | |
} | |
} | |
if (existingDetail.pw_unit_id !== newDetail.pw_unit_id) { | |
const oldUnitName = existingDetail.pw_unit_id | |
? (await fetchUnitsById(existingDetail.pw_unit_id))?.name || 'Unknown Unit' | |
: 'null'; | |
const newUnitName = newDetail.pw_unit_id | |
? (await fetchUnitsById(newDetail.pw_unit_id))?.name || '' | |
: 'null'; | |
if (oldUnitName !== newUnitName) { | |
await logInvoiceAction({ | |
invoice_id: invoiceId, | |
user_id: userId, | |
activity_type: 'update bill split', | |
field_name: 'unit', | |
old_value: oldUnitName, | |
new_value: newUnitName, | |
}); | |
} | |
} | |
if (existingDetail.pw_gl_account_id !== newDetail.pw_gl_account_id) { | |
const oldGlAccountName = existingDetail.pw_gl_account_id | |
? (await fetchGLAccountById(existingDetail.pw_gl_account_id))?.name || 'Unknown GL Account' | |
: 'null'; | |
const newGlAccountName = newDetail.pw_gl_account_id | |
? (await fetchGLAccountById(newDetail.pw_gl_account_id))?.name || '' | |
: 'null'; | |
if (oldGlAccountName !== newGlAccountName) { | |
await logInvoiceAction({ | |
invoice_id: invoiceId, | |
user_id: userId, | |
activity_type: 'update bill split', | |
field_name: 'gl account', | |
old_value: oldGlAccountName, | |
new_value: newGlAccountName, | |
}); | |
} | |
} | |
if (newDetail.amount !== existingDetail.amount) { | |
await logInvoiceAction({ | |
invoice_id: invoiceId, | |
user_id: userId, | |
activity_type: 'update bill split', | |
field_name: 'amount', | |
old_value: String(existingDetail.amount || 0), | |
new_value: String(newDetail.amount || 0), | |
}); | |
} | |
if (newDetail.description !== existingDetail.description) { | |
await logInvoiceAction({ | |
invoice_id: invoiceId, | |
user_id: userId, | |
activity_type: 'update bill split', | |
field_name: 'description', | |
old_value: existingDetail.description || '', | |
new_value: newDetail.description || '', | |
}); | |
} | |
} | |
} | |
return invoiceDetails; | |
}; | |
export const updateInvoiceData = async (invoiceId: number, updatedInvoiceData: any, userId: number) => { | |
const invoice = await Invoice.findByPk(invoiceId); | |
if (!invoice) { | |
throw new Error('Invoice not found'); | |
} | |
const originalInvoiceData: Record<string, unknown> = { ...invoice.dataValues }; | |
for (const key in updatedInvoiceData) { | |
if (originalInvoiceData[key] !== updatedInvoiceData[key]) { | |
await logInvoiceAction({ invoice_id: invoiceId, user_id: userId, activity_type: 'update', field_name: key, old_value: originalInvoiceData[key] as string, new_value: updatedInvoiceData[key] as string }); | |
} | |
} | |
return await invoice.update(updatedInvoiceData); | |
}; | |
export const updateInvoice = async (req: Request & { user: { id: number } }, res: Response): Promise<Response> => { | |
const { id } = req.params; | |
const { invoice, billSplit } = req.body; | |
const userId = req.user?.id; | |
if (!userId) { | |
return res.status(401).json({ error: 'Unauthorized: User ID is missing' }); | |
} | |
try { | |
const existingInvoice = await Invoice.findByPk(id); | |
if (!existingInvoice) { | |
return res.status(404).json({ error: 'Invoice not found' }); | |
} | |
// Prepare updated invoice data | |
const updatedInvoiceData = { | |
reference_number: invoice.reference_number, | |
invoice_number: invoice.invoice_number, | |
vendor_name: invoice.vendor_name, | |
pw_vendor_id: invoice.pw_vendor_id, | |
invoice_date: new Date(invoice.invoice_date), | |
due_date: new Date(invoice.due_date), | |
total: invoice.total, | |
description: invoice.description, | |
status: 'pending', // change status to pending whenever details are updated | |
amount_paid: invoice.amount_paid, | |
term: invoice.term, | |
pw_work_order_id: invoice.pw_work_order_id, | |
filename: invoice.filename, | |
pdf_url: invoice.pdf_url, | |
}; | |
await updateInvoiceData(existingInvoice.id as number, updatedInvoiceData, userId); | |
await updateInvoiceDetails(existingInvoice.id as number, billSplit, userId); | |
const updatedInvoice = await Invoice.findByPk(id, { | |
include: [{ model: InvoiceDetail }], | |
}); | |
return res.status(200).json({ invoice: updatedInvoice, message: 'Invoice updated successfully.' }); | |
} catch (error) { | |
logger.error(error); | |
logger.error('Error updating invoice:', error); | |
return res.status(500).json({ error: 'Internal server error' }); | |
} | |
}; | |
// Delete an invoice | |
export const deleteInvoice = async (req: Request, res: Response): Promise<Response> => { | |
try { | |
const { id } = req.params; | |
const invoice = await Invoice.findByPk(id); | |
if (!invoice) { | |
return res.status(404).json({ error: "Invoice not found" }); | |
} | |
if (invoice.status === "sync success") { | |
return res | |
.status(400) | |
.json({ error: "Invoice has already been synced and cannot be modified" }); | |
} | |
invoice.status = "archived"; | |
await invoice.save(); | |
await invoice.destroy(); | |
return res.status(204).send(); | |
} catch (error) { | |
logger.error("Error deleting invoice:"); | |
logger.error(error); | |
return res.status(500).json({ error: "Internal server error" }); | |
} | |
}; | |
// Function to approve invoice | |
export const approveInvoice = async (req: AuthenticatedRequest, res: Response): Promise<void> => { | |
const { id } = req.params; | |
const user = req.user; | |
const { comment } = req.body; | |
try { | |
const invoice = await Invoice.findOne({ | |
where: { id: id }, | |
include: [{ model: InvoiceDetail }] | |
}); | |
if (!invoice) { | |
res.status(404).json({ error: 'Invoice not found' }); | |
return; | |
} | |
const missingPwPortfolioId = invoice.InvoiceDetails.some( | |
(detail: InvoiceDetail) => !detail.pw_portfolio_id | |
); | |
if (missingPwPortfolioId) { | |
res.status(400).json({ error: 'Invoice cannot be approved without property address' }); | |
return; | |
} | |
let roleData; | |
if (user && typeof user !== 'string') { | |
const userData = await User.findByPk(user?.id, { | |
include: [{ model: Role, as: 'role' }], | |
}); | |
if (!userData) { | |
res.status(400).json({ error: 'Invalid User' }); | |
return; | |
} | |
roleData = await Role.findByPk(userData.role_id); | |
if (!roleData) { | |
res.status(400).json({ error: 'Invalid approval role ID' }); | |
return; | |
} | |
if (roleData.name === "Admin") { | |
await approveAndCreateRecord(invoice.id as number, userData.id, roleData.id, comment); | |
invoice.status = 'Approved'; | |
await invoice.save(); | |
res.status(200).json({ message: 'Invoice approved' }); | |
} else if (invoice.total < 1500) { | |
if (roleData.name === 'Property Manager' || roleData.name === 'Accounting Supervisor') { | |
await approveAndCreateRecord(invoice.id as number, userData.id, roleData.id, comment); | |
invoice.status = 'Approved'; | |
await invoice.save(); | |
res.status(200).json({ message: 'Invoice approved' }); | |
} else { | |
res.status(403).json({ error: 'Only Property Manager or Accounting Supervisor can approve this invoice' }); | |
} | |
} else { | |
if (roleData.name === 'Property Manager') { | |
await approveAndCreateRecord(invoice.id as number, userData.id, roleData.id, comment); | |
invoice.status = 'PM Approved'; | |
await invoice.save(); | |
res.status(200).json({ message: 'Invoice approved by PM' }); | |
} else if (roleData.name === 'Accounting Supervisor' && invoice.status === 'PM Approved') { | |
await approveAndCreateRecord(invoice.id as number, userData.id, roleData.id, comment); | |
invoice.status = 'Approved'; | |
await invoice.save(); | |
res.status(200).json({ message: 'Invoice approved by Accounting Supervisor' }); | |
} else { | |
res.status(403).json({ error: 'Invoice needs to be approved by Property Manager first' }); | |
} | |
} | |
} else { | |
res.status(400).json({ error: 'Invalid User' }); | |
return; | |
} | |
} catch (error) { | |
logger.error(error); | |
res.status(500).json({ error: error }); | |
} | |
}; | |
export const disapproveInvoice = async (req: AuthenticatedRequest, res: Response): Promise<void> => { | |
const { id } = req.params; | |
const user = req.user; | |
try { | |
const invoice = await Invoice.findOne({ | |
where: { id: id }, | |
include: [{ model: InvoiceDetail }] | |
}); | |
if (!invoice) { | |
res.status(404).json({ error: 'Invoice not found' }); | |
return; | |
} | |
if (invoice.status === 'sync success') { | |
res.status(400).json({ error: 'Invoice has already been synced and cannot be modified' }); | |
return; | |
} | |
if (user && typeof user !== 'string') { | |
const userData = await User.findByPk(user?.id, { | |
include: [{ model: Role, as: 'role' }], | |
}); | |
if (!userData) { | |
res.status(400).json({ error: 'Invalid User' }); | |
return; | |
} | |
const roleData = await Role.findByPk(userData.role_id); | |
if (!roleData) { | |
res.status(400).json({ error: 'Invalid approval role ID' }); | |
return; | |
} | |
if (roleData.name === "Admin" || roleData.name === 'Property Manager') { | |
invoice.status = 'pending'; | |
await invoice.save(); | |
await logInvoiceAction({ | |
invoice_id: id as unknown as number, | |
user_id: req.user?.id as number, | |
activity_type: 'approval', | |
field_name: 'status', | |
old_value: 'approve', | |
new_value: 'pending', | |
}); | |
res.status(200).json({ message: 'Invoice approval reverted' }); | |
} else { | |
res.status(403).json({ error: 'Only Admin or Property Manager can revert invoice approval' }); | |
} | |
} else { | |
res.status(400).json({ error: 'Invalid User' }); | |
} | |
} catch (error) { | |
logger.error(error); | |
res.status(500).json({ error: 'An error occurred while processing the request' }); | |
} | |
}; | |
const approveAndCreateRecord = async ( | |
invoiceId: number, | |
userId: number, | |
approvalRoleId: number, | |
comment: string | |
): Promise<void> => { | |
await InvoiceApproval.create({ | |
invoice_id: invoiceId, | |
approved_by: userId, | |
approval_role_id: approvalRoleId, | |
comment, | |
created_at: new Date() | |
}); | |
}; | |