UNPKG

@accounter/server

Version:
690 lines • 32.1 kB
import { GraphQLError } from 'graphql'; import { EMPTY_UUID } from '../../../shared/constants.js'; import { ChargeSortByField, ChargeTypeEnum } from '../../../shared/enums.js'; import { errorSimplifier } from '../../../shared/errors.js'; import { AdminContextProvider } from '../../admin-context/providers/admin-context.provider.js'; import { ScopeProvider } from '../../auth/providers/scope.provider.js'; import { BusinessTripsProvider } from '../../business-trips/providers/business-trips.provider.js'; import { isInvoice, isReceipt } from '../../documents/helpers/common.helper.js'; import { DocumentsProvider } from '../../documents/providers/documents.provider.js'; import { IssuedDocumentsProvider } from '../../documents/providers/issued-documents.provider.js'; import { ledgerGenerationByCharge } from '../../ledger/helpers/ledger-by-charge-type.helper.js'; import { isChargeLocked } from '../../ledger/helpers/ledger-lock.js'; import { ledgerRecordsGenerationFullMatchComparison } from '../../ledger/helpers/ledgrer-storage.helper.js'; import { LedgerProvider } from '../../ledger/providers/ledger.provider.js'; import { MiscExpensesProvider } from '../../misc-expenses/providers/misc-expenses.provider.js'; import { ChargeTagsProvider } from '../../tags/providers/charge-tags.provider.js'; import { TransactionsProvider } from '../../transactions/providers/transactions.provider.js'; import { batchUpdateChargesBusinessTrip, batchUpdateChargesTags, batchUpdateChargesYearsSpread, } from '../helpers/batch-update-charges.js'; import { getChargeType, normalizeDbType } from '../helpers/charge-type.js'; import { getChargeBusinesses, getChargeDocumentsMeta, getChargeLedgerMeta, getChargeTransactionsMeta, } from '../helpers/common.helper.js'; import { deleteCharges } from '../helpers/delete-charges.helper.js'; import { mergeChargesExecutor } from '../helpers/merge-charges.helper.js'; import { ChargeSpreadProvider } from '../providers/charge-spread.provider.js'; import { ChargesProvider } from '../providers/charges.provider.js'; import { commonChargeFields, commonDocumentsFields } from './common.js'; async function chargeTypeChecker(chargeType, charge, injector) { try { return (await getChargeType(charge, injector)) === chargeType; } catch (error) { throw errorSimplifier('Failed to determine charge type', error); } } export const chargesResolvers = { Query: { charge: async (_, { chargeId }, { injector }) => { try { const charge = await injector.get(ChargesProvider).getChargeByIdLoader.load(chargeId); if (!charge) { throw new GraphQLError(`Charge ID="${chargeId}" not found`); } return charge; } catch (error) { throw errorSimplifier('Failed to fetch charge by ID', error); } }, chargesByIDs: async (_, { chargeIDs }, { injector }) => { if (chargeIDs.length === 0) { return []; } try { const dbCharges = await injector .get(ChargesProvider) .getChargeByIdLoader.loadMany(chargeIDs); if (!dbCharges) { if (chargeIDs.length === 1) { throw new GraphQLError(`Charge ID="${chargeIDs[0]}" not found`); } else { throw new GraphQLError(`Couldn't find any charges`); } } const charges = chargeIDs.map(id => { const charge = dbCharges.find(charge => charge && !(charge instanceof Error) && charge.id === id); if (!charge) { throw new GraphQLError(`Charge ID="${id}" not found`); } return charge; }); return charges; } catch (error) { throw errorSimplifier('Failed to fetch charges by IDs', error); } }, allCharges: async (_, { filters, page, limit }, { injector }) => { // handle sort column let sortColumn = 'event_date'; switch (filters?.sortBy?.field) { case ChargeSortByField.Amount: sortColumn = 'event_amount'; break; case ChargeSortByField.AbsAmount: sortColumn = 'abs_event_amount'; break; case ChargeSortByField.Date: sortColumn = 'event_date'; break; } const charges = await injector .get(ChargesProvider) .getChargesByFilters({ ownerIds: filters?.byOwners, fromAnyDate: filters?.fromAnyDate, toAnyDate: filters?.toAnyDate, sortColumn, asc: filters?.sortBy?.asc !== false, chargeType: filters?.chargesType, businessIds: filters?.byBusinesses, businessTripIds: filters?.byBusinessTrips, withMissingCounterparty: filters?.withMissingCounterparty, withoutInvoice: filters?.withoutInvoice, withoutReceipt: filters?.withoutReceipt, withoutDocuments: filters?.withoutDocuments, withOpenDocuments: filters?.withOpenDocuments, withoutTransactions: filters?.withoutTransactions, withoutLedger: filters?.withoutLedger, freeText: filters?.freeText?.trim().toLowerCase(), tags: filters?.byTags, accountantStatuses: filters?.accountantStatus, }) .catch(error => { throw errorSimplifier('Error fetching charges', error); }); // charge __typename is resolved dynamically, so filter by concrete type post-fetch let filteredCharges = charges; if (filters?.byChargeTypes?.length) { const wantedTypes = new Set(filters.byChargeTypes?.map(normalizeDbType)); const chargeTypes = await Promise.all(charges.map(charge => getChargeType(charge, injector).catch(error => { throw errorSimplifier('Failed to determine charge type', error); }))); filteredCharges = charges.filter((_, index) => wantedTypes.has(chargeTypes[index])); } const pageCharges = filteredCharges.slice(page * limit, (page + 1) * limit); return { __typename: 'PaginatedCharges', nodes: pageCharges, pageInfo: { totalPages: Math.ceil(filteredCharges.length / limit), totalRecords: filteredCharges.length, }, }; }, chargesWithMissingRequiredInfo: async (_, { page, limit }, { injector }) => { try { const chargeIds = new Set(); // get by transactions const getByTransactionsPromise = injector .get(TransactionsProvider) .getTransactionsByMissingRequiredInfo() .then(transactions => { transactions.map(transaction => { if (transaction.charge_id) { chargeIds.add(transaction.charge_id); } }); }); // get by documents const getByDocumentsPromise = injector .get(DocumentsProvider) .getDocumentsByMissingRequiredInfo() .then(documents => { documents.map(document => { if (document.charge_id) { chargeIds.add(document.charge_id); } }); }); // get by charge const getByChargesPromise = injector .get(ChargesProvider) .getChargesByMissingRequiredInfo() .then(charges => { charges.map(charge => { if (charge.id) { chargeIds.add(charge.id); } }); }); await Promise.all([getByTransactionsPromise, getByDocumentsPromise, getByChargesPromise]); const charges = await Promise.all(Array.from(chargeIds).map(async (id) => { const [charge, { transactionsMinDebitDate, transactionsMinEventDate }, { ledgerMinInvoiceDate, ledgerMinValueDate }, { documentsMinDate },] = await Promise.all([ injector .get(ChargesProvider) .getChargeByIdLoader.load(id) .then(charge => { if (!charge) { throw new GraphQLError(`Charge ID="${id}" not found`); } return charge; }) .catch(e => { const message = `Error loading charge ID="${id}"`; console.error(`${message}: ${e}`); throw new GraphQLError(message); }), getChargeTransactionsMeta(id, injector), getChargeLedgerMeta(id, injector), getChargeDocumentsMeta(id, injector), ]); return { ...charge, transactionsMinDebitDate, transactionsMinEventDate, ledgerMinInvoiceDate, ledgerMinValueDate, documentsMinDate, }; })); const readScope = await injector.get(ScopeProvider).getReadScope(); const pageCharges = charges .filter(charge => !!charge.owner_id && readScope.includes(charge.owner_id)) .sort((chargeA, chargeB) => { const dateA = (chargeA.documentsMinDate || chargeA.transactionsMinDebitDate || chargeA.transactionsMinEventDate || chargeA.ledgerMinValueDate || chargeA.ledgerMinInvoiceDate)?.getTime() ?? 0; const dateB = (chargeB.documentsMinDate || chargeB.transactionsMinDebitDate || chargeB.transactionsMinEventDate || chargeB.ledgerMinValueDate || chargeB.ledgerMinInvoiceDate)?.getTime() ?? 0; if (dateA > dateB) { return -1; } if (dateA < dateB) { return 1; } return chargeA.id.localeCompare(chargeB.id); }) .slice(page * limit - limit, page * limit); return { __typename: 'PaginatedCharges', nodes: pageCharges, pageInfo: { totalPages: Math.ceil(charges.length / limit), totalRecords: charges.length, }, }; } catch (error) { throw errorSimplifier('Failed to fetch charges with missing required info', error); } }, }, Mutation: { updateCharge: async (_, { chargeId, fields }, { injector }) => { const adjustedFields = { accountantStatus: fields.accountantApproval, type: fields.type, isProperty: fields.isDecreasedVAT, isInvoicePaymentDifferentCurrency: fields.isInvoicePaymentDifferentCurrency, userDescription: fields.userDescription, taxCategoryId: fields.defaultTaxCategoryID, optionalVAT: fields.optionalVAT, optionalDocuments: fields.optionalDocuments, chargeId, }; try { const updatedCharge = await injector .get(ChargesProvider) .updateCharge({ ...adjustedFields }) .catch(e => { console.error(e); throw new GraphQLError(`Error updating charge ID="${chargeId}"`); }); if (!updatedCharge) { throw new Error(`Charge ID="${chargeId}" not found`); } const indirectUpdatesPromises = []; // handle tags if (fields?.tags) { const newTagIDs = fields.tags.map(t => t.id); const updateTagsPromise = async () => { const tagsPromises = []; const pastTags = await injector .get(ChargeTagsProvider) .getTagsByChargeIDLoader.load(chargeId) .then(res => res.map(tag => tag.id)); // clear removed tags const tagsToRemove = pastTags.filter(tagId => !newTagIDs.includes(tagId)); if (tagsToRemove.length) { tagsPromises.push(injector .get(ChargeTagsProvider) .clearChargeTags({ chargeId, tagIDs: tagsToRemove }) .catch(e => { console.error(e); throw new GraphQLError(`Error clearing tags IDs="${tagsToRemove}" to charge ID="${chargeId}"`); })); } // add new tags const tagIDsToAdd = newTagIDs.filter(tagId => !pastTags.includes(tagId)); for (const tagId of tagIDsToAdd) { tagsPromises.push(injector .get(ChargeTagsProvider) .insertChargeTag({ chargeId, tagId }) .catch(e => { console.error(e); throw new GraphQLError(`Error adding tag ID="${tagId}" to charge ID="${chargeId}"`); })); } await Promise.all(tagsPromises); }; indirectUpdatesPromises.push(updateTagsPromise()); } // handle business trip if (fields?.businessTripID) { indirectUpdatesPromises.push(injector .get(BusinessTripsProvider) .updateChargeBusinessTrip(chargeId, fields.businessTripID === EMPTY_UUID ? null : fields.businessTripID) .catch(e => { console.error(e); throw new GraphQLError(`Error updating business trip for charge ID="${chargeId}"`); })); } // handle charge spread if (fields?.yearsOfRelevance?.length) { const updateSpreadRecords = async () => { await injector .get(ChargeSpreadProvider) .deleteAllChargeSpreadByChargeIds({ chargeIds: [chargeId] }) .catch(e => { console.error(e); throw new GraphQLError(`Error deleting spread records for charge ID="${chargeId}"`); }); return injector .get(ChargeSpreadProvider) .insertChargeSpread({ chargeSpread: fields.yearsOfRelevance.map(record => ({ ownerId: updatedCharge.owner_id, chargeId, yearOfRelevance: record.year, amount: record.amount, })), }) .catch(e => { console.error(e); throw new GraphQLError(`Error inserting spread records for charge ID="${chargeId}"`); }); }; indirectUpdatesPromises.push(updateSpreadRecords()); } await Promise.all(indirectUpdatesPromises); return { charge: updatedCharge }; } catch (e) { let message = 'Error updating charges'; if (e instanceof GraphQLError) { message = e.message; } else { console.error(e); } return { __typename: 'CommonError', message, }; } }, batchUpdateCharges: async (_, { chargeIds, fields }, { injector }) => { if (!chargeIds || chargeIds.length === 0) { return { __typename: 'CommonError', message: 'No charges provided for update', }; } const adjustedFields = { accountantStatus: fields.accountantApproval, type: fields.type, isProperty: fields.isDecreasedVAT, isInvoicePaymentDifferentCurrency: fields.isInvoicePaymentDifferentCurrency, userDescription: fields.userDescription, taxCategoryId: fields.defaultTaxCategoryID, optionalVAT: fields.optionalVAT, optionalDocuments: fields.optionalDocuments, chargeIds, }; try { const res = await injector .get(ChargesProvider) .batchUpdateCharges({ ...adjustedFields }) .catch(e => { console.error(e); throw new GraphQLError(`Error updating charges (IDs "${chargeIds.join('", "')}")`); }); const updatedCharges = await injector .get(ChargesProvider) .getChargeByIdLoader.loadMany(res.map(c => c.id)) .catch(e => { console.error(e); throw new GraphQLError(`Error loading updated charges (IDs "${chargeIds.join('", "')}")`); }); // check if all charges were updated successfully if (!updatedCharges) { throw new GraphQLError(`Updated charges not found (IDs "${chargeIds.join('", "')}")`); } if (updatedCharges.some(charge => !charge || charge instanceof Error)) { throw new GraphQLError(`Some updated charges not found (IDs "${chargeIds.join('", "')}")`); } // Type assertion as error handling is done above const charges = updatedCharges; const indirectUpdatesPromises = []; // handle tags if (fields?.tags) { indirectUpdatesPromises.push(batchUpdateChargesTags(injector, fields.tags.map(t => t.id), chargeIds)); } // handle business trip if (fields?.businessTripID) { indirectUpdatesPromises.push(batchUpdateChargesBusinessTrip(injector, fields.businessTripID, chargeIds)); } // handle charge spread if (fields?.yearsOfRelevance?.length) { indirectUpdatesPromises.push(batchUpdateChargesYearsSpread(injector, fields.yearsOfRelevance, chargeIds)); } await Promise.all(indirectUpdatesPromises); return { charges }; } catch (e) { let message = 'Error updating charges'; if (e instanceof GraphQLError) { message = e.message; } else { console.error(e); } return { __typename: 'CommonError', message, }; } }, mergeCharges: async (_, { baseChargeID, chargeIdsToMerge, fields }, { injector }) => { try { const charge = await injector.get(ChargesProvider).getChargeByIdLoader.load(baseChargeID); if (!charge) { throw new Error(`Charge not found`); } if (fields) { const adjustedFields = { accountantStatus: fields?.accountantApproval, type: fields?.type, isProperty: fields?.isDecreasedVAT, isInvoicePaymentDifferentCurrency: fields?.isInvoicePaymentDifferentCurrency, optionalVAT: fields?.optionalVAT, optionalDocuments: fields?.optionalDocuments, userDescription: fields?.userDescription, chargeId: baseChargeID, }; await injector .get(ChargesProvider) .updateCharge({ ...adjustedFields }) .catch(e => { const message = `Failed to update charge ID="${baseChargeID}" before merge`; console.error(`${message}: ${e}`); if (e instanceof GraphQLError) { throw e; } throw new Error(message); }); } await mergeChargesExecutor(chargeIdsToMerge, baseChargeID, injector); return { charge }; } catch (e) { if (e instanceof GraphQLError) { throw e; } console.error(e); return { __typename: 'CommonError', message: e?.message ?? e?.errors.map(e => e.message).toString() ?? 'Unknown error', }; } }, deleteCharge: async (_, { chargeId }, { injector }) => { try { const [charge, transactions, documents] = await Promise.all([ injector.get(ChargesProvider).getChargeByIdLoader.load(chargeId), injector.get(TransactionsProvider).transactionsByChargeIDLoader.load(chargeId), injector.get(DocumentsProvider).getDocumentsByChargeIdLoader.load(chargeId), ]); if (!charge) { throw new GraphQLError(`Charge ID="${chargeId}" not found`); } if (documents.length > 0 || transactions.length > 0) { throw new GraphQLError(`Charge ID="${chargeId}" has linked documents/transactions`); } await deleteCharges([chargeId], injector); return true; } catch (e) { if (e instanceof GraphQLError) { throw e; } console.error(e); throw new GraphQLError(`Error deleting charge ID="${chargeId}"`); } }, }, UpdateChargeResult: { __resolveType: (obj, _context, _info) => { if (('__typename' in obj && obj.__typename === 'CommonError') || 'message' in obj) return 'CommonError'; return 'UpdateChargeSuccessfulResult'; }, }, BatchUpdateChargesResult: { __resolveType: (obj, _context, _info) => { if (('__typename' in obj && obj.__typename === 'CommonError') || 'message' in obj) return 'CommonError'; return 'BatchUpdateChargesSuccessfulResult'; }, }, MergeChargeResult: { __resolveType: (obj, _context, _info) => { if (('__typename' in obj && obj.__typename === 'CommonError') || 'message' in obj) return 'CommonError'; return 'MergeChargeSuccessfulResult'; }, }, CommonCharge: { __isTypeOf: async (DbCharge, { injector }) => chargeTypeChecker(ChargeTypeEnum.Common, DbCharge, injector), ...commonChargeFields, }, ConversionCharge: { __isTypeOf: async (DbCharge, { injector }) => chargeTypeChecker(ChargeTypeEnum.Conversion, DbCharge, injector), ...commonChargeFields, }, SalaryCharge: { __isTypeOf: async (DbCharge, { injector }) => chargeTypeChecker(ChargeTypeEnum.Salary, DbCharge, injector), ...commonChargeFields, }, InternalTransferCharge: { __isTypeOf: async (DbCharge, { injector }) => chargeTypeChecker(ChargeTypeEnum.InternalTransfer, DbCharge, injector), ...commonChargeFields, }, DividendCharge: { __isTypeOf: async (DbCharge, { injector }) => chargeTypeChecker(ChargeTypeEnum.Dividend, DbCharge, injector), ...commonChargeFields, }, BusinessTripCharge: { __isTypeOf: async (DbCharge, { injector }) => chargeTypeChecker(ChargeTypeEnum.BusinessTrip, DbCharge, injector), ...commonChargeFields, }, MonthlyVatCharge: { __isTypeOf: async (DbCharge, { injector }) => chargeTypeChecker(ChargeTypeEnum.MonthlyVat, DbCharge, injector), ...commonChargeFields, }, BankDepositCharge: { __isTypeOf: async (DbCharge, { injector }) => chargeTypeChecker(ChargeTypeEnum.BankDeposit, DbCharge, injector), ...commonChargeFields, }, ForeignSecuritiesCharge: { __isTypeOf: async (DbCharge, { injector }) => chargeTypeChecker(ChargeTypeEnum.ForeignSecurities, DbCharge, injector), ...commonChargeFields, }, CreditcardBankCharge: { __isTypeOf: async (DbCharge, { injector }) => chargeTypeChecker(ChargeTypeEnum.CreditcardBankCharge, DbCharge, injector), ...commonChargeFields, }, Invoice: { ...commonDocumentsFields, }, InvoiceReceipt: { ...commonDocumentsFields, }, CreditInvoice: { ...commonDocumentsFields, }, Proforma: { ...commonDocumentsFields, }, Unprocessed: { ...commonDocumentsFields, }, OtherDocument: { ...commonDocumentsFields, }, Receipt: { ...commonDocumentsFields, }, ChargeMetadata: { createdAt: DbCharge => DbCharge.created_at, updatedAt: DbCharge => DbCharge.updated_at, invoicesCount: async (DbCharge, _, { injector }) => { try { return injector .get(DocumentsProvider) .getDocumentsByChargeIdLoader.load(DbCharge.id) .then(docs => docs.filter(doc => isInvoice(doc.type)).length); } catch (err) { throw errorSimplifier('Error loading invoices', err); } }, receiptsCount: async (DbCharge, _, { injector }) => { try { return injector .get(DocumentsProvider) .getDocumentsByChargeIdLoader.load(DbCharge.id) .then(docs => docs.filter(doc => isReceipt(doc.type)).length); } catch (err) { throw errorSimplifier('Error loading receipts', err); } }, documentsCount: async (DbCharge, _, { injector }) => { try { return injector .get(DocumentsProvider) .getDocumentsByChargeIdLoader.load(DbCharge.id) .then(docs => docs.length); } catch (err) { throw errorSimplifier('Error loading documents', err); } }, openDocuments: async (DbCharge, _, { injector }) => { try { const res = await injector .get(IssuedDocumentsProvider) .getIssuedDocumentsStatusByChargeIdLoader.load(DbCharge.id); return res?.open_docs_flag ?? false; } catch (error) { throw errorSimplifier('Error loading open documents status', error); } }, transactionsCount: async (DbCharge, _, { injector }) => { const transactions = await injector .get(TransactionsProvider) .transactionsByChargeIDLoader.load(DbCharge.id) .catch(error => { throw errorSimplifier('Error loading transactions', error); }); return transactions.length; }, ledgerCount: async (DbCharge, _, { injector }) => injector .get(LedgerProvider) .getLedgerRecordsByChargesIdLoader.load(DbCharge.id) .catch(error => { throw errorSimplifier('Error loading ledger records', error); }) .then(records => records.length), invalidLedger: async (DbCharge, _, context, info) => { const { injector } = context; try { const { ledgerLock } = await injector.get(AdminContextProvider).getVerifiedAdminContext(); if (await isChargeLocked(DbCharge, injector, ledgerLock)) { return 'VALID'; } const generatedLedgerPromise = ledgerGenerationByCharge(DbCharge, { insertLedgerRecordsIfNotExists: false }, context, info); const currentRecordPromise = injector .get(LedgerProvider) .getLedgerRecordsByChargesIdLoader.load(DbCharge.id); const [currentRecord, generated] = await Promise.all([ currentRecordPromise, generatedLedgerPromise, ]); if (!generated || 'message' in generated || generated.balance?.isBalanced === false || generated.errors?.length > 0) { return 'INVALID'; } const fullMatching = ledgerRecordsGenerationFullMatchComparison(currentRecord, generated.records); return fullMatching.isFullyMatched ? 'VALID' : 'DIFF'; } catch (err) { console.error(err); return 'INVALID'; } }, miscExpensesCount: async (DbCharge, _, { injector }) => { try { return injector .get(MiscExpensesProvider) .getExpensesByChargeIdLoader.load(DbCharge.id) .then(res => res.length); } catch (err) { throw errorSimplifier('Error loading misc expenses', err); } }, optionalBusinesses: async (DbCharge, _, { injector }) => { try { const { allBusinessIds } = await getChargeBusinesses(DbCharge.id, injector); return allBusinessIds.length > 1 ? allBusinessIds : []; } catch (error) { throw errorSimplifier('Failed to fetch optional businesses', error); } }, }, }; //# sourceMappingURL=charges.resolver.js.map