diff --git a/packages/client/src/components/all-charges/charge-errors.tsx b/packages/client/src/components/all-charges/charge-errors.tsx new file mode 100644 index 000000000..7639e94ab --- /dev/null +++ b/packages/client/src/components/all-charges/charge-errors.tsx @@ -0,0 +1,43 @@ +import { ReactElement } from 'react'; +import { List, Paper, Text } from '@mantine/core'; +import { AllChargesErrorsFieldsFragmentDoc } from '../../gql/graphql.js'; +import { FragmentType, getFragmentData } from '../../gql/index.js'; + +// eslint-disable-next-line @typescript-eslint/no-unused-expressions -- used by codegen +/* GraphQL */ ` + fragment AllChargesErrorsFields on Charge { + id + ... on Charge @defer { + ledger { + ... on Ledger @defer { + validate { + ... on LedgerValidation @defer { + errors + } + } + } + } + } + } +`; + +interface Props { + data?: FragmentType; +} + +export const ChargeErrors = ({ data }: Props): ReactElement | null => { + const charge = getFragmentData(AllChargesErrorsFieldsFragmentDoc, data); + + return charge?.ledger?.validate?.errors?.length ? ( + + Errors: + + {charge.ledger.validate.errors.map((error, i) => ( + + {error} + + ))} + + + ) : null; +}; diff --git a/packages/client/src/components/all-charges/charge-extended-info.tsx b/packages/client/src/components/all-charges/charge-extended-info.tsx index 372e5dd4a..10744c029 100644 --- a/packages/client/src/components/all-charges/charge-extended-info.tsx +++ b/packages/client/src/components/all-charges/charge-extended-info.tsx @@ -4,6 +4,7 @@ import { useQuery } from 'urql'; import { Accordion, ActionIcon, Box, Burger, Collapse, Loader, Menu, Tooltip } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; import { + AllChargesErrorsFieldsFragmentDoc, ConversionChargeInfoFragmentDoc, DocumentsGalleryFieldsFragmentDoc, FetchChargeDocument, @@ -19,6 +20,7 @@ import { ConfirmationModal, RegenerateLedgerRecordsButton, } from '../common/index.js'; +import { ChargeErrors } from './charge-errors.js'; import { DocumentsGallery } from './documents/documents-gallery.js'; import { DocumentsTable } from './documents/documents-table.js'; import { ConversionInfo } from './extended-info/conversion-info.js'; @@ -54,6 +56,7 @@ import { TransactionsTable } from './transactions/transactions-table.js'; } } } + ...AllChargesErrorsFields @defer } } `; @@ -153,6 +156,9 @@ export function ChargeExtendedInfo({ {fetching && ( )} + {isFragmentReady(FetchChargeDocument, AllChargesErrorsFieldsFragmentDoc, charge) && ( + + )} {!fetching && charge && (
0 + ) { return 'INVALID'; } diff --git a/packages/server/src/modules/ledger/helpers/common-charge-ledger.helper.ts b/packages/server/src/modules/ledger/helpers/common-charge-ledger.helper.ts new file mode 100644 index 000000000..7f9745f5d --- /dev/null +++ b/packages/server/src/modules/ledger/helpers/common-charge-ledger.helper.ts @@ -0,0 +1,285 @@ +import { Injector } from 'graphql-modules'; +import type { IGetDocumentsByChargeIdResult } from '@modules/documents/types'; +import { getRateForCurrency } from '@modules/exchange-rates/helpers/exchange.helper.js'; +import { ExchangeProvider } from '@modules/exchange-rates/providers/exchange.provider.js'; +import { FiatExchangeProvider } from '@modules/exchange-rates/providers/fiat-exchange.provider.js'; +import type { IGetTransactionsByChargeIdsResult } from '@modules/transactions/types.js'; +import { + BALANCE_CANCELLATION_TAX_CATEGORY_ID, + DEFAULT_LOCAL_CURRENCY, + INPUT_VAT_TAX_CATEGORY_ID, + INTERNAL_WALLETS_IDS, + OUTPUT_VAT_TAX_CATEGORY_ID, +} from '@shared/constants'; +import { Currency } from '@shared/enums'; +import { formatCurrency } from '@shared/helpers'; +import type { LedgerProto, StrictLedgerProto } from '@shared/types'; +import { IGetBalanceCancellationByChargesIdsResult } from '../types.js'; +import { + getFinancialAccountTaxCategoryId, + LedgerError, + validateTransactionBasicVariables, +} from './utils.helper.js'; + +export async function ledgerEntryFromDocument( + document: IGetDocumentsByChargeIdResult, + injector: Injector, + chargeId: string, + ownerId: string, + taxCategoryId: string, +): Promise { + 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`); + } + + 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) { + amountWithoutVat = amountWithoutVat - vatAmount; + vatTaxCategory = isCreditorCounterparty + ? OUTPUT_VAT_TAX_CATEGORY_ID + : INPUT_VAT_TAX_CATEGORY_ID; + } + + // handle non-local currencies + if (document.currency_code !== DEFAULT_LOCAL_CURRENCY) { + const exchangeRate = await getExchangeRateForDate(injector, document.date, currency); + + // 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 = null; + let localCurrencyCreditAmount1 = 0; + let debitAmount1: number | null = null; + let localCurrencyDebitAmount1 = 0; + 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: document.description ?? undefined, + reference1: document.serial_number ?? undefined, + isCreditorCounterparty, + ownerId, + chargeId, + }; + + return ledgerEntry; +} + +export async function ledgerEntryFromMainTransaction( + transaction: IGetTransactionsByChargeIdsResult, + injector: Injector, + chargeId: string, + ownerId: string, + businessId?: string, + gotRelevantDocuments = false, +): Promise { + const { currency, valueDate, transactionBusinessId } = + validateTransactionBasicVariables(transaction); + + let mainAccountId: string = transactionBusinessId; + + if ( + !gotRelevantDocuments && + transaction.source_reference && + businessId && + INTERNAL_WALLETS_IDS.includes(businessId) + ) { + mainAccountId = await getFinancialAccountTaxCategoryId(injector, transaction, currency, true); + } + + let amount = Number(transaction.amount); + let foreignAmount: number | undefined = undefined; + + if (currency !== DEFAULT_LOCAL_CURRENCY) { + // get exchange rate for currency + const exchangeRate = await injector + .get(ExchangeProvider) + .getExchangeRates(currency, DEFAULT_LOCAL_CURRENCY, valueDate); + + foreignAmount = amount; + // calculate amounts in ILS + amount = exchangeRate * amount; + } + + const accountTaxCategoryId = await getFinancialAccountTaxCategoryId( + injector, + transaction, + currency, + ); + + 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, + reference1: transaction.source_id, + isCreditorCounterparty, + ownerId, + currencyRate: transaction.currency_rate ? Number(transaction.currency_rate) : undefined, + chargeId, + }; + + return ledgerEntry; +} + +export function ledgerEntryFromBalanceCancellation( + balanceCancellation: IGetBalanceCancellationByChargesIdsResult, + ledgerBalance: Map, + financialAccountLedgerEntries: StrictLedgerProto[], + chargeId: string, + ownerId: string, +): LedgerProto { + 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 !== DEFAULT_LOCAL_CURRENCY && + 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 ? BALANCE_CANCELLATION_TAX_CATEGORY_ID : entityId, + creditAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined, + localCurrencyCreditAmount1: Math.abs(amount), + debitAccountID1: isCreditorCounterparty ? entityId : BALANCE_CANCELLATION_TAX_CATEGORY_ID, + debitAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined, + localCurrencyDebitAmount1: Math.abs(amount), + description: balanceCancellation.description ?? undefined, + reference1: financialAccountEntry.reference1, + isCreditorCounterparty, + ownerId, + currencyRate: financialAccountEntry.currencyRate, + chargeId, + }; + + return ledgerEntry; +} + +async function getExchangeRateForDate(injector: Injector, date: Date, currency: Currency) { + const exchangeRates = await injector + .get(FiatExchangeProvider) + .getExchangeRatesByDatesLoader.load(date); + return getRateForCurrency(currency, exchangeRates); +} diff --git a/packages/server/src/modules/ledger/helpers/conversion-charge-ledger.helper.ts b/packages/server/src/modules/ledger/helpers/conversion-charge-ledger.helper.ts index ac11f32ae..9d367b267 100644 --- a/packages/server/src/modules/ledger/helpers/conversion-charge-ledger.helper.ts +++ b/packages/server/src/modules/ledger/helpers/conversion-charge-ledger.helper.ts @@ -1,16 +1,16 @@ -import { GraphQLError } from 'graphql'; import { DEFAULT_LOCAL_CURRENCY } from '@shared/constants'; import { Currency } from '@shared/gql-types'; import type { LedgerProto } from '@shared/types'; +import { LedgerError } from './utils.helper.js'; export function getConversionBankRate(base: LedgerProto, quote: LedgerProto) { const baseRate = base.currencyRate ?? 0; const quoteRate = quote.currencyRate ?? 0; if (!baseRate && !quoteRate) { - throw new GraphQLError('Conversion records are missing currency rate'); + throw new LedgerError('Conversion records are missing currency rate'); } if (!!baseRate && !!quoteRate && baseRate !== quoteRate) { - throw new GraphQLError('Conversion records have mismatching currency rates'); + throw new LedgerError('Conversion records have mismatching currency rates'); } const bankRate = baseRate || quoteRate; @@ -24,7 +24,7 @@ export function conversionFeeCalculator( localCurrencyRate?: number, ): { localAmount: number; foreignAmount?: number; currency: Currency } { if (base.currency === quote.currency) { - throw new GraphQLError('Conversion records must have different currencies'); + throw new LedgerError('Conversion records must have different currencies'); } const eventRate = getConversionBankRate(base, quote); @@ -40,7 +40,7 @@ export function conversionFeeCalculator( const baseAmountConvertedByEventRate = baseAmount / eventRate; const minimalPrecision = Math.max(baseAmount / 10_000_000, 0.005); if (baseAmountConvertedByEventRate - quoteAmount > minimalPrecision) { - throw new GraphQLError( + throw new LedgerError( 'Conversion records have mismatching amounts, taking the bank rate into account', ); } @@ -55,7 +55,7 @@ export function conversionFeeCalculator( if (base.currency === DEFAULT_LOCAL_CURRENCY) { localCurrencyRate = 1 / officialRate; } else { - throw new GraphQLError('Conversion records are missing local currency rate'); + throw new LedgerError('Conversion records are missing local currency rate'); } } const feeAmountByLocalCurrency = feeAmountByQuoteCurrency * localCurrencyRate; diff --git a/packages/server/src/modules/ledger/helpers/dividend-ledger.helper.ts b/packages/server/src/modules/ledger/helpers/dividend-ledger.helper.ts index 0d52e52ca..c6b7ada39 100644 --- a/packages/server/src/modules/ledger/helpers/dividend-ledger.helper.ts +++ b/packages/server/src/modules/ledger/helpers/dividend-ledger.helper.ts @@ -1,4 +1,3 @@ -import { GraphQLError } from 'graphql'; import type { IGetTransactionsByChargeIdsResult } from '@modules/transactions/types'; import { DIVIDEND_PAYMENT_BUSINESS_IDS, @@ -10,14 +9,15 @@ export function splitDividendTransactions(transactions: Array> { - const { injector } = context; const ledgerEntries: Array = []; if (!transaction.is_fee) { - throw new GraphQLError( - `Who did a non-fee transaction marked as fee? (Transaction ID="${transaction.id}")`, + throw new LedgerError( + `Seems like did a non-fee transaction marked as fee (Transaction ID="${transaction.id}")`, ); } diff --git a/packages/server/src/modules/ledger/helpers/ledgrer-storage.helper.ts b/packages/server/src/modules/ledger/helpers/ledgrer-storage.helper.ts index 6dee1404b..71b377389 100644 --- a/packages/server/src/modules/ledger/helpers/ledgrer-storage.helper.ts +++ b/packages/server/src/modules/ledger/helpers/ledgrer-storage.helper.ts @@ -265,6 +265,9 @@ export async function storeInitialGeneratedRecords( const ledgerRecords: IInsertLedgerRecordsParams['ledgerRecords'] = records.map( convertToStorageInputRecord, ); - await injector.get(LedgerProvider).insertLedgerRecords({ ledgerRecords }); + if (ledgerRecords.length) { + return injector.get(LedgerProvider).insertLedgerRecords({ ledgerRecords }); + } } + return void 0; } diff --git a/packages/server/src/modules/ledger/helpers/salary-charge-ledger.helper.ts b/packages/server/src/modules/ledger/helpers/salary-charge-ledger.helper.ts index f806c057b..b9a2bce2d 100644 --- a/packages/server/src/modules/ledger/helpers/salary-charge-ledger.helper.ts +++ b/packages/server/src/modules/ledger/helpers/salary-charge-ledger.helper.ts @@ -1,4 +1,3 @@ -import { GraphQLError } from 'graphql'; import type { IGetChargesByIdsResult } from '@modules/charges/types'; import type { IGetSalaryRecordsByChargeIdsResult } from '@modules/salaries/types.js'; import { @@ -14,6 +13,7 @@ import { ZKUFOT_INCOME_TAX_CATEGORY_ID, } from '@shared/constants'; import type { LedgerProto } from '@shared/types'; +import { LedgerError } from './utils.helper.js'; function generateEntryRaw( accountId: string, @@ -71,7 +71,7 @@ export function generateEntriesFromSalaryRecords( const chargeId = charge.id; if (!salaryRecords.length) { - throw new GraphQLError(`No salary records found for charge ${chargeId}`); + throw new LedgerError(`No salary records found for charge ${chargeId}`); } const entries: LedgerProto[] = []; @@ -110,7 +110,7 @@ export function generateEntriesFromSalaryRecords( for (const salaryRecord of salaryRecords) { // record validations if (!salaryRecord.base_salary) { - throw new GraphQLError( + throw new LedgerError( `Base salary record for ${salaryRecord.month}, employee ID=${salaryRecord.employee_id} is missing amount`, ); } @@ -168,7 +168,7 @@ export function generateEntriesFromSalaryRecords( const pensionAccount = salaryRecord.pension_fund_id; if (totalPension > 0) { if (!pensionAccount) { - throw new GraphQLError(`Missing pension account for ${chargeId}`); + throw new LedgerError(`Missing pension account for charge ID=${chargeId}`); } amountPerBusiness[pensionAccount] ??= 0; amountPerBusiness[pensionAccount] += totalPension; @@ -183,7 +183,7 @@ export function generateEntriesFromSalaryRecords( const trainingFundAccount = salaryRecord.training_fund_id; if (totalTrainingFund > 0) { if (!trainingFundAccount) { - throw new GraphQLError(`Missing training fund account for ${chargeId}`); + throw new LedgerError(`Missing training fund account for charge ID=${chargeId}`); } amountPerBusiness[trainingFundAccount] ??= 0; amountPerBusiness[trainingFundAccount] += totalTrainingFund; @@ -199,7 +199,7 @@ export function generateEntriesFromSalaryRecords( } if (!month) { - throw new GraphQLError(`No month found for salary charge ${chargeId}`); + throw new LedgerError(`No month found for salary charge ID=${chargeId}`); } // generate pension/training funds entries diff --git a/packages/server/src/modules/ledger/helpers/utils.helper.ts b/packages/server/src/modules/ledger/helpers/utils.helper.ts index 9269dd0f5..99dc2cb71 100644 --- a/packages/server/src/modules/ledger/helpers/utils.helper.ts +++ b/packages/server/src/modules/ledger/helpers/utils.helper.ts @@ -1,4 +1,3 @@ -import { GraphQLError } from 'graphql'; import { Injector } from 'graphql-modules'; import { IGetChargesByIdsResult } from '@modules/charges/types.js'; import { FinancialAccountsProvider } from '@modules/financial-accounts/providers/financial-accounts.provider.js'; @@ -14,14 +13,20 @@ import { formatCurrency, formatFinancialAmount } from '@shared/helpers'; import type { LedgerBalanceInfoType, LedgerProto, StrictLedgerProto } from '@shared/types'; import type { IGetLedgerRecordsByChargesIdsResult } from '../types.js'; +export class LedgerError extends Error { + constructor(message: string) { + super(message); + } +} + export function isTransactionsOppositeSign([first, second]: IGetTransactionsByChargeIdsResult[]) { if (!first || !second) { - throw new GraphQLError('Transactions are missing'); + throw new LedgerError('Transactions are missing'); } const firstAmount = Number(first.amount); const secondAmount = Number(second.amount); if (Number.isNaN(firstAmount) || Number.isNaN(secondAmount)) { - throw new Error('Transaction amount is not a number'); + throw new LedgerError('Transaction amount is not a number'); } return Number(first.amount) > 0 !== Number(second.amount) > 0; } @@ -53,7 +58,9 @@ export function getTaxCategoryNameByAccountCurrency( console.error(`Unknown currency for account's tax category: ${currency}`); } if (!taxCategoryName) { - throw new GraphQLError(`Account ID="${account.id}" is missing tax category name`); + throw new LedgerError( + `Account number "${account.account_number}" is missing tax category name`, + ); } return taxCategoryName; } @@ -61,14 +68,16 @@ export function getTaxCategoryNameByAccountCurrency( export function validateTransactionBasicVariables(transaction: IGetTransactionsByChargeIdsResult) { const currency = formatCurrency(transaction.currency); if (!transaction.debit_date) { - throw new GraphQLError( - `Transaction ID="${transaction.id}" is missing debit date for currency ${currency}`, + throw new LedgerError( + `Transaction reference "${transaction.source_reference}" is missing debit date for currency ${currency}`, ); } const valueDate = transaction.debit_timestamp ?? transaction.debit_date; if (!transaction.business_id) { - throw new GraphQLError(`Transaction ID="${transaction.id}" is missing business_id`); + throw new LedgerError( + `Transaction reference "${transaction.source_reference}" is missing business_id`, + ); } const transactionBusinessId = transaction.business_id; @@ -90,13 +99,15 @@ export function validateTransactionRequiredVariables( transaction: IGetTransactionsByChargeIdsResult, ): ValidateTransaction { if (!transaction.debit_date) { - throw new GraphQLError( - `Transaction ID="${transaction.id}" is missing debit date for currency ${transaction.currency}`, + throw new LedgerError( + `Transaction reference "${transaction.source_reference}" is missing debit date for currency ${transaction.currency}`, ); } if (!transaction.business_id) { - throw new GraphQLError(`Transaction ID="${transaction.id}" is missing business_id`); + throw new LedgerError( + `Transaction reference "${transaction.source_reference}" is missing business_id`, + ); } const debit_timestamp = transaction.debit_timestamp ?? transaction.debit_date; @@ -274,8 +285,8 @@ export async function getFinancialAccountTaxCategoryId( useSourceReference?: boolean, ): Promise { if (useSourceReference && !transaction.source_reference) { - throw new GraphQLError( - `Transaction ID="${transaction.id}" is missing source reference, which is required for fetching the financial account`, + throw new LedgerError( + `Transaction reference "${transaction.source_reference}" is missing source reference, which is required for fetching the financial account`, ); } const account = await (useSourceReference @@ -286,7 +297,9 @@ export async function getFinancialAccountTaxCategoryId( .get(FinancialAccountsProvider) .getFinancialAccountByAccountIDLoader.load(transaction.account_id)); if (!account) { - throw new GraphQLError(`Transaction ID="${transaction.id}" is missing account`); + throw new LedgerError( + `Transaction reference "${transaction.source_reference}" is missing account`, + ); } const taxCategoryName = getTaxCategoryNameByAccountCurrency( account, @@ -297,7 +310,7 @@ export async function getFinancialAccountTaxCategoryId( .taxCategoryByNamesLoader.load(taxCategoryName); if (!taxCategory) { - throw new GraphQLError(`Account ID="${account.id}" is missing tax category`); + throw new LedgerError(`Account number "${account.account_number}" is missing tax category`); } return taxCategory.id; @@ -366,7 +379,7 @@ export function multipleForeignCurrenciesBalanceEntries( [entry.creditAccountID1, entry.debitAccountID1].includes(mainBusiness), ); if (!transactionEntry || !documentEntry) { - throw new GraphQLError( + throw new LedgerError( `Failed to locate transaction or document entry for business ID="${mainBusiness}"`, ); } diff --git a/packages/server/src/modules/ledger/resolvers/common.resolver.ts b/packages/server/src/modules/ledger/resolvers/common.resolver.ts index 1a6bf250a..c4b63001d 100644 --- a/packages/server/src/modules/ledger/resolvers/common.resolver.ts +++ b/packages/server/src/modules/ledger/resolvers/common.resolver.ts @@ -10,6 +10,7 @@ export const commonChargeLedgerResolver: LedgerModule.ChargeResolvers = { return { records: ledgerRecords, charge: DbCharge, + errors: [], }; }, }; diff --git a/packages/server/src/modules/ledger/resolvers/ledger-generation/bank-deposit-ledger-generation.resolver.ts b/packages/server/src/modules/ledger/resolvers/ledger-generation/bank-deposit-ledger-generation.resolver.ts index 4504658e0..eca6cc58e 100644 --- a/packages/server/src/modules/ledger/resolvers/ledger-generation/bank-deposit-ledger-generation.resolver.ts +++ b/packages/server/src/modules/ledger/resolvers/ledger-generation/bank-deposit-ledger-generation.resolver.ts @@ -1,21 +1,18 @@ -import { GraphQLError } from 'graphql'; import { ExchangeProvider } from '@modules/exchange-rates/providers/exchange.provider.js'; import { TaxCategoriesProvider } from '@modules/financial-entities/providers/tax-categories.provider.js'; +import { ledgerEntryFromMainTransaction } from '@modules/ledger/helpers/common-charge-ledger.helper.js'; import { LedgerProvider } from '@modules/ledger/providers/ledger.provider.js'; import { BankDepositTransactionsProvider } from '@modules/transactions/providers/bank-deposit-transactions.provider.js'; import { TransactionsProvider } from '@modules/transactions/providers/transactions.provider.js'; import { IGetTransactionsByChargeIdsResult } from '@modules/transactions/types.js'; -import { - DEFAULT_LOCAL_CURRENCY, - EXCHANGE_RATE_TAX_CATEGORY_ID, - INTERNAL_WALLETS_IDS, -} from '@shared/constants'; +import { DEFAULT_LOCAL_CURRENCY, EXCHANGE_RATE_TAX_CATEGORY_ID } from '@shared/constants'; import type { Maybe, ResolverFn, ResolversParentTypes, ResolversTypes } from '@shared/gql-types'; import type { LedgerProto, StrictLedgerProto } from '@shared/types'; import { storeInitialGeneratedRecords } from '../../helpers/ledgrer-storage.helper.js'; import { getFinancialAccountTaxCategoryId, getLedgerBalanceInfo, + LedgerError, ledgerProtoToRecordsConverter, updateLedgerBalanceByEntry, validateTransactionBasicVariables, @@ -30,6 +27,8 @@ export const generateLedgerRecordsForBankDeposit: ResolverFn< const chargeId = charge.id; const { injector } = context; + const errors: Set = new Set(); + try { // validate ledger records are balanced const ledgerBalance = new Map(); @@ -76,72 +75,25 @@ export const generateLedgerRecordsForBankDeposit: ResolverFn< isWithdrawal = true; } - const mainTransactionPromise = async () => { - // for each transaction, create a ledger record - const { currency, valueDate, transactionBusinessId } = - validateTransactionBasicVariables(mainTransaction); - - let mainAccountId: string = transactionBusinessId; - - if ( - mainTransaction.source_reference && - charge.business_id && - INTERNAL_WALLETS_IDS.includes(charge.business_id) - ) { - mainAccountId = await getFinancialAccountTaxCategoryId( - injector, - mainTransaction, - currency, - true, - ); - } - - let amount = Number(mainTransaction.amount); - let foreignAmount: number | undefined = undefined; - - if (currency !== DEFAULT_LOCAL_CURRENCY) { - // get exchange rate for currency - const exchangeRate = await injector - .get(ExchangeProvider) - .getExchangeRates(currency, DEFAULT_LOCAL_CURRENCY, valueDate); - - foreignAmount = amount; - // calculate amounts in ILS - amount = exchangeRate * amount; - } - - const accountTaxCategoryId = await getFinancialAccountTaxCategoryId( - injector, + const mainTransactionPromise = async () => + ledgerEntryFromMainTransaction( mainTransaction, - currency, - ); - - const isCreditorCounterparty = amount > 0; - - const ledgerEntry: StrictLedgerProto = { - id: mainTransaction.id, - invoiceDate: mainTransaction.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: mainTransaction.source_description ?? undefined, - reference1: mainTransaction.source_id, - isCreditorCounterparty, - ownerId: charge.owner_id, - currencyRate: mainTransaction.currency_rate - ? Number(mainTransaction.currency_rate) - : undefined, + injector, chargeId, - }; - - financialAccountLedgerEntries.push(ledgerEntry); - updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); - }; + charge.owner_id, + charge.business_id ?? undefined, + ) + .then(ledgerEntry => { + financialAccountLedgerEntries.push(ledgerEntry); + updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); + }) + .catch(e => { + if (e instanceof LedgerError) { + errors.add(e.message); + } else { + throw e; + } + }); // create a ledger record for fee transactions const interestTransactionsPromises = interestTransactions.map(async transaction => { @@ -156,7 +108,8 @@ export const generateLedgerRecordsForBankDeposit: ResolverFn< ownerId: charge.owner_id, }); if (!businessTaxCategory) { - throw new GraphQLError(`Business ID="${transactionBusinessId}" is missing tax category`); + errors.add(`Business ID="${transactionBusinessId}" is missing tax category`); + return; } let amount = Number(transaction.amount); @@ -215,15 +168,15 @@ export const generateLedgerRecordsForBankDeposit: ResolverFn< if (isWithdrawal && mainTransaction.currency !== DEFAULT_LOCAL_CURRENCY) { const mainLedgerEntry = financialAccountLedgerEntries[0]; if (!mainLedgerEntry) { - throw new GraphQLError('Main ledger entry not found'); + throw new LedgerError('Main ledger entry not found'); } const depositTransactions = bankDepositTransactions.filter(t => Number(t.amount) < 0); if (depositTransactions.length !== 1) { if (depositTransactions.length === 0) { - throw new GraphQLError('Deposit transaction not found'); + throw new LedgerError('Deposit transaction not found'); } - throw new GraphQLError('Multiple deposit transactions found'); + throw new LedgerError('Multiple deposit transactions found'); } const depositTransaction = depositTransactions[0]; @@ -232,7 +185,7 @@ export const generateLedgerRecordsForBankDeposit: ResolverFn< mainTransaction.amount.replace('-', '') !== depositTransaction.amount.replace('-', '') || mainTransaction.currency !== depositTransaction.currency ) { - throw new GraphQLError('Deposit transaction does not match the withdrawal transaction'); + throw new LedgerError('Deposit transaction does not match the withdrawal transaction'); } const depositLedgerRecords = await injector @@ -241,14 +194,14 @@ export const generateLedgerRecordsForBankDeposit: ResolverFn< if (depositLedgerRecords.length !== 1) { if (depositLedgerRecords.length === 0) { - throw new GraphQLError('Deposit ledger record not found'); + throw new LedgerError('Deposit ledger record not found'); } - throw new GraphQLError('Multiple deposit ledger records found'); + throw new LedgerError('Multiple deposit ledger records found'); } const depositLedgerRecord = depositLedgerRecords[0]; if (Number.isNaN(depositLedgerRecord.credit_local_amount1)) { - throw new GraphQLError('Deposit ledger record has invalid local amount'); + throw new LedgerError('Deposit ledger record has invalid local amount'); } const rawAmount = @@ -279,7 +232,13 @@ export const generateLedgerRecordsForBankDeposit: ResolverFn< return; }; - await miscLedgerEntriesPromise(); + await miscLedgerEntriesPromise().catch(e => { + if (e instanceof LedgerError) { + errors.add(e.message); + } else { + throw e; + } + }); const records = [ ...financialAccountLedgerEntries, @@ -293,6 +252,7 @@ export const generateLedgerRecordsForBankDeposit: ResolverFn< records: ledgerProtoToRecordsConverter(records), charge, balance: ledgerBalanceInfo, + errors: Array.from(errors), }; } catch (e) { return { diff --git a/packages/server/src/modules/ledger/resolvers/ledger-generation/business-trip-ledger-generation.resolver.ts b/packages/server/src/modules/ledger/resolvers/ledger-generation/business-trip-ledger-generation.resolver.ts index 8c7496b4b..50328ae89 100644 --- a/packages/server/src/modules/ledger/resolvers/ledger-generation/business-trip-ledger-generation.resolver.ts +++ b/packages/server/src/modules/ledger/resolvers/ledger-generation/business-trip-ledger-generation.resolver.ts @@ -1,11 +1,10 @@ -import { GraphQLError } from 'graphql'; import { Injector } from 'graphql-modules'; import { BusinessTripAttendeesProvider } from '@modules/business-trips/providers/business-trips-attendees.provider.js'; import { BusinessTripTransactionsProvider } from '@modules/business-trips/providers/business-trips-transactions.provider.js'; import { ExchangeProvider } from '@modules/exchange-rates/providers/exchange.provider.js'; import { storeInitialGeneratedRecords } from '@modules/ledger/helpers/ledgrer-storage.helper.js'; import { TransactionsProvider } from '@modules/transactions/providers/transactions.provider.js'; -import { DEFAULT_LOCAL_CURRENCY, FEE_TAX_CATEGORY_ID } from '@shared/constants'; +import { DEFAULT_LOCAL_CURRENCY } from '@shared/constants'; import { Currency, Maybe, @@ -15,16 +14,16 @@ import { } from '@shared/gql-types'; import type { LedgerProto, StrictLedgerProto } from '@shared/types'; import { - isSupplementalFeeTransaction, + getEntriesFromFeeTransaction, splitFeeTransactions, } from '../../helpers/fee-transactions.js'; import { generatePartialLedgerEntry, getFinancialAccountTaxCategoryId, getLedgerBalanceInfo, + LedgerError, ledgerProtoToRecordsConverter, updateLedgerBalanceByEntry, - validateTransactionBasicVariables, validateTransactionRequiredVariables, } from '../../helpers/utils.helper.js'; @@ -36,8 +35,10 @@ export const generateLedgerRecordsForBusinessTrip: ResolverFn< > = async (charge, _, { injector }) => { const chargeId = charge.id; + const errors: Set = new Set(); + if (!charge.tax_category_id) { - throw new GraphQLError(`Business trip charge ID="${charge.id}" is missing tax category`); + errors.add(`Business trip is missing tax category`); } const tripTaxCategory = charge.tax_category_id; @@ -71,113 +72,65 @@ export const generateLedgerRecordsForBusinessTrip: ResolverFn< // for each transaction, create a ledger record const mainTransactionsPromises = mainTransactions.map(async preValidatedTransaction => { - const transaction = validateTransactionRequiredVariables(preValidatedTransaction); - - // get tax category - const accountTaxCategoryId = await getFinancialAccountTaxCategoryId(injector, transaction); - - // preparations for core ledger entries - let exchangeRate: number | undefined = undefined; - if (transaction.currency !== DEFAULT_LOCAL_CURRENCY) { - // get exchange rate for currency - exchangeRate = await injector - .get(ExchangeProvider) - .getExchangeRates( - transaction.currency, - DEFAULT_LOCAL_CURRENCY, - transaction.debit_timestamp, - ); - } - - const partialEntry = generatePartialLedgerEntry(transaction, charge.owner_id, exchangeRate); - const ledgerEntry: StrictLedgerProto = { - ...partialEntry, - creditAccountID1: partialEntry.isCreditorCounterparty - ? transaction.business_id - : accountTaxCategoryId, - debitAccountID1: partialEntry.isCreditorCounterparty - ? accountTaxCategoryId - : transaction.business_id, - }; - - financialAccountLedgerEntries.push(ledgerEntry); - updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); - }); - - // create a ledger record for fee transactions - const feeTransactionsPromises = feeTransactions.map(async transaction => { - if (!transaction.is_fee) { - throw new GraphQLError( - `Who did a non-fee transaction marked as fee? (Transaction ID="${transaction.id}")`, - ); - } - - const isSupplementalFee = isSupplementalFeeTransaction(transaction); - const { currency, valueDate, transactionBusinessId } = - validateTransactionBasicVariables(transaction); - - let amount = Number(transaction.amount); - let foreignAmount: number | undefined = undefined; - - if (currency !== DEFAULT_LOCAL_CURRENCY) { - // get exchange rate for currency - const exchangeRate = await injector - .get(ExchangeProvider) - .getExchangeRates(currency, DEFAULT_LOCAL_CURRENCY, valueDate); - - foreignAmount = amount; - // calculate amounts in ILS - amount = exchangeRate * amount; - } - - const isCreditorCounterparty = amount > 0; - - let mainAccount = transactionBusinessId; - - const partialLedgerEntry: Omit = { - id: transaction.id, - invoiceDate: transaction.event_date, - valueDate, - currency, - creditAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined, - localCurrencyCreditAmount1: Math.abs(amount), - debitAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined, - localCurrencyDebitAmount1: Math.abs(amount), - description: transaction.source_description ?? undefined, - reference1: transaction.source_id, - isCreditorCounterparty: isSupplementalFee - ? isCreditorCounterparty - : !isCreditorCounterparty, - ownerId: charge.owner_id, - currencyRate: transaction.currency_rate ? Number(transaction.currency_rate) : undefined, - chargeId: transaction.charge_id, - }; - - if (isSupplementalFee) { - mainAccount = await getFinancialAccountTaxCategoryId(injector, transaction, currency); - } else { - const mainBusiness = charge.business_id ?? undefined; + try { + const transaction = validateTransactionRequiredVariables(preValidatedTransaction); + + // get tax category + const accountTaxCategoryId = await getFinancialAccountTaxCategoryId(injector, transaction); + + // preparations for core ledger entries + let exchangeRate: number | undefined = undefined; + if (transaction.currency !== DEFAULT_LOCAL_CURRENCY) { + // get exchange rate for currency + exchangeRate = await injector + .get(ExchangeProvider) + .getExchangeRates( + transaction.currency, + DEFAULT_LOCAL_CURRENCY, + transaction.debit_timestamp, + ); + } - const ledgerEntry: LedgerProto = { - ...partialLedgerEntry, - creditAccountID1: isCreditorCounterparty ? mainAccount : mainBusiness, - debitAccountID1: isCreditorCounterparty ? mainBusiness : mainAccount, + const partialEntry = generatePartialLedgerEntry(transaction, charge.owner_id, exchangeRate); + const ledgerEntry: StrictLedgerProto = { + ...partialEntry, + creditAccountID1: partialEntry.isCreditorCounterparty + ? transaction.business_id + : accountTaxCategoryId, + debitAccountID1: partialEntry.isCreditorCounterparty + ? accountTaxCategoryId + : transaction.business_id, }; - feeFinancialAccountLedgerEntries.push(ledgerEntry); + financialAccountLedgerEntries.push(ledgerEntry); updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); + } catch (e) { + if (e instanceof LedgerError) { + errors.add(e.message); + } else { + throw e; + } } - - const ledgerEntry: StrictLedgerProto = { - ...partialLedgerEntry, - creditAccountID1: isCreditorCounterparty ? FEE_TAX_CATEGORY_ID : mainAccount, - debitAccountID1: isCreditorCounterparty ? mainAccount : FEE_TAX_CATEGORY_ID, - }; - - feeFinancialAccountLedgerEntries.push(ledgerEntry); - updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); }); + // create a ledger record for fee transactions + const feeTransactionsPromises = feeTransactions.map(async transaction => + getEntriesFromFeeTransaction(transaction, charge, injector) + .then(entries => { + entries.map(entry => { + feeFinancialAccountLedgerEntries.push(entry); + updateLedgerBalanceByEntry(entry, ledgerBalance); + }); + }) + .catch(e => { + if (e instanceof LedgerError) { + errors.add(e.message); + } else { + throw e; + } + }), + ); + entriesPromises.push(...mainTransactionsPromises, ...feeTransactionsPromises); await Promise.all(entriesPromises); @@ -185,15 +138,20 @@ export const generateLedgerRecordsForBusinessTrip: ResolverFn< entriesPromises = []; const businessTripTransactionsPromises = businessTripTransactions.map( async businessTripTransaction => { + if (!tripTaxCategory) { + return; + } + const isTransactionBased = !!businessTripTransaction.transaction_id; if (isTransactionBased) { const matchingEntry = financialAccountLedgerEntries.find( entry => entry.id === businessTripTransaction.transaction_id, ); if (!matchingEntry) { - throw new GraphQLError( + errors.add( `Flight transaction ID="${businessTripTransaction.transaction_id}" is missing from transactions`, ); + return; } const isCreditorCounterparty = !matchingEntry.isCreditorCounterparty; @@ -217,9 +175,10 @@ export const generateLedgerRecordsForBusinessTrip: ResolverFn< !businessTripTransaction.amount || !businessTripTransaction.currency ) { - throw new GraphQLError( + errors.add( `Business trip flight transaction ID="${businessTripTransaction.id}" is missing required fields`, ); + return; } // preparations for core ledger entries @@ -293,6 +252,7 @@ export const generateLedgerRecordsForBusinessTrip: ResolverFn< records: ledgerProtoToRecordsConverter(records), charge, balance: ledgerBalanceInfo, + errors: Array.from(errors), }; } catch (e) { return { diff --git a/packages/server/src/modules/ledger/resolvers/ledger-generation/common-ledger-generation.resolver.ts b/packages/server/src/modules/ledger/resolvers/ledger-generation/common-ledger-generation.resolver.ts index 65196b726..79b5dd24d 100644 --- a/packages/server/src/modules/ledger/resolvers/ledger-generation/common-ledger-generation.resolver.ts +++ b/packages/server/src/modules/ledger/resolvers/ledger-generation/common-ledger-generation.resolver.ts @@ -1,23 +1,17 @@ -import { GraphQLError } from 'graphql'; import { DocumentsProvider } from '@modules/documents/providers/documents.provider.js'; -import { getRateForCurrency } from '@modules/exchange-rates/helpers/exchange.helper.js'; -import { ExchangeProvider } from '@modules/exchange-rates/providers/exchange.provider.js'; -import { FiatExchangeProvider } from '@modules/exchange-rates/providers/fiat-exchange.provider.js'; import { BusinessesProvider } from '@modules/financial-entities/providers/businesses.provider.js'; import { TaxCategoriesProvider } from '@modules/financial-entities/providers/tax-categories.provider.js'; +import { + ledgerEntryFromBalanceCancellation, + ledgerEntryFromDocument, + ledgerEntryFromMainTransaction, +} from '@modules/ledger/helpers/common-charge-ledger.helper.js'; import { handleCrossYearLedgerEntries } from '@modules/ledger/helpers/cross-year-ledger.helper.js'; import { TransactionsProvider } from '@modules/transactions/providers/transactions.provider.js'; import type { currency } from '@modules/transactions/types.js'; -import { - BALANCE_CANCELLATION_TAX_CATEGORY_ID, - DEFAULT_LOCAL_CURRENCY, - INCOME_EXCHANGE_RATE_TAX_CATEGORY_ID, - INPUT_VAT_TAX_CATEGORY_ID, - INTERNAL_WALLETS_IDS, - OUTPUT_VAT_TAX_CATEGORY_ID, -} from '@shared/constants'; +import { DEFAULT_LOCAL_CURRENCY, INCOME_EXCHANGE_RATE_TAX_CATEGORY_ID } from '@shared/constants'; import type { Maybe, ResolverFn, ResolversParentTypes, ResolversTypes } from '@shared/gql-types'; -import { formatCurrency } from '@shared/helpers'; +import { formatStringifyAmount } from '@shared/helpers'; import type { LedgerProto, StrictLedgerProto } from '@shared/types'; import { getEntriesFromFeeTransaction, @@ -25,12 +19,11 @@ import { } from '../../helpers/fee-transactions.js'; import { storeInitialGeneratedRecords } from '../../helpers/ledgrer-storage.helper.js'; import { - getFinancialAccountTaxCategoryId, getLedgerBalanceInfo, + LedgerError, ledgerProtoToRecordsConverter, multipleForeignCurrenciesBalanceEntries, updateLedgerBalanceByEntry, - validateTransactionBasicVariables, } from '../../helpers/utils.helper.js'; import { BalanceCancellationProvider } from '../../providers/balance-cancellation.provider.js'; import { UnbalancedBusinessesProvider } from '../../providers/unbalanced-businesses.provider.js'; @@ -40,9 +33,10 @@ export const generateLedgerRecordsForCommonCharge: ResolverFn< ResolversParentTypes['Charge'], GraphQLModules.Context, object -> = async (charge, _, context) => { +> = async (charge, _, { injector }) => { const chargeId = charge.id; - const { injector } = context; + + const errors: Set = new Set(); try { // validate ledger records are balanced @@ -51,24 +45,30 @@ export const generateLedgerRecordsForCommonCharge: ResolverFn< const dates = new Set(); const currencies = new Set(); - const shouldFetchDocuments = - Number(charge.invoices_count ?? '0') + Number(charge.receipts_count ?? 0) > 0; - const shouldFetchTransactions = !!charge.transactions_count; + const gotRelevantDocuments = + Number(charge.invoices_count ?? 0) + Number(charge.receipts_count ?? 0) > 0; + const gotTransactions = !!charge.transactions_count; - const documentsTaxCategoryIdPromise = shouldFetchDocuments - ? charge.tax_category_id - ? Promise.resolve(charge.tax_category_id) - : injector - .get(TaxCategoriesProvider) - .taxCategoryByChargeIDsLoader.load(charge.id) - .then(res => res?.id) - : undefined; - - const documentsPromise = shouldFetchDocuments + const documentsTaxCategoryIdPromise = new Promise((resolve, reject) => { + if (charge.tax_category_id) { + resolve(charge.tax_category_id); + } + if (!gotRelevantDocuments) { + resolve(undefined); + } + injector + .get(TaxCategoriesProvider) + .taxCategoryByChargeIDsLoader.load(charge.id) + .then(res => res?.id) + .then(resolve) + .catch(reject); + }); + + const documentsPromise = gotRelevantDocuments ? injector.get(DocumentsProvider).getDocumentsByChargeIdLoader.load(chargeId) : []; - const transactionsPromise = shouldFetchTransactions + const transactionsPromise = gotTransactions ? injector.get(TransactionsProvider).getTransactionsByChargeIDLoader.load(chargeId) : []; @@ -100,11 +100,9 @@ export const generateLedgerRecordsForCommonCharge: ResolverFn< const feeFinancialAccountLedgerEntries: LedgerProto[] = []; // generate ledger from documents - if (shouldFetchDocuments) { - if (!documentsTaxCategoryId) { - throw new GraphQLError(`Tax category not found for charge ID="${charge.id}"`); - } - + if (!documentsTaxCategoryId) { + errors.add('Tax category not found'); + } else if (gotRelevantDocuments) { // Get all relevant documents for charge const relevantDocuments = documents.filter(d => ['INVOICE', 'INVOICE_RECEIPT'].includes(d.type), @@ -122,220 +120,84 @@ export const generateLedgerRecordsForCommonCharge: ResolverFn< // for each invoice - generate accounting ledger entry const documentsEntriesPromises = relevantDocuments.map(async document => { - if (!document.date) { - throw new GraphQLError(`Document ID="${document.id}" is missing the date`); - } - - if (!document.debtor_id) { - throw new GraphQLError(`Document ID="${document.id}" is missing the debtor`); - } - if (!document.creditor_id) { - throw new GraphQLError(`Document ID="${document.id}" is missing the creditor`); - } - if (!document.total_amount) { - throw new GraphQLError(`Document ID="${document.id}" is missing amount`); - } - let totalAmount = document.total_amount; - - const isCreditorCounterparty = document.debtor_id === charge.owner_id; - const counterpartyId = isCreditorCounterparty ? document.creditor_id : document.debtor_id; - if (totalAmount < 0) { - totalAmount = Math.abs(totalAmount); + if (!documentsTaxCategoryId) { + return; } - const debitAccountID1 = isCreditorCounterparty ? documentsTaxCategoryId : counterpartyId; - const creditAccountID1 = isCreditorCounterparty ? counterpartyId : documentsTaxCategoryId; - let creditAccountID2: string | null = null; - let debitAccountID2: string | null = null; - - if (!document.currency_code) { - throw new GraphQLError(`Document ID="${document.id}" is missing currency code`); - } - 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 && vatAmount > 0) { - amountWithoutVat = amountWithoutVat - vatAmount; - vatTaxCategory = isCreditorCounterparty - ? OUTPUT_VAT_TAX_CATEGORY_ID - : INPUT_VAT_TAX_CATEGORY_ID; - } - - // handle non-local currencies - if (document.currency_code !== DEFAULT_LOCAL_CURRENCY) { - // get exchange rate for currency - const exchangeRates = await injector - .get(FiatExchangeProvider) - .getExchangeRatesByDatesLoader.load(document.date); - const exchangeRate = getRateForCurrency(document.currency_code, exchangeRates); - - // 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 creditAmount1: number | null = null; - let localCurrencyCreditAmount1 = 0; - let debitAmount1: number | null = null; - let localCurrencyDebitAmount1 = 0; - 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: document.description ?? undefined, - reference1: document.serial_number ?? undefined, - isCreditorCounterparty, - ownerId: charge.owner_id, + return ledgerEntryFromDocument( + document, + injector, chargeId, - }; - - accountingLedgerEntries.push(ledgerEntry); - updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); - dates.add(document.date.getTime()); - currencies.add(document.currency_code); + charge.owner_id, + documentsTaxCategoryId!, + ) + .then(ledgerEntry => { + accountingLedgerEntries.push(ledgerEntry); + updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); + dates.add(ledgerEntry.valueDate.getTime()); + currencies.add(ledgerEntry.currency); + }) + .catch(e => { + if (e instanceof LedgerError) { + errors.add(e.message); + } else { + throw e; + } + }); }); entriesPromises.push(...documentsEntriesPromises); } // generate ledger from transactions - if (shouldFetchTransactions) { + if (gotTransactions) { const { mainTransactions, feeTransactions } = splitFeeTransactions(transactions); // for each transaction, create a ledger record const mainTransactionsPromises = mainTransactions.map(async transaction => { - const { currency, valueDate, transactionBusinessId } = - validateTransactionBasicVariables(transaction); - - let mainAccountId: string = transactionBusinessId; - - if ( - !shouldFetchDocuments && - transaction.source_reference && - charge.business_id && - INTERNAL_WALLETS_IDS.includes(charge.business_id) - ) { - mainAccountId = await getFinancialAccountTaxCategoryId( - injector, - transaction, - currency, - true, - ); - } - - let amount = Number(transaction.amount); - let foreignAmount: number | undefined = undefined; - - if (currency !== DEFAULT_LOCAL_CURRENCY) { - // get exchange rate for currency - const exchangeRate = await injector - .get(ExchangeProvider) - .getExchangeRates(currency, DEFAULT_LOCAL_CURRENCY, valueDate); - - foreignAmount = amount; - // calculate amounts in ILS - amount = exchangeRate * amount; - } - - const accountTaxCategoryId = await getFinancialAccountTaxCategoryId( - injector, + const ledgerEntry = await ledgerEntryFromMainTransaction( transaction, - currency, - ); - - 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, - reference1: transaction.source_id, - isCreditorCounterparty, - ownerId: charge.owner_id, - currencyRate: transaction.currency_rate ? Number(transaction.currency_rate) : undefined, + injector, chargeId, - }; + charge.owner_id, + charge.business_id ?? undefined, + gotRelevantDocuments, + ).catch(e => { + if (e instanceof LedgerError) { + errors.add(e.message); + } else { + throw e; + } + }); + + if (!ledgerEntry) { + return; + } financialAccountLedgerEntries.push(ledgerEntry); updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); - dates.add(valueDate.getTime()); - currencies.add(currency); + dates.add(ledgerEntry.valueDate.getTime()); + currencies.add(ledgerEntry.currency); }); // create a ledger record for fee transactions const feeTransactionsPromises = feeTransactions.map(async transaction => { - await getEntriesFromFeeTransaction(transaction, charge, context).then(ledgerEntries => { - feeFinancialAccountLedgerEntries.push(...ledgerEntries); - ledgerEntries.map(ledgerEntry => { - updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); - dates.add(ledgerEntry.valueDate.getTime()); - currencies.add(ledgerEntry.currency); + await getEntriesFromFeeTransaction(transaction, charge, injector) + .then(ledgerEntries => { + feeFinancialAccountLedgerEntries.push(...ledgerEntries); + ledgerEntries.map(ledgerEntry => { + updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); + dates.add(ledgerEntry.valueDate.getTime()); + currencies.add(ledgerEntry.currency); + }); + }) + .catch(e => { + if (e instanceof LedgerError) { + errors.add(e.message); + } else { + throw e; + } }); - }); }); entriesPromises.push(...mainTransactionsPromises, ...feeTransactionsPromises); @@ -347,62 +209,25 @@ export const generateLedgerRecordsForCommonCharge: ResolverFn< // generate ledger from balance cancellation for (const balanceCancellation of balanceCancellations) { - const entityBalance = ledgerBalance.get(balanceCancellation.business_id); - if (!entityBalance) { - console.log( - `Balance cancellation for business ${balanceCancellation.business_id} redundant - already balanced`, - ); - continue; - } - - 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 GraphQLError( - `Balance cancellation for business ${balanceCancellation.business_id} failed - no financial account entry found`, + try { + const ledgerEntry = ledgerEntryFromBalanceCancellation( + balanceCancellation, + ledgerBalance, + financialAccountLedgerEntries, + chargeId, + charge.owner_id, ); - } - let foreignAmount: number | undefined = undefined; - - if ( - financialAccountEntry.currency !== DEFAULT_LOCAL_CURRENCY && - financialAccountEntry.currencyRate - ) { - foreignAmount = financialAccountEntry.currencyRate * amount; + miscLedgerEntries.push(ledgerEntry); + updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); + } catch (e) { + if (e instanceof LedgerError) { + errors.add(e.message); + continue; + } else { + throw e; + } } - - const isCreditorCounterparty = amount > 0; - - const ledgerEntry: LedgerProto = { - id: balanceCancellation.charge_id, - invoiceDate: financialAccountEntry.invoiceDate, - valueDate: financialAccountEntry.valueDate, - currency: financialAccountEntry.currency, - creditAccountID1: isCreditorCounterparty ? BALANCE_CANCELLATION_TAX_CATEGORY_ID : entityId, - creditAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined, - localCurrencyCreditAmount1: Math.abs(amount), - debitAccountID1: isCreditorCounterparty ? entityId : BALANCE_CANCELLATION_TAX_CATEGORY_ID, - debitAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined, - localCurrencyDebitAmount1: Math.abs(amount), - description: balanceCancellation.description ?? undefined, - reference1: financialAccountEntry.reference1, - isCreditorCounterparty, - ownerId: charge.owner_id, - currencyRate: financialAccountEntry.currencyRate, - chargeId, - }; - - miscLedgerEntries.push(ledgerEntry); - updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); } const allowedUnbalancedBusinesses = new Set( @@ -412,24 +237,49 @@ export const generateLedgerRecordsForCommonCharge: ResolverFn< // multiple currencies balance const foreignCurrencyCount = currencies.size - (currencies.has(DEFAULT_LOCAL_CURRENCY) ? 1 : 0); if (foreignCurrencyCount >= 2) { - const entries = multipleForeignCurrenciesBalanceEntries( - accountingLedgerEntries, - financialAccountLedgerEntries, - feeFinancialAccountLedgerEntries, - charge, - ); - for (const ledgerEntry of entries) { - miscLedgerEntries.push(ledgerEntry); - updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); + try { + const entries = multipleForeignCurrenciesBalanceEntries( + accountingLedgerEntries, + financialAccountLedgerEntries, + feeFinancialAccountLedgerEntries, + charge, + ); + for (const ledgerEntry of entries) { + miscLedgerEntries.push(ledgerEntry); + updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); + } + } catch (e) { + if (e instanceof LedgerError) { + errors.add(e.message); + } else { + throw e; + } } } // Add ledger completion entries const { balanceSum, isBalanced, unbalancedEntities, financialEntities } = await getLedgerBalanceInfo(injector, ledgerBalance, allowedUnbalancedBusinesses); + if (errors.size) { + const records = [ + ...financialAccountLedgerEntries, + ...feeFinancialAccountLedgerEntries, + ...accountingLedgerEntries, + ...miscLedgerEntries, + ]; + if (records.length) { + await storeInitialGeneratedRecords(charge, records, injector); + } + return { + records: ledgerProtoToRecordsConverter(records), + charge, + balance: { balanceSum, isBalanced, unbalancedEntities }, + errors: Array.from(errors), + }; + } if (Math.abs(balanceSum) > 0.005) { - throw new GraphQLError( - `Failed to balance: ${balanceSum} diff; ${unbalancedEntities.join(', ')} are unbalanced`, + errors.add( + `Failed to balance: ${formatStringifyAmount(balanceSum)} diff; ${unbalancedEntities.map(entity => entity.entityId).join(', ')} are not balanced`, ); } else if (!isBalanced) { // check if business doesn't require documents @@ -444,6 +294,7 @@ export const generateLedgerRecordsForCommonCharge: ResolverFn< records: ledgerProtoToRecordsConverter(records), charge, balance: { balanceSum, isBalanced, unbalancedEntities }, + errors: Array.from(errors), }; } } @@ -482,30 +333,30 @@ export const generateLedgerRecordsForCommonCharge: ResolverFn< } } - if (!exchangeRateTaxCategory) { - throw new GraphQLError( + if (exchangeRateTaxCategory) { + const ledgerEntry: StrictLedgerProto = { + id: transactionEntry.id + '|fee', // NOTE: this field is dummy + creditAccountID1: isCreditorCounterparty ? entityId : exchangeRateTaxCategory, + localCurrencyCreditAmount1: amount, + debitAccountID1: isCreditorCounterparty ? exchangeRateTaxCategory : entityId, + localCurrencyDebitAmount1: amount, + description: 'Exchange ledger record', + isCreditorCounterparty, + invoiceDate: documentEntry.invoiceDate, + valueDate: transactionEntry.valueDate, + currency: transactionEntry.currency, // NOTE: this field is dummy + ownerId: transactionEntry.ownerId, + chargeId, + }; + miscLedgerEntries.push(ledgerEntry); + updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); + } else { + errors.add( `Failed to locate tax category for exchange rate for business ID="${entityId}"`, ); } - - const ledgerEntry: StrictLedgerProto = { - id: transactionEntry.id + '|fee', // NOTE: this field is dummy - creditAccountID1: isCreditorCounterparty ? entityId : exchangeRateTaxCategory, - localCurrencyCreditAmount1: amount, - debitAccountID1: isCreditorCounterparty ? exchangeRateTaxCategory : entityId, - localCurrencyDebitAmount1: amount, - description: 'Exchange ledger record', - isCreditorCounterparty, - invoiceDate: documentEntry.invoiceDate, - valueDate: transactionEntry.valueDate, - currency: transactionEntry.currency, // NOTE: this field is dummy - ownerId: transactionEntry.ownerId, - chargeId, - }; - miscLedgerEntries.push(ledgerEntry); - updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); } else { - throw new GraphQLError( + errors.add( `Failed to balance: ${ hasMultipleDates ? 'Dates are different' : 'Dates are consistent' } and ${foreignCurrencyCount ? 'currencies are foreign' : 'currencies are local'}`, @@ -536,6 +387,7 @@ export const generateLedgerRecordsForCommonCharge: ResolverFn< records: ledgerProtoToRecordsConverter(records), charge, balance: ledgerBalanceInfo, + errors: Array.from(errors), }; } catch (e) { return { diff --git a/packages/server/src/modules/ledger/resolvers/ledger-generation/conversion-ledger-generation.resolver.ts b/packages/server/src/modules/ledger/resolvers/ledger-generation/conversion-ledger-generation.resolver.ts index d5310ead6..07ce5f3b7 100644 --- a/packages/server/src/modules/ledger/resolvers/ledger-generation/conversion-ledger-generation.resolver.ts +++ b/packages/server/src/modules/ledger/resolvers/ledger-generation/conversion-ledger-generation.resolver.ts @@ -1,4 +1,3 @@ -import { GraphQLError } from 'graphql'; import { Injector } from 'graphql-modules'; import { ExchangeProvider } from '@modules/exchange-rates/providers/exchange.provider.js'; import { storeInitialGeneratedRecords } from '@modules/ledger/helpers/ledgrer-storage.helper.js'; @@ -21,6 +20,7 @@ import { getFinancialAccountTaxCategoryId, getLedgerBalanceInfo, isTransactionsOppositeSign, + LedgerError, ledgerProtoToRecordsConverter, updateLedgerBalanceByEntry, validateTransactionBasicVariables, @@ -34,6 +34,8 @@ export const generateLedgerRecordsForConversion: ResolverFn< > = async (charge, _, { injector }) => { const chargeId = charge.id; + const errors: Set = new Set(); + try { // validate ledger records are balanced const ledgerBalance = new Map(); @@ -41,6 +43,7 @@ export const generateLedgerRecordsForConversion: ResolverFn< // generate ledger from transactions const mainFinancialAccountLedgerEntries: LedgerProto[] = []; const feeFinancialAccountLedgerEntries: LedgerProto[] = []; + const miscLedgerEntries: LedgerProto[] = []; let baseEntry: LedgerProto | undefined = undefined; let quoteEntry: LedgerProto | undefined = undefined; @@ -51,210 +54,254 @@ export const generateLedgerRecordsForConversion: ResolverFn< const { mainTransactions, feeTransactions } = splitFeeTransactions(transactions); if (mainTransactions.length !== 2) { - throw new GraphQLError(`Conversion Charge must include two main transactions`); - } - - if (!isTransactionsOppositeSign(mainTransactions)) { - throw new GraphQLError( - `Conversion Charge must include two main transactions with opposite sign`, - ); + errors.add(`Conversion Charge must include two main transactions`); } - // for each transaction, create a ledger record - for (const transaction of mainTransactions) { - const { currency, valueDate } = validateTransactionBasicVariables(transaction); - - let amount = Number(transaction.amount); - let foreignAmount: number | undefined = undefined; - - if (currency !== DEFAULT_LOCAL_CURRENCY) { - // get exchange rate for currency - const exchangeRate = await injector - .get(ExchangeProvider) - .getExchangeRates(currency, DEFAULT_LOCAL_CURRENCY, valueDate); - - foreignAmount = amount; - // calculate amounts in ILS - amount = exchangeRate * amount; + try { + if (!isTransactionsOppositeSign(mainTransactions)) { + errors.add(`Conversion Charge must include two main transactions with opposite sign`); } - - const accountTaxCategoryId = await getFinancialAccountTaxCategoryId( - injector, - transaction, - currency, - ); - - const isCreditorCounterparty = amount > 0; - - const ledgerEntry: LedgerProto = { - id: transaction.id, - invoiceDate: transaction.event_date, - valueDate, - currency, - ...(isCreditorCounterparty - ? { - debitAccountID1: accountTaxCategoryId, - } - : { - creditAccountID1: accountTaxCategoryId, - }), - creditAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined, - localCurrencyCreditAmount1: Math.abs(amount), - debitAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined, - localCurrencyDebitAmount1: Math.abs(amount), - description: transaction.source_description ?? undefined, - reference1: transaction.source_id, - isCreditorCounterparty, - ownerId: charge.owner_id, - currencyRate: transaction.currency_rate ? Number(transaction.currency_rate) : undefined, - chargeId, - }; - - if (amount < 0) { - baseEntry = ledgerEntry; - } else if (amount > 0) { - quoteEntry = ledgerEntry; + } catch (e) { + if (e instanceof LedgerError) { + errors.add(e.message); + } else { + throw e; } - - mainFinancialAccountLedgerEntries.push(ledgerEntry); - updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); } - if (!baseEntry || !quoteEntry) { - throw new GraphQLError(`Conversion Charge must include two main transactions`); - } - - // create a ledger record for fee transactions - for (const transaction of feeTransactions) { - if (!transaction.is_fee) { - continue; - } - - const isSupplementalFee = isSupplementalFeeTransaction(transaction); - const { currency, valueDate, transactionBusinessId } = - validateTransactionBasicVariables(transaction); - - let amount = Number(transaction.amount); - if (amount === 0) { - continue; - } - let foreignAmount: number | undefined = undefined; - - if (currency !== DEFAULT_LOCAL_CURRENCY) { - // get exchange rate for currency - const exchangeRate = await injector - .get(ExchangeProvider) - .getExchangeRates(currency, DEFAULT_LOCAL_CURRENCY, valueDate); - - foreignAmount = amount; - // calculate amounts in ILS - amount = exchangeRate * amount; - } - - const isCreditorCounterparty = amount > 0; + // for each transaction, create a ledger record + const mainTransactionsPromises = mainTransactions.map(async transaction => { + try { + const { currency, valueDate } = validateTransactionBasicVariables(transaction); + + let amount = Number(transaction.amount); + let foreignAmount: number | undefined = undefined; + + if (currency !== DEFAULT_LOCAL_CURRENCY) { + // get exchange rate for currency + const exchangeRate = await injector + .get(ExchangeProvider) + .getExchangeRates(currency, DEFAULT_LOCAL_CURRENCY, valueDate); + + foreignAmount = amount; + // calculate amounts in ILS + amount = exchangeRate * amount; + } - if (isSupplementalFee) { - const financialAccountTaxCategoryId = await getFinancialAccountTaxCategoryId( + const accountTaxCategoryId = await getFinancialAccountTaxCategoryId( injector, transaction, currency, ); - feeFinancialAccountLedgerEntries.push({ - id: transaction.id, - invoiceDate: transaction.event_date, - valueDate, - currency, - creditAccountID1: isCreditorCounterparty - ? FEE_TAX_CATEGORY_ID - : financialAccountTaxCategoryId, - creditAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined, - localCurrencyCreditAmount1: Math.abs(amount), - debitAccountID1: isCreditorCounterparty - ? financialAccountTaxCategoryId - : FEE_TAX_CATEGORY_ID, - debitAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined, - localCurrencyDebitAmount1: Math.abs(amount), - description: transaction.source_description ?? undefined, - reference1: transaction.source_id, - isCreditorCounterparty, - ownerId: charge.owner_id, - currencyRate: transaction.currency_rate ? Number(transaction.currency_rate) : undefined, - chargeId, - }); - } else { - const businessTaxCategory = quoteEntry.debitAccountID1!; - if (!businessTaxCategory) { - throw new GraphQLError( - `Quote ledger entry for charge ID=${chargeId} is missing Tax category`, - ); - } + const isCreditorCounterparty = amount > 0; - const ledgerEntry: StrictLedgerProto = { + const ledgerEntry: LedgerProto = { id: transaction.id, invoiceDate: transaction.event_date, valueDate, currency, - creditAccountID1: isCreditorCounterparty ? FEE_TAX_CATEGORY_ID : transactionBusinessId, + ...(isCreditorCounterparty + ? { + debitAccountID1: accountTaxCategoryId, + } + : { + creditAccountID1: accountTaxCategoryId, + }), creditAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined, localCurrencyCreditAmount1: Math.abs(amount), - debitAccountID1: isCreditorCounterparty ? transactionBusinessId : FEE_TAX_CATEGORY_ID, debitAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined, localCurrencyDebitAmount1: Math.abs(amount), description: transaction.source_description ?? undefined, reference1: transaction.source_id, - isCreditorCounterparty: !isCreditorCounterparty, + isCreditorCounterparty, ownerId: charge.owner_id, currencyRate: transaction.currency_rate ? Number(transaction.currency_rate) : undefined, chargeId, }; - feeFinancialAccountLedgerEntries.push(ledgerEntry); + mainFinancialAccountLedgerEntries.push(ledgerEntry); updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); + } catch (e) { + if (e instanceof LedgerError) { + errors.add(e.message); + } else { + throw e; + } + } + }); + + await Promise.all(mainTransactionsPromises); + + for (const entry of mainFinancialAccountLedgerEntries) { + if (entry.isCreditorCounterparty) { + quoteEntry = entry; + } else { + baseEntry = entry; } } - const miscLedgerEntries: LedgerProto[] = []; + if (!baseEntry || !quoteEntry) { + errors.add(`Conversion Charge must include two main transactions`); + } else { + // create a ledger record for fee transactions + for (const transaction of feeTransactions) { + if (!transaction.is_fee) { + continue; + } + + try { + const isSupplementalFee = isSupplementalFeeTransaction(transaction); + const { currency, valueDate, transactionBusinessId } = + validateTransactionBasicVariables(transaction); + + let amount = Number(transaction.amount); + if (amount === 0) { + continue; + } + let foreignAmount: number | undefined = undefined; + + if (currency !== DEFAULT_LOCAL_CURRENCY) { + // get exchange rate for currency + const exchangeRate = await injector + .get(ExchangeProvider) + .getExchangeRates(currency, DEFAULT_LOCAL_CURRENCY, valueDate); + + foreignAmount = amount; + // calculate amounts in ILS + amount = exchangeRate * amount; + } + + const isCreditorCounterparty = amount > 0; + + if (isSupplementalFee) { + const financialAccountTaxCategoryId = await getFinancialAccountTaxCategoryId( + injector, + transaction, + currency, + ); + + feeFinancialAccountLedgerEntries.push({ + id: transaction.id, + invoiceDate: transaction.event_date, + valueDate, + currency, + creditAccountID1: isCreditorCounterparty + ? FEE_TAX_CATEGORY_ID + : financialAccountTaxCategoryId, + creditAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined, + localCurrencyCreditAmount1: Math.abs(amount), + debitAccountID1: isCreditorCounterparty + ? financialAccountTaxCategoryId + : FEE_TAX_CATEGORY_ID, + debitAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined, + localCurrencyDebitAmount1: Math.abs(amount), + description: transaction.source_description ?? undefined, + reference1: transaction.source_id, + isCreditorCounterparty, + ownerId: charge.owner_id, + currencyRate: transaction.currency_rate + ? Number(transaction.currency_rate) + : undefined, + chargeId, + }); + } else { + const businessTaxCategory = quoteEntry.debitAccountID1; + if (!businessTaxCategory) { + throw new LedgerError( + `Quote ledger entry for charge ID=${chargeId} is missing Tax category`, + ); + } + + const ledgerEntry: StrictLedgerProto = { + id: transaction.id, + invoiceDate: transaction.event_date, + valueDate, + currency, + creditAccountID1: isCreditorCounterparty + ? FEE_TAX_CATEGORY_ID + : transactionBusinessId, + creditAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined, + localCurrencyCreditAmount1: Math.abs(amount), + debitAccountID1: isCreditorCounterparty ? transactionBusinessId : FEE_TAX_CATEGORY_ID, + debitAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined, + localCurrencyDebitAmount1: Math.abs(amount), + description: transaction.source_description ?? undefined, + reference1: transaction.source_id, + isCreditorCounterparty: !isCreditorCounterparty, + ownerId: charge.owner_id, + currencyRate: transaction.currency_rate + ? Number(transaction.currency_rate) + : undefined, + chargeId, + }; + + feeFinancialAccountLedgerEntries.push(ledgerEntry); + updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); + } + } catch (e) { + if (e instanceof LedgerError) { + errors.add(e.message); + } else { + throw e; + } + } + } + + // calculate conversion fee + const [quoteRate, baseRate] = await Promise.all( + [quoteEntry.currency, baseEntry.currency].map(currency => + injector + .get(ExchangeProvider) + .getExchangeRates(currency as Currency, DEFAULT_LOCAL_CURRENCY, baseEntry!.valueDate), + ), + ); + const toLocalRate = quoteRate; + const directRate = quoteRate / baseRate; + + try { + const conversionFee = conversionFeeCalculator( + baseEntry, + quoteEntry, + directRate, + toLocalRate, + ); - // calculate conversion fee - const [quoteRate, baseRate] = await Promise.all( - [quoteEntry.currency, baseEntry.currency].map(currency => - injector - .get(ExchangeProvider) - .getExchangeRates(currency as Currency, DEFAULT_LOCAL_CURRENCY, baseEntry!.valueDate), - ), - ); - const toLocalRate = quoteRate; - const directRate = quoteRate / baseRate; - const conversionFee = conversionFeeCalculator(baseEntry, quoteEntry, directRate, toLocalRate); - - if (conversionFee.localAmount !== 0) { - const isDebitConversion = conversionFee.localAmount >= 0; - - const ledgerEntry: LedgerProto = { - id: quoteEntry.id + '|fee', // NOTE: this field is dummy - creditAccountID1: isDebitConversion ? FEE_TAX_CATEGORY_ID : undefined, - creditAmount1: conversionFee.foreignAmount - ? Math.abs(conversionFee.foreignAmount) - : undefined, - localCurrencyCreditAmount1: Math.abs(conversionFee.localAmount), - debitAccountID1: isDebitConversion ? undefined : FEE_TAX_CATEGORY_ID, - debitAmount1: conversionFee.foreignAmount - ? Math.abs(conversionFee.foreignAmount) - : undefined, - localCurrencyDebitAmount1: Math.abs(conversionFee.localAmount), - description: 'Conversion fee', - isCreditorCounterparty: true, - invoiceDate: quoteEntry.invoiceDate, - valueDate: quoteEntry.valueDate, - currency: quoteEntry.currency, - reference1: quoteEntry.reference1, - ownerId: quoteEntry.ownerId, - chargeId, - }; - - miscLedgerEntries.push(ledgerEntry); - updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); + if (conversionFee.localAmount !== 0) { + const isDebitConversion = conversionFee.localAmount >= 0; + + const ledgerEntry: LedgerProto = { + id: quoteEntry.id + '|fee', // NOTE: this field is dummy + creditAccountID1: isDebitConversion ? FEE_TAX_CATEGORY_ID : undefined, + creditAmount1: conversionFee.foreignAmount + ? Math.abs(conversionFee.foreignAmount) + : undefined, + localCurrencyCreditAmount1: Math.abs(conversionFee.localAmount), + debitAccountID1: isDebitConversion ? undefined : FEE_TAX_CATEGORY_ID, + debitAmount1: conversionFee.foreignAmount + ? Math.abs(conversionFee.foreignAmount) + : undefined, + localCurrencyDebitAmount1: Math.abs(conversionFee.localAmount), + description: 'Conversion fee', + isCreditorCounterparty: true, + invoiceDate: quoteEntry.invoiceDate, + valueDate: quoteEntry.valueDate, + currency: quoteEntry.currency, + reference1: quoteEntry.reference1, + ownerId: quoteEntry.ownerId, + chargeId, + }; + + miscLedgerEntries.push(ledgerEntry); + updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); + } + } catch (e) { + if (e instanceof LedgerError) { + errors.add(e.message); + } else { + throw e; + } + } } const ledgerBalanceInfo = await getLedgerBalanceInfo(injector, ledgerBalance); @@ -270,6 +317,7 @@ export const generateLedgerRecordsForConversion: ResolverFn< records: ledgerProtoToRecordsConverter(records), charge, balance: ledgerBalanceInfo, + errors: Array.from(errors), }; } catch (e) { return { diff --git a/packages/server/src/modules/ledger/resolvers/ledger-generation/dividend-ledger-generation.resolver.ts b/packages/server/src/modules/ledger/resolvers/ledger-generation/dividend-ledger-generation.resolver.ts index ed8716ca9..871e2f1ae 100644 --- a/packages/server/src/modules/ledger/resolvers/ledger-generation/dividend-ledger-generation.resolver.ts +++ b/packages/server/src/modules/ledger/resolvers/ledger-generation/dividend-ledger-generation.resolver.ts @@ -1,4 +1,3 @@ -import { GraphQLError } from 'graphql'; import { DividendsProvider } from '@modules/dividends/providers/dividends.provider.js'; import { ExchangeProvider } from '@modules/exchange-rates/providers/exchange.provider.js'; import { storeInitialGeneratedRecords } from '@modules/ledger/helpers/ledgrer-storage.helper.js'; @@ -17,6 +16,7 @@ import { generatePartialLedgerEntry, getFinancialAccountTaxCategoryId, getLedgerBalanceInfo, + LedgerError, ledgerProtoToRecordsConverter, updateLedgerBalanceByEntry, validateTransactionRequiredVariables, @@ -27,9 +27,10 @@ export const generateLedgerRecordsForDividend: ResolverFn< ResolversParentTypes['Charge'], GraphQLModules.Context, object -> = async (charge, _, context) => { +> = async (charge, _, { injector }) => { const chargeId = charge.id; - const { injector } = context; + + const errors: Set = new Set(); try { // validate ledger records are balanced @@ -44,221 +45,266 @@ export const generateLedgerRecordsForDividend: ResolverFn< const transactions = await injector .get(TransactionsProvider) .getTransactionsByChargeIDLoader.load(chargeId); - const { withholdingTaxTransactions, paymentsTransactions, feeTransactions } = - splitDividendTransactions(transactions); + const { + withholdingTaxTransactions, + paymentsTransactions, + feeTransactions, + errors: splitErrors, + } = splitDividendTransactions(transactions); + + splitErrors.map(errors.add); // create a ledger record for tax deduction origin - for (const preValidatedTransaction of withholdingTaxTransactions) { - const transaction = validateTransactionRequiredVariables(preValidatedTransaction); - if (transaction.currency !== DEFAULT_LOCAL_CURRENCY) { - throw new GraphQLError( - `Withholding tax currency supposed to be local, got ${transaction.currency}`, - ); - } - - // get tax category - const financialAccountTaxCategoryId = await getFinancialAccountTaxCategoryId( - injector, - transaction, - ); - - // set main account for dividend - mainAccountId ||= financialAccountTaxCategoryId; - if (mainAccountId !== financialAccountTaxCategoryId) { - throw new GraphQLError(`Tax category is not consistent`); - } - - const partialEntry = generatePartialLedgerEntry(transaction, charge.owner_id, undefined); - const ledgerEntry: LedgerProto = { - ...partialEntry, - creditAccountID1: partialEntry.isCreditorCounterparty - ? transaction.business_id - : financialAccountTaxCategoryId, - debitAccountID1: partialEntry.isCreditorCounterparty - ? financialAccountTaxCategoryId - : transaction.business_id, - }; + const withholdingTaxTransactionsPromises = withholdingTaxTransactions.map( + async preValidatedTransaction => { + try { + const transaction = validateTransactionRequiredVariables(preValidatedTransaction); + if (transaction.currency !== DEFAULT_LOCAL_CURRENCY) { + errors.add( + `Withholding tax currency supposed to be local, got ${transaction.currency}`, + ); + } + + // get tax category + const financialAccountTaxCategoryId = await getFinancialAccountTaxCategoryId( + injector, + transaction, + ); - withholdingTaxLedgerEntries.push(ledgerEntry); - updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); - } + // set main account for dividend + mainAccountId ||= financialAccountTaxCategoryId; + if (mainAccountId !== financialAccountTaxCategoryId) { + errors.add(`Tax category is not consistent`); + } + + const partialEntry = generatePartialLedgerEntry(transaction, charge.owner_id, undefined); + const ledgerEntry: LedgerProto = { + ...partialEntry, + creditAccountID1: partialEntry.isCreditorCounterparty + ? transaction.business_id + : financialAccountTaxCategoryId, + debitAccountID1: partialEntry.isCreditorCounterparty + ? financialAccountTaxCategoryId + : transaction.business_id, + }; + + withholdingTaxLedgerEntries.push(ledgerEntry); + updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); + } catch (e) { + if (e instanceof LedgerError) { + errors.add(e.message); + } else { + throw e; + } + } + }, + ); - const dividendRecords = await injector + const dividendRecordsPromise = injector .get(DividendsProvider) .getDividendsByChargeIdLoader.load(chargeId); + const [dividendRecords] = await Promise.all([ + dividendRecordsPromise, + ...withholdingTaxTransactionsPromises, + ]); + const totalDividendSumMap = new Map(); const allowedUnbalancedBusinesses = new Set(); // create a ledger record for dividend payments - for (const preValidatedTransaction of paymentsTransactions) { - const dividendRecord = dividendRecords.find( - record => record.transaction_id === preValidatedTransaction.id, - ); - - // run validations - if (!dividendRecord) { - throw new GraphQLError( - `Transaction ID="${preValidatedTransaction.id}" is missing matching dividend record`, - ); - } - const transaction = validateTransactionRequiredVariables(preValidatedTransaction); - if (Number(transaction.amount) >= 0) { - throw new GraphQLError( - `Dividend transaction amount cannot be positive (ID: ${transaction.id})`, - ); - } - if (Number(dividendRecord.amount) <= 0) { - throw new GraphQLError(`Dividend amount is not positive (ID: ${dividendRecord.id})`); - } - if ( - charge.owner_id !== dividendRecord.owner_id || - transaction.debit_date.getTime() !== dividendRecord.date.getTime() - ) { - throw new GraphQLError( - `Transaction ID="${transaction.id}" is not matching dividend record ID="${dividendRecord.id}"`, - ); - } - - const withholdingTaxPercentage = dividendRecord.withholding_tax_percentage_override - ? Number(dividendRecord.withholding_tax_percentage_override) - : DIVIDEND_WITHHOLDING_TAX_PERCENTAGE; - - // generate closing ledger entry out of the dividend record - const dividendRecordAbsAmount = Math.abs(Number(dividendRecord.amount)); - const closingEntry: LedgerProto = { - id: dividendRecord.id, - invoiceDate: dividendRecord.date, - valueDate: dividendRecord.date, - currency: DEFAULT_LOCAL_CURRENCY, - isCreditorCounterparty: true, - ownerId: dividendRecord.owner_id, - creditAccountID1: dividendRecord.business_id, - localCurrencyCreditAmount1: dividendRecordAbsAmount, - localCurrencyDebitAmount1: dividendRecordAbsAmount, - chargeId, - }; + const paymentsLedgerEntriesPromises = paymentsTransactions.map( + async preValidatedTransaction => { + try { + const dividendRecord = dividendRecords.find( + record => record.transaction_id === preValidatedTransaction.id, + ); - paymentsLedgerEntries.push(closingEntry); - updateLedgerBalanceByEntry(closingEntry, ledgerBalance); - totalDividendSumMap.set( - dividendRecord.date.getTime(), - (totalDividendSumMap.get(dividendRecord.date.getTime()) ?? 0) + dividendRecordAbsAmount, - ); - - // preparations for core ledger entries - let exchangeRate: number | undefined = undefined; - if (transaction.currency !== DEFAULT_LOCAL_CURRENCY) { - // get exchange rate for currency - exchangeRate = await injector - .get(ExchangeProvider) - .getExchangeRates( - transaction.currency, - DEFAULT_LOCAL_CURRENCY, - transaction.debit_timestamp, + // run validations + if (!dividendRecord) { + throw new LedgerError( + `Transaction reference "${preValidatedTransaction.source_reference}" is missing matching dividend record`, + ); + } + const transaction = validateTransactionRequiredVariables(preValidatedTransaction); + if (Number(transaction.amount) >= 0) { + throw new LedgerError( + `Dividend transaction amount cannot be positive (reference: ${transaction.source_reference})`, + ); + } + if (Number(dividendRecord.amount) <= 0) { + throw new LedgerError(`Dividend amount is not positive (ID: ${dividendRecord.id})`); + } + if ( + charge.owner_id !== dividendRecord.owner_id || + transaction.debit_date.getTime() !== dividendRecord.date.getTime() + ) { + throw new LedgerError( + `Transaction reference "${transaction.source_reference}" is not matching dividend record ID="${dividendRecord.id}"`, + ); + } + + const withholdingTaxPercentage = dividendRecord.withholding_tax_percentage_override + ? Number(dividendRecord.withholding_tax_percentage_override) + : DIVIDEND_WITHHOLDING_TAX_PERCENTAGE; + + // generate closing ledger entry out of the dividend record + const dividendRecordAbsAmount = Math.abs(Number(dividendRecord.amount)); + const closingEntry: LedgerProto = { + id: dividendRecord.id, + invoiceDate: dividendRecord.date, + valueDate: dividendRecord.date, + currency: DEFAULT_LOCAL_CURRENCY, + isCreditorCounterparty: true, + ownerId: dividendRecord.owner_id, + creditAccountID1: dividendRecord.business_id, + localCurrencyCreditAmount1: dividendRecordAbsAmount, + localCurrencyDebitAmount1: dividendRecordAbsAmount, + chargeId, + }; + + paymentsLedgerEntries.push(closingEntry); + updateLedgerBalanceByEntry(closingEntry, ledgerBalance); + totalDividendSumMap.set( + dividendRecord.date.getTime(), + (totalDividendSumMap.get(dividendRecord.date.getTime()) ?? 0) + dividendRecordAbsAmount, ); - } - - const partialEntry = generatePartialLedgerEntry(transaction, charge.owner_id, exchangeRate); - - const isForeignCurrency = partialEntry.currency !== DEFAULT_LOCAL_CURRENCY; - const amountDiff = - partialEntry.localCurrencyCreditAmount1 - - Number(dividendRecord.amount) * (1 - withholdingTaxPercentage); - if (Math.abs(amountDiff) > 0.005) { - if (isForeignCurrency) { - allowedUnbalancedBusinesses.add(transaction.business_id); - } else { - throw new GraphQLError( - `Transaction ID="${transaction.id}" and dividend record ID="${dividendRecord.id}" amounts mismatch`, + + // preparations for core ledger entries + let exchangeRate: number | undefined = undefined; + if (transaction.currency !== DEFAULT_LOCAL_CURRENCY) { + // get exchange rate for currency + exchangeRate = await injector + .get(ExchangeProvider) + .getExchangeRates( + transaction.currency, + DEFAULT_LOCAL_CURRENCY, + transaction.debit_timestamp, + ); + } + + const partialEntry = generatePartialLedgerEntry( + transaction, + charge.owner_id, + exchangeRate, ); + + const isForeignCurrency = partialEntry.currency !== DEFAULT_LOCAL_CURRENCY; + const amountDiff = + partialEntry.localCurrencyCreditAmount1 - + Number(dividendRecord.amount) * (1 - withholdingTaxPercentage); + if (Math.abs(amountDiff) > 0.005) { + if (isForeignCurrency) { + allowedUnbalancedBusinesses.add(transaction.business_id); + } else { + throw new LedgerError( + `Transaction reference "${transaction.source_reference}" and dividend record ID="${dividendRecord.id}" amounts mismatch`, + ); + } + } + + // generate core ledger entries + let foreignAccountTaxCategoryId: string | undefined = undefined; + if (isForeignCurrency) { + foreignAccountTaxCategoryId = await getFinancialAccountTaxCategoryId( + injector, + transaction, + ); + + const coreLedgerEntry: LedgerProto = { + id: dividendRecord.id, + invoiceDate: dividendRecord.date, + valueDate: dividendRecord.date, + currency: DEFAULT_LOCAL_CURRENCY, + description: 'נ20', + isCreditorCounterparty: false, + ownerId: dividendRecord.owner_id, + debitAccountID1: dividendRecord.business_id, + localCurrencyDebitAmount1: dividendRecordAbsAmount, + creditAccountID1: mainAccountId, + localCurrencyCreditAmount1: dividendRecordAbsAmount * (1 - withholdingTaxPercentage), + creditAccountID2: DIVIDEND_WITHHOLDING_TAX_BUSINESS_ID, + localCurrencyCreditAmount2: dividendRecordAbsAmount * withholdingTaxPercentage, + chargeId, + }; + + paymentsLedgerEntries.push(coreLedgerEntry); + updateLedgerBalanceByEntry(coreLedgerEntry, ledgerBalance); + + // create conversion ledger entries + const conversionEntry1: LedgerProto = { + ...partialEntry, + isCreditorCounterparty: false, + creditAccountID1: foreignAccountTaxCategoryId, + debitAccountID1: transaction.business_id, + }; + + paymentsLedgerEntries.push(conversionEntry1); + updateLedgerBalanceByEntry(conversionEntry1, ledgerBalance); + + const conversionEntry2: LedgerProto = { + id: dividendRecord.id, + invoiceDate: dividendRecord.date, + valueDate: dividendRecord.date, + description: 'Conversion entry', + isCreditorCounterparty: true, + ownerId: dividendRecord.owner_id, + currency: DEFAULT_LOCAL_CURRENCY, + creditAccountID1: dividendRecord.business_id, + localCurrencyCreditAmount1: dividendRecordAbsAmount * (1 - withholdingTaxPercentage), + debitAccountID1: mainAccountId, + localCurrencyDebitAmount1: dividendRecordAbsAmount * (1 - withholdingTaxPercentage), + chargeId, + }; + + paymentsLedgerEntries.push(conversionEntry2); + updateLedgerBalanceByEntry(conversionEntry2, ledgerBalance); + } else { + const coreLedgerEntry: LedgerProto = { + ...partialEntry, + isCreditorCounterparty: false, + description: 'נ20', + debitAccountID1: transaction.business_id, + localCurrencyDebitAmount1: dividendRecordAbsAmount, + creditAccountID1: mainAccountId, + localCurrencyCreditAmount1: dividendRecordAbsAmount * (1 - withholdingTaxPercentage), + creditAccountID2: DIVIDEND_WITHHOLDING_TAX_BUSINESS_ID, + localCurrencyCreditAmount2: dividendRecordAbsAmount * withholdingTaxPercentage, + }; + + paymentsLedgerEntries.push(coreLedgerEntry); + updateLedgerBalanceByEntry(coreLedgerEntry, ledgerBalance); + } + } catch (e) { + if (e instanceof LedgerError) { + errors.add(e.message); + } else { + throw e; + } } - } - - // generate core ledger entries - let foreignAccountTaxCategoryId: string | undefined = undefined; - if (isForeignCurrency) { - foreignAccountTaxCategoryId = await getFinancialAccountTaxCategoryId(injector, transaction); - - const coreLedgerEntry: LedgerProto = { - id: dividendRecord.id, - invoiceDate: dividendRecord.date, - valueDate: dividendRecord.date, - currency: DEFAULT_LOCAL_CURRENCY, - description: 'נ20', - isCreditorCounterparty: false, - ownerId: dividendRecord.owner_id, - debitAccountID1: dividendRecord.business_id, - localCurrencyDebitAmount1: dividendRecordAbsAmount, - creditAccountID1: mainAccountId, - localCurrencyCreditAmount1: dividendRecordAbsAmount * (1 - withholdingTaxPercentage), - creditAccountID2: DIVIDEND_WITHHOLDING_TAX_BUSINESS_ID, - localCurrencyCreditAmount2: dividendRecordAbsAmount * withholdingTaxPercentage, - chargeId, - }; - - paymentsLedgerEntries.push(coreLedgerEntry); - updateLedgerBalanceByEntry(coreLedgerEntry, ledgerBalance); - - // create conversion ledger entries - const conversionEntry1: LedgerProto = { - ...partialEntry, - isCreditorCounterparty: false, - creditAccountID1: foreignAccountTaxCategoryId, - debitAccountID1: transaction.business_id, - }; - - paymentsLedgerEntries.push(conversionEntry1); - updateLedgerBalanceByEntry(conversionEntry1, ledgerBalance); - - const conversionEntry2: LedgerProto = { - id: dividendRecord.id, - invoiceDate: dividendRecord.date, - valueDate: dividendRecord.date, - description: 'Conversion entry', - isCreditorCounterparty: true, - ownerId: dividendRecord.owner_id, - currency: DEFAULT_LOCAL_CURRENCY, - creditAccountID1: dividendRecord.business_id, - localCurrencyCreditAmount1: dividendRecordAbsAmount * (1 - withholdingTaxPercentage), - debitAccountID1: mainAccountId, - localCurrencyDebitAmount1: dividendRecordAbsAmount * (1 - withholdingTaxPercentage), - chargeId, - }; - - paymentsLedgerEntries.push(conversionEntry2); - updateLedgerBalanceByEntry(conversionEntry2, ledgerBalance); - } else { - const coreLedgerEntry: LedgerProto = { - ...partialEntry, - isCreditorCounterparty: false, - description: 'נ20', - debitAccountID1: transaction.business_id, - localCurrencyDebitAmount1: dividendRecordAbsAmount, - creditAccountID1: mainAccountId, - localCurrencyCreditAmount1: dividendRecordAbsAmount * (1 - withholdingTaxPercentage), - creditAccountID2: DIVIDEND_WITHHOLDING_TAX_BUSINESS_ID, - localCurrencyCreditAmount2: dividendRecordAbsAmount * withholdingTaxPercentage, - }; - - paymentsLedgerEntries.push(coreLedgerEntry); - updateLedgerBalanceByEntry(coreLedgerEntry, ledgerBalance); - } - } + }, + ); // create a ledger record for fee transactions - const entriesPromises: Array> = []; const feeFinancialAccountLedgerEntries: LedgerProto[] = []; const feeTransactionsPromises = feeTransactions.map(async transaction => { - await getEntriesFromFeeTransaction(transaction, charge, context).then(ledgerEntries => { - feeFinancialAccountLedgerEntries.push(...ledgerEntries); - ledgerEntries.map(ledgerEntry => { - updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); + await getEntriesFromFeeTransaction(transaction, charge, injector) + .then(ledgerEntries => { + feeFinancialAccountLedgerEntries.push(...ledgerEntries); + ledgerEntries.map(ledgerEntry => { + updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); + }); + }) + .catch(e => { + if (e instanceof LedgerError) { + errors.add(e.message); + } else { + throw e; + } }); - }); }); - entriesPromises.push(...feeTransactionsPromises); + const entriesPromises = [...feeTransactionsPromises, ...paymentsLedgerEntriesPromises]; await Promise.all(entriesPromises); // create ledger entry for summary dividend tax category @@ -297,6 +343,7 @@ export const generateLedgerRecordsForDividend: ResolverFn< records: ledgerProtoToRecordsConverter(records), charge, balance: ledgerBalanceInfo, + errors: Array.from(errors), }; } catch (e) { return { diff --git a/packages/server/src/modules/ledger/resolvers/ledger-generation/internal-transfer-ledger-generation.resolver.ts b/packages/server/src/modules/ledger/resolvers/ledger-generation/internal-transfer-ledger-generation.resolver.ts index 33e6c7774..2b020a556 100644 --- a/packages/server/src/modules/ledger/resolvers/ledger-generation/internal-transfer-ledger-generation.resolver.ts +++ b/packages/server/src/modules/ledger/resolvers/ledger-generation/internal-transfer-ledger-generation.resolver.ts @@ -1,4 +1,3 @@ -import { GraphQLError } from 'graphql'; import { Injector } from 'graphql-modules'; import { ExchangeProvider } from '@modules/exchange-rates/providers/exchange.provider.js'; import { TaxCategoriesProvider } from '@modules/financial-entities/providers/tax-categories.provider.js'; @@ -20,6 +19,7 @@ import { getFinancialAccountTaxCategoryId, getLedgerBalanceInfo, isTransactionsOppositeSign, + LedgerError, ledgerProtoToRecordsConverter, updateLedgerBalanceByEntry, validateTransactionBasicVariables, @@ -33,6 +33,8 @@ export const generateLedgerRecordsForInternalTransfer: ResolverFn< > = async (charge, _, { injector }) => { const chargeId = charge.id; + const errors: Set = new Set(); + try { // validate ledger records are balanced const ledgerBalance = new Map(); @@ -53,132 +55,66 @@ export const generateLedgerRecordsForInternalTransfer: ResolverFn< const { mainTransactions, feeTransactions } = splitFeeTransactions(transactions); if (mainTransactions.length !== 2) { - throw new GraphQLError(`Internal transfer Charge must include two main transactions`); - } - - if (!isTransactionsOppositeSign(mainTransactions)) { - throw new GraphQLError( - `Internal transfer Charge must include two main transactions with opposite sign`, - ); + errors.add(`Internal transfer Charge must include two main transactions`); } - // create a ledger record for main transactions - for (const transaction of mainTransactions) { - const { currency, valueDate } = validateTransactionBasicVariables(transaction); - - let amount = Number(transaction.amount); - let foreignAmount: number | undefined = undefined; - - if (currency !== DEFAULT_LOCAL_CURRENCY) { - // get exchange rate for currency - const exchangeRate = await injector - .get(ExchangeProvider) - .getExchangeRates(currency, DEFAULT_LOCAL_CURRENCY, valueDate); - - foreignAmount = amount; - // calculate amounts in ILS - amount = exchangeRate * amount; + try { + if (!isTransactionsOppositeSign(mainTransactions)) { + errors.add( + `Internal transfer Charge must include two main transactions with opposite sign`, + ); } - - const financialAccountTaxCategoryId = await getFinancialAccountTaxCategoryId( - injector, - transaction, - currency, - ); - - const isCreditorCounterparty = amount > 0; - - const ledgerEntry: LedgerProto = { - id: transaction.id, - invoiceDate: transaction.event_date, - valueDate, - currency, - ...(isCreditorCounterparty - ? { - debitAccountID1: financialAccountTaxCategoryId, - } - : { - creditAccountID1: financialAccountTaxCategoryId, - }), - debitAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined, - localCurrencyDebitAmount1: Math.abs(amount), - creditAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined, - localCurrencyCreditAmount1: Math.abs(amount), - description: transaction.source_description ?? undefined, - reference1: transaction.source_id, - isCreditorCounterparty, - ownerId: charge.owner_id, - currencyRate: transaction.currency_rate ? Number(transaction.currency_rate) : undefined, - chargeId, - }; - - if (amount < 0) { - originEntry = ledgerEntry; - } else if (amount > 0) { - destinationEntry = ledgerEntry; + } catch (e) { + if (e instanceof LedgerError) { + errors.add(e.message); + } else { + throw e; } - - mainFinancialAccountLedgerEntries.push(ledgerEntry); - updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); - dates.add(valueDate.getTime()); - currencies.add(currency); - } - - if (!originEntry || !destinationEntry) { - throw new GraphQLError(`Internal transfer Charge must include two main transactions`); } - // create a ledger record for fee transactions - for (const transaction of feeTransactions) { - if (!transaction.is_fee) { - continue; - } - - const isSupplementalFee = isSupplementalFeeTransaction(transaction); - const { currency, valueDate, transactionBusinessId } = - validateTransactionBasicVariables(transaction); - - let amount = Number(transaction.amount); - if (amount === 0) { - continue; - } - let foreignAmount: number | undefined = undefined; - - if (currency !== DEFAULT_LOCAL_CURRENCY) { - // get exchange rate for currency - const exchangeRate = await injector - .get(ExchangeProvider) - .getExchangeRates(currency, DEFAULT_LOCAL_CURRENCY, valueDate); - - foreignAmount = amount; - // calculate amounts in ILS - amount = exchangeRate * amount; - } - - const isCreditorCounterparty = amount > 0; + // create a ledger record for main transactions + const mainFinancialAccountLedgerEntriesPromises = mainTransactions.map(async transaction => { + try { + const { currency, valueDate } = validateTransactionBasicVariables(transaction); + + let amount = Number(transaction.amount); + let foreignAmount: number | undefined = undefined; + + if (currency !== DEFAULT_LOCAL_CURRENCY) { + // get exchange rate for currency + const exchangeRate = await injector + .get(ExchangeProvider) + .getExchangeRates(currency, DEFAULT_LOCAL_CURRENCY, valueDate); + + foreignAmount = amount; + // calculate amounts in ILS + amount = exchangeRate * amount; + } - if (isSupplementalFee) { const financialAccountTaxCategoryId = await getFinancialAccountTaxCategoryId( injector, transaction, currency, ); - const ledgerEntry: StrictLedgerProto = { + const isCreditorCounterparty = amount > 0; + + const ledgerEntry: LedgerProto = { id: transaction.id, invoiceDate: transaction.event_date, valueDate, currency, - creditAccountID1: isCreditorCounterparty - ? FEE_TAX_CATEGORY_ID - : financialAccountTaxCategoryId, - creditAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined, - localCurrencyCreditAmount1: Math.abs(amount), - debitAccountID1: isCreditorCounterparty - ? financialAccountTaxCategoryId - : FEE_TAX_CATEGORY_ID, + ...(isCreditorCounterparty + ? { + debitAccountID1: financialAccountTaxCategoryId, + } + : { + creditAccountID1: financialAccountTaxCategoryId, + }), debitAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined, localCurrencyDebitAmount1: Math.abs(amount), + creditAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined, + localCurrencyCreditAmount1: Math.abs(amount), description: transaction.source_description ?? undefined, reference1: transaction.source_id, isCreditorCounterparty, @@ -187,50 +123,145 @@ export const generateLedgerRecordsForInternalTransfer: ResolverFn< chargeId, }; - feeFinancialAccountLedgerEntries.push(ledgerEntry); + mainFinancialAccountLedgerEntries.push(ledgerEntry); updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); - } else { - const businessTaxCategory = await injector - .get(TaxCategoriesProvider) - .taxCategoryByBusinessAndOwnerIDsLoader.load({ - businessId: transactionBusinessId, + dates.add(valueDate.getTime()); + currencies.add(currency); + } catch (e) { + if (e instanceof LedgerError) { + errors.add(e.message); + } else { + throw e; + } + } + }); + + // create a ledger record for fee transactions + const feeFinancialAccountLedgerEntriesPromises = feeTransactions.map(async transaction => { + if (!transaction.is_fee) { + return; + } + + try { + const isSupplementalFee = isSupplementalFeeTransaction(transaction); + const { currency, valueDate, transactionBusinessId } = + validateTransactionBasicVariables(transaction); + + let amount = Number(transaction.amount); + if (amount === 0) { + return; + } + let foreignAmount: number | undefined = undefined; + + if (currency !== DEFAULT_LOCAL_CURRENCY) { + // get exchange rate for currency + const exchangeRate = await injector + .get(ExchangeProvider) + .getExchangeRates(currency, DEFAULT_LOCAL_CURRENCY, valueDate); + + foreignAmount = amount; + // calculate amounts in ILS + amount = exchangeRate * amount; + } + + const isCreditorCounterparty = amount > 0; + + if (isSupplementalFee) { + const financialAccountTaxCategoryId = await getFinancialAccountTaxCategoryId( + injector, + transaction, + currency, + ); + + const ledgerEntry: StrictLedgerProto = { + id: transaction.id, + invoiceDate: transaction.event_date, + valueDate, + currency, + creditAccountID1: isCreditorCounterparty + ? FEE_TAX_CATEGORY_ID + : financialAccountTaxCategoryId, + creditAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined, + localCurrencyCreditAmount1: Math.abs(amount), + debitAccountID1: isCreditorCounterparty + ? financialAccountTaxCategoryId + : FEE_TAX_CATEGORY_ID, + debitAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined, + localCurrencyDebitAmount1: Math.abs(amount), + description: transaction.source_description ?? undefined, + reference1: transaction.source_id, + isCreditorCounterparty, + ownerId: charge.owner_id, + currencyRate: transaction.currency_rate ? Number(transaction.currency_rate) : undefined, + chargeId, + }; + + feeFinancialAccountLedgerEntries.push(ledgerEntry); + updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); + } else { + const businessTaxCategory = await injector + .get(TaxCategoriesProvider) + .taxCategoryByBusinessAndOwnerIDsLoader.load({ + businessId: transactionBusinessId, + ownerId: charge.owner_id, + }); + if (!businessTaxCategory) { + throw new LedgerError(`Business ID="${transactionBusinessId}" is missing tax category`); + } + + const ledgerEntry: LedgerProto = { + id: transaction.id, + invoiceDate: transaction.event_date, + valueDate, + currency, + creditAccountID1: isCreditorCounterparty ? businessTaxCategory.id : undefined, + creditAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined, + localCurrencyCreditAmount1: Math.abs(amount), + debitAccountID1: isCreditorCounterparty ? undefined : businessTaxCategory.id, + debitAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined, + localCurrencyDebitAmount1: Math.abs(amount), + description: transaction.source_description ?? undefined, + reference1: transaction.source_id, + isCreditorCounterparty: !isCreditorCounterparty, ownerId: charge.owner_id, - }); - if (!businessTaxCategory) { - throw new GraphQLError(`Business ID="${transactionBusinessId}" is missing tax category`); + currencyRate: transaction.currency_rate ? Number(transaction.currency_rate) : undefined, + chargeId, + }; + + feeFinancialAccountLedgerEntries.push(ledgerEntry); + updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); + dates.add(valueDate.getTime()); + currencies.add(currency); + } + } catch (e) { + if (e instanceof LedgerError) { + errors.add(e.message); + } else { + throw e; } + } + }); - const ledgerEntry: LedgerProto = { - id: transaction.id, - invoiceDate: transaction.event_date, - valueDate, - currency, - creditAccountID1: isCreditorCounterparty ? businessTaxCategory.id : undefined, - creditAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined, - localCurrencyCreditAmount1: Math.abs(amount), - debitAccountID1: isCreditorCounterparty ? undefined : businessTaxCategory.id, - debitAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined, - localCurrencyDebitAmount1: Math.abs(amount), - description: transaction.source_description ?? undefined, - reference1: transaction.source_id, - isCreditorCounterparty: !isCreditorCounterparty, - ownerId: charge.owner_id, - currencyRate: transaction.currency_rate ? Number(transaction.currency_rate) : undefined, - chargeId, - }; + await Promise.all([ + ...feeFinancialAccountLedgerEntriesPromises, + ...mainFinancialAccountLedgerEntriesPromises, + ]); - feeFinancialAccountLedgerEntries.push(ledgerEntry); - updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); - dates.add(valueDate.getTime()); - currencies.add(currency); + for (const ledgerEntry of mainFinancialAccountLedgerEntries) { + if (ledgerEntry.isCreditorCounterparty) { + destinationEntry = ledgerEntry; + } else { + originEntry = ledgerEntry; } } const { balanceSum } = await getLedgerBalanceInfo(injector, ledgerBalance); - const miscLedgerEntries: LedgerProto[] = []; - // Add ledger completion entries - if (Math.abs(balanceSum) > 0.005) { + + if (!originEntry || !destinationEntry) { + errors.add(`Internal transfer Charge must include two main transactions`); + } else if (Math.abs(balanceSum) > 0.005) { + // Add ledger completion entries const hasMultipleDates = dates.size > 1; const hasForeignCurrency = currencies.size > (currencies.has(DEFAULT_LOCAL_CURRENCY) ? 1 : 0); if (hasMultipleDates && hasForeignCurrency) { @@ -273,6 +304,7 @@ export const generateLedgerRecordsForInternalTransfer: ResolverFn< records: ledgerProtoToRecordsConverter(records), charge, balance: ledgerBalanceInfo, + errors: Array.from(errors), }; } catch (e) { return { diff --git a/packages/server/src/modules/ledger/resolvers/ledger-generation/monthly-vat-ledger-generation.resolver.ts b/packages/server/src/modules/ledger/resolvers/ledger-generation/monthly-vat-ledger-generation.resolver.ts index b8a1eabc3..0d0dc4863 100644 --- a/packages/server/src/modules/ledger/resolvers/ledger-generation/monthly-vat-ledger-generation.resolver.ts +++ b/packages/server/src/modules/ledger/resolvers/ledger-generation/monthly-vat-ledger-generation.resolver.ts @@ -1,5 +1,4 @@ import { format } from 'date-fns'; -import { GraphQLError } from 'graphql'; import { ExchangeProvider } from '@modules/exchange-rates/providers/exchange.provider.js'; import { TaxCategoriesProvider } from '@modules/financial-entities/providers/tax-categories.provider.js'; import { storeInitialGeneratedRecords } from '@modules/ledger/helpers/ledgrer-storage.helper.js'; @@ -21,6 +20,7 @@ import { generatePartialLedgerEntry, getFinancialAccountTaxCategoryId, getLedgerBalanceInfo, + LedgerError, ledgerProtoToRecordsConverter, updateLedgerBalanceByEntry, validateTransactionRequiredVariables, @@ -35,21 +35,25 @@ export const generateLedgerRecordsForMonthlyVat: ResolverFn< const { injector } = context; const chargeId = charge.id; + const errors: Set = new Set(); + try { // figure out VAT month const transactionDate = charge.transactions_min_debit_date ?? charge.transactions_min_event_date ?? undefined; if (!charge.user_description) { - throw new GraphQLError( + errors.add( `Monthly VAT charge must have description that indicates it's month (ID="${chargeId}")`, ); } - const vatDate = getMonthFromDescription(charge.user_description, transactionDate); + const vatDate = charge.user_description + ? getMonthFromDescription(charge.user_description, transactionDate) + : null; if (!vatDate) { - throw new GraphQLError(`Cannot extract charge ID="${chargeId}" VAT month`); + errors.add(`Cannot extract charge ID="${chargeId}" VAT month`); } - const [year, month] = vatDate.split('-').map(Number); + const [year, month] = (vatDate ?? format(new Date(), 'yyyy-MM')).split('-').map(Number); const ledgerDate = new Date(year, month, 0); const fromDate = format(new Date(year, month - 1, 1), 'yyyy-MM-dd') as TimelessDateString; const toDate = format(ledgerDate, 'yyyy-MM-dd') as TimelessDateString; @@ -81,144 +85,156 @@ export const generateLedgerRecordsForMonthlyVat: ResolverFn< outputsVatTaxCategoryPromise, ]); + const accountingLedgerEntries: LedgerProto[] = []; + const financialAccountLedgerEntries: LedgerProto[] = []; + const ledgerBalance = new Map(); + if (!inputsVatTaxCategory || !outputsVatTaxCategory) { - throw new GraphQLError(`Missing some of the VAT tax categories`); - } + errors.add(`Missing some of the VAT tax categories`); + } else { + const [incomeVat, roundedIncomeVat] = getVatDataFromVatReportRecords( + income as RawVatReportRecord[], + ); + const [expensesVat, roundedExpensesVat] = getVatDataFromVatReportRecords( + expenses as RawVatReportRecord[], + ); - const [incomeVat, roundedIncomeVat] = getVatDataFromVatReportRecords( - income as RawVatReportRecord[], - ); - const [expensesVat, roundedExpensesVat] = getVatDataFromVatReportRecords( - expenses as RawVatReportRecord[], - ); + // validate ledger records are balanced + ledgerBalance.set(outputsVatTaxCategory.id, { + amount: incomeVat, + entityId: outputsVatTaxCategory.id, + }); + ledgerBalance.set(inputsVatTaxCategory.id, { + amount: expensesVat * -1, + entityId: inputsVatTaxCategory.id, + }); - // validate ledger records are balanced - const ledgerBalance = new Map(); - ledgerBalance.set(outputsVatTaxCategory.id, { - amount: incomeVat, - entityId: outputsVatTaxCategory.id, - }); - ledgerBalance.set(inputsVatTaxCategory.id, { - amount: expensesVat * -1, - entityId: inputsVatTaxCategory.id, - }); + // for each transaction, create a ledger record + const mainTransactionsPromises = transactions.map(async preValidatedTransaction => { + try { + const transaction = validateTransactionRequiredVariables(preValidatedTransaction); + if (transaction.currency !== DEFAULT_LOCAL_CURRENCY) { + throw new LedgerError( + `Monthly VAT currency supposed to be local, got ${transaction.currency}`, + ); + } - const accountingLedgerEntries: LedgerProto[] = []; - const financialAccountLedgerEntries: LedgerProto[] = []; + // preparations for core ledger entries + let exchangeRate: number | undefined = undefined; + if (transaction.currency !== DEFAULT_LOCAL_CURRENCY) { + // get exchange rate for currency + exchangeRate = await injector + .get(ExchangeProvider) + .getExchangeRates( + transaction.currency, + DEFAULT_LOCAL_CURRENCY, + transaction.debit_timestamp, + ); + } - // for each transaction, create a ledger record - const mainTransactionsPromises = transactions.map(async preValidatedTransaction => { - const transaction = validateTransactionRequiredVariables(preValidatedTransaction); - if (transaction.currency !== DEFAULT_LOCAL_CURRENCY) { - throw new GraphQLError( - `Monthly VAT currency supposed to be local, got ${transaction.currency}`, - ); - } + const partialEntry = generatePartialLedgerEntry( + transaction, + charge.owner_id, + exchangeRate, + ); - // preparations for core ledger entries - let exchangeRate: number | undefined = undefined; - if (transaction.currency !== DEFAULT_LOCAL_CURRENCY) { - // get exchange rate for currency - exchangeRate = await injector - .get(ExchangeProvider) - .getExchangeRates( - transaction.currency, + const financialAccountTaxCategoryId = await getFinancialAccountTaxCategoryId( + injector, + transaction, DEFAULT_LOCAL_CURRENCY, - transaction.debit_timestamp, ); - } - const partialEntry = generatePartialLedgerEntry(transaction, charge.owner_id, exchangeRate); + const ledgerEntry: LedgerProto = { + ...partialEntry, + ...(partialEntry.isCreditorCounterparty + ? { + creditAccountID1: VAT_BUSINESS_ID, + debitAccountID1: financialAccountTaxCategoryId, + } + : { + creditAccountID1: financialAccountTaxCategoryId, + debitAccountID1: VAT_BUSINESS_ID, + }), + }; - const financialAccountTaxCategoryId = await getFinancialAccountTaxCategoryId( - injector, - transaction, - DEFAULT_LOCAL_CURRENCY, - ); + financialAccountLedgerEntries.push(ledgerEntry); + updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); + } catch (e) { + if (e instanceof LedgerError) { + errors.add(e.message); + } else { + throw e; + } + } + }); - const ledgerEntry: LedgerProto = { - ...partialEntry, - ...(partialEntry.isCreditorCounterparty - ? { - creditAccountID1: VAT_BUSINESS_ID, - debitAccountID1: financialAccountTaxCategoryId, - } - : { - creditAccountID1: financialAccountTaxCategoryId, - debitAccountID1: VAT_BUSINESS_ID, - }), - }; - - financialAccountLedgerEntries.push(ledgerEntry); - updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); - }); - - await Promise.all(mainTransactionsPromises); - - const accountingLedgerMaterials: Array<{ - amount: number; - taxCategoryId: string; - counterpartyId?: string; - }> = []; - accountingLedgerMaterials.push( - { - amount: roundedIncomeVat, - taxCategoryId: outputsVatTaxCategory.id, - counterpartyId: VAT_BUSINESS_ID, - }, - { - amount: roundedExpensesVat * -1, - taxCategoryId: inputsVatTaxCategory.id, - counterpartyId: VAT_BUSINESS_ID, - }, - ); + await Promise.all(mainTransactionsPromises); - const roundedIncomeVatDiff = Math.round((roundedIncomeVat - incomeVat) * 100) / 100; - if (roundedIncomeVatDiff) { - accountingLedgerMaterials.push({ - amount: roundedIncomeVatDiff * -1, - taxCategoryId: outputsVatTaxCategory.id, - counterpartyId: BALANCE_CANCELLATION_TAX_CATEGORY_ID, - }); - } + const accountingLedgerMaterials: Array<{ + amount: number; + taxCategoryId: string; + counterpartyId?: string; + }> = []; + accountingLedgerMaterials.push( + { + amount: roundedIncomeVat, + taxCategoryId: outputsVatTaxCategory.id, + counterpartyId: VAT_BUSINESS_ID, + }, + { + amount: roundedExpensesVat * -1, + taxCategoryId: inputsVatTaxCategory.id, + counterpartyId: VAT_BUSINESS_ID, + }, + ); - const roundedExpensesVatDiff = Math.round((roundedExpensesVat - expensesVat) * 100) / 100; - if (roundedExpensesVatDiff) { - accountingLedgerMaterials.push({ - amount: roundedExpensesVatDiff, - taxCategoryId: inputsVatTaxCategory.id, - counterpartyId: BALANCE_CANCELLATION_TAX_CATEGORY_ID, - }); - } + const roundedIncomeVatDiff = Math.round((roundedIncomeVat - incomeVat) * 100) / 100; + if (roundedIncomeVatDiff) { + accountingLedgerMaterials.push({ + amount: roundedIncomeVatDiff * -1, + taxCategoryId: outputsVatTaxCategory.id, + counterpartyId: BALANCE_CANCELLATION_TAX_CATEGORY_ID, + }); + } - accountingLedgerMaterials.map(({ amount, taxCategoryId, counterpartyId }) => { - if (amount === 0) { - return; + const roundedExpensesVatDiff = Math.round((roundedExpensesVat - expensesVat) * 100) / 100; + if (roundedExpensesVatDiff) { + accountingLedgerMaterials.push({ + amount: roundedExpensesVatDiff, + taxCategoryId: inputsVatTaxCategory.id, + counterpartyId: BALANCE_CANCELLATION_TAX_CATEGORY_ID, + }); } - const isCreditorCounterparty = amount > 0; - - const ledgerProto: LedgerProto = { - id: chargeId, - invoiceDate: ledgerDate, - valueDate: ledgerDate, - currency: DEFAULT_LOCAL_CURRENCY, - creditAccountID1: isCreditorCounterparty ? counterpartyId : taxCategoryId, - localCurrencyCreditAmount1: Math.abs(amount), - debitAccountID1: isCreditorCounterparty ? taxCategoryId : counterpartyId, - localCurrencyDebitAmount1: Math.abs(amount), - description: - counterpartyId === BALANCE_CANCELLATION_TAX_CATEGORY_ID - ? 'Balance cancellation' - : `VAT command ${vatDate}`, - isCreditorCounterparty, - ownerId: charge.owner_id, - chargeId, - }; - - accountingLedgerEntries.push(ledgerProto); - updateLedgerBalanceByEntry(ledgerProto, ledgerBalance); - }); + accountingLedgerMaterials.map(({ amount, taxCategoryId, counterpartyId }) => { + if (amount === 0) { + return; + } + + const isCreditorCounterparty = amount > 0; + + const ledgerProto: LedgerProto = { + id: chargeId, + invoiceDate: ledgerDate, + valueDate: ledgerDate, + currency: DEFAULT_LOCAL_CURRENCY, + creditAccountID1: isCreditorCounterparty ? counterpartyId : taxCategoryId, + localCurrencyCreditAmount1: Math.abs(amount), + debitAccountID1: isCreditorCounterparty ? taxCategoryId : counterpartyId, + localCurrencyDebitAmount1: Math.abs(amount), + description: + counterpartyId === BALANCE_CANCELLATION_TAX_CATEGORY_ID + ? 'Balance cancellation' + : `VAT command ${vatDate}`, + isCreditorCounterparty, + ownerId: charge.owner_id, + chargeId, + }; + + accountingLedgerEntries.push(ledgerProto); + updateLedgerBalanceByEntry(ledgerProto, ledgerBalance); + }); + } const ledgerBalanceInfo = await getLedgerBalanceInfo(injector, ledgerBalance); @@ -229,6 +245,7 @@ export const generateLedgerRecordsForMonthlyVat: ResolverFn< records: ledgerProtoToRecordsConverter(records), charge, balance: ledgerBalanceInfo, + errors: Array.from(errors), }; } catch (e) { return { diff --git a/packages/server/src/modules/ledger/resolvers/ledger-generation/salary-ledger-generation.resolver.ts b/packages/server/src/modules/ledger/resolvers/ledger-generation/salary-ledger-generation.resolver.ts index 666055d68..be2b87fc0 100644 --- a/packages/server/src/modules/ledger/resolvers/ledger-generation/salary-ledger-generation.resolver.ts +++ b/packages/server/src/modules/ledger/resolvers/ledger-generation/salary-ledger-generation.resolver.ts @@ -1,4 +1,3 @@ -import { GraphQLError } from 'graphql'; import { ExchangeProvider } from '@modules/exchange-rates/providers/exchange.provider.js'; import { TaxCategoriesProvider } from '@modules/financial-entities/providers/tax-categories.provider.js'; import { storeInitialGeneratedRecords } from '@modules/ledger/helpers/ledgrer-storage.helper.js'; @@ -26,6 +25,7 @@ import { generatePartialLedgerEntry, getFinancialAccountTaxCategoryId, getLedgerBalanceInfo, + LedgerError, ledgerProtoToRecordsConverter, updateLedgerBalanceByEntry, ValidateTransaction, @@ -46,6 +46,7 @@ export const generateLedgerRecordsForSalary: ResolverFn< } const chargeId = charge.id; const { injector } = context; + const errors: Set = new Set(); try { // validate ledger records are balanced @@ -55,10 +56,10 @@ export const generateLedgerRecordsForSalary: ResolverFn< const accountingLedgerEntries: LedgerProto[] = []; // generate ledger from salary records - const transactionDate = - charge.transactions_min_debit_date ?? charge.transactions_min_event_date; + let transactionDate = charge.transactions_min_debit_date ?? charge.transactions_min_event_date; if (!transactionDate) { - throw new GraphQLError(`Charge ID="${chargeId}" is missing transaction date`); + errors.add(`Charge ID="${chargeId}" is missing transaction date`); + transactionDate = new Date(); } // Get relevant data for generation @@ -87,47 +88,57 @@ export const generateLedgerRecordsForSalary: ResolverFn< ]); // generate ledger from salary records - const { entries, monthlyEntriesProto, month } = generateEntriesFromSalaryRecords( - salaryRecords, - charge, - transactionDate, - ); - - const salaryEntriesPromises = entries.map(async ledgerEntry => { - accountingLedgerEntries.push(ledgerEntry); - updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); - }); - entriesPromises.push(...salaryEntriesPromises); - - // generate monthly expenses ledger entries - const monthlyEntriesPromises = monthlyEntriesProto - .filter(({ amount }) => amount !== 0) - .map(async ({ taxCategoryId, amount, isCredit }) => { - const taxCategory = await injector - .get(TaxCategoriesProvider) - .taxCategoryByIDsLoader.load(taxCategoryId); - if (!taxCategory) { - throw new GraphQLError(`Tax category "${taxCategoryId}" not found`); - } - - const ledgerEntry: LedgerProto = { - id: taxCategoryId, - invoiceDate: transactionDate, - valueDate: transactionDate, - currency: DEFAULT_LOCAL_CURRENCY, - ...(isCredit ? { creditAccountID1: taxCategoryId } : { debitAccountID1: taxCategoryId }), - localCurrencyCreditAmount1: amount, - localCurrencyDebitAmount1: amount, - description: `${month} salary: ${taxCategory.name}`, - isCreditorCounterparty: false, - ownerId: charge.owner_id, - chargeId, - }; + try { + const { entries, monthlyEntriesProto, month } = generateEntriesFromSalaryRecords( + salaryRecords, + charge, + transactionDate, + ); + const salaryEntriesPromises = entries.map(async ledgerEntry => { accountingLedgerEntries.push(ledgerEntry); updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); }); - entriesPromises.push(...monthlyEntriesPromises); + entriesPromises.push(...salaryEntriesPromises); + + // generate monthly expenses ledger entries + const monthlyEntriesPromises = monthlyEntriesProto + .filter(({ amount }) => amount !== 0) + .map(async ({ taxCategoryId, amount, isCredit }) => { + const taxCategory = await injector + .get(TaxCategoriesProvider) + .taxCategoryByIDsLoader.load(taxCategoryId); + if (!taxCategory) { + throw new LedgerError(`Tax category "${taxCategoryId}" not found`); + } + + const ledgerEntry: LedgerProto = { + id: taxCategoryId, + invoiceDate: transactionDate, + valueDate: transactionDate, + currency: DEFAULT_LOCAL_CURRENCY, + ...(isCredit + ? { creditAccountID1: taxCategoryId } + : { debitAccountID1: taxCategoryId }), + localCurrencyCreditAmount1: amount, + localCurrencyDebitAmount1: amount, + description: `${month} salary: ${taxCategory.name}`, + isCreditorCounterparty: false, + ownerId: charge.owner_id, + chargeId, + }; + + accountingLedgerEntries.push(ledgerEntry); + updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); + }); + entriesPromises.push(...monthlyEntriesPromises); + } catch (e) { + if (e instanceof LedgerError) { + errors.add(e.message); + } else { + throw e; + } + } const { mainTransactions, feeTransactions } = splitFeeTransactions(transactions); @@ -141,52 +152,63 @@ export const generateLedgerRecordsForSalary: ResolverFn< // for each common transaction, create a ledger record const financialAccountLedgerEntries: LedgerProto[] = []; const transactionEntriesPromises = mainTransactions.map(async preValidatedTransaction => { - const transaction = validateTransactionRequiredVariables(preValidatedTransaction); - - // preparations for core ledger entries - let exchangeRate: number | undefined = undefined; - if (transaction.currency !== DEFAULT_LOCAL_CURRENCY) { - // get exchange rate for currency - exchangeRate = await injector - .get(ExchangeProvider) - .getExchangeRates( - transaction.currency, - DEFAULT_LOCAL_CURRENCY, - transaction.debit_timestamp, - ); - } - - const partialEntry = generatePartialLedgerEntry(transaction, charge.owner_id, exchangeRate); + try { + const transaction = validateTransactionRequiredVariables(preValidatedTransaction); + + // preparations for core ledger entries + let exchangeRate: number | undefined = undefined; + if (transaction.currency !== DEFAULT_LOCAL_CURRENCY) { + // get exchange rate for currency + exchangeRate = await injector + .get(ExchangeProvider) + .getExchangeRates( + transaction.currency, + DEFAULT_LOCAL_CURRENCY, + transaction.debit_timestamp, + ); + } - const financialAccountTaxCategoryId = await getFinancialAccountTaxCategoryId( - injector, - transaction, - ); + const partialEntry = generatePartialLedgerEntry(transaction, charge.owner_id, exchangeRate); - if (transaction.business_id && SALARY_BATCHED_BUSINESSES.includes(transaction.business_id)) { - batchedTransactionEntriesMaterials.push({ + const financialAccountTaxCategoryId = await getFinancialAccountTaxCategoryId( + injector, transaction, - partialEntry, - taxCategoryId: financialAccountTaxCategoryId, - }); - return; - } + ); - const ledgerEntry: StrictLedgerProto = { - ...partialEntry, - ...(partialEntry.isCreditorCounterparty - ? { - creditAccountID1: transaction.business_id, - debitAccountID1: financialAccountTaxCategoryId, - } - : { - creditAccountID1: financialAccountTaxCategoryId, - debitAccountID1: transaction.business_id, - }), - }; + if ( + transaction.business_id && + SALARY_BATCHED_BUSINESSES.includes(transaction.business_id) + ) { + batchedTransactionEntriesMaterials.push({ + transaction, + partialEntry, + taxCategoryId: financialAccountTaxCategoryId, + }); + return; + } - financialAccountLedgerEntries.push(ledgerEntry); - updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); + const ledgerEntry: StrictLedgerProto = { + ...partialEntry, + ...(partialEntry.isCreditorCounterparty + ? { + creditAccountID1: transaction.business_id, + debitAccountID1: financialAccountTaxCategoryId, + } + : { + creditAccountID1: financialAccountTaxCategoryId, + debitAccountID1: transaction.business_id, + }), + }; + + financialAccountLedgerEntries.push(ledgerEntry); + updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); + } catch (e) { + if (e instanceof LedgerError) { + errors.add(e.message); + } else { + throw e; + } + } }); entriesPromises.push(...transactionEntriesPromises); @@ -250,94 +272,103 @@ export const generateLedgerRecordsForSalary: ResolverFn< entriesPromises = []; const batchedTransactionEntriesPromises = batchedTransactionEntriesMaterials.map( async ({ transaction, partialEntry, taxCategoryId }) => { - const unbatchedBusinesses: Array = []; - switch (transaction.business_id) { - case BATCHED_EMPLOYEE_BUSINESS_ID: { - const employees = await injector - .get(EmployeesProvider) - .getEmployeesByEmployerLoader.load(charge.owner_id); - unbatchedBusinesses.push(...employees.map(({ business_id }) => business_id)); - break; - } - case BATCHED_PENSION_BUSINESS_ID: { - const funds = await injector.get(FundsProvider).getAllFunds(); - unbatchedBusinesses.push(...funds.map(({ id }) => id)); - break; - } - default: { - throw new GraphQLError( - `Business ID="${transaction.business_id}" is not supported as batched ID`, - ); + try { + const unbatchedBusinesses: Array = []; + switch (transaction.business_id) { + case BATCHED_EMPLOYEE_BUSINESS_ID: { + const employees = await injector + .get(EmployeesProvider) + .getEmployeesByEmployerLoader.load(charge.owner_id); + unbatchedBusinesses.push(...employees.map(({ business_id }) => business_id)); + break; + } + case BATCHED_PENSION_BUSINESS_ID: { + const funds = await injector.get(FundsProvider).getAllFunds(); + unbatchedBusinesses.push(...funds.map(({ id }) => id)); + break; + } + default: { + throw new LedgerError( + `Business ID="${transaction.business_id}" is not supported as batched ID`, + ); + } } - } - const batchedEntries = accountingLedgerEntries - .filter(accountingLedgerEntry => - accountingLedgerEntry.creditAccountID1 - ? unbatchedBusinesses.includes(accountingLedgerEntry.creditAccountID1) - : false, - ) - .map(accountingLedgerEntry => { - const externalPayments = financialAccountLedgerEntries - .filter( - financialAccountEntry => - !!financialAccountEntry.debitAccountID1 && - accountingLedgerEntry.creditAccountID1 === financialAccountEntry.debitAccountID1, - ) - .map(entry => entry.localCurrencyCreditAmount1) - .reduce((a, b) => a + b, 0); - if (externalPayments === 0) { - return accountingLedgerEntry; - } - if (externalPayments < accountingLedgerEntry.localCurrencyCreditAmount1) { + const batchedEntries = accountingLedgerEntries + .filter(accountingLedgerEntry => + accountingLedgerEntry.creditAccountID1 + ? unbatchedBusinesses.includes(accountingLedgerEntry.creditAccountID1) + : false, + ) + .map(accountingLedgerEntry => { + const externalPayments = financialAccountLedgerEntries + .filter( + financialAccountEntry => + !!financialAccountEntry.debitAccountID1 && + accountingLedgerEntry.creditAccountID1 === + financialAccountEntry.debitAccountID1, + ) + .map(entry => entry.localCurrencyCreditAmount1) + .reduce((a, b) => a + b, 0); + if (externalPayments === 0) { + return accountingLedgerEntry; + } + if (externalPayments < accountingLedgerEntry.localCurrencyCreditAmount1) { + return { + ...accountingLedgerEntry, + localCurrencyCreditAmount1: + accountingLedgerEntry.localCurrencyCreditAmount1 - externalPayments, + localCurrencyDebitAmount1: + accountingLedgerEntry.localCurrencyDebitAmount1 - externalPayments, + }; + } return { ...accountingLedgerEntry, localCurrencyCreditAmount1: - accountingLedgerEntry.localCurrencyCreditAmount1 - externalPayments, + externalPayments - accountingLedgerEntry.localCurrencyCreditAmount1, + creditAccountID1: accountingLedgerEntry.debitAccountID1, localCurrencyDebitAmount1: - accountingLedgerEntry.localCurrencyDebitAmount1 - externalPayments, + externalPayments - accountingLedgerEntry.localCurrencyDebitAmount1, + debitAccountID1: accountingLedgerEntry.creditAccountID1, }; - } - return { - ...accountingLedgerEntry, - localCurrencyCreditAmount1: - externalPayments - accountingLedgerEntry.localCurrencyCreditAmount1, - creditAccountID1: accountingLedgerEntry.debitAccountID1, - localCurrencyDebitAmount1: - externalPayments - accountingLedgerEntry.localCurrencyDebitAmount1, - debitAccountID1: accountingLedgerEntry.creditAccountID1, - }; - }); - - const balance = batchedEntries - .map(entry => entry.localCurrencyCreditAmount1) - .reduce((a, b) => a + b, 0); - if (balance !== partialEntry.localCurrencyCreditAmount1) { - throw new GraphQLError( - `Batched business ID="${transaction.business_id}" cannot be unbatched as it is not balanced`, - ); - } + }); + + const balance = batchedEntries + .map(entry => entry.localCurrencyCreditAmount1) + .reduce((a, b) => a + b, 0); + if (balance !== partialEntry.localCurrencyCreditAmount1) { + throw new LedgerError( + `Batched business ID="${transaction.business_id}" cannot be unbatched as it is not balanced`, + ); + } - for (const batchedEntry of batchedEntries) { - const ledgerEntry: StrictLedgerProto = { - ...partialEntry, - ...(partialEntry.isCreditorCounterparty - ? { - creditAccountID1: batchedEntry.creditAccountID1!, - debitAccountID1: taxCategoryId, - } - : { - creditAccountID1: taxCategoryId, - debitAccountID1: batchedEntry.creditAccountID1!, - }), - creditAmount1: batchedEntry.creditAmount1, - localCurrencyCreditAmount1: batchedEntry.localCurrencyCreditAmount1, - debitAmount1: batchedEntry.creditAmount1, - localCurrencyDebitAmount1: batchedEntry.localCurrencyCreditAmount1, - }; + for (const batchedEntry of batchedEntries) { + const ledgerEntry: StrictLedgerProto = { + ...partialEntry, + ...(partialEntry.isCreditorCounterparty + ? { + creditAccountID1: batchedEntry.creditAccountID1!, + debitAccountID1: taxCategoryId, + } + : { + creditAccountID1: taxCategoryId, + debitAccountID1: batchedEntry.creditAccountID1!, + }), + creditAmount1: batchedEntry.creditAmount1, + localCurrencyCreditAmount1: batchedEntry.localCurrencyCreditAmount1, + debitAmount1: batchedEntry.creditAmount1, + localCurrencyDebitAmount1: batchedEntry.localCurrencyCreditAmount1, + }; - batchedLedgerEntries.push(ledgerEntry); - updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); + batchedLedgerEntries.push(ledgerEntry); + updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); + } + } catch (e) { + if (e instanceof LedgerError) { + errors.add(e.message); + } else { + throw e; + } } }, ); @@ -346,7 +377,7 @@ export const generateLedgerRecordsForSalary: ResolverFn< // create a ledger record for fee transactions const feeFinancialAccountLedgerEntries: LedgerProto[] = []; const feeFinancialAccountLedgerEntriesPromises = feeTransactions.map(async transaction => { - await getEntriesFromFeeTransaction(transaction, charge, context).then(ledgerEntries => { + await getEntriesFromFeeTransaction(transaction, charge, injector).then(ledgerEntries => { feeFinancialAccountLedgerEntries.push(...ledgerEntries); ledgerEntries.map(ledgerEntry => { updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); @@ -359,62 +390,71 @@ export const generateLedgerRecordsForSalary: ResolverFn< // generate ledger from balance cancellation for (const balanceCancellation of balanceCancellations) { - const entityBalance = ledgerBalance.get(balanceCancellation.business_id); - if (!entityBalance) { - console.log( - `Balance cancellation for business ${balanceCancellation.business_id} redundant - already balanced`, - ); - continue; - } + try { + 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 { amount, entityId } = entityBalance; - const financialAccountEntry = financialAccountLedgerEntries.find(entry => - [ - entry.creditAccountID1, - entry.creditAccountID2, - entry.debitAccountID1, - entry.debitAccountID2, - ].includes(balanceCancellation.business_id), - ); - if (!financialAccountEntry) { - throw new GraphQLError( - `Balance cancellation for business ${balanceCancellation.business_id} failed - no financial account entry found`, + 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; + let foreignAmount: number | undefined = undefined; - if ( - financialAccountEntry.currency !== DEFAULT_LOCAL_CURRENCY && - financialAccountEntry.currencyRate - ) { - foreignAmount = financialAccountEntry.currencyRate * amount; - } + if ( + financialAccountEntry.currency !== DEFAULT_LOCAL_CURRENCY && + 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 ? BALANCE_CANCELLATION_TAX_CATEGORY_ID : entityId, - creditAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined, - localCurrencyCreditAmount1: Math.abs(amount), - debitAccountID1: isCreditorCounterparty ? entityId : BALANCE_CANCELLATION_TAX_CATEGORY_ID, - debitAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined, - localCurrencyDebitAmount1: Math.abs(amount), - description: balanceCancellation.description ?? undefined, - reference1: financialAccountEntry.reference1, - isCreditorCounterparty, - ownerId: charge.owner_id, - currencyRate: financialAccountEntry.currencyRate, - chargeId, - }; + const isCreditorCounterparty = amount > 0; - miscLedgerEntries.push(ledgerEntry); - updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); + const ledgerEntry: LedgerProto = { + id: balanceCancellation.charge_id, + invoiceDate: financialAccountEntry.invoiceDate, + valueDate: financialAccountEntry.valueDate, + currency: financialAccountEntry.currency, + creditAccountID1: isCreditorCounterparty + ? BALANCE_CANCELLATION_TAX_CATEGORY_ID + : entityId, + creditAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined, + localCurrencyCreditAmount1: Math.abs(amount), + debitAccountID1: isCreditorCounterparty ? entityId : BALANCE_CANCELLATION_TAX_CATEGORY_ID, + debitAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined, + localCurrencyDebitAmount1: Math.abs(amount), + description: balanceCancellation.description ?? undefined, + reference1: financialAccountEntry.reference1, + isCreditorCounterparty, + ownerId: charge.owner_id, + currencyRate: financialAccountEntry.currencyRate, + chargeId, + }; + + miscLedgerEntries.push(ledgerEntry); + updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); + } catch (e) { + if (e instanceof LedgerError) { + errors.add(e.message); + } else { + throw e; + } + } } const allowedUnbalancedBusinesses = new Set( @@ -439,6 +479,7 @@ export const generateLedgerRecordsForSalary: ResolverFn< records: ledgerProtoToRecordsConverter(records), charge, balance: ledgerBalanceInfo, + errors: Array.from(errors), }; } catch (e) { return { diff --git a/packages/server/src/modules/ledger/resolvers/ledger.resolver.ts b/packages/server/src/modules/ledger/resolvers/ledger.resolver.ts index d33f2d947..f72e5a45e 100644 --- a/packages/server/src/modules/ledger/resolvers/ledger.resolver.ts +++ b/packages/server/src/modules/ledger/resolvers/ledger.resolver.ts @@ -54,6 +54,7 @@ export const ledgerResolvers: LedgerModule.Resolvers & Pick & {