UNPKG

@accounter/server

Version:

373 lines (337 loc) • 12.2 kB
import { GraphQLError } from 'graphql'; import { Injector } from 'graphql-modules'; import type { Document } from '@accounter/green-invoice-graphql'; import { GreenInvoiceClientProvider } from '@modules/app-providers/green-invoice-client.js'; import { ChargesProvider } from '@modules/charges/providers/charges.provider.js'; import { IssuedDocumentsProvider } from '@modules/documents/providers/issued-documents.provider.js'; import { IGetDocumentsByChargeIdResult, IGetIssuedDocumentsByIdsResult, } from '@modules/documents/types'; import { FinancialAccountsProvider } from '@modules/financial-accounts/providers/financial-accounts.provider.js'; import { BusinessesProvider } from '@modules/financial-entities/providers/businesses.provider.js'; import { ClientsProvider } from '@modules/financial-entities/providers/clients.provider.js'; import { IGetTransactionsByChargeIdsResult } from '@modules/transactions/types'; import { DocumentType } from '@shared/enums'; import { Currency, GreenInvoiceClient, GreenInvoiceIncome, GreenInvoicePayment, GreenInvoicePaymentType, GreenInvoiceVatType, NewDocumentInfo, NewDocumentInput, } from '@shared/gql-types'; import { dateToTimelessDateString } from '@shared/helpers'; import { TimelessDateString } from '@shared/types'; import { convertDocumentInputIntoGreenInvoiceInput, getVatTypeFromGreenInvoiceDocument, insertNewDocumentFromGreenInvoice, normalizeDocumentType, } from './green-invoice.helper.js'; export async function getPaymentsFromTransactions( injector: Injector, transactions: IGetTransactionsByChargeIdsResult[], ): Promise<GreenInvoicePayment[]> { 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: GreenInvoicePaymentType = 'OTHER'; switch (account.type) { case 'BANK_ACCOUNT': type = 'WIRE_TRANSFER'; break; case 'CREDIT_CARD': type = 'CREDIT_CARD'; break; } // get further fields let paymentTypeSpecificAttributes: Partial<GreenInvoicePayment> = {}; switch (type as string) { case 'CHEQUE': paymentTypeSpecificAttributes = { // chequeNum: _____, }; break; case 'CREDIT_CARD': paymentTypeSpecificAttributes = { cardType: 'MASTERCARD', // TODO: add logic to support other card types cardNum: account.account_number, dealType: 'STANDARD', // TODO: add logic to support other deal types numPayments: 1, firstPayment: transaction.event_date.getTime() / 1000, // assuming first payment is the transaction date }; break; case 'WIRE_TRANSFER': paymentTypeSpecificAttributes = { bankName: account.bank_number?.toString(), bankBranch: account.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: GreenInvoicePayment = { currency: transaction.currency as 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 getIncomeFromDocuments( documents: { document: IGetDocumentsByChargeIdResult; issuedDocument: IGetIssuedDocumentsByIdsResult; greenInvoiceDocument: Document; }[], ): GreenInvoiceIncome[] { const incomes = documents .map(({ greenInvoiceDocument }) => { return greenInvoiceDocument.income.filter(Boolean).map(originIncome => { if (!originIncome?.currency) { throw new Error('Income currency is missing'); } const income: GreenInvoiceIncome = { description: originIncome.description, quantity: originIncome.quantity, price: originIncome.price, currency: originIncome.currency as Currency, currencyRate: undefined, vatRate: originIncome.vatRate, itemId: originIncome.itemId, vatType: getVatTypeFromGreenInvoiceDocument(greenInvoiceDocument.vatType), }; return income; }); }) .flat(); return incomes; } export function getTypeFromDocumentsAndTransactions( greenInvoiceDocuments: Document[], transactions: IGetTransactionsByChargeIdsResult[], ): DocumentType { if (!greenInvoiceDocuments.length) { if (transactions.length) { return DocumentType.InvoiceReceipt; } return DocumentType.Proforma; } const documentsTypes = new Set<DocumentType>( greenInvoiceDocuments.map(doc => normalizeDocumentType(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: IGetTransactionsByChargeIdsResult[], swiftBusinessId: string | null, ): IGetTransactionsByChargeIdsResult[] { const swiftTransactions: IGetTransactionsByChargeIdsResult[] = []; const incomeTransactions: IGetTransactionsByChargeIdsResult[] = []; 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: IGetIssuedDocumentsByIdsResult[], ): Pick<NewDocumentInfo, 'linkedDocumentIds' | 'linkType'> { const linkedDocumentIds = issuedDocuments.map(doc => doc.external_id); return { linkedDocumentIds, linkType: linkedDocumentIds.length ? 'LINK' : undefined, }; } export function getDocumentDateOutOfTransactions( transactions: IGetTransactionsByChargeIdsResult[], ): TimelessDateString | undefined { const debitDates = transactions.map(tx => tx.debit_date).filter(Boolean) as Date[]; // 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 getClientFromGreenInvoiceClient( injector: Injector, businessId: string, useGreenInvoiceId = false, ): Promise<GreenInvoiceClient | undefined> { const client = await injector.get(ClientsProvider).getClientByIdLoader.load(businessId); if (!client) { return useGreenInvoiceId ? undefined : { id: businessId }; } const greenInvoiceClient = await injector .get(GreenInvoiceClientProvider) .clientLoader.load(client.green_invoice_id); if (!greenInvoiceClient) { return useGreenInvoiceId ? undefined : { id: businessId }; } return { id: useGreenInvoiceId && greenInvoiceClient.id ? greenInvoiceClient.id : businessId, country: greenInvoiceClient.country, emails: [ ...((greenInvoiceClient.emails?.filter(Boolean) as string[]) ?? []), 'ap@the-guild.dev', ], name: greenInvoiceClient.name, phone: greenInvoiceClient.phone, taxId: greenInvoiceClient.taxId, address: greenInvoiceClient.address, city: greenInvoiceClient.city, zip: greenInvoiceClient.zip, fax: greenInvoiceClient.fax, mobile: greenInvoiceClient.mobile, }; } export async function deduceVatTypeFromBusiness( injector: Injector, businessId?: string | null, ): Promise<GreenInvoiceVatType> { 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 === 'Israel') { return 'MIXED'; } return 'EXEMPT'; } export async function executeDocumentIssue( injector: Injector, adminBusinessId: string, initialInput: NewDocumentInput, emailContent?: string, attachment = true, chargeId?: string, 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'); } 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 => { throw new Error(`Failed to close linked document with ID ${id}: ${e.message}`); }); } }), ); } 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'); } }