UNPKG

@accounter/server

Version:
245 lines (220 loc) • 8.9 kB
import { endOfDay, lastDayOfMonth, startOfDay, startOfMonth } from 'date-fns'; import { GraphQLError } from 'graphql'; import type { Injector } from 'graphql-modules'; import type { QueryVatReportArgs, ResolversTypes } from '../../../__generated__/types.js'; import { DocumentType } from '../../../shared/enums.js'; import { dateToTimelessDateString } from '../../../shared/helpers/index.js'; import { AdminContextProvider } from '../../admin-context/providers/admin-context.provider.js'; import { BusinessTripsProvider } from '../../business-trips/providers/business-trips.provider.js'; import { getChargeBusinesses } from '../../charges/helpers/common.helper.js'; import { validateCharge } from '../../charges/helpers/validate.helper.js'; import { ChargesProvider } from '../../charges/providers/charges.provider.js'; import { IGetChargesByIdsResult } from '../../charges/types.js'; import { DocumentsProvider } from '../../documents/providers/documents.provider.js'; import { BusinessesProvider } from '../../financial-entities/providers/businesses.provider.js'; import { isRefundCharge } from '../../ledger/helpers/common-charge-ledger.helper.js'; import { adjustTaxRecord, type RawVatReportRecord, type VatReportRecordSources, } from '../helpers/vat-report.helper.js'; export const getVatRecords = async ( { filters }: Partial<QueryVatReportArgs>, injector: Injector, ): Promise<ResolversTypes['VatReportResult']> => { const { authorities: { vatReportExcludedBusinessNames }, } = await injector.get(AdminContextProvider).getVerifiedAdminContext(); 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>(); const reportIssuerId = filters?.financialEntityId; const fromDate = filters?.monthDate ? dateToTimelessDateString(startOfMonth(new Date(filters.monthDate))) : undefined; const toDate = filters?.monthDate ? dateToTimelessDateString(lastDayOfMonth(new Date(filters.monthDate))) : undefined; // get all documents by date filters const relevantDocumentsPromise = injector .get(DocumentsProvider) .getDocumentsByFilters({ fromVatDate: fromDate, toVatDate: toDate, }) .then(documents => documents.filter(doc => { if (doc.charge_id) { docsChargesIDs.add(doc.charge_id); } // filter documents with vat_report_date_override outside of the date range if (doc.vat_report_date_override) { const isBeforeFromDate = fromDate && doc.vat_report_date_override < startOfDay(fromDate); const isAfterToDate = toDate && doc.vat_report_date_override > endOfDay(toDate); if (isBeforeFromDate || isAfterToDate) { return false; } } if (!doc.charge_id || !doc.creditor_id || !doc.debtor_id) { // filter invoice documents with linked charge 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, toDate, ownerIds: reportIssuerId ? [reportIssuerId] : undefined, chargeType: filters?.chargesType, }); const [relevantDocuments, businesses, charges] = await Promise.all([ relevantDocumentsPromise, businessesPromise, chargesPromise, ]); 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: [reportIssuerId], }); charges.push(...moreDocsCharges); docsCharges.push(...moreDocsCharges); } const extendedCharges = await Promise.all( charges.map(async charge => { const { allBusinessIds } = await getChargeBusinesses(charge.id, injector); return { ...charge, allBusinessIds, }; }), ); docsCharges = extendedCharges.filter(charge => { for (const businessId of charge.allBusinessIds) { if (vatReportExcludedBusinessNames.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); 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 === doc.owner_id ? doc.debtor_id : doc.debtor_id === doc.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}`, ); } const isReimbursement = isRefundCharge(charge.user_description); // add charge to income/expense records if ( doc.vat_amount && (doc.type === DocumentType.CreditInvoice ? doc.creditor_id : doc.debtor_id) === reportIssuerId ) { 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 === reportIssuerId : doc.creditor_id === reportIssuerId) && Number(doc.total_amount) > 0 && !isReimbursement ) { 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? await Promise.all([ ...incomeRecords.map(async rawRecord => adjustTaxRecord(rawRecord, injector).then(res => { response.income.push(res); }), ), ...expenseRecords.map(async rawRecord => adjustTaxRecord(rawRecord, injector).then(res => { response.expenses.push(res); }), ), ]); } // validate charges for missing info const validatedCharges = await Promise.all<{ charge: IGetChargesByIdsResult; isValid: boolean; businessTripId: string | null; }>( charges.map(async charge => { const [validation, businessTrip] = await Promise.all([ validateCharge(charge, injector), injector.get(BusinessTripsProvider).getBusinessTripsByChargeIdLoader.load(charge.id), ]); if (!('isValid' in validation)) { throw new Error('Error validating charge'); } return { charge, isValid: validation.isValid, businessTripId: businessTrip?.id ?? null, }; }), ); for (const { charge, isValid, businessTripId } of validatedCharges) { if (businessTripId) { // 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), ); 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'); } };