UNPKG

@accounter/server

Version:

277 lines • 12 kB
import { endOfDay, startOfDay } from 'date-fns'; import { DeelClientProvider } from '../../app-providers/deel/deel-client.provider.js'; import { ChargesProvider } from '../../charges/providers/charges.provider.js'; import { uploadToCloudinary } from '../../documents/helpers/upload.helper.js'; import { DocumentsProvider } from '../../documents/providers/documents.provider.js'; import { TaxCategoriesProvider } from '../../financial-entities/providers/tax-categories.provider.js'; import { DocumentType } from '../../../shared/enums.js'; import { dateToTimelessDateString } from '../../../shared/helpers/index.js'; import { DeelContractsProvider } from '../providers/deel-contracts.provider.js'; import { DeelInvoicesProvider } from '../providers/deel-invoices.provider.js'; const DEEL_BUSINESS_ID = '8d34f668-7233-4ce3-9c9c-82550b0839ff'; // TODO: replace with DB based business id export function isDeelDocument(document) { const isDeelSide = document.creditor_id === DEEL_BUSINESS_ID || document.debtor_id === DEEL_BUSINESS_ID; const isFinancialDocument = document.type === 'INVOICE' || document.type === 'INVOICE_RECEIPT' || document.type === 'CREDIT_INVOICE'; return isDeelSide && isFinancialDocument; } export async function getDeelEmployeeId(context, document, ledgerEntry, ledgerEntries, updateLedgerBalance) { if (!isDeelDocument(document)) { return; } const isDeelCreditor = ledgerEntry.creditAccountID1 === DEEL_BUSINESS_ID; // naive fetch employee id from deel let employeeId = await context.injector .get(DeelContractsProvider) .getEmployeeIdByDocumentIdLoader.load(document.id); if (!employeeId && document.date && document.type) { // figure out through deel records const records = await context.injector .get(DeelInvoicesProvider) .getInvoicesByIssueDates(startOfDay(document.date), endOfDay(document.date)); const matchingRecord = records.find(r => { if (dateToTimelessDateString(r.issued_at) !== dateToTimelessDateString(document.date)) { return false; } if (r.label !== document.serial_number) { return false; } if (r.currency !== document.currency_code) { return false; } if (Number(r.total) !== document.total_amount) { if (records.length === 1) { return false; } const spreadRecords = records.filter(r => r.label === document.serial_number); if (spreadRecords.length === 1) { return false; } const totalAmount = spreadRecords.reduce((acc, r) => acc + Number(r.amount), 0); if (totalAmount !== document.total_amount) { return false; } } return true; }); if (matchingRecord?.contract_id) { employeeId = await context.injector .get(DeelContractsProvider) .getEmployeeIDByContractIdLoader.load(matchingRecord.contract_id); } } if (employeeId) { // get employee tax category const taxCategoryId = await context.injector .get(TaxCategoriesProvider) .taxCategoryByBusinessAndOwnerIDsLoader.load({ businessId: employeeId, ownerId: context.adminContext.defaultAdminBusinessId, }) .then(taxCategory => taxCategory?.id); let newEntry; if (isDeelCreditor) { newEntry = { ...ledgerEntry, debitAccountID1: employeeId, }; ledgerEntry.creditAccountID1 = employeeId; ledgerEntry.debitAccountID1 = taxCategoryId ?? ledgerEntry.debitAccountID1; } else { newEntry = { ...ledgerEntry, creditAccountID1: employeeId, }; ledgerEntry.creditAccountID1 = taxCategoryId ?? ledgerEntry.creditAccountID1; ledgerEntry.debitAccountID1 = employeeId; } updateLedgerBalance(newEntry); ledgerEntries.push(newEntry); } return; } export async function uploadDeelInvoice(receiptChargeMap, match, injector, ownerId) { try { const chargeId = receiptChargeMap.get(match.breakdown_receipt_id); if (!chargeId) { throw new Error('Charge not found for invoice'); } // fetch file from Deel const file = await injector.get(DeelClientProvider).getSalaryInvoiceFile(match.id); // upload file to cloudinary const { fileUrl, imageUrl } = await uploadToCloudinary(injector, file); // create the new document object const newDocumentFromInvoice = { image: imageUrl ?? null, file: fileUrl ?? null, documentType: match.breakdown_contract_type === 'prepaid_billing' ? DocumentType.Other : DocumentType.Invoice, serialNumber: match.label, date: match.issued_at, amount: Number(match.breakdown_total_payment_currency), currencyCode: match.breakdown_payment_currency, vat: Number(match.vat_total), chargeId, vatReportDateOverride: null, noVatAmount: null, debtorId: ownerId, creditorId: DEEL_BUSINESS_ID, allocationNumber: null, exchangeRateOverride: null, }; // upload the document const [document] = await injector.get(DocumentsProvider).insertDocuments({ document: [newDocumentFromInvoice], }); if (!document.id) { throw new Error('Document not uploaded to DB'); } return document.id; } catch (error) { console.error(error); throw new Error('Error uploading Deel invoice'); } } function nullifyEmptyStrings(raw) { return raw === '' ? null : raw; } export function convertMatchToDeelInvoiceRecord(match, documentId) { return { adjustment: match.breakdown_adjustment, amount: match.amount, approveDate: nullifyEmptyStrings(match.breakdown_approve_date), approvers: match.breakdown_approvers, bonus: match.breakdown_bonus, commissions: match.breakdown_commissions, contractCountry: nullifyEmptyStrings(match.breakdown_contract_country), contractId: match.contract_id, contractStartDate: nullifyEmptyStrings(match.breakdown_contract_start_date), contractType: nullifyEmptyStrings(match.breakdown_contract_type), contractorEmail: nullifyEmptyStrings(match.breakdown_contractor_email), contractorEmployeeName: match.breakdown_contractor_employee_name, contractorUniqueIdentifier: nullifyEmptyStrings(match.breakdown_contractor_unique_identifier), createdAt: match.created_at, currency: match.currency, deductions: match.breakdown_deductions, deelFee: match.deel_fee, documentId, dueDate: match.due_date, expenses: match.breakdown_expenses, frequency: nullifyEmptyStrings(match.breakdown_frequency), generalLedgerAccount: nullifyEmptyStrings(match.breakdown_general_ledger_account), groupId: nullifyEmptyStrings(match.breakdown_group_id), id: match.id, isOverdue: match.is_overdue, issuedAt: match.issued_at, label: match.label, others: match.breakdown_others, overtime: match.breakdown_overtime, paidAt: match.paid_at, paymentCurrency: match.breakdown_payment_currency, paymentId: match.breakdown_receipt_id, processingFee: match.breakdown_processing_fee, proRata: match.breakdown_pro_rata, status: match.status, total: match.total, totalPaymentCurrency: match.breakdown_total_payment_currency, vatId: nullifyEmptyStrings(match.vat_id), vatPercentage: nullifyEmptyStrings(match.vat_percentage), vatTotal: match.vat_total, work: match.breakdown_work, }; } export async function getDeelChargeDescription(injector, workers) { const contractIds = workers?.map(w => w.contract_id).filter(id => !!id) ?? []; const contracts = await injector .get(DeelContractsProvider) .getEmployeeByContractIdLoader.loadMany(contractIds) .then(contract => contract.filter(id => !!id && !(id instanceof Error))); const workerNames = contracts.map(c => c.contractor_name.split(' ')[0]); const workersDescription = workerNames?.length ? ` for ${workerNames.join(', ')}` : ''; const description = `Deel payment${workersDescription}`; return description; } export async function fetchAndFilterInvoices(injector) { try { const invoices = await injector.get(DeelClientProvider).getSalaryInvoices(); // TODO: enable setting up period const filteredInvoices = []; await Promise.all(invoices.data.map(async (invoice) => { const dbInvoice = await injector .get(DeelInvoicesProvider) .getInvoicesByIdLoader.load(invoice.id) .catch(e => { console.error(e); throw new Error('Error fetching invoice'); }); if (!dbInvoice) { filteredInvoices.push(invoice); } })); return { invoices: filteredInvoices }; } catch (error) { console.error(error); throw new Error('Error fetching Deel invoices'); } } export async function fetchReceipts(injector) { try { const res = await injector.get(DeelClientProvider).getPaymentReceipts(); // TODO: use PERION_IN_MONTHS return res.data.rows; } catch (error) { console.error(error); throw new Error('Error fetching Deel receipts'); } } export async function fetchPaymentBreakdowns(injector, receipts) { const receiptsBreakDown = []; for (const receipt of receipts) { if (receipt.id) { const breakDown = await injector.get(DeelClientProvider).getPaymentBreakdown(receipt.id); receiptsBreakDown.push(...breakDown.data.map(row => ({ ...row, receipt_id: receipt.id }))); } } return receiptsBreakDown; } export async function getChargeMatchesForPayments(injector, ownerId, receipts) { const receiptChargeMap = await injector.get(DeelInvoicesProvider).getReceiptToCharge(); for (const receipt of receipts) { if (receiptChargeMap.has(receipt.id)) { continue; } const description = await getDeelChargeDescription(injector, receipt.workers); const [charge] = await injector.get(ChargesProvider).generateCharge({ ownerId, userDescription: description, }); receiptChargeMap.set(receipt.id, charge.id); // TODO: upload receipt whenever available via Deel API } return receiptChargeMap; } export function matchInvoicesWithPayments(invoices, paymentBreakdowns) { const matches = []; invoices.map(invoice => { const optionalMatches = paymentBreakdowns.filter(receipt => invoice.currency === receipt.currency && invoice.total === receipt.total && invoice.created_at === receipt.date && invoice.paid_at === receipt.payment_date); if (optionalMatches.length === 1) { const adjustedBreakdown = {}; Object.entries(optionalMatches[0]).map(([key, value]) => { adjustedBreakdown[`breakdown_${key}`] = value; }); matches.push({ ...adjustedBreakdown, ...invoice }); } else { throw new Error(`No payment match found for invoice ${invoice.id}`); } }); return matches; } //# sourceMappingURL=deel.helper.js.map