UNPKG

@accounter/server

Version:
533 lines (504 loc) • 19.1 kB
import { GraphQLError } from 'graphql'; import type { Resolvers } from '../../../__generated__/types.js'; import { EMPTY_UUID } from '../../../shared/constants.js'; import { DocumentType } from '../../../shared/enums.js'; import { AdminContextProvider } from '../../admin-context/providers/admin-context.provider.js'; import { GoogleDriveProvider } from '../../app-providers/google-drive/google-drive.provider.js'; import { GreenInvoiceClientProvider } from '../../app-providers/green-invoice-client.js'; import { deleteCharges } from '../../charges/helpers/delete-charges.helper.js'; import { ChargesProvider } from '../../charges/providers/charges.provider.js'; import type { IGetChargesByIdsResult } from '../../charges/types.js'; import { TransactionsProvider } from '../../transactions/providers/transactions.provider.js'; import { getDocumentFromFile } from '../helpers/upload.helper.js'; import { DocumentsProvider } from '../providers/documents.provider.js'; import { IssuedDocumentsProvider } from '../providers/issued-documents.provider.js'; import type { DocumentsModule, IGetAllDocumentsResult, IInsertDocumentsParams, IUpdateDocumentParams, } from '../types.js'; import { commonChargeFields, commonDocumentsFields, commonFinancialDocumentsFields, } from './common.js'; export const documentsResolvers: DocumentsModule.Resolvers & Pick< Resolvers, | 'UpdateDocumentResult' | 'InsertDocumentResult' | 'UploadDocumentResult' | 'Document' | 'FinancialDocument' > = { Query: { documents: async (_, __, { injector }) => { const dbDocs = await injector.get(DocumentsProvider).getAllDocuments(); return dbDocs; }, documentsByFilters: async (_, { filters }, { injector }) => { const dbDocs = await injector.get(DocumentsProvider).getDocumentsByExtendedFilters(filters); return dbDocs; }, documentById: async (_, { documentId }, { injector }) => { const doc = await injector.get(DocumentsProvider).getDocumentsByIdLoader.load(documentId); return doc ?? null; }, recentDocumentsByBusiness: async (_, { businessId, limit }, { injector }) => { const businessDocs = await injector .get(DocumentsProvider) .getDocumentsByBusinessIdLoader.load(businessId); if (!businessDocs?.length) { return []; } const sortedDocs = [...businessDocs].sort( (a, b) => (b.date ?? b.created_at).getTime() - (a.date ?? a.created_at).getTime(), ); return sortedDocs.slice(0, limit ?? 7); }, recentDocumentsByClient: async (_, { clientId, limit }, { injector }) => { const clientDocs = await injector .get(IssuedDocumentsProvider) .getIssuedDocumentsByClientIdLoader.load(clientId); if (!clientDocs?.length) { return []; } const sortedDocs = [...clientDocs].sort( (a, b) => (b.date ?? b.created_at).getTime() - (a.date ?? a.created_at).getTime(), ); return sortedDocs.slice(0, limit ?? 7); }, recentIssuedDocumentsByType: async (_, { documentType, limit = 3 }, { injector }) => { const docs = await injector .get(IssuedDocumentsProvider) .getIssuedDocumentsByType({ type: documentType, limit }); return docs; }, }, Mutation: { uploadDocument: async (_, { file, chargeId }, { injector }) => { try { const newDocument = await getDocumentFromFile(injector, file, chargeId); const [document] = await injector .get(DocumentsProvider) .insertDocuments({ documents: [newDocument] }); return { document }; } catch (e) { const message = 'Error uploading document'; console.error(`${message}: ${e}`); return { __typename: 'CommonError', message, }; } }, batchUploadDocuments: async (_, { documents, isSensitive, chargeId }, { injector }) => { if (!chargeId) { const { ownerId } = await injector.get(AdminContextProvider).getVerifiedAdminContext(); // generate new charge const newCharge = await injector.get(ChargesProvider).generateCharge({ ownerId, userDescription: 'New uploaded documents', }); if (!newCharge) { throw new GraphQLError(`Failed to generate new charge for new document`); } chargeId = newCharge.id; } const newDocuments: Array<IInsertDocumentsParams['documents'][number]> = []; await Promise.all( documents.map(async document => { // get new document data const newDocument = await getDocumentFromFile(injector, document, chargeId, isSensitive); newDocuments.push(newDocument); }), ); const res = await injector .get(DocumentsProvider) .insertDocuments({ documents: newDocuments }); return res.map(document => ({ document: document as IGetAllDocumentsResult })); }, batchUploadDocumentsFromGoogleDrive: async ( _, { sharedFolderUrl, chargeId, isSensitive }, { injector }, ) => { const isValidGoogleDriveUrl = (url: string): boolean => { try { const parsedUrl = new URL(url); return ( parsedUrl.hostname === 'drive.google.com' && parsedUrl.pathname.includes('/folders/') ); } catch { return false; } }; if (!isValidGoogleDriveUrl(sharedFolderUrl)) { throw new GraphQLError('Invalid Google Drive folder URL'); } const files = await injector .get(GoogleDriveProvider) .fetchFilesFromSharedFolder(sharedFolderUrl); if (!chargeId) { const { ownerId } = await injector.get(AdminContextProvider).getVerifiedAdminContext(); // generate new charge const newCharge = await injector.get(ChargesProvider).generateCharge({ ownerId, userDescription: 'New uploaded documents', }); if (!newCharge) { throw new GraphQLError(`Failed to generate new charge for new document`); } chargeId = newCharge.id; } const newDocuments: Array<IInsertDocumentsParams['documents'][number]> = []; await Promise.all( files.map(async file => { // get new document data try { const newDocument = await getDocumentFromFile(injector, file, chargeId, isSensitive); newDocuments.push(newDocument); } catch (error) { // Skip this file and continue with other files console.error(error); } }), ); const res = await injector .get(DocumentsProvider) .insertDocuments({ documents: newDocuments }); return res.map(document => ({ document: document as IGetAllDocumentsResult })); }, updateDocument: async (_, { fields, documentId }, { injector }) => { let postUpdateActions = async (): Promise<void> => void 0; try { let chargeId: string | undefined = undefined; if (fields.chargeId === EMPTY_UUID) { // case unlinked from charge const document = await injector .get(DocumentsProvider) .getDocumentsByIdLoader.load(documentId); if (!document) { throw new GraphQLError(`Document ID="${documentId}" not valid`); } if (document.charge_id) { const [charge, transactions, documents] = await Promise.all([ injector.get(ChargesProvider).getChargeByIdLoader.load(document.charge_id), injector .get(TransactionsProvider) .transactionsByChargeIDLoader.load(document.charge_id), injector.get(DocumentsProvider).getDocumentsByChargeIdLoader.load(document.charge_id), ]); if (!charge) { throw new GraphQLError( `Former document's charge ID ("${fields.chargeId}") not valid`, ); } // generate new charge const newCharge = await injector.get(ChargesProvider).generateCharge({ ownerId: charge.owner_id, userDescription: 'Document unlinked from charge', }); if (!newCharge) { throw new GraphQLError( `Failed to generate new charge for document ID="${documentId}"`, ); } chargeId = newCharge.id; if (documents.length === 1 && transactions.length === 0) { postUpdateActions = async () => { try { await deleteCharges([charge.id], injector); } catch (e) { if (e instanceof GraphQLError) { throw e; } console.error(e); throw new GraphQLError( `Failed to delete the empty former charge ID="${charge.id}"`, ); } return postUpdateActions(); }; } } } else if (fields.chargeId) { // case new charge ID const charge = await injector .get(ChargesProvider) .getChargeByIdLoader.load(fields.chargeId); if (!charge) { throw new GraphQLError(`Charge ID="${fields.chargeId}" not valid`); } chargeId = charge.id; } const adjustedFields: IUpdateDocumentParams = { documentId, chargeId: chargeId ?? null, currencyCode: fields.amount?.currency ?? null, creditorId: fields.creditorId ?? null, debtorId: fields.debtorId ?? null, date: fields.date ? new Date(fields.date) : null, fileUrl: fields.file ? fields.file.toString() : null, imageUrl: fields.image ? fields.image.toString() : null, serialNumber: fields.serialNumber ?? null, totalAmount: fields.amount?.raw ?? null, type: fields.documentType ?? null, vatAmount: fields.vat?.raw ?? null, vatReportDateOverride: fields.vatReportDateOverride ? new Date(fields.vatReportDateOverride) : null, noVatAmount: fields.noVatAmount ?? null, isReviewed: true, allocationNumber: fields.allocationNumber ?? null, exchangeRateOverride: fields.exchangeRateOverride ?? null, description: fields.description ?? null, remarks: fields.remarks ?? null, }; const res = await injector.get(DocumentsProvider).updateDocument({ ...adjustedFields }); if (!res || res.length === 0) { throw new Error(`Document ID="${documentId}" not found`); } await postUpdateActions(); return { document: res[0], }; } catch (e) { if (e instanceof GraphQLError) { throw e; } return { __typename: 'CommonError', message: (e as Error)?.message ?? 'Unknown error', }; } }, deleteDocument: async (_, { documentId }, { injector }) => { try { const document = await injector .get(DocumentsProvider) .getDocumentsByIdLoader.load(documentId); if (!document) { throw new GraphQLError(`Document ID="${documentId}" not found`); } const res = await injector.get(DocumentsProvider).deleteDocument({ documentId }); if (res.length === 1) { if (document.charge_id) { const [charge, transactions, documents] = await Promise.all([ injector.get(ChargesProvider).getChargeByIdLoader.load(document.charge_id), injector .get(TransactionsProvider) .transactionsByChargeIDLoader.load(document.charge_id), injector.get(DocumentsProvider).getDocumentsByChargeIdLoader.load(document.charge_id), ]); if (charge && documents.length === 0 && transactions.length === 0) { await deleteCharges([charge.id], injector); } } return true; } throw new GraphQLError( res.length === 0 ? 'Document not found' : `More than one document found and deleted: ${res}`, ); } catch (e) { if (e instanceof GraphQLError) { throw e; } throw new GraphQLError(`Failed to delete document ID="${documentId}": ${e}`); } }, insertDocument: async (_, { record }, { injector }) => { let charge: IGetChargesByIdsResult | undefined; try { if (record.chargeId) { charge = await injector.get(ChargesProvider).getChargeByIdLoader.load(record.chargeId); if (!charge) { throw new Error(`Charge ID='${record.chargeId}' not found`); } } const { ownerId } = await injector.get(AdminContextProvider).getVerifiedAdminContext(); const newDocument: IInsertDocumentsParams['documents']['0'] = { ownerId: charge?.owner_id ?? ownerId, image: record.image ? record.image.toString() : null, file: record.file ? record.file.toString() : null, documentType: record.documentType ?? DocumentType.Unprocessed, serialNumber: record.serialNumber ?? null, date: record.date ? new Date(record.date) : null, amount: record.amount?.raw ?? null, currencyCode: record.amount?.currency ?? null, vat: record.vat?.raw ?? null, chargeId: record.chargeId ?? null, vatReportDateOverride: record.vatReportDateOverride ? new Date(record.vatReportDateOverride) : null, noVatAmount: record.noVatAmount ?? null, creditorId: record.creditorId ?? null, debtorId: record.debtorId ?? null, allocationNumber: record.allocationNumber ?? null, exchangeRateOverride: record.exchangeRateOverride ?? null, fileHash: null, description: record.description ?? null, remarks: record.remarks ?? null, }; const res = await injector .get(DocumentsProvider) .insertDocuments({ documents: [newDocument] }); if (!res || res.length === 0) { throw new Error(`Failed to insert ledger record to charge ID='${record.chargeId}'`); } return { document: res[0] }; } catch (e) { return { __typename: 'CommonError', message: `Error inserting new ledger record:\n ${ (e as Error)?.message ?? 'Unknown error' }`, }; } }, closeDocument: async (_, { id }, { injector }) => { try { const issuedDocument = await injector .get(IssuedDocumentsProvider) .getIssuedDocumentsByIdLoader.load(id); if (!issuedDocument) { throw new GraphQLError(`Issued document ID="${id}" not found. Are you sure it exists?`); } if (!issuedDocument.external_id) { throw new GraphQLError(`Issued document ID="${id}" has no external ID. Cannot close it.`); } const res = await injector .get(GreenInvoiceClientProvider) .closeDocument(issuedDocument.external_id); if (res) { // update document's status await injector .get(IssuedDocumentsProvider) .updateIssuedDocument({ documentId: id, status: 'CLOSED', }) .catch(e => { const message = `Failed to update document's status (ID ${id})`; console.error(`${message}: ${e}`); if (e instanceof GraphQLError) { throw e; } throw new GraphQLError(message); }); return true; } throw new GraphQLError(`Failed to close document ID="${id}"`); } catch (e) { console.error(`Failed to close document ID="${id}":`, e); throw new GraphQLError(`Failed to close document ID="${id}"`); } }, }, Document: { __resolveType: (documentRoot, _context, _info) => { switch (documentRoot?.type) { case DocumentType.Invoice: { return 'Invoice'; } case DocumentType.Receipt: { return 'Receipt'; } case DocumentType.InvoiceReceipt: { return 'InvoiceReceipt'; } case DocumentType.CreditInvoice: { return 'CreditInvoice'; } case DocumentType.Proforma: { return 'Proforma'; } default: { return 'Unprocessed'; } } }, }, FinancialDocument: { __resolveType: (documentRoot, _context, _info) => { switch (documentRoot?.type) { case DocumentType.Invoice: { return 'Invoice'; } case DocumentType.Receipt: { return 'Receipt'; } case DocumentType.InvoiceReceipt: { return 'InvoiceReceipt'; } case DocumentType.CreditInvoice: { return 'CreditInvoice'; } case DocumentType.Proforma: { return 'Proforma'; } default: { throw new GraphQLError(`FinancialDocument type "${documentRoot?.type}" is not supported`); } } }, }, UpdateDocumentResult: { __resolveType: (obj, _context, _info) => { if ('__typename' in obj && obj.__typename === 'CommonError') return 'CommonError'; return 'UpdateDocumentSuccessfulResult'; }, }, InsertDocumentResult: { __resolveType: (obj, _context, _info) => { if ('__typename' in obj && obj.__typename === 'CommonError') return 'CommonError'; return 'InsertDocumentSuccessfulResult'; }, }, UploadDocumentResult: { __resolveType: (obj, _context, _info) => { if ('__typename' in obj && obj.__typename === 'CommonError') return 'CommonError'; return 'UploadDocumentSuccessfulResult'; }, }, Invoice: { ...commonDocumentsFields, ...commonFinancialDocumentsFields, }, InvoiceReceipt: { ...commonDocumentsFields, ...commonFinancialDocumentsFields, }, CreditInvoice: { ...commonDocumentsFields, ...commonFinancialDocumentsFields, }, Proforma: { ...commonDocumentsFields, ...commonFinancialDocumentsFields, }, Unprocessed: { ...commonDocumentsFields, }, OtherDocument: { ...commonDocumentsFields, }, Receipt: { ...commonDocumentsFields, ...commonFinancialDocumentsFields, }, CommonCharge: commonChargeFields, FinancialCharge: commonChargeFields, ConversionCharge: commonChargeFields, SalaryCharge: commonChargeFields, InternalTransferCharge: commonChargeFields, DividendCharge: commonChargeFields, BusinessTripCharge: commonChargeFields, MonthlyVatCharge: commonChargeFields, BankDepositCharge: commonChargeFields, ForeignSecuritiesCharge: commonChargeFields, CreditcardBankCharge: commonChargeFields, };