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() | |
| }); | |
| }; | |