UNPKG

@accounter/server

Version:
384 lines • 21.5 kB
import { addMonths, endOfMonth, format, startOfMonth, subMonths } from 'date-fns'; import { GraphQLError } from 'graphql'; import { DocumentType } from '../../../shared/enums.js'; import { errorSimplifier } from '../../../shared/errors.js'; import { dateToTimelessDateString } from '../../../shared/helpers/index.js'; import { AdminContextProvider } from '../../admin-context/providers/admin-context.provider.js'; import { GreenInvoiceClientProvider } from '../../app-providers/green-invoice-client.js'; import { getChargeBusinesses, getChargeDocumentsMeta, } from '../../charges/helpers/common.helper.js'; import { ChargesProvider } from '../../charges/providers/charges.provider.js'; import { getProductName, getSubscriptionPlanName, normalizeProduct, normalizeSubscriptionPlan, } from '../../contracts/helpers/contracts.helper.js'; import { ContractsProvider } from '../../contracts/providers/contracts.provider.js'; import { validateClientIntegrations } from '../../financial-entities/helpers/clients.helper.js'; import { BusinessesProvider } from '../../financial-entities/providers/businesses.provider.js'; import { ClientsProvider } from '../../financial-entities/providers/clients.provider.js'; import { convertDocumentInputIntoGreenInvoiceInput, getDocumentNameFromGreenInvoiceType, normalizeGreenInvoiceDocumentType, } from '../../green-invoice/helpers/green-invoice.helper.js'; import { TransactionsProvider } from '../../transactions/providers/transactions.provider.js'; import { getDocumentNameFromType } from '../helpers/common.helper.js'; import { convertContractToDraft, createRemarks, deduceVatTypeFromBusiness, executeDocumentIssue, filterAndHandleSwiftTransactions, getDocumentDateOutOfTransactions, getIncomeRecordsFromDocuments, getLinkedDocumentsAttributes, getPaymentsFromTransactions, getTypeFromDocumentsAndTransactions, } from '../helpers/issue-document.helper.js'; import { DocumentsProvider } from '../providers/documents.provider.js'; import { IssuedDocumentsProvider } from '../providers/issued-documents.provider.js'; import { normalizeDocumentType } from './common.js'; export const documentsIssuingResolvers = { Query: { newDocumentDraftByCharge: async (_, { chargeId }, { injector }) => { if (!chargeId) { throw new GraphQLError('Charge ID is required to fetch document draft'); } const { defaultCryptoConversionFiatCurrency, financialAccounts: { swiftBusinessId }, locality, } = await injector.get(AdminContextProvider).getVerifiedAdminContext(); const chargePromise = injector.get(ChargesProvider).getChargeByIdLoader.load(chargeId); const documentsPromise = injector .get(DocumentsProvider) .getDocumentsByChargeIdLoader.load(chargeId); const transactionsPromise = injector .get(TransactionsProvider) .transactionsByChargeIDLoader.load(chargeId) .then(res => filterAndHandleSwiftTransactions(res, swiftBusinessId)); const [charge, documents, transactions, { documentsCurrency }, { mainBusinessId }] = await Promise.all([ chargePromise, documentsPromise, transactionsPromise, getChargeDocumentsMeta(chargeId, injector), getChargeBusinesses(chargeId, injector), ]); if (!charge) { throw new GraphQLError(`Charge with ID "${chargeId}" not found`); } const clientPromise = mainBusinessId ? injector.get(ClientsProvider).getClientByIdLoader.load(mainBusinessId) : Promise.resolve(undefined); const openIssuedDocumentsPromise = injector .get(IssuedDocumentsProvider) .getIssuedDocumentsByIdLoader.loadMany(documents.map(d => d.id)) .then(res => res.filter(r => { if (!r) return false; if (r instanceof Error) { console.error('Failed to fetch issued document', r); return false; } if (r.status !== 'OPEN') { return false; } return true; })); const paymentPromise = getPaymentsFromTransactions(injector, transactions); const vatTypePromise = deduceVatTypeFromBusiness(injector, locality, mainBusinessId); const [client, openIssuedDocuments, payment, vatType] = await Promise.all([ clientPromise, openIssuedDocumentsPromise, paymentPromise, vatTypePromise, ]); const greenInvoiceDocuments = await Promise.all(openIssuedDocuments.map(doc => injector .get(GreenInvoiceClientProvider) .documentLoader.load(doc.external_id) .then(res => { if (!res) { console.error('Failed to fetch document from Green Invoice', doc.external_id); return null; } return res; }))).then(res => res.filter(Boolean)); const greenInvoiceClientId = validateClientIntegrations(client?.integrations ?? {}).greenInvoiceId; if (!greenInvoiceClientId) { throw new GraphQLError(`Green invoice integration missing for business ID="${mainBusinessId}"`); } const income = getIncomeRecordsFromDocuments(openIssuedDocuments.map(doc => greenInvoiceDocuments.find(gd => gd.id === doc.external_id))); if (income.length === 0 && transactions.length > 0) { income.push(...transactions.map(transaction => ({ description: transaction.source_description ?? '', quantity: 1, price: Number(transaction.amount), currency: transaction.currency, currencyRate: undefined, vatType, }))); } const type = getTypeFromDocumentsAndTransactions(greenInvoiceDocuments.map(doc => ({ type: normalizeGreenInvoiceDocumentType(doc.type) })), transactions); const linkedDocsAttributes = getLinkedDocumentsAttributes(openIssuedDocuments, type === DocumentType.CreditInvoice); let remarks = charge.user_description; if (greenInvoiceDocuments.length) { remarks = greenInvoiceDocuments.map(doc => doc.remarks).join(', '); switch (type) { case DocumentType.Receipt: case DocumentType.InvoiceReceipt: remarks = `${getDocumentNameFromType(type)} for ${greenInvoiceDocuments.map(doc => `${getDocumentNameFromGreenInvoiceType(doc.type)} ${doc.number}`).join(', ')}`; break; } } else if (client?.remark) { remarks = client.remark; } const documentDate = getDocumentDateOutOfTransactions(transactions); const transactionsCurrencies = Array.from(new Set(transactions.map(t => t.currency))); const transactionsCurrency = transactionsCurrencies.length === 1 ? transactionsCurrencies[0] : undefined; const draft = { remarks, // description: ____, // footer: ____, type, date: documentDate, dueDate: dateToTimelessDateString(endOfMonth(new Date())), language: 'ENGLISH', currency: (transactionsCurrency || documentsCurrency || defaultCryptoConversionFiatCurrency), vatType, rounding: false, signed: true, client, income, payment, // linkedPaymentId: ____, // maxPayments: _____, // discount: _____, ...linkedDocsAttributes, }; return draft; }, newDocumentDraftByDocument: async (_, { documentId }, { injector }) => { if (!documentId) { throw new GraphQLError('Document ID is required to fetch document draft'); } const document = await injector .get(DocumentsProvider) .getDocumentsByIdLoader.load(documentId); if (!document) { throw new GraphQLError(`Document with ID "${documentId}" not found`); } const { defaultCryptoConversionFiatCurrency, financialAccounts: { swiftBusinessId }, locality, } = await injector.get(AdminContextProvider).getVerifiedAdminContext(); const chargePromise = document.charge_id ? injector.get(ChargesProvider).getChargeByIdLoader.load(document.charge_id) : Promise.resolve(undefined); const transactionsPromise = document.charge_id ? injector .get(TransactionsProvider) .transactionsByChargeIDLoader.load(document.charge_id) .then(res => filterAndHandleSwiftTransactions(res, swiftBusinessId)) : Promise.resolve(undefined); const mainBusinessIdPromise = document.charge_id ? getChargeBusinesses(document.charge_id, injector).then(res => res.mainBusinessId) : Promise.resolve(undefined); const [charge, transactions, mainBusinessId] = await Promise.all([ chargePromise, transactionsPromise, mainBusinessIdPromise, ]); if (!charge) { throw new GraphQLError(`Charge with ID "${document.charge_id}" not found`); } const clientPromise = mainBusinessId ? injector.get(ClientsProvider).getClientByIdLoader.load(mainBusinessId) : Promise.resolve(undefined); const openIssuedDocumentPromise = injector .get(IssuedDocumentsProvider) .getIssuedDocumentsByIdLoader.load(document.id); const vatTypePromise = deduceVatTypeFromBusiness(injector, locality, mainBusinessId); const [client, openIssuedDocument, vatType] = await Promise.all([ clientPromise, openIssuedDocumentPromise, vatTypePromise, ]); if (!openIssuedDocument) { throw new GraphQLError(`Document with ID "${document.id}" doesn't seem like a document we issued`); } if (openIssuedDocument.status !== 'OPEN') { throw new GraphQLError(`Document with ID "${document.id}" is closed`); } const greenInvoiceDocumentPromise = injector .get(GreenInvoiceClientProvider) .documentLoader.load(openIssuedDocument.external_id) .then(res => { if (!res) { console.error('Failed to fetch document from Green Invoice', openIssuedDocument.external_id); return null; } return res; }); const paymentPromise = getPaymentsFromTransactions(injector, transactions ?? []); const greenInvoiceClientId = validateClientIntegrations(client?.integrations ?? {}).greenInvoiceId; if (!greenInvoiceClientId) { throw new GraphQLError(`Green invoice integration missing for business ID="${mainBusinessId}"`); } const [greenInvoiceDocument, payment, { documentsCurrency }] = await Promise.all([ greenInvoiceDocumentPromise, paymentPromise, getChargeDocumentsMeta(charge.id, injector), ]); if (!greenInvoiceDocument) { throw new GraphQLError(`Document with ID "${document.id}" doesn't have a Green Invoice matching document`); } const income = getIncomeRecordsFromDocuments([greenInvoiceDocument]); const type = getTypeFromDocumentsAndTransactions([{ type: normalizeDocumentType(document.type) }], transactions ?? []); const linkedDocsAttributes = getLinkedDocumentsAttributes([openIssuedDocument], type === DocumentType.CreditInvoice); let remarks = greenInvoiceDocument.remarks; switch (type) { case DocumentType.Receipt: case DocumentType.InvoiceReceipt: remarks = `${getDocumentNameFromType(type)} for ${getDocumentNameFromGreenInvoiceType(greenInvoiceDocument.type)} ${greenInvoiceDocument.number}`; break; } if (!remarks && client?.remark) { remarks = client.remark; } const documentDate = getDocumentDateOutOfTransactions(transactions ?? []); const draft = { remarks, // description: ____, // footer: ____, type, date: documentDate, dueDate: dateToTimelessDateString(endOfMonth(new Date())), language: 'ENGLISH', currency: (documentsCurrency || defaultCryptoConversionFiatCurrency), vatType, rounding: false, signed: true, client, income, payment, // linkedPaymentId: ____, // maxPayments: _____, // discount: _____, ...linkedDocsAttributes, }; return draft; }, periodicalDocumentDrafts: async (_, { issueMonth }, { injector }) => { const { locality } = await injector.get(AdminContextProvider).getVerifiedAdminContext(); const openContracts = await injector.get(ContractsProvider).getAllOpenContracts(); const monthlyBillingCycle = 'MONTHLY'; const monthlyContracts = openContracts.filter(contract => contract.billing_cycle.toLocaleUpperCase() === monthlyBillingCycle); const drafts = await Promise.all(monthlyContracts.map(async (contract) => convertContractToDraft(injector, contract, locality, issueMonth))); return drafts; }, periodicalDocumentDraftsByContracts: async (_, { issueMonth, contractIds }, { injector }) => { const { locality } = await injector.get(AdminContextProvider).getVerifiedAdminContext(); const contracts = await injector .get(ContractsProvider) .getContractsByIdLoader.loadMany(contractIds) .then(res => res.filter(c => !!c && !(c instanceof Error))); const drafts = await Promise.all(contracts.map(async (contract) => convertContractToDraft(injector, contract, locality, issueMonth))); return drafts; }, clientMonthlyChargeDraft: async (_, { clientId, issueMonth }, { injector }) => { const clientContracts = await injector .get(ContractsProvider) .getContractsByClientIdLoader.load(clientId); const contracts = clientContracts.filter(clientContract => clientContract.is_active); if (contracts.length === 0) { throw new GraphQLError(`No active contracts found for client ID="${clientId}"`); } if (contracts.length > 1) { throw new GraphQLError(`Multiple active contracts found for client ID="${clientId}", cannot deduce which one to use`); } const contract = contracts[0]; if (!contract) { throw new GraphQLError(`Contract not found for client ID="${clientId}"`); } const businessPromise = injector .get(BusinessesProvider) .getBusinessByIdLoader.load(contract.client_id); const clientPromise = injector .get(ClientsProvider) .getClientByIdLoader.load(contract.client_id); const [business, client] = await Promise.all([businessPromise, clientPromise]); if (!business) { throw new GraphQLError(`Business ID="${contract.client_id}" not found`); } if (!client) { throw new GraphQLError(`Client not found for business ID="${contract.client_id}"`); } const greenInvoiceId = validateClientIntegrations(client.integrations)?.greenInvoiceId; if (!greenInvoiceId) { throw new GraphQLError(`Green invoice match not found for business ID="${contract.client_id}"`); } const today = issueMonth ? addMonths(new Date(issueMonth), 1) : 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 vatType = await deduceVatTypeFromBusiness(injector, business.country, contract.client_id); const draft = { remarks: createRemarks(contract), description: `${getProductName(normalizeProduct(contract.product ?? ''))} ${getSubscriptionPlanName(normalizeSubscriptionPlan(contract.plan ?? ''))} - ${month} ${year}`, type: normalizeDocumentType(contract.document_type), date: monthStart, dueDate: monthEnd, language: 'ENGLISH', currency: contract.currency, vatType, rounding: false, signed: true, client, income: [ { description: `${getProductName(normalizeProduct(contract.product ?? ''))} ${getSubscriptionPlanName(normalizeSubscriptionPlan(contract.plan ?? ''))} - ${month} ${year}`, quantity: 1, price: contract.amount, currency: contract.currency, vatType, }, ], }; return draft; }, }, Mutation: { issueGreenInvoiceDocuments: async (_, { generateDocumentsInfo }, { injector }) => { const errors = []; const { ownerId } = await injector.get(AdminContextProvider).getVerifiedAdminContext(); await Promise.all(generateDocumentsInfo.map(async (document) => executeDocumentIssue(injector, ownerId, document, undefined, true, undefined, true).catch(e => { console.error(e); errors.push(`${document.client?.name ?? document.client?.id}: ${e.message}`); }))); return { success: true, errors, }; }, issueGreenInvoiceDocument: async (_, { input: initialInput, emailContent, attachment, chargeId, sendEmail = false }, { injector }) => { const { ownerId } = await injector.get(AdminContextProvider).getVerifiedAdminContext(); return executeDocumentIssue(injector, ownerId, initialInput, emailContent ?? undefined, attachment ?? undefined, chargeId ?? undefined, sendEmail ?? false).then(charge => { if (!charge) { throw new GraphQLError('Failed to issue document'); } return charge; }); }, previewDocument: async (_, { input: initialInput }, { injector }) => { try { const input = await convertDocumentInputIntoGreenInvoiceInput(initialInput, injector); const document = await injector .get(GreenInvoiceClientProvider) .previewDocuments({ input }) .catch(e => { if (e instanceof GraphQLError) { const response = e.extensions?.response; if (response && response.status === 400 && response.body?.errorMessage) { throw new GraphQLError(`Failed to generate document preview, Green Invoice returned: ${response.body.errorMessage}`); } } throw e; }); if (!document) { throw new GraphQLError('Failed to generate document preview'); } if ('errorMessage' in document) { console.error('Failed to generate document preview', document); throw new GraphQLError(`Failed to generate document preview, Green Invoice returned: ${document.errorMessage}`); } if ('file' in document && document.file) { return document.file; } console.error('Document preview does not contain a file', document); throw new GraphQLError('Document preview does not contain a file'); } catch (e) { throw errorSimplifier('Error generating document preview', e); } }, }, }; //# sourceMappingURL=documents-issuing.resolver.js.map