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): 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 => { try { const { sort_by, sort_order, page, limit } = req.query; const filter = req.query.filter as Record; 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 => { 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 = { ...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 => { 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 => { 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 => { 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 => { 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 => { await InvoiceApproval.create({ invoice_id: invoiceId, approved_by: userId, approval_role_id: approvalRoleId, comment, created_at: new Date() }); };