@accounter/server
Version:
Accounter GraphQL server
377 lines (341 loc) • 12.8 kB
text/typescript
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;
}