UNPKG

@accounter/server

Version:
384 lines (340 loc) 13.6 kB
import type { Injector } from 'graphql-modules'; import { DECREASED_VAT_RATIO } from '../../../shared/constants.js'; import { dateToTimelessDateString, formatCurrency } from '../../../shared/helpers/index.js'; import type { LedgerProto, StrictLedgerProto } from '../../../shared/types/index.js'; import { AdminContextProvider } from '../../admin-context/providers/admin-context.provider.js'; import type { IGetChargesByIdsResult } from '../../charges/types.js'; import { validateDocumentAllocation, validateDocumentVat, } from '../../documents/helpers/validate-document.helper.js'; import type { IGetDocumentsByChargeIdResult } from '../../documents/types.js'; import { ExchangeProvider } from '../../exchange-rates/providers/exchange.provider.js'; import { FinancialAccountsProvider } from '../../financial-accounts/providers/financial-accounts.provider.js'; import type { IGetTransactionsByChargeIdsResult } from '../../transactions/types.js'; import { VatProvider } from '../../vat/providers/vat.provider.js'; import { IGetBalanceCancellationByChargesIdsResult } from '../types.js'; import { getFinancialAccountTaxCategoryId, LedgerError, validateTransactionBasicVariables, } from './utils.helper.js'; export async function ledgerEntryFromDocument( injector: Injector, document: IGetDocumentsByChargeIdResult, charge: IGetChargesByIdsResult, ownerId: string, taxCategoryId: string, ): Promise<StrictLedgerProto> { if (!document.date) { throw new LedgerError(`Document serial "${document.serial_number}" is missing the date`); } if (!document.debtor_id) { throw new LedgerError(`Document serial "${document.serial_number}" is missing the debtor`); } if (!document.creditor_id) { throw new LedgerError(`Document serial "${document.serial_number}" is missing the creditor`); } if (!document.total_amount) { throw new LedgerError(`Document serial "${document.serial_number}" is missing amount`); } if (!document.currency_code) { throw new LedgerError(`Document serial "${document.serial_number}" is missing currency code`); } const { defaultLocalCurrency, authorities: { inputVatTaxCategoryId, outputVatTaxCategoryId, propertyOutputVatTaxCategoryId }, } = await injector.get(AdminContextProvider).getVerifiedAdminContext(); let totalAmount = Math.abs(document.total_amount); const isCreditorCounterparty = document.debtor_id === ownerId; const counterpartyId = isCreditorCounterparty ? document.creditor_id : document.debtor_id; const debitAccountID1 = isCreditorCounterparty ? taxCategoryId : counterpartyId; const creditAccountID1 = isCreditorCounterparty ? counterpartyId : taxCategoryId; const currency = formatCurrency(document.currency_code); let foreignTotalAmount: number | null = null; let amountWithoutVat = totalAmount; let foreignAmountWithoutVat: number | null = null; let vatAmount = document.vat_amount == null ? null : Math.abs(document.vat_amount); let foreignVatAmount: number | null = null; let vatTaxCategory: string | null = null; if (vatAmount != null && charge.is_property) { vatAmount = vatAmount * DECREASED_VAT_RATIO; // Adjust VAT for property charges } const isCreditInvoice = document.type === 'CREDIT_INVOICE'; if (vatAmount) { amountWithoutVat = amountWithoutVat - vatAmount; const adjustedOutputVatTaxCategoryId = charge.is_property ? propertyOutputVatTaxCategoryId : outputVatTaxCategoryId; vatTaxCategory = isCreditorCounterparty === isCreditInvoice ? inputVatTaxCategoryId : adjustedOutputVatTaxCategoryId; const vatValue = await injector .get(VatProvider) .getVatValueByDateLoader.load(dateToTimelessDateString(document.date)); if (!vatValue) { throw new LedgerError(`VAT value is missing for document ID=${document.id}`); } validateDocumentVat(document, vatValue, message => { throw new LedgerError(message); }); await validateDocumentAllocation(document, injector).then(valid => { if (!valid) { throw new LedgerError(`Allocation number missing for document [${document.serial_number}]`); } }); } // handle non-local currencies if (document.currency_code !== defaultLocalCurrency) { let exchangeRate: number; if (document.exchange_rate_override) { exchangeRate = Number(document.exchange_rate_override); } else { exchangeRate = await injector .get(ExchangeProvider) .getExchangeRates(currency, defaultLocalCurrency, document.date); } // Set foreign amounts foreignTotalAmount = totalAmount; foreignAmountWithoutVat = amountWithoutVat; // calculate amounts in ILS totalAmount = exchangeRate * totalAmount; amountWithoutVat = exchangeRate * amountWithoutVat; if (vatAmount && vatAmount > 0) { foreignVatAmount = vatAmount; vatAmount = exchangeRate * vatAmount; } } let creditAccountID2: string | null = null; let debitAccountID2: string | null = null; let creditAmount1: number | null; let localCurrencyCreditAmount1: number; let debitAmount1: number | null; let localCurrencyDebitAmount1: number; let creditAmount2: number | null = null; let localCurrencyCreditAmount2: number | null = null; let debitAmount2: number | null = null; let localCurrencyDebitAmount2: number | null = null; if (isCreditorCounterparty) { localCurrencyCreditAmount1 = totalAmount; creditAmount1 = foreignTotalAmount; localCurrencyDebitAmount1 = amountWithoutVat; debitAmount1 = foreignAmountWithoutVat; if (vatAmount && vatAmount > 0) { // add vat to debtor2 debitAmount2 = foreignVatAmount; localCurrencyDebitAmount2 = vatAmount; debitAccountID2 = vatTaxCategory; } } else { localCurrencyDebitAmount1 = totalAmount; debitAmount1 = foreignTotalAmount; localCurrencyCreditAmount1 = amountWithoutVat; creditAmount1 = foreignAmountWithoutVat; if (vatAmount && vatAmount > 0) { // add vat to creditor2 creditAmount2 = foreignVatAmount; localCurrencyCreditAmount2 = vatAmount; creditAccountID2 = vatTaxCategory; } } const ledgerEntry: StrictLedgerProto = { id: document.id, invoiceDate: document.date, valueDate: document.date, currency, creditAccountID1, creditAmount1: creditAmount1 ?? undefined, localCurrencyCreditAmount1, debitAccountID1, debitAmount1: debitAmount1 ?? undefined, localCurrencyDebitAmount1, creditAccountID2: creditAccountID2 ?? undefined, creditAmount2: creditAmount2 ?? undefined, localCurrencyCreditAmount2: localCurrencyCreditAmount2 ?? undefined, debitAccountID2: debitAccountID2 ?? undefined, debitAmount2: debitAmount2 ?? undefined, localCurrencyDebitAmount2: localCurrencyDebitAmount2 ?? undefined, description: undefined, reference: document.serial_number ?? undefined, isCreditorCounterparty, isCreditInvoice, ownerId, chargeId: charge.id, }; return ledgerEntry; } function getCreditCardAccountByTransactionReferenceAndDescription( sourceReference: string, sourceDescription: string | null, ): string | null { if (sourceReference === '8547') { return '6fed9073-f571-402e-9da0-ac0669f658d2'; // 9200 } if (sourceReference === '13795') { if (sourceDescription?.includes('2260')) { return '0e86514c-9ddb-4c82-b731-6107f7293b1f'; //2260 } if (sourceDescription?.includes('4333')) { return '782070ce-ec7e-4be0-ab69-46c89528bf04'; //4333 } } if (sourceReference === '22200' && sourceDescription?.includes('מסטרקארד')) { return '782070ce-ec7e-4be0-ab69-46c89528bf04'; //4333 } if (sourceReference === '11200' && sourceDescription?.includes('כ.א.ל')) { return '6fed9073-f571-402e-9da0-ac0669f658d2'; // 9200 } return null; } export async function ledgerEntryFromMainTransaction( transaction: IGetTransactionsByChargeIdsResult, injector: Injector, chargeId: string, ownerId: string, businessId?: string, gotRelevantDocuments = false, ): Promise<StrictLedgerProto> { const { currency, valueDate, transactionBusinessId } = validateTransactionBasicVariables(transaction); let mainAccountId: string = transactionBusinessId; const { defaultLocalCurrency, financialAccounts: { internalWalletsIds }, } = await injector.get(AdminContextProvider).getVerifiedAdminContext(); if ( !gotRelevantDocuments && transaction.source_reference && businessId && internalWalletsIds.includes(businessId) ) { const account = await injector .get(FinancialAccountsProvider) .getFinancialAccountByAccountNumberLoader.load(transaction.source_reference); let accountId = account?.id ?? null; accountId ??= getCreditCardAccountByTransactionReferenceAndDescription( transaction.source_reference, transaction.source_description, ); if (!accountId) { throw new LedgerError( `Transaction reference "${transaction.source_reference}" is missing account`, ); } mainAccountId = await getFinancialAccountTaxCategoryId(injector, { ...transaction, account_id: accountId, }); } let amount = Number(transaction.amount); let foreignAmount: number | undefined = undefined; if (currency !== defaultLocalCurrency) { // get exchange rate for currency const exchangeRate = await injector .get(ExchangeProvider) .getExchangeRates(currency, defaultLocalCurrency, valueDate); foreignAmount = amount; // calculate amounts in ILS amount = exchangeRate * amount; } const accountTaxCategoryId = await getFinancialAccountTaxCategoryId(injector, transaction); const isCreditorCounterparty = amount > 0; const ledgerEntry: StrictLedgerProto = { id: transaction.id, invoiceDate: transaction.event_date, valueDate, currency, creditAccountID1: isCreditorCounterparty ? mainAccountId : accountTaxCategoryId, creditAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined, localCurrencyCreditAmount1: Math.abs(amount), debitAccountID1: isCreditorCounterparty ? accountTaxCategoryId : mainAccountId, debitAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined, localCurrencyDebitAmount1: Math.abs(amount), description: transaction.source_description ?? undefined, reference: transaction.origin_key, isCreditorCounterparty, ownerId, currencyRate: transaction.currency_rate ? Number(transaction.currency_rate) : undefined, chargeId, }; return ledgerEntry; } export async function ledgerEntryFromBalanceCancellation( balanceCancellation: IGetBalanceCancellationByChargesIdsResult, ledgerBalance: Map<string, { amount: number; entityId: string }>, financialAccountLedgerEntries: StrictLedgerProto[], chargeId: string, ownerId: string, injector: Injector, ): Promise<LedgerProto> { const { defaultLocalCurrency, general: { taxCategories: { balanceCancellationTaxCategoryId }, }, } = await injector.get(AdminContextProvider).getVerifiedAdminContext(); const entityBalance = ledgerBalance.get(balanceCancellation.business_id); if (!entityBalance) { throw new LedgerError( `Balance cancellation for business ${balanceCancellation.business_id} redundant - already balanced`, ); } const { amount, entityId } = entityBalance; const financialAccountEntry = financialAccountLedgerEntries.find(entry => [ entry.creditAccountID1, entry.creditAccountID2, entry.debitAccountID1, entry.debitAccountID2, ].includes(balanceCancellation.business_id), ); if (!financialAccountEntry) { throw new LedgerError( `Balance cancellation for business ${balanceCancellation.business_id} failed - no financial account entry found`, ); } let foreignAmount: number | undefined = undefined; if ( financialAccountEntry.currency !== defaultLocalCurrency && financialAccountEntry.currencyRate ) { foreignAmount = financialAccountEntry.currencyRate * amount; } const isCreditorCounterparty = amount > 0; const ledgerEntry: LedgerProto = { id: balanceCancellation.charge_id, invoiceDate: financialAccountEntry.invoiceDate, valueDate: financialAccountEntry.valueDate, currency: financialAccountEntry.currency, creditAccountID1: isCreditorCounterparty ? balanceCancellationTaxCategoryId : entityId, creditAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined, localCurrencyCreditAmount1: Math.abs(amount), debitAccountID1: isCreditorCounterparty ? entityId : balanceCancellationTaxCategoryId, debitAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined, localCurrencyDebitAmount1: Math.abs(amount), description: balanceCancellation.description ?? undefined, reference: financialAccountEntry.reference, isCreditorCounterparty, ownerId, currencyRate: financialAccountEntry.currencyRate, chargeId, }; return ledgerEntry; } export function isRefundCharge(description?: string | null): boolean { if (!description) { return false; } const normalizedDescription = description.toLocaleLowerCase(); return ( normalizedDescription.includes('refund') || normalizedDescription.includes('reimbursement') ); } export function getExchangeDates(financialAccountLedgerEntries: LedgerProto[]): Date { const timeValue = Math.max( ...financialAccountLedgerEntries.map(entry => entry.valueDate.getTime()), ); const date = new Date(timeValue); return date; }