UNPKG

@accounter/server

Version:
344 lines • 15 kB
import { addMonths, endOfMonth, format, startOfMonth, subMonths } from 'date-fns'; import { GraphQLError } from 'graphql'; import { DocumentType } from '../../../shared/enums.js'; import { dateToTimelessDateString } from '../../../shared/helpers/index.js'; import { GreenInvoiceClientProvider } from '../../app-providers/green-invoice-client.js'; import { ChargesProvider } from '../../charges/providers/charges.provider.js'; import { getProductName, getSubscriptionPlanName, normalizeProduct, normalizeSubscriptionPlan, } from '../../contracts/helpers/contracts.helper.js'; import { FinancialAccountsProvider } from '../../financial-accounts/providers/financial-accounts.provider.js'; import { FinancialBankAccountsProvider } from '../../financial-accounts/providers/financial-bank-accounts.provider.js'; import { validateClientIntegrations } from '../../financial-entities/helpers/clients.helper.js'; import { BusinessesProvider } from '../../financial-entities/providers/businesses.provider.js'; import { ClientsProvider } from '../../financial-entities/providers/clients.provider.js'; import { convertDocumentInputIntoGreenInvoiceInput, getTypeFromGreenInvoiceDocument, getVatTypeFromGreenInvoiceDocument, insertNewDocumentFromGreenInvoice, } from '../../green-invoice/helpers/green-invoice.helper.js'; import { IssuedDocumentsProvider } from '../providers/issued-documents.provider.js'; import { normalizeDocumentType } from '../resolvers/common.js'; export async function getPaymentsFromTransactions(injector, transactions) { const payments = await Promise.all(transactions.map(async (transaction) => { // get account const account = await injector .get(FinancialAccountsProvider) .getFinancialAccountByAccountIDLoader.load(transaction.account_id); if (!account) { throw new Error(`Account with ID ${transaction.account_id} not found`); } // get account type let type = 'OTHER'; switch (account.type) { case 'BANK_ACCOUNT': type = 'WIRE_TRANSFER'; break; case 'CREDIT_CARD': type = 'CREDIT_CARD'; break; } let bankAccount = undefined; if (account.type === 'BANK_ACCOUNT') { bankAccount = await injector .get(FinancialBankAccountsProvider) .getFinancialBankAccountByIdLoader.load(account.id); if (!bankAccount) { throw new GraphQLError(`Bank account details not found for financial account ID "${account.id}"`); } } // get further fields let paymentTypeSpecificAttributes = {}; switch (type) { case 'CHEQUE': paymentTypeSpecificAttributes = { // chequeNum: _____, }; break; case 'CREDIT_CARD': paymentTypeSpecificAttributes = { cardType: 'MASTERCARD', // TODO: add logic to support other card types cardNum: account.account_number, numPayments: 1, firstPayment: transaction.event_date.getTime() / 1000, // assuming first payment is the transaction date }; break; case 'WIRE_TRANSFER': paymentTypeSpecificAttributes = { bankName: bankAccount?.bank_number?.toString(), bankBranch: bankAccount?.branch_number?.toString(), bankAccount: account.account_number?.toString(), }; break; case 'PAYMENT_APP': paymentTypeSpecificAttributes = { // appType: _____, // accountId: _____, // transactionId: _____, }; break; case 'OTHER': paymentTypeSpecificAttributes = { // subType: _____, // accountId: _____, // transactionId: _____, }; break; case 'PAYPAL': paymentTypeSpecificAttributes = { // accountId: _____, // transactionId: _____, }; break; case 'CASH': case 'TAX_DEDUCTION': case 'OTHER_DEDUCTION': break; } const payment = { currency: transaction.currency, currencyRate: undefined, date: dateToTimelessDateString(transaction.debit_date ?? transaction.event_date), price: Number(transaction.amount), type, transactionId: transaction.id, accountId: account.id, ...paymentTypeSpecificAttributes, }; return payment; })); return payments; } export function getIncomeRecordsFromDocuments(greenInvoiceDocuments) { const incomes = greenInvoiceDocuments .map(greenInvoiceDocument => { return greenInvoiceDocument.income.filter(Boolean).map(originIncome => { if (!originIncome?.currency) { throw new Error('Income currency is missing'); } const income = { description: originIncome.description, quantity: originIncome.quantity, price: originIncome.price, currency: originIncome.currency, currencyRate: undefined, vatRate: originIncome.vatRate, itemId: originIncome.itemId, vatType: getVatTypeFromGreenInvoiceDocument(greenInvoiceDocument.vatType), }; return income; }); }) .flat(); return incomes; } export function getTypeFromDocumentsAndTransactions(documents, transactions) { if (!documents.length) { if (transactions.length) { return DocumentType.InvoiceReceipt; } return DocumentType.Proforma; } const documentsTypes = new Set(documents.map(doc => doc.type)); if (documentsTypes.size === 1) { switch (Array.from(documentsTypes)[0]) { case DocumentType.Invoice: return DocumentType.Receipt; case DocumentType.Proforma: return DocumentType.InvoiceReceipt; } } return DocumentType.Proforma; } export function filterAndHandleSwiftTransactions(originTransactions, swiftBusinessId) { const swiftTransactions = []; const incomeTransactions = []; for (const transaction of originTransactions) { if (swiftBusinessId && transaction.business_id === swiftBusinessId) { swiftTransactions.push(transaction); } else if (Number(transaction.amount) > 0) { incomeTransactions.push(transaction); } } if (swiftTransactions.length === 0) { return incomeTransactions; } if (swiftTransactions.length === 1 && incomeTransactions.length === 1) { const incomeTransaction = incomeTransactions[0]; return [ { ...incomeTransaction, amount: (Number(incomeTransaction.amount) - Number(swiftTransactions[0].amount)).toFixed(2), }, ]; } throw new GraphQLError("Unable to process transactions. Couldn't match swift fees to transactions"); } export function getLinkedDocumentsAttributes(issuedDocuments, shouldCancel = false) { const linkedDocumentIds = issuedDocuments.map(doc => doc.external_id); let linkType = undefined; if (linkedDocumentIds.length) { if (shouldCancel) { linkType = 'CANCEL'; } else { linkType = 'LINK'; } } return { linkedDocumentIds, linkType, }; } export function getDocumentDateOutOfTransactions(transactions) { const debitDates = transactions.map(tx => tx.debit_date).filter(Boolean); // if no debit dates, use current date if (!debitDates.length) return dateToTimelessDateString(new Date()); // Sort dates and take the first one const sortedDates = [...debitDates].sort((a, b) => b.getTime() - a.getTime()); const firstDate = sortedDates[0]; // Return the date in the required format return dateToTimelessDateString(firstDate); } export async function deduceVatTypeFromBusiness(injector, locality, businessId) { if (!businessId) return 'DEFAULT'; const business = await injector.get(BusinessesProvider).getBusinessByIdLoader.load(businessId); if (!business) return 'DEFAULT'; // Deduce VAT type based on business information if (business.country === locality && !business.exempt_dealer) { return 'MIXED'; } return 'EXEMPT'; } export function createRemarks(contract) { const remarks = []; if (contract.purchase_orders?.length) { const lastPo = contract.purchase_orders[contract.purchase_orders.length - 1]; if (lastPo) { remarks.push(`PO: ${lastPo}`); } } if (contract.remarks) { remarks.push(contract.remarks); } return remarks.join(', '); } export const convertContractToDraft = async (injector, contract, locality, issueMonth) => { const businessPromise = injector .get(BusinessesProvider) .getBusinessByIdLoader.load(contract.client_id); const clientPromise = injector.get(ClientsProvider).getClientByIdLoader.load(contract.client_id); const [business, client] = await Promise.all([businessPromise, clientPromise]); if (!business) { throw new GraphQLError(`Business ID="${contract.client_id}" not found`); } if (!client) { throw new GraphQLError(`Client not found for business ID="${contract.client_id}"`); } const greenInvoiceId = validateClientIntegrations(client.integrations)?.greenInvoiceId; if (!greenInvoiceId) { throw new GraphQLError(`Green invoice match not found for business ID="${contract.client_id}"`); } const today = issueMonth ? addMonths(new Date(issueMonth), 1) : new Date(); const monthStart = dateToTimelessDateString(startOfMonth(today)); const monthEnd = dateToTimelessDateString(endOfMonth(today)); const year = today.getFullYear() + (today.getMonth() === 0 ? -1 : 0); const month = format(subMonths(today, 1), 'MMMM'); const vatType = await deduceVatTypeFromBusiness(injector, locality, contract.client_id); const documentInput = { remarks: createRemarks(contract), description: `${getProductName(normalizeProduct(contract.product ?? ''))} ${getSubscriptionPlanName(normalizeSubscriptionPlan(contract.plan ?? ''))} - ${month} ${year}`, type: normalizeDocumentType(contract.document_type), date: monthStart, dueDate: monthEnd, language: 'ENGLISH', currency: contract.currency, vatType, rounding: false, signed: true, client, income: [ { description: `${getProductName(normalizeProduct(contract.product ?? ''))} ${getSubscriptionPlanName(normalizeSubscriptionPlan(contract.plan ?? ''))} - ${month} ${year}`, quantity: 1, price: contract.amount, currency: contract.currency, vatType: 'EXEMPT', }, ], }; return documentInput; }; export async function executeDocumentIssue(injector, adminBusinessId, initialInput, emailContent, attachment = true, chargeId, sendEmail = false) { try { const coreInput = await convertDocumentInputIntoGreenInvoiceInput(initialInput, injector); const input = { ...coreInput, client: !coreInput.client || sendEmail ? coreInput.client : { ...coreInput.client, emails: [], // if client exists, and sendEmail is false, we don't want to send emails }, emailContent, attachment, }; const document = await injector.get(GreenInvoiceClientProvider).addDocuments({ input }); if (!document) { throw new GraphQLError('Failed to issue new document'); } // validate all linked documents are closed if (input.linkedDocumentIds?.length) { await Promise.all(input.linkedDocumentIds.map(async (id) => { if (id) { await injector .get(GreenInvoiceClientProvider) .closeDocument(id) .catch(e => { const message = `ERROR: Failed to close Green Invoice linked document with ID ${id}`; console.error(`${message}: ${e}`); }); } })); } if ('id' in document && document.id) { const greenInvoiceDocument = await injector .get(GreenInvoiceClientProvider) .documentLoader.load(document.id); if (!greenInvoiceDocument) { console.error('Failed to fetch issued document from Green Invoice', document); throw new GraphQLError('Failed to issue new document'); } // Insert new issued document to DB const newDocument = await insertNewDocumentFromGreenInvoice(injector, greenInvoiceDocument, adminBusinessId, chargeId); if (!newDocument.charge_id) { console.error('New document does not have a charge ID', newDocument); throw new GraphQLError('Failed to issue new document'); } // Close linked documents if (coreInput.linkedDocumentIds?.length) { await Promise.all(coreInput.linkedDocumentIds.map(async (id) => { if (id) { await injector .get(IssuedDocumentsProvider) .updateIssuedDocumentByExternalId({ externalId: id, status: 'CLOSED' }) .catch(e => { const message = `Failed to close linked document with ID ${id}`; console.error(`${message}: ${e}`); throw new Error(message); }); } })); } if (getTypeFromGreenInvoiceDocument(coreInput.type) === DocumentType.CreditInvoice) { // Close origin } return injector.get(ChargesProvider).getChargeByIdLoader.load(newDocument.charge_id); } console.error('Document issue failed', document); throw new GraphQLError('Document issue failed'); } catch (error) { console.error('Error issuing document:', error); throw new GraphQLError('Error issuing document'); } } //# sourceMappingURL=issue-document.helper.js.map