@accounter/server
Version:
Accounter GraphQL server
296 lines (262 loc) • 10.5 kB
text/typescript
import { format } from 'date-fns';
import type {
Maybe,
ResolverFn,
ResolversParentTypes,
ResolversTypes,
} from '../../../../__generated__/types.js';
import {
dateToTimelessDateString,
getMonthFromDescription,
} from '../../../../shared/helpers/index.js';
import type { LedgerProto } from '../../../../shared/types/index.js';
import { AdminContextProvider } from '../../../admin-context/providers/admin-context.provider.js';
import { getChargeTransactionsMeta } from '../../../charges/helpers/common.helper.js';
import { ExchangeProvider } from '../../../exchange-rates/providers/exchange.provider.js';
import { RawVatReportRecord } from '../../../reports/helpers/vat-report.helper.js';
import { getVatRecords } from '../../../reports/resolvers/get-vat-records.resolver.js';
import { TransactionsProvider } from '../../../transactions/providers/transactions.provider.js';
import { storeInitialGeneratedRecords } from '../../helpers/ledgrer-storage.helper.js';
import { generateMiscExpensesLedger } from '../../helpers/misc-expenses-ledger.helper.js';
import { getVatDataFromVatReportRecords } from '../../helpers/monthly-vat-ledger-generation.helper.js';
import {
generatePartialLedgerEntry,
getFinancialAccountTaxCategoryId,
getLedgerBalanceInfo,
LedgerError,
ledgerProtoToRecordsConverter,
updateLedgerBalanceByEntry,
validateTransactionRequiredVariables,
} from '../../helpers/utils.helper.js';
import { UnbalancedBusinessesProvider } from '../../providers/unbalanced-businesses.provider.js';
export const generateLedgerRecordsForMonthlyVat: ResolverFn<
Maybe<ResolversTypes['GeneratedLedgerRecords']>,
ResolversParentTypes['Charge'],
GraphQLModules.Context,
{ insertLedgerRecordsIfNotExists: boolean }
> = async (charge, { insertLedgerRecordsIfNotExists }, { injector }, __) => {
const chargeId = charge.id;
const errors: Set<string> = new Set();
try {
const {
defaultLocalCurrency,
authorities: {
vatBusinessId,
inputVatTaxCategoryId,
outputVatTaxCategoryId,
propertyOutputVatTaxCategoryId,
},
general: {
taxCategories: { balanceCancellationTaxCategoryId },
},
} = await injector.get(AdminContextProvider).getVerifiedAdminContext();
// figure out VAT month
const { transactionsMinDebitDate, transactionsMinEventDate } = await getChargeTransactionsMeta(
chargeId,
injector,
);
const transactionDate = transactionsMinDebitDate ?? transactionsMinEventDate ?? undefined;
if (!charge.user_description) {
errors.add(
`Monthly VAT charge must have description that indicates it's month (ID="${chargeId}")`,
);
}
const vatDates = charge.user_description
? getMonthFromDescription(charge.user_description, transactionDate)
: null;
if (!vatDates?.length) {
errors.add(`Cannot extract charge ID="${chargeId}" VAT month`);
}
// get VAT relevant records
const vatRecordsPromises = (vatDates ?? []).map(async vatDate => {
const [year, month] = (vatDate ?? format(new Date(), 'yyyy-MM')).split('-').map(Number);
const ledgerDate = new Date(year, month, 0);
const monthDate = dateToTimelessDateString(new Date(year, month - 1, 15));
const { income, expenses } = await getVatRecords(
{ filters: { financialEntityId: charge.owner_id, monthDate } },
injector,
);
return {
income,
expenses,
ledgerDate,
vatDate,
};
});
const transactionsPromise = injector
.get(TransactionsProvider)
.transactionsByChargeIDLoader.load(chargeId);
const unbalancedBusinessesPromise = injector
.get(UnbalancedBusinessesProvider)
.getChargeUnbalancedBusinessesByChargeIds.load(chargeId);
const [vatRecords, transactions, unbalancedBusinesses] = await Promise.all([
Promise.all(vatRecordsPromises),
transactionsPromise,
unbalancedBusinessesPromise,
]);
const accountingLedgerEntries: LedgerProto[] = [];
const financialAccountLedgerEntries: LedgerProto[] = [];
const miscExpensesLedgerEntries: LedgerProto[] = [];
const ledgerBalance = new Map<string, { amount: number; entityId: string }>();
// for each transaction, create a ledger record
const mainTransactionsPromises = transactions.map(async preValidatedTransaction => {
try {
const transaction = validateTransactionRequiredVariables(preValidatedTransaction);
if (transaction.currency !== defaultLocalCurrency) {
throw new LedgerError(
`Monthly VAT currency supposed to be local, got ${transaction.currency}`,
);
}
// preparations for core ledger entries
let exchangeRate: number | undefined = undefined;
if (transaction.currency !== defaultLocalCurrency) {
// get exchange rate for currency
exchangeRate = await injector
.get(ExchangeProvider)
.getExchangeRates(
transaction.currency,
defaultLocalCurrency,
transaction.debit_timestamp,
);
}
const partialEntry = generatePartialLedgerEntry(transaction, charge.owner_id, exchangeRate);
const financialAccountTaxCategoryId = await getFinancialAccountTaxCategoryId(
injector,
transaction,
);
const ledgerEntry: LedgerProto = {
...partialEntry,
...(partialEntry.isCreditorCounterparty
? {
creditAccountID1: vatBusinessId,
debitAccountID1: financialAccountTaxCategoryId,
}
: {
creditAccountID1: financialAccountTaxCategoryId,
debitAccountID1: vatBusinessId,
}),
};
financialAccountLedgerEntries.push(ledgerEntry);
updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance, defaultLocalCurrency);
} catch (e) {
if (e instanceof LedgerError) {
errors.add(e.message);
} else {
throw e;
}
}
});
const miscExpensesLedgerPromise = generateMiscExpensesLedger(charge, injector).then(entries => {
entries.map(entry => {
entry.ownerId = charge.owner_id;
miscExpensesLedgerEntries.push(entry);
updateLedgerBalanceByEntry(entry, ledgerBalance, defaultLocalCurrency);
});
});
const isProperty = charge.is_property;
if (isProperty && !propertyOutputVatTaxCategoryId) {
console.error(
`Charge ID="${chargeId}" is marked as property but no Property Output VAT tax category is configured in admin context`,
);
return {
__typename: 'CommonError',
message: `One of the charges is marked as property but no Property Output VAT tax category is configured in admin context`,
};
}
await Promise.all([...mainTransactionsPromises, miscExpensesLedgerPromise]);
for (const { income, expenses, ledgerDate, vatDate } of vatRecords) {
const [incomeVat, roundedIncomeVat] = getVatDataFromVatReportRecords(
income as RawVatReportRecord[],
);
const [expensesVat, roundedExpensesVat] = getVatDataFromVatReportRecords(
expenses as RawVatReportRecord[],
);
const accountingLedgerMaterials: Array<{
amount: number;
taxCategoryId: string;
counterpartyId?: string;
}> = [];
accountingLedgerMaterials.push(
{
amount: roundedIncomeVat,
taxCategoryId: inputVatTaxCategoryId,
counterpartyId: vatBusinessId,
},
{
amount: roundedExpensesVat * -1,
taxCategoryId: isProperty ? propertyOutputVatTaxCategoryId! : outputVatTaxCategoryId,
counterpartyId: vatBusinessId,
},
);
const roundedIncomeVatDiff = Math.round((roundedIncomeVat - incomeVat) * 100) / 100;
if (roundedIncomeVatDiff) {
accountingLedgerMaterials.push({
amount: roundedIncomeVatDiff * -1,
taxCategoryId: inputVatTaxCategoryId,
counterpartyId: balanceCancellationTaxCategoryId,
});
}
const roundedExpensesVatDiff = Math.round((roundedExpensesVat - expensesVat) * 100) / 100;
if (roundedExpensesVatDiff) {
accountingLedgerMaterials.push({
amount: roundedExpensesVatDiff,
taxCategoryId: isProperty ? propertyOutputVatTaxCategoryId! : outputVatTaxCategoryId,
counterpartyId: balanceCancellationTaxCategoryId,
});
}
accountingLedgerMaterials.map(({ amount, taxCategoryId, counterpartyId }) => {
if (amount === 0) {
return;
}
const isCreditorCounterparty = amount > 0;
const ledgerProto: LedgerProto = {
id: chargeId,
invoiceDate: ledgerDate,
valueDate: ledgerDate,
currency: defaultLocalCurrency,
creditAccountID1: isCreditorCounterparty ? counterpartyId : taxCategoryId,
localCurrencyCreditAmount1: Math.abs(amount),
debitAccountID1: isCreditorCounterparty ? taxCategoryId : counterpartyId,
localCurrencyDebitAmount1: Math.abs(amount),
description:
counterpartyId === balanceCancellationTaxCategoryId
? 'Balance cancellation'
: `VAT command ${vatDate}`,
isCreditorCounterparty,
ownerId: charge.owner_id,
chargeId,
};
accountingLedgerEntries.push(ledgerProto);
updateLedgerBalanceByEntry(ledgerProto, ledgerBalance, defaultLocalCurrency);
});
}
const allowedUnbalancedBusinesses = new Set(
unbalancedBusinesses.map(({ business_id }) => business_id),
);
const ledgerBalanceInfo = await getLedgerBalanceInfo(
injector,
ledgerBalance,
errors,
allowedUnbalancedBusinesses,
);
const records = [
...financialAccountLedgerEntries,
...accountingLedgerEntries,
...miscExpensesLedgerEntries,
];
if (insertLedgerRecordsIfNotExists) {
await storeInitialGeneratedRecords(charge.id, records, injector);
}
return {
records: ledgerProtoToRecordsConverter(records),
charge,
balance: ledgerBalanceInfo,
errors: Array.from(errors),
};
} catch (e) {
return {
__typename: 'CommonError',
message: `Failed to generate ledger records for charge ID="${chargeId}"\n${e}`,
};
}
};