UNPKG

@accounter/server

Version:

176 lines (152 loc) • 5.65 kB
import { CONTEXT, Inject, Injectable, Scope } from 'graphql-modules'; import { init, type Sdk } from '@accounter/green-invoice-graphql'; import type { Currency } from '@shared/enums'; import { dateToTimelessDateString } from '@shared/helpers'; import { ENVIRONMENT } from '@shared/tokens'; import type { Environment } from '@shared/types'; export type ExpenseDraft = NonNullable< Extract< Awaited<ReturnType<Sdk['searchExpenseDrafts_query']>>['searchExpenseDrafts'], { items: Array<unknown> } >['items'][number] >; @Injectable({ scope: Scope.Operation, global: true, }) export class GreenInvoiceProvider { localCurrency: Currency; constructor( @Inject(CONTEXT) private context: GraphQLModules.Context, @Inject(ENVIRONMENT) private env: Environment, ) { this.localCurrency = this.context.adminContext.defaultLocalCurrency; } public authToken: string | null = null; private async init() { const id = this.env.greenInvoice.id; const secret = this.env.greenInvoice.secret; if (!id || !secret) { throw new Error('Environment variables not found'); } const greenInvoice = await init(id, secret); return greenInvoice; } public async getSDK(): Promise<Sdk> { try { const { sdk, authToken } = await this.init(); this.authToken = authToken; return sdk; } catch (e) { console.error(e); throw new Error(`Green Invoice error: ${(e as Error).message}`); } } public async addExpenseDraftByFile(file: File | Blob): Promise<ExpenseDraft> { try { // convert what-wg file to nodejs file const newFile = new File( [Buffer.from(await file.arrayBuffer())], 'name' in file ? file.name : 'new-file', { type: file.type, }, ); // get file upload url + params const sdk = await this.getSDK(); const { getFileUploadUrl: fileUploadParams } = await sdk.getFileUploadUrl_query({ context: 'expense', data: { source: 5 }, }); if (!fileUploadParams) { throw new Error('No file upload params returned'); } const form = new FormData(); for (const [field, value] of Object.entries(fileUploadParams.fields)) { if (value) { // replace field names underscores with dashes (originally dashes, Mesh converted to underscores to match GraphQL spec) form.append(field.replaceAll('_', '-'), value as string); } } form.append('file', newFile, newFile.name); const uploadTime = new Date().getTime(); // upload the file await fetch(fileUploadParams.url, { method: 'POST', body: form, }).then(async res => { if (![200, 201, 204].includes(res.status)) { throw new Error('Failed to upload file'); } return; }); // TODO: next section is temporary fix until decent webhook will be set up let retryCounter = 15; // 30 seconds while (retryCounter > 0) { await new Promise(resolve => setTimeout(resolve, 2000)); // 2 second delay to allow Green Invoice to process the file. const res = await sdk.searchExpenseDrafts_query({ input: { fromDate: dateToTimelessDateString(new Date()), toDate: dateToTimelessDateString(new Date()), }, }); if ( res.searchExpenseDrafts && 'items' in res.searchExpenseDrafts && res.searchExpenseDrafts.items.length ) { const items = res.searchExpenseDrafts.items.filter(item => { return !!item && item?.creationDate > uploadTime / 1000; }) as ExpenseDraft[]; if (items.length > 1) { // currently we cannot differentiate between drafts throw new Error('Multiple drafts found'); } if (items.length === 1) { const { expense } = items[0]; // check if any actual OCR fields exists if ( expense.amount !== null || expense.currency !== this.localCurrency || expense.date !== null || expense.documentType !== '_20' || expense.number !== null || expense.vat !== 0 ) { console.log(`Finally, it took only ${(15 - retryCounter) * 2} seconds...`); return items[0]; } } } retryCounter -= 1; } console.log('Failed to fetch OCR data, returning empty draft'); const emptyDraft = { expense: { documentType: '_405', // "receipt on donation" which is not supported, resulting in unprocessed document number: undefined, date: undefined, amount: 0, currency: this.localCurrency, vat: 0, }, } as ExpenseDraft; return emptyDraft; } catch (e) { console.error(e); throw new Error(`Green Invoice error: ${(e as Error).message}`); } } public async searchDocuments(...params: Parameters<Sdk['searchDocuments_query']>) { const sdk = await this.getSDK(); return sdk.searchDocuments_query(...params).then(res => res.searchDocuments); } public async addDocuments(...params: Parameters<Sdk['addDocument_mutation']>) { const sdk = await this.getSDK(); return sdk.addDocument_mutation(...params).then(res => res.addDocument); } public async previewDocuments(...params: Parameters<Sdk['previewDocument_query']>) { const sdk = await this.getSDK(); return sdk.previewDocument_query(...params).then(res => res.previewDocument); } }