@accounter/server
Version:
Accounter GraphQL server
170 lines (149 loc) • 5.94 kB
text/typescript
import type {
Maybe,
ResolverFn,
ResolversParentTypes,
ResolversTypes,
} from '../../../../../__generated__/types.js';
import { EMPTY_UUID } from '../../../../../shared/constants.js';
import type { LedgerProto } from '../../../../../shared/types/index.js';
import { AdminContextProvider } from '../../../../admin-context/providers/admin-context.provider.js';
import { FinancialEntitiesProvider } from '../../../../financial-entities/providers/financial-entities.provider.js';
import {
decorateLedgerRecords,
getProfitLossReportAmounts,
type DecoratedLedgerRecord,
} from '../../../../reports/helpers/profit-and-loss.helper.js';
import {
calculateCumulativeRnDExpenses,
calculateTaxAmounts,
} from '../../../../reports/helpers/tax.helper.js';
import { storeInitialGeneratedRecords } from '../../../helpers/ledgrer-storage.helper.js';
import { generateMiscExpensesLedger } from '../../../helpers/misc-expenses-ledger.helper.js';
import { ledgerProtoToRecordsConverter } from '../../../helpers/utils.helper.js';
import { LedgerProvider } from '../../../providers/ledger.provider.js';
export const generateLedgerRecordsForTaxExpenses: ResolverFn<
Maybe<ResolversTypes['GeneratedLedgerRecords']>,
ResolversParentTypes['Charge'],
GraphQLModules.Context,
{ insertLedgerRecordsIfNotExists: boolean }
> = async (charge, { insertLedgerRecordsIfNotExists }, { injector }) => {
try {
const {
defaultLocalCurrency,
authorities: { taxBusinessId, taxExpensesTaxCategoryId },
} = await injector.get(AdminContextProvider).getVerifiedAdminContext();
if (!charge.user_description) {
return {
__typename: 'CommonError',
message: `Tax expenses charge must include user description with designated year`,
};
}
// get revaluation date - search for "yyyy-mm-dd" in description
const dateRegex = /\b(\d{4})\b/;
const matches = charge.user_description.match(dateRegex);
if (!matches?.length) {
return {
__typename: 'CommonError',
message: `Tax expenses charge description must include full year`,
};
}
const stringYear = matches[0];
const year = Number(stringYear);
if (Number.isNaN(year) || year < 2000 || year > new Date().getFullYear()) {
return {
__typename: 'CommonError',
message: `Tax expenses charge description must include valid year (2000 - current year)`,
};
}
const from = new Date(year - 2, 0, 1, 0, 0, 1);
const to = new Date(year + 1, 0, 0);
const ledgerRecords = await injector
.get(LedgerProvider)
.getLedgerRecordsByDates({ fromDate: from, toDate: to });
const financialEntities = await injector
.get(FinancialEntitiesProvider)
.getAllFinancialEntities();
const financialEntitiesDict = new Map(financialEntities.map(entity => [entity.id, entity]));
const decoratedLedgerByYear = new Map<number, DecoratedLedgerRecord[]>();
for (let year = from.getFullYear(); year <= to.getFullYear(); year++) {
if (from.getFullYear() > to.getFullYear()) {
break;
}
decoratedLedgerByYear.set(year, []);
}
ledgerRecords.map(record => {
const year = record.invoice_date.getFullYear();
const [decoratedRecord] = decorateLedgerRecords([record], financialEntitiesDict);
decoratedLedgerByYear.get(year)?.push(decoratedRecord);
});
const profitLossByYear = new Map<number, ReturnType<typeof getProfitLossReportAmounts>>();
function getProfitLossReportAmountsByYear(year: number) {
let amounts = profitLossByYear.get(year);
if (!amounts) {
const decoratedLedgerRecords = decoratedLedgerByYear.get(year) ?? [];
amounts = getProfitLossReportAmounts(decoratedLedgerRecords);
profitLossByYear.set(year, amounts);
}
return amounts;
}
const cumulativeResearchAndDevelopmentExpensesAmount = calculateCumulativeRnDExpenses(
year,
decoratedLedgerByYear,
profitLossByYear,
);
const taxableCumulativeResearchAndDevelopmentExpensesAmount =
cumulativeResearchAndDevelopmentExpensesAmount / 3;
const { researchAndDevelopmentExpenses, profitBeforeTax } =
getProfitLossReportAmountsByYear(year);
const { annualTaxExpenseAmount } = await calculateTaxAmounts(
injector,
year,
decoratedLedgerByYear.get(year) ?? [],
researchAndDevelopmentExpenses.amount * -1,
taxableCumulativeResearchAndDevelopmentExpensesAmount,
profitBeforeTax.amount,
);
const ledgerEntry: LedgerProto = {
id: EMPTY_UUID,
invoiceDate: new Date(year, 11, 31),
valueDate: new Date(year, 11, 31),
currency: defaultLocalCurrency,
isCreditorCounterparty: true,
creditAccountID1: taxBusinessId,
debitAccountID1: taxExpensesTaxCategoryId,
localCurrencyCreditAmount1: Math.abs(annualTaxExpenseAmount),
localCurrencyDebitAmount1: Math.abs(annualTaxExpenseAmount),
description: `Tax expenses for ${year}`,
ownerId: charge.owner_id,
chargeId: charge.id,
currencyRate: 1,
};
const ledgerEntries = [ledgerEntry];
// generate ledger from misc expenses
await generateMiscExpensesLedger(charge, injector).then(entries => {
entries.map(entry => {
entry.ownerId = charge.owner_id;
ledgerEntries.push(entry);
});
});
if (insertLedgerRecordsIfNotExists) {
await storeInitialGeneratedRecords(charge.id, ledgerEntries, injector);
}
return {
records: ledgerProtoToRecordsConverter(ledgerEntries),
charge,
balance: {
isBalanced: true,
unbalancedEntities: [],
balanceSum: 0,
financialEntities: [],
},
errors: [],
};
} catch (e) {
return {
__typename: 'CommonError',
message: `Failed to generate ledger records for charge ID="${charge.id}"\n${e}`,
};
}
};