@accounter/server
Version:
394 lines (363 loc) • 14 kB
text/typescript
import { format } from 'date-fns';
import { GraphQLError } from 'graphql';
import { ChargesProvider } from '@modules/charges/providers/charges.provider.js';
import { LedgerProvider } from '@modules/ledger/providers/ledger.provider.js';
import { Currency } from '@shared/enums';
import type { Resolvers } from '@shared/gql-types';
import { formatFinancialAmount } from '@shared/helpers';
import type {
BusinessTransactionProto,
RawBusinessTransactionsSum,
TimelessDateString,
} from '@shared/types';
import {
handleBusinessLedgerRecord,
handleBusinessTransaction,
} from '../helpers/business-transactions.helper.js';
import { FinancialEntitiesProvider } from '../providers/financial-entities.provider.js';
import type {
FinancialEntitiesModule,
IGetBusinessesByIdsResult,
IGetFinancialEntitiesByIdsResult,
} from '../types.js';
export const businessTransactionsResolvers: FinancialEntitiesModule.Resolvers &
Pick<
Resolvers,
'BusinessTransactionsSumFromLedgerRecordsResult' | 'BusinessTransactionsFromLedgerRecordsResult'
> = {
Query: {
businessTransactionsSumFromLedgerRecords: async (_, { filters }, context, _info) => {
const injector = context.injector;
const { ownerIds, businessIDs, fromDate, toDate } = filters || {};
const financialEntities = await injector
.get(FinancialEntitiesProvider)
.getFinancialEntityByIdLoader.loadMany(businessIDs ?? []);
const isFilteredByFinancialEntities = !!businessIDs?.length;
const financialEntitiesIDs = financialEntities
?.filter(fe => fe && 'id' in fe)
.map(fe => (fe as IGetFinancialEntitiesByIdsResult).id);
try {
const charges = await injector.get(ChargesProvider).getChargesByFilters({
ownerIds: ownerIds ?? undefined,
fromAnyDate: fromDate,
toAnyDate: toDate,
});
const ledgerRecordSets = await Promise.all(
charges.map(charge =>
injector.get(LedgerProvider).getLedgerRecordsByChargesIdLoader.load(charge.id),
),
);
const ledgerRecords = ledgerRecordSets.flat();
const rawRes: Record<string, RawBusinessTransactionsSum> = {};
for (const ledger of ledgerRecords) {
// re-filter ledger records by date (to prevent charge's out-of-range dates from affecting the sum)
if (!!fromDate && format(ledger.invoice_date, 'yyyy-MM-dd') < fromDate) {
continue;
}
if (!!toDate && format(ledger.invoice_date, 'yyyy-MM-dd') > toDate) {
continue;
}
if (
ledger.credit_entity1 &&
(!isFilteredByFinancialEntities || financialEntitiesIDs.includes(ledger.credit_entity1))
) {
handleBusinessLedgerRecord(
rawRes,
ledger.credit_entity1,
ledger.currency as Currency,
true,
ledger.credit_local_amount1,
ledger.credit_foreign_amount1,
);
}
if (
ledger.credit_entity2 &&
(!isFilteredByFinancialEntities || financialEntitiesIDs.includes(ledger.credit_entity2))
) {
handleBusinessLedgerRecord(
rawRes,
ledger.credit_entity2,
ledger.currency as Currency,
true,
ledger.credit_local_amount2,
ledger.credit_foreign_amount2,
);
}
if (
ledger.debit_entity1 &&
(!isFilteredByFinancialEntities || financialEntitiesIDs.includes(ledger.debit_entity1))
) {
handleBusinessLedgerRecord(
rawRes,
ledger.debit_entity1,
ledger.currency as Currency,
false,
ledger.debit_local_amount1,
ledger.debit_foreign_amount1,
);
}
if (
ledger.debit_entity2 &&
(!isFilteredByFinancialEntities || financialEntitiesIDs.includes(ledger.debit_entity2))
) {
handleBusinessLedgerRecord(
rawRes,
ledger.debit_entity2,
ledger.currency as Currency,
false,
ledger.debit_local_amount2,
ledger.debit_foreign_amount2,
);
}
}
return {
__typename: 'BusinessTransactionsSumFromLedgerRecordsSuccessfulResult',
businessTransactionsSum: Object.values(rawRes),
};
} catch (e) {
console.error(e);
return {
__typename: 'CommonError',
message: 'Error fetching business transactions summary from ledger records',
};
}
},
businessTransactionsFromLedgerRecords: async (_, { filters }, context, _info) => {
const injector = context.injector;
const { ownerIds, businessIDs, fromDate, toDate } = filters || {};
const isFilteredByFinancialEntities = !!filters?.businessIDs?.length;
const financialEntities = await injector
.get(FinancialEntitiesProvider)
.getFinancialEntityByIdLoader.loadMany(businessIDs ?? []);
const financialEntitiesIDs = financialEntities
?.filter(business => business && 'id' in business)
.map(business => (business as IGetBusinessesByIdsResult).id);
try {
const charges = await injector.get(ChargesProvider).getChargesByFilters({
ownerIds: ownerIds ?? undefined,
fromAnyDate: fromDate,
toAnyDate: toDate,
});
const ledgerRecordSets = await Promise.all(
charges.map(charge =>
injector.get(LedgerProvider).getLedgerRecordsByChargesIdLoader.load(charge.id),
),
);
const ledgerRecords = ledgerRecordSets.flat();
const rawTransactions: BusinessTransactionProto[] = [];
for (const record of ledgerRecords) {
// re-filter ledger records by date (to prevent charge's out-of-range dates from affecting the sum)
if (!!fromDate && format(record.invoice_date, 'yyyy-MM-dd') < fromDate) {
continue;
}
if (!!toDate && format(record.invoice_date, 'yyyy-MM-dd') > toDate) {
continue;
}
if (
record.credit_entity1 &&
(!isFilteredByFinancialEntities || financialEntitiesIDs.includes(record.credit_entity1))
) {
const transaction = handleBusinessTransaction(
record,
record.credit_entity1,
record.debit_entity1,
true,
record.credit_local_amount1,
record.credit_foreign_amount1,
);
rawTransactions.push(transaction);
}
if (
record.credit_entity2 &&
(!isFilteredByFinancialEntities || financialEntitiesIDs.includes(record.credit_entity2))
) {
const transaction = handleBusinessTransaction(
record,
record.credit_entity2,
record.debit_entity2 ?? record.debit_entity2,
true,
record.credit_local_amount2,
record.credit_foreign_amount2,
);
rawTransactions.push(transaction);
}
if (
record.debit_entity2 &&
(!isFilteredByFinancialEntities || financialEntitiesIDs.includes(record.debit_entity2))
) {
const transaction = handleBusinessTransaction(
record,
record.debit_entity2,
record.credit_entity1,
false,
record.debit_local_amount1,
record.debit_foreign_amount1,
);
rawTransactions.push(transaction);
}
if (
record.debit_entity2 &&
(!isFilteredByFinancialEntities || financialEntitiesIDs.includes(record.debit_entity2))
) {
const transaction = handleBusinessTransaction(
record,
record.debit_entity2,
record.credit_entity2 ?? record.credit_entity1,
false,
record.debit_local_amount2,
record.debit_foreign_amount2,
);
rawTransactions.push(transaction);
}
}
return {
businessTransactions: rawTransactions.sort((a, b) => {
const chargeA = charges.find(charge => charge.id === a.chargeId);
const chargeB = charges.find(charge => charge.id === b.chargeId);
const dateA = Math.min(
...([
chargeA?.documents_min_date?.getTime(),
chargeA?.transactions_min_event_date?.getTime(),
].filter(Boolean) as number[]),
);
const dateB = Math.min(
...([
chargeB?.documents_min_date?.getTime(),
chargeB?.transactions_min_event_date?.getTime(),
].filter(Boolean) as number[]),
);
if (dateA < dateB) return -1;
if (dateA > dateB) return 1;
if (a.chargeId < b.chargeId) return -1;
if (a.chargeId > b.chargeId) return 1;
if (a.date.getTime() < b.date.getTime()) return -1;
if (a.date.getTime() > b.date.getTime()) return 1;
return 0;
}),
};
} catch (e) {
console.error(e);
return {
__typename: 'CommonError',
message: 'Error fetching business transactions from ledger records',
};
}
},
},
BusinessTransactionsSumFromLedgerRecordsResult: {
__resolveType: (obj, _context, _info) => {
if ('__typename' in obj && obj.__typename === 'CommonError') return 'CommonError';
return 'BusinessTransactionsSumFromLedgerRecordsSuccessfulResult';
},
},
BusinessTransactionSum: {
business: (rawSum, _, { injector }) =>
injector
.get(FinancialEntitiesProvider)
.getFinancialEntityByIdLoader.load(rawSum.businessId)
.then(res => {
if (!res) {
throw new GraphQLError(`Business with id ${rawSum.businessId} not found`);
}
return res;
}),
credit: rawSum => formatFinancialAmount(rawSum.ils.credit, Currency.Ils),
debit: rawSum => formatFinancialAmount(rawSum.ils.debit, Currency.Ils),
total: rawSum => formatFinancialAmount(rawSum.ils.total, Currency.Ils),
eurSum: rawSum =>
rawSum.eur.credit || rawSum.eur.debit
? {
credit: formatFinancialAmount(rawSum.eur.credit, Currency.Eur),
debit: formatFinancialAmount(rawSum.eur.debit, Currency.Eur),
total: formatFinancialAmount(rawSum.eur.total, Currency.Eur),
}
: null,
gbpSum: rawSum =>
rawSum.gbp.credit || rawSum.gbp.debit
? {
credit: formatFinancialAmount(rawSum.gbp.credit, Currency.Gbp),
debit: formatFinancialAmount(rawSum.gbp.debit, Currency.Gbp),
total: formatFinancialAmount(rawSum.gbp.total, Currency.Gbp),
}
: null,
usdSum: rawSum =>
rawSum.usd.credit | rawSum.usd.debit
? {
credit: formatFinancialAmount(rawSum.usd.credit, Currency.Usd),
debit: formatFinancialAmount(rawSum.usd.debit, Currency.Usd),
total: formatFinancialAmount(rawSum.usd.total, Currency.Usd),
}
: null,
},
BusinessTransactionsFromLedgerRecordsResult: {
__resolveType: parent =>
'businessTransactions' in parent
? 'BusinessTransactionsFromLedgerRecordsSuccessfulResult'
: 'CommonError',
},
BusinessTransaction: {
__isTypeOf: parent => !!parent.businessId,
amount: parent =>
formatFinancialAmount(
Number.isNaN(parent.foreignAmount)
? parent.amount
: Number(parent.amount) * (parent.isCredit ? 1 : -1),
Currency.Ils,
),
business: (parent, _, { injector }) =>
injector
.get(FinancialEntitiesProvider)
.getFinancialEntityByIdLoader.load(parent.businessId)
.then(res => {
if (!res) {
throw new GraphQLError(`Financial entity with id ${parent.businessId} not found`);
}
return res;
}),
eurAmount: parent =>
parent.currency === Currency.Eur
? formatFinancialAmount(
Number.isNaN(parent.foreignAmount)
? parent.foreignAmount
: Number(parent.foreignAmount) * (parent.isCredit ? 1 : -1),
Currency.Eur,
)
: null,
gbpAmount: parent =>
parent.currency === Currency.Gbp
? formatFinancialAmount(
Number.isNaN(parent.foreignAmount)
? parent.foreignAmount
: Number(parent.foreignAmount) * (parent.isCredit ? 1 : -1),
Currency.Gbp,
)
: null,
usdAmount: parent =>
parent.currency === Currency.Usd
? formatFinancialAmount(parent.foreignAmount * (parent.isCredit ? 1 : -1), Currency.Usd)
: null,
invoiceDate: parent => format(parent.date!, 'yyyy-MM-dd') as TimelessDateString,
reference1: parent => parent.reference1 ?? null,
reference2: _ => null,
details: parent => parent.details ?? null,
counterAccount: (parent, _, { injector }) =>
parent.counterAccountId
? injector
.get(FinancialEntitiesProvider)
.getFinancialEntityByIdLoader.load(parent.counterAccountId)
.then(res => {
if (!res) {
throw new GraphQLError(
`Financial entity with id ${parent.counterAccountId} not found`,
);
}
return res;
})
: null,
},
TaxCategory: {
__isTypeOf: parent => 'hashavshevet_name' in parent,
id: parent => parent.id,
name: parent => parent.name,
},
};