UNPKG

@accounter/server

Version:

531 lines • 25.2 kB
import { endOfMonth, format, startOfMonth, subMonths } from 'date-fns'; import { GraphQLError } from 'graphql'; import { CloudinaryProvider } from '../../app-providers/cloudinary.js'; import { GoogleDriveProvider } from '../../app-providers/google-drive/google-drive.provider.js'; import { GreenInvoiceProvider } from '../../app-providers/green-invoice.js'; import { deleteCharges } from '../../charges/helpers/delete-charges.helper.js'; import { ChargesProvider } from '../../charges/providers/charges.provider.js'; import { BusinessesGreenInvoiceMatcherProvider } from '../../financial-entities/providers/businesses-green-invoice-match.provider.js'; import { BusinessesProvider } from '../../financial-entities/providers/businesses.provider.js'; import { EMPTY_UUID } from '../../../shared/constants.js'; import { Currency, DocumentType } from '../../../shared/enums.js'; import { dateToTimelessDateString, formatCurrency, optionalDateToTimelessDateString, } from '../../../shared/helpers/index.js'; import { getGreenInvoiceDocumentType, normalizeDocumentType, } from '../helpers/green-invoice.helper.js'; import { getDocumentFromFile } from '../helpers/upload.helper.js'; import { DocumentsProvider } from '../providers/documents.provider.js'; import { commonChargeFields, commonDocumentsFields, commonFinancialDocumentsFields, } from './common.js'; export const documentsResolvers = { 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; }, }, Mutation: { uploadDocument: async (_, { file, chargeId }, context) => { const { injector } = context; try { const newDocument = await getDocumentFromFile(context, file, chargeId); const res = await injector .get(DocumentsProvider) .insertDocuments({ document: [{ ...newDocument }] }); return { document: res[0] }; } catch (e) { const message = e?.message ?? 'Unknown error'; return { __typename: 'CommonError', message: `Error uploading document: ${message}`, }; } }, batchUploadDocuments: async (_, { documents, isSensitive, chargeId }, context) => { const { injector, adminContext: { defaultAdminBusinessId }, } = context; if (!chargeId) { // generate new charge const [newCharge] = await injector.get(ChargesProvider).generateCharge({ ownerId: defaultAdminBusinessId, userDescription: 'New uploaded documents', }); if (!newCharge) { throw new GraphQLError(`Failed to generate new charge for new document`); } chargeId = newCharge.id; } const newDocuments = []; await Promise.all(documents.map(async (document) => { // get new document data const newDocument = await getDocumentFromFile(context, document, chargeId, isSensitive); newDocuments.push(newDocument); })); const res = await injector.get(DocumentsProvider).insertDocuments({ document: newDocuments }); return res.map(document => ({ document: document })); }, batchUploadDocumentsFromGoogleDrive: async (_, { sharedFolderUrl, chargeId, isSensitive }, context) => { const isValidGoogleDriveUrl = (url) => { 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 { injector, adminContext: { defaultAdminBusinessId }, } = context; const files = await injector .get(GoogleDriveProvider) .fetchFilesFromSharedFolder(sharedFolderUrl); if (!chargeId) { // generate new charge const [newCharge] = await injector.get(ChargesProvider).generateCharge({ ownerId: defaultAdminBusinessId, userDescription: 'New uploaded documents', }); if (!newCharge) { throw new GraphQLError(`Failed to generate new charge for new document`); } chargeId = newCharge.id; } const newDocuments = []; await Promise.all(files.map(async (file) => { // get new document data try { const newDocument = await getDocumentFromFile(context, 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({ document: newDocuments }); return res.map(document => ({ document: document })); }, updateDocument: async (_, { fields, documentId }, { injector }) => { let postUpdateActions = async () => void 0; try { let chargeId = 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 = await injector .get(ChargesProvider) .getChargeByIdLoader.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 || newCharge.length === 0) { throw new GraphQLError(`Failed to generate new charge for document ID="${documentId}"`); } chargeId = newCharge?.[0]?.id; if (Number(charge.documents_count ?? 1) === 1 && Number(charge.transactions_count ?? 0) === 0) { postUpdateActions = async () => { try { await deleteCharges([charge.id], injector); } catch (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 = { 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, }; 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) { return { __typename: 'CommonError', message: e?.message ?? 'Unknown error', }; } }, deleteDocument: async (_, { documentId }, { injector }) => { 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 = await injector .get(ChargesProvider) .getChargeByIdLoader.load(document.charge_id); if (charge && !charge.documents_count && !charge.transactions_count) { await deleteCharges([charge.id], injector); } } return true; } throw new GraphQLError(res.length === 0 ? 'Document not found' : `More than one document found and deleted: ${res}`); }, insertDocument: async (_, { record }, { injector }) => { try { if (record.chargeId) { const charge = await injector .get(ChargesProvider) .getChargeByIdLoader.load(record.chargeId); if (!charge) { throw new Error(`Charge ID='${record.chargeId}' not found`); } } const newDocument = { 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, }; const res = await injector .get(DocumentsProvider) .insertDocuments({ document: [{ ...newDocument }] }); if (!res || res.length === 0) { throw new Error(`Failed to insert ledger record to charge ID='${record.chargeId}'`); } if (record.chargeId) { /* clear cache */ injector.get(DocumentsProvider).getDocumentsByChargeIdLoader.clear(record.chargeId); } return { document: res[0] }; } catch (e) { return { __typename: 'CommonError', message: `Error inserting new ledger record:\n ${e?.message ?? 'Unknown error'}`, }; } }, fetchIncomeDocuments: async (_, { ownerId }, { injector, adminContext: { defaultAdminBusinessId } }) => { const data = await injector.get(GreenInvoiceProvider).searchDocuments({ input: { pageSize: 100, sort: 'creationDate' }, }); if (!data?.items) { throw new GraphQLError('Failed to fetch documents'); } if (data.items.length === 0) { return []; } const documents = await injector.get(DocumentsProvider).getAllDocuments(); const newDocuments = data.items.filter(item => item && !documents.some(doc => doc.vat_amount === item.vat && doc.total_amount === item.amount && doc.serial_number === item.number && optionalDateToTimelessDateString(doc.date) === item.documentDate) && item.type !== '_300'); const addedDocs = []; await Promise.all(newDocuments.map(async (greenInvoiceDoc) => { if (!greenInvoiceDoc) { return; } const documentType = normalizeDocumentType(greenInvoiceDoc.type); const isOwnerCreditor = greenInvoiceDoc.amount > 0 && documentType !== DocumentType.CreditInvoice; try { // generate preview image via cloudinary const imagePromise = injector .get(CloudinaryProvider) .uploadInvoiceToCloudinary(greenInvoiceDoc.url.origin); // Generate parent charge const chargePromise = injector.get(ChargesProvider).generateCharge({ ownerId, userDescription: greenInvoiceDoc.description && greenInvoiceDoc.description !== '' ? greenInvoiceDoc.description : 'Green Invoice generated charge', }); // Get matching business const businessPromise = injector .get(BusinessesGreenInvoiceMatcherProvider) .getBusinessMatchByGreenInvoiceIdLoader.load(greenInvoiceDoc.client.id); const [{ imageUrl }, [charge], business] = await Promise.all([ imagePromise, chargePromise, businessPromise, ]); if (!charge) { throw new Error('Failed to generate charge'); } const counterpartyId = business?.business_id ?? null; // insert document const rawDocument = { image: imageUrl, file: greenInvoiceDoc.url.origin, documentType, serialNumber: greenInvoiceDoc.number, date: greenInvoiceDoc.documentDate, amount: greenInvoiceDoc.amount, currencyCode: formatCurrency(greenInvoiceDoc.currency), vat: greenInvoiceDoc.vat, chargeId: charge.id, vatReportDateOverride: null, noVatAmount: null, creditorId: isOwnerCreditor ? defaultAdminBusinessId : counterpartyId, debtorId: isOwnerCreditor ? counterpartyId : defaultAdminBusinessId, }; const newDocumentPromise = injector .get(DocumentsProvider) .insertDocuments({ document: [rawDocument] }); const chargeDescriptionUpdate = new Promise(resolve => { const income = greenInvoiceDoc.income; if (!income || income.length === 0 || !income[0]?.description || income[0].description === '') { resolve(undefined); } const userDescription = income .filter(item => item?.description) .map(item => item.description) .join(', '); injector .get(ChargesProvider) .updateCharge({ chargeId: charge.id, userDescription, }) .then(() => resolve(undefined)); }); const [newDocument] = await Promise.all([newDocumentPromise, chargeDescriptionUpdate]); addedDocs.push(newDocument[0]); } catch (e) { throw new GraphQLError(`Error adding Green Invoice document: ${e}\n\n${JSON.stringify(greenInvoiceDoc, null, 2)}`); } })); return addedDocs; }, generateMonthlyClientDocuments: async (_, __, { injector }) => { const jsonArray = [ { businessId: '147d3415-55e3-497f-acba-352dcc37cb8d', // uri test amount: 1000, currency: Currency.Ils, }, ]; const errors = []; const proformaProtos = await Promise.all(jsonArray.map(async (json) => { const businessPromise = injector .get(BusinessesProvider) .getBusinessByIdLoader.load(json.businessId); const businessGreenInvoiceMatchPromise = injector .get(BusinessesGreenInvoiceMatcherProvider) .getBusinessMatchByIdLoader.load(json.businessId); const [business, businessGreenInvoiceMatch] = await Promise.all([ businessPromise, businessGreenInvoiceMatchPromise, ]); if (!business) { throw new GraphQLError(`Business ID="${json.businessId}" not found`); } if (!businessGreenInvoiceMatch) { throw new GraphQLError(`Green invoice match not found for business ID="${json.businessId}"`); } const today = new Date(); const monthStart = dateToTimelessDateString(startOfMonth(today)); const monthEnd = dateToTimelessDateString(endOfMonth(today)); const year = today.getFullYear() + (today.getMonth() === 0 ? -1 : 0); const month = format(subMonths(today, 1), 'MMMM'); const documentInput = { businessName: business.name, type: getGreenInvoiceDocumentType(businessGreenInvoiceMatch.document_type), remarks: businessGreenInvoiceMatch.remark ?? undefined, date: monthStart, dueDate: monthEnd, lang: 'en', currency: json.currency, vatType: 1, rounding: false, signed: true, attachment: true, client: { id: businessGreenInvoiceMatch.green_invoice_id, emails: [...(businessGreenInvoiceMatch.emails ?? []), 'ap@the-guild.dev'], }, income: [ { description: `GraphQL Hive Enterprise License - ${month} ${year}`, quantity: 1, price: json.amount, currency: json.currency, vatType: 1, }, ], }; return documentInput; })); for (const proformaProto of proformaProtos) { const { businessName, ...input } = proformaProto; await injector .get(GreenInvoiceProvider) .addDocuments({ input }) .then(res => { if (res && 'errorMessage' in res) { errors.push(`${businessName}: ${res.errorMessage}`); } }) .catch(e => { console.error(e); errors.push(`${businessName}: ${e.message}`); }); } return { success: true, errors, }; }, }, 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'; } } }, }, 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, CreditcardBankCharge: commonChargeFields, }; //# sourceMappingURL=documents.resolver.js.map