@accounter/server
Version:
273 lines (251 loc) • 9.6 kB
text/typescript
import { GraphQLError } from 'graphql';
import { Injector } from 'graphql-modules';
import type { IGetFinancialAccountsByAccountIDsResult } from '@modules/financial-accounts/types';
import { FinancialEntitiesProvider } from '@modules/financial-entities/providers/financial-entities.provider.js';
import { IGetFinancialEntitiesByIdsResult } from '@modules/financial-entities/types';
import type { IGetTransactionsByChargeIdsResult } from '@modules/transactions/types';
import { DEFAULT_LOCAL_CURRENCY, EMPTY_UUID } from '@shared/constants';
import { Currency } from '@shared/enums';
import type { FinancialAmount } from '@shared/gql-types';
import { formatCurrency, formatFinancialAmount } from '@shared/helpers';
import type { LedgerBalanceInfoType, LedgerProto, StrictLedgerProto } from '@shared/types';
import type { IGetLedgerRecordsByChargesIdsResult } from '../types.js';
export function isTransactionsOppositeSign([first, second]: IGetTransactionsByChargeIdsResult[]) {
if (!first || !second) {
throw new GraphQLError('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');
}
return Number(first.amount) > 0 !== Number(second.amount) > 0;
}
export function getTaxCategoryNameByAccountCurrency(
account: IGetFinancialAccountsByAccountIDsResult,
currency: Currency,
): string {
let taxCategoryName = account.hashavshevet_account_ils;
switch (currency) {
case Currency.Ils:
taxCategoryName = account.hashavshevet_account_ils;
break;
case Currency.Usd:
taxCategoryName = account.hashavshevet_account_usd;
break;
case Currency.Eur:
taxCategoryName = account.hashavshevet_account_eur;
break;
case Currency.Gbp:
taxCategoryName = account.hashavshevet_account_gbp;
break;
case Currency.Usdc:
case Currency.Grt:
case Currency.Eth:
taxCategoryName = account.hashavshevet_account_ils;
break;
default:
console.error(`Unknown currency for account's tax category: ${currency}`);
}
if (!taxCategoryName) {
throw new GraphQLError(`Account ID="${account.id}" is missing tax category name`);
}
return taxCategoryName;
}
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}`,
);
}
const valueDate = transaction.debit_timestamp ?? transaction.debit_date;
if (!transaction.business_id) {
throw new GraphQLError(`Transaction ID="${transaction.id}" is missing business_id`);
}
const transactionBusinessId = transaction.business_id;
return {
currency,
valueDate,
transactionBusinessId,
};
}
type WithRequired<T, K extends keyof T> = T & { [P in K]-?: NonNullable<T[P]> };
export type ValidateTransaction = Omit<
WithRequired<IGetTransactionsByChargeIdsResult, 'debit_date' | 'business_id' | 'debit_timestamp'>,
'currency'
> & { currency: Currency };
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}`,
);
}
if (!transaction.business_id) {
throw new GraphQLError(`Transaction ID="${transaction.id}" is missing business_id`);
}
const debit_timestamp = transaction.debit_timestamp ?? transaction.debit_date;
return {
...transaction,
debit_timestamp,
currency: formatCurrency(transaction.currency),
} as ValidateTransaction;
}
export function generatePartialLedgerEntry(
transaction: ValidateTransaction,
ownerId: string,
exchangeRate?: number,
): Omit<StrictLedgerProto, 'creditAccountID1' | 'debitAccountID1'> {
// set amounts
let amount = Number(transaction.amount);
let foreignAmount: number | undefined = undefined;
if (exchangeRate) {
foreignAmount = amount;
// calculate amounts in ILS
amount = exchangeRate * amount;
}
const absAmount = Math.abs(amount);
const absForeignAmount = foreignAmount ? Math.abs(foreignAmount) : undefined;
const isCreditorCounterparty = amount > 0;
return {
id: transaction.id,
invoiceDate: transaction.event_date,
valueDate: transaction.debit_timestamp,
currency: transaction.currency,
creditAmount1: absForeignAmount,
localCurrencyCreditAmount1: absAmount,
debitAmount1: absForeignAmount,
localCurrencyDebitAmount1: absAmount,
description: transaction.source_description ?? undefined,
reference1: transaction.source_reference ?? undefined,
isCreditorCounterparty,
ownerId,
currencyRate: transaction.currency_rate ? Number(transaction.currency_rate) : undefined,
chargeId: transaction.charge_id,
};
}
export function updateLedgerBalanceByEntry(
entry: LedgerProto,
ledgerBalance: Map<string, { amount: number; entityId: string }>,
): void {
if (entry.creditAccountID1) {
const name = entry.creditAccountID1;
ledgerBalance.set(name, {
amount: (ledgerBalance.get(name)?.amount ?? 0) + (entry.localCurrencyCreditAmount1 ?? 0),
entityId: entry.creditAccountID1,
});
}
if (entry.debitAccountID1) {
const name = entry.debitAccountID1;
ledgerBalance.set(name, {
amount: (ledgerBalance.get(name)?.amount ?? 0) - (entry.localCurrencyDebitAmount1 ?? 0),
entityId: entry.debitAccountID1,
});
}
if (entry.creditAccountID2) {
const name = entry.creditAccountID2;
ledgerBalance.set(name, {
amount: (ledgerBalance.get(name)?.amount ?? 0) + (entry.localCurrencyCreditAmount2 ?? 0),
entityId: entry.creditAccountID2,
});
}
if (entry.debitAccountID2) {
const name = entry.debitAccountID2;
ledgerBalance.set(name, {
amount: (ledgerBalance.get(name)?.amount ?? 0) - (entry.localCurrencyDebitAmount2 ?? 0),
entityId: entry.debitAccountID2,
});
}
return;
}
export async function getLedgerBalanceInfo(
injector: Injector,
ledgerBalance: Map<string, { amount: number; entityId: string }>,
allowedUnbalancedBusinesses: Set<string> = new Set(),
financialEntities?: Array<IGetFinancialEntitiesByIdsResult>,
): Promise<
LedgerBalanceInfoType & {
financialEntities: Array<IGetFinancialEntitiesByIdsResult>;
}
> {
let ledgerBalanceSum = 0;
let isBalanced = true;
const unbalancedEntities: Array<{ entityId: string; balance: FinancialAmount }> = [];
if (!financialEntities) {
const financialEntityIDs = new Set<string>(
Array.from(ledgerBalance.values()).map(v => v.entityId),
);
financialEntities = (await injector
.get(FinancialEntitiesProvider)
.getFinancialEntityByIdLoader.loadMany(Array.from(financialEntityIDs))
.then(res =>
res.filter(fe => !!fe && 'id' in fe),
)) as Array<IGetFinancialEntitiesByIdsResult>;
}
for (const { amount, entityId } of ledgerBalance.values()) {
if (Math.abs(amount) < 0.005) {
continue;
}
const isBusiness = financialEntities?.some(
financialEntity => financialEntity.id === entityId && financialEntity.type === 'business',
);
const isBusinessEntity =
isBusiness &&
(financialEntities
? financialEntities.some(
financialEntity =>
financialEntity.id === entityId && financialEntity.type === 'business',
)
: true);
if (isBusinessEntity && !allowedUnbalancedBusinesses.has(entityId)) {
isBalanced = false;
}
unbalancedEntities.push({
entityId,
balance: formatFinancialAmount(amount, DEFAULT_LOCAL_CURRENCY),
});
ledgerBalanceSum += amount;
}
if (Math.abs(ledgerBalanceSum) >= 0.005) {
isBalanced = false;
}
return {
isBalanced,
unbalancedEntities,
balanceSum: ledgerBalanceSum,
financialEntities,
};
}
export function ledgerProtoToRecordsConverter(
records: LedgerProto[],
): IGetLedgerRecordsByChargesIdsResult[] {
return records.map(record => {
const adjustedRecord: IGetLedgerRecordsByChargesIdsResult = {
charge_id: record.chargeId,
created_at: new Date(),
credit_entity1: record.creditAccountID1 ?? null,
credit_entity2: record.creditAccountID2 ?? null,
credit_foreign_amount1: record.creditAmount1?.toString() ?? null,
credit_foreign_amount2: record.creditAmount2?.toString() ?? null,
credit_local_amount1: record.localCurrencyCreditAmount1?.toString(),
credit_local_amount2: record.localCurrencyCreditAmount2?.toString() ?? null,
currency: record.currency,
debit_entity1: record.debitAccountID1 ?? null,
debit_entity2: record.debitAccountID2 ?? null,
debit_foreign_amount1: record.debitAmount1?.toString() ?? null,
debit_foreign_amount2: record.debitAmount2?.toString() ?? null,
debit_local_amount1: record.localCurrencyDebitAmount1?.toString(),
debit_local_amount2: record.localCurrencyDebitAmount2?.toString() ?? null,
description: record.description ?? null,
id: EMPTY_UUID,
invoice_date: record.invoiceDate,
owner_id: record.ownerId ?? null,
reference1: record.reference1 ?? null,
updated_at: new Date(),
value_date: record.valueDate,
};
return adjustedRecord;
});
}