@accounter/server
Version:
203 lines (184 loc) • 7.37 kB
text/typescript
import { GraphQLError } from 'graphql';
import { validateCharge } from '@modules/charges/helpers/validate.helper.js';
import { ChargesProvider } from '@modules/charges/providers/charges.provider.js';
import { IGetChargesByFiltersResult } from '@modules/charges/types.js';
import { DocumentsProvider } from '@modules/documents/providers/documents.provider.js';
import { FiatExchangeProvider } from '@modules/exchange-rates/providers/fiat-exchange.provider.js';
import { BusinessesProvider } from '@modules/financial-entities/providers/businesses.provider.js';
import { VAT_REPORT_EXCLUDED_BUSINESS_NAMES } from '@shared/constants';
import {
DocumentType,
type QueryVatReportArgs,
type ResolverFn,
type ResolversParentTypes,
type ResolversTypes,
} from '@shared/gql-types';
import {
adjustTaxRecords,
type RawVatReportRecord,
type VatReportRecordSources,
} from '../helpers/vat-report.helper.js';
export const getVatRecords: ResolverFn<
ResolversTypes['VatReportResult'],
ResolversParentTypes['Query'],
GraphQLModules.Context,
Partial<QueryVatReportArgs>
> = async (_, { filters }, { injector }) => {
try {
const response = {
income: [] as Array<RawVatReportRecord>,
expenses: [] as Array<RawVatReportRecord>,
missingInfo: [] as Array<ResolversTypes['Charge']>,
differentMonthDoc: [] as Array<ResolversTypes['Charge']>,
businessTrips: [] as Array<ResolversTypes['Charge']>,
};
const docsChargesIDs = new Set<string>();
// get all documents by date filters
const relevantDocumentsPromise = injector
.get(DocumentsProvider)
.getDocumentsByFilters({
fromDate: filters?.fromDate,
toDate: filters?.toDate,
ownerIDs: [filters?.financialEntityId],
})
.then(documents =>
documents.filter(doc => {
if (doc.charge_id_new) {
docsChargesIDs.add(doc.charge_id_new);
}
// filter invoice documents with linked charge
if (!doc.charge_id_new || !doc.creditor_id || !doc.debtor_id) {
return false;
}
const isRelevantDoc = ['INVOICE', 'INVOICE_RECEIPT', 'CREDIT_INVOICE'].includes(doc.type);
return isRelevantDoc;
}),
);
// get all businesses
const businessesPromise = injector.get(BusinessesProvider).getAllBusinesses();
const chargesPromise = injector.get(ChargesProvider).getChargesByFilters({
fromDate: filters?.fromDate,
toDate: filters?.toDate,
ownerIds: filters?.financialEntityId ? [filters?.financialEntityId] : undefined,
chargeType: filters?.chargesType,
});
const exchangeRatesPromises = injector
.get(FiatExchangeProvider)
.getExchangeRatesByDates({ fromDate: filters?.fromDate, toDate: filters?.toDate });
const [relevantDocuments, businesses, charges, exchangeRates] = await Promise.all([
relevantDocumentsPromise,
businessesPromise,
chargesPromise,
exchangeRatesPromises,
]);
let docsCharges = charges.filter(charge => docsChargesIDs.has(charge.id));
if (docsCharges.length < docsChargesIDs.size) {
const moreDocsCharges = await injector.get(ChargesProvider).getChargesByFilters({
IDs: Array.from(docsChargesIDs).filter(id => !docsCharges.find(charge => charge.id === id)),
ownerIds: [filters?.financialEntityId],
});
charges.push(...moreDocsCharges);
docsCharges.push(...moreDocsCharges);
}
docsCharges = docsCharges.filter(charge => {
for (const businessId of charge.business_array ?? []) {
if (VAT_REPORT_EXCLUDED_BUSINESS_NAMES.includes(businessId)) {
return false;
}
}
return true;
});
const incomeRecords: Array<VatReportRecordSources> = [];
const expenseRecords: Array<VatReportRecordSources> = [];
const includedChargeIDs = new Set<string>();
relevantDocuments.map(doc => {
// filter charge linked to document
const charge = docsCharges.find(charge => charge.id === doc.charge_id_new);
if (!charge) {
throw new GraphQLError(`for some weird reason no charge found for document ID=${doc.id}`);
}
// filter business linked to document
const counterpartyId =
doc.creditor_id === charge.owner_id
? doc.debtor_id
: doc.debtor_id === charge.owner_id
? doc.creditor_id
: null;
const business = businesses.find(business => business.id === counterpartyId);
if (!business) {
throw new GraphQLError(
`for some weird reason no counterparty found for document ID=${doc.id}`,
);
}
// add charge to income/expense records
if (doc.vat_amount && doc.debtor_id === filters?.financialEntityId) {
includedChargeIDs.add(charge.id);
if (filters?.chargesType !== 'EXPENSE') {
expenseRecords.push({ charge, doc, business });
}
} else if (
doc.vat_amount != null &&
(doc.type === DocumentType.CreditInvoice
? doc.debtor_id === filters?.financialEntityId
: doc.creditor_id === filters?.financialEntityId) &&
Number(doc.total_amount) > 0
) {
includedChargeIDs.add(charge.id);
if (filters?.chargesType !== 'INCOME') {
incomeRecords.push({ charge, doc, business });
}
}
});
if (incomeRecords.length + expenseRecords.length > 0) {
// TODO: what if no exchange dates found?
response.income.push(...adjustTaxRecords(incomeRecords, exchangeRates));
response.expenses.push(...adjustTaxRecords(expenseRecords, exchangeRates));
}
// validate charges for missing info
const validatedCharges = await Promise.all<{
charge: IGetChargesByFiltersResult;
isValid: boolean;
}>(
charges.map(
charge =>
new Promise((resolve, reject) => {
validateCharge(charge, injector)
.then(res => {
if ('isValid' in res) {
resolve({ charge, isValid: res.isValid });
} else {
reject('Error validating charge');
}
})
.catch(reject);
}),
),
);
for (const { charge, isValid } of validatedCharges) {
if (charge.business_trip_id) {
// If valid and has business trip, add to business trips
response.businessTrips.push(charge);
} else if (isValid) {
if (!includedChargeIDs.has(charge.id)) {
// If valid but not yet included, add to charges with different month doc
response.differentMonthDoc.push(charge);
}
} else {
// add to charges with missing info
response.missingInfo.push(charge);
}
}
response.income = response.income.sort(
(a, b) =>
(b.documentDate?.getDate() ?? 0) - (a.documentDate?.getDate() ?? 0) ??
(b.documentDate?.getDate() ?? 0) - (a.documentDate?.getDate() ?? 0),
);
response.expenses = response.expenses.sort(
(a, b) => (b.documentDate?.getDate() ?? 0) - (a.documentDate?.getDate() ?? 0),
);
return response;
} catch (e) {
console.error('Error fetching vat report records:', e);
throw new GraphQLError((e as Error)?.message ?? 'Error fetching vat report records');
}
};