UNPKG

@accounter/server

Version:
377 lines (341 loc) • 12.8 kB
import { subDays } from 'date-fns'; import { GraphQLResolveInfo } from 'graphql'; import type { Injector } from 'graphql-modules'; import type { Account, BusinessMetadata, CurrencyCode, JournalEntry, } from '@accounter/shaam-uniform-format-generator'; import { Currency } from '../../../shared/enums.js'; import { dateToTimelessDateString } from '../../../shared/helpers/index.js'; import { TimelessDateString } from '../../../shared/types/index.js'; import { AdminContextProvider } from '../../admin-context/providers/admin-context.provider.js'; import { isCryptoCurrency } from '../../exchange-rates/helpers/exchange.helper.js'; import { AdminBusinessesProvider } from '../../financial-entities/providers/admin-businesses.provider.js'; import { BusinessesProvider } from '../../financial-entities/providers/businesses.provider.js'; import { FinancialEntitiesProvider } from '../../financial-entities/providers/financial-entities.provider.js'; import { businessTransactionsSumFromLedgerRecords } from '../../financial-entities/resolvers/business-transactions-sum-from-ledger-records.resolver.js'; import type { IGetBusinessesByIdsResult } from '../../financial-entities/types.js'; import { LedgerProvider } from '../../ledger/providers/ledger.provider.js'; import { SortCodesProvider } from '../../sort-codes/providers/sort-codes.provider.js'; export async function businessForUniformFormat( injector: Injector, fromDate: TimelessDateString, toDate: TimelessDateString, ): Promise<BusinessMetadata> { const { ownerId } = await injector.get(AdminContextProvider).getVerifiedAdminContext(); const adminBusiness = await injector .get(AdminBusinessesProvider) .getAdminBusinessByIdLoader.load(ownerId); if (!adminBusiness) { throw new Error('Admin business not found'); } if (!adminBusiness.vat_number) { throw new Error('Admin business has no VAT number'); } return { businessId: adminBusiness.id, name: adminBusiness.name, taxId: adminBusiness.vat_number, reportingPeriod: { startDate: fromDate, endDate: toDate, }, }; } export async function journalEntriesForUniformFormat( injector: Injector, fromDate: TimelessDateString, toDate: TimelessDateString, ): Promise<JournalEntry[]> { const { ownerId } = await injector.get(AdminContextProvider).getVerifiedAdminContext(); const ledgerRecords = await injector .get(LedgerProvider) .getLedgerRecordsByDates({ ownerId, fromDate, toDate }); const entries: JournalEntry[] = []; for (const record of ledgerRecords) { const isCrypto = isCryptoCurrency(record.currency.toUpperCase() as Currency); const commonEntry: Omit< JournalEntry, | 'accountId' | 'amount' | 'counterAccountKey' | 'debitCreditIndicator' | 'foreignCurrencyAmount' | 'transactionLineNumber' > = { id: record.id, date: dateToTimelessDateString(record.invoice_date), description: record.description ?? '', transactionNumber: Number(record.id.replace(/\D/g, '').slice(-10)), batchNumber: 1, // "transactionType": '', referenceDocument: record.reference1 ?? undefined, // "referenceDocumentType": '', referenceDocument2: undefined, // "referenceDocumentType2": '', valueDate: dateToTimelessDateString(record.value_date), currencyCode: isCrypto ? 'ILS' : (record.currency as unknown as CurrencyCode), transactionAmount: Math.max( Math.abs(Number(record.credit_local_amount1)), Math.abs(Number(record.debit_local_amount1)), ), // "quantityField": '', matchingField1: record.charge_id, matchingField2: undefined, // "branchId": '', entryDate: dateToTimelessDateString(record.created_at), // "operatorUsername": '', }; let lineNumber = 1; // for credit1 if (record.credit_entity1 && Math.abs(Number(record.credit_local_amount1)) > 0) { entries.push({ ...commonEntry, id: `c1-${commonEntry.id}`, accountId: record.credit_entity1, amount: Math.abs(Number(record.credit_local_amount1)), counterAccountKey: record.debit_entity1 ?? undefined, debitCreditIndicator: '2', // 2 for credit foreignCurrencyAmount: record.credit_foreign_amount1 && !isCrypto ? Math.abs(Number(record.credit_foreign_amount1)) : undefined, transactionLineNumber: lineNumber, }); lineNumber++; } // for credit2 if (record.credit_entity2 && Math.abs(Number(record.credit_local_amount1)) > 0) { entries.push({ ...commonEntry, id: `c2-${commonEntry.id}`, accountId: record.credit_entity2, amount: Math.abs(Number(record.credit_local_amount2)), counterAccountKey: record.debit_entity1 ?? undefined, debitCreditIndicator: '2', // 2 for credit foreignCurrencyAmount: record.credit_foreign_amount2 && !isCrypto ? Math.abs(Number(record.credit_foreign_amount2)) : undefined, transactionLineNumber: lineNumber, }); lineNumber++; } // for debit1 if (record.debit_entity1 && Math.abs(Number(record.debit_local_amount1)) > 0) { entries.push({ ...commonEntry, id: `d1-${commonEntry.id}`, accountId: record.debit_entity1, amount: Math.abs(Number(record.debit_local_amount1)), counterAccountKey: record.credit_entity1 ?? undefined, debitCreditIndicator: '1', // 1 for debit foreignCurrencyAmount: record.debit_foreign_amount1 && !isCrypto ? Math.abs(Number(record.debit_foreign_amount1)) : undefined, transactionLineNumber: lineNumber, }); lineNumber++; } // for debit2 if (record.debit_entity2 && Math.abs(Number(record.debit_local_amount2)) > 0) { entries.push({ ...commonEntry, id: `d2-${commonEntry.id}`, accountId: record.debit_entity2, amount: Math.abs(Number(record.debit_local_amount2)), counterAccountKey: record.credit_entity1 ?? undefined, debitCreditIndicator: '1', // 1 for debit foreignCurrencyAmount: record.debit_foreign_amount2 && !isCrypto ? Math.abs(Number(record.debit_foreign_amount2)) : undefined, transactionLineNumber: lineNumber, }); } } return entries; } export async function accountsForUniformFormat( context: GraphQLModules.ModuleContext, info: GraphQLResolveInfo, fromDate: TimelessDateString, toDate: TimelessDateString, ): Promise<Account[]> { const { injector } = context; const { ownerId } = await injector.get(AdminContextProvider).getVerifiedAdminContext(); const financialEntitiesPromise = injector .get(FinancialEntitiesProvider) .getAllFinancialEntities(); // get admin business info const adminBusinessPromise = injector .get(AdminBusinessesProvider) .getAdminBusinessByIdLoader.load(ownerId); const [financialEntities, adminBusiness, sortCodes] = await Promise.all([ financialEntitiesPromise, adminBusinessPromise, injector.get(SortCodesProvider).getSortCodesByOwnerIdLoader.load(ownerId), ]); if (!adminBusiness) { throw new Error('Admin business not found'); } const openingBalances = await businessTransactionsSumFromLedgerRecords( {}, { filters: { ownerIds: [ownerId], toDate: dateToTimelessDateString(subDays(new Date(fromDate), 1)), includeRevaluation: true, }, }, context, info, ); const businessesBalance = new Map< string, { ILS: { opening: number; credit: number; debit: number; }; foreign?: { currency: string; total: number; }; } >(); if (openingBalances.__typename === 'CommonError') { throw new Error(`Failed to fetch opening balances: ${openingBalances.message}`); } else if ( openingBalances.__typename === 'BusinessTransactionsSumFromLedgerRecordsSuccessfulResult' ) { await Promise.all( openingBalances.businessTransactionsSum.map(async businessSumPromise => { const businessSum = await businessSumPromise; for (const [code, amounts] of Object.entries(businessSum)) { if ( isCryptoCurrency(code.toUpperCase() as Currency) || code === 'ILS' || code === 'businessId' ) { continue; } if (typeof amounts !== 'string' && amounts.total !== 0) { businessesBalance.set(businessSum.businessId, { ILS: { opening: businessSum.ILS.total, credit: 0, debit: 0, }, foreign: { currency: code, total: amounts.total }, }); return; } } if (businessSum.ILS.total !== 0) { businessesBalance.set(businessSum.businessId, { ILS: { opening: businessSum.ILS.total, credit: 0, debit: 0, }, }); } }), ); } const totalDebitCredit = await businessTransactionsSumFromLedgerRecords( {}, { filters: { ownerIds: [ownerId], fromDate, toDate, includeRevaluation: true, }, }, context, info, ); if (totalDebitCredit.__typename === 'CommonError') { throw new Error(`Failed to fetch opening balances: ${totalDebitCredit.message}`); } else if ( totalDebitCredit.__typename === 'BusinessTransactionsSumFromLedgerRecordsSuccessfulResult' ) { await Promise.all( totalDebitCredit.businessTransactionsSum.map(async businessSumPromise => { const businessSum = await businessSumPromise; const existing = businessesBalance.get(businessSum.businessId); if (existing) { existing.ILS.credit += businessSum.ILS.credit; existing.ILS.debit += businessSum.ILS.debit; } else { businessesBalance.set(businessSum.businessId, { ILS: { opening: 0, credit: businessSum.ILS.credit, debit: businessSum.ILS.debit, }, }); } }), ); } const businesses = await injector .get(BusinessesProvider) .getBusinessByIdLoader.loadMany(Array.from(businessesBalance.keys())); const businessesMap = new Map( (businesses.filter(b => b && !(b instanceof Error)) as IGetBusinessesByIdsResult[]).map( business => [business.id, business], ), ); const sortCodesMap = new Map(sortCodes.map(code => [code.key, code.name ?? ''])); const financialEntitiesMap = new Map(financialEntities.map(entity => [entity.id, entity])); const accounts: Account[] = []; for (const [businessId, balance] of Array.from(businessesBalance.entries())) { const financialEntity = financialEntitiesMap.get(businessId); if (!financialEntity) { throw new Error(`Financial entity not found for business ID: ${businessId}`); } if (!financialEntity.sort_code) { throw new Error(`Financial entity ${financialEntity.id} has no sort code`); } const sortCodeName = financialEntity.sort_code ? sortCodesMap.get(financialEntity.sort_code) : undefined; if (!sortCodeName) { throw new Error(`Sort code not found for business ID: ${businessId}`); } accounts.push({ id: businessId, name: financialEntity.name, sortCode: { key: financialEntity.sort_code.toString(), name: sortCodeName, }, address: { // street: businessesMap.get(businessId)?.address?.street, // houseNumber: businessesMap.get(businessId)?.address?.houseNumber, // city: businessesMap.get(businessId)?.address?.city, // zip: businessesMap.get(businessId)?.address?.zip, country: businessesMap.get(businessId)?.country === 'ISR' ? 'IL' : undefined, }, // countryCode: businessesMap.get(businessId)?.countryCode, // parentAccountKey: businessesMap.get(businessId)?.parentAccountKey, vatId: businessesMap.get(businessId)?.vat_number ?? undefined, accountOpeningBalance: balance.ILS.opening, totalDebits: balance.ILS.debit || undefined, totalCredits: balance.ILS.credit || undefined, // accountingClassificationCode: // businessesMap.get(businessId)?.accountingClassificationCode ?? '', // branchId: businessesMap.get(businessId)?.branchId ?? '', openingBalanceForeignCurrency: balance.foreign?.total, // TODO: handle foreign currency foreignCurrencyCode: balance.foreign?.currency, }); } return accounts; }