UNPKG

@accounter/server

Version:
206 lines (179 loc) • 6.13 kB
import type { Injector } from 'graphql-modules'; import { Currency, DocumentType } from '../../../shared/enums.js'; import { hashStringToInt } from '../../../shared/helpers/index.js'; import { AdminContextProvider } from '../../admin-context/providers/admin-context.provider.js'; import { AnthropicProvider } from '../../app-providers/anthropic.js'; import { CloudinaryProvider } from '../../app-providers/cloudinary.js'; import type { IInsertDocumentsParams } from '../types.js'; const toBase64 = async (file: File | Blob): Promise<string> => { const base64string = Buffer.from(await file.arrayBuffer()).toString('base64'); return 'data:' + file.type + ';base64,' + base64string; }; export const uploadToCloudinary = async (injector: Injector, file: File | Blob) => { const base64string = await toBase64(file).catch(err => { throw new Error(`Failed to convert file to base64: ${err.message}`); }); try { return injector.get(CloudinaryProvider).uploadInvoiceToCloudinary(base64string); } catch (e) { const message = 'Error on uploading file to cloudinary'; console.error(`${message}: ${e}`); throw new Error(message, { cause: e }); } }; export type OcrData = { isOwnerIssuer?: boolean; counterpartyId?: string; documentType: DocumentType; serial?: string; date?: Date; amount?: number; currency?: Currency; vat?: number; allocationNumber?: string; description?: string; remarks?: string; }; export async function getOcrData( injector: Injector, file: File | Blob, isSensitive: boolean | null = true, ): Promise<OcrData> { const validateNumber = (value: unknown): number | undefined => { return typeof value === 'number' && !Number.isNaN(value) ? value : undefined; }; const validateDate = (value?: string): Date | undefined => { if (!value) return undefined; const date = new Date(value); return Number.isNaN(date.getTime()) ? undefined : date; }; if (isSensitive) { return { documentType: DocumentType.Unprocessed, }; } const draft = await injector.get(AnthropicProvider).extractInvoiceDetails(file); if (!draft) { throw new Error('No data returned from Anthropic OCR'); } let isOwnerIssuer: boolean | undefined = undefined; if (draft.recipient?.toLocaleLowerCase().includes('the guild')) { isOwnerIssuer = false; } if (draft.issuer?.toLocaleLowerCase().includes('the guild')) { isOwnerIssuer = true; } return { isOwnerIssuer, documentType: draft.type ?? DocumentType.Unprocessed, serial: draft.referenceCode ?? undefined, date: validateDate(draft.date ?? undefined), amount: validateNumber(draft.fullAmount), currency: draft.currency, vat: validateNumber(draft.vatAmount), allocationNumber: draft.allocationNumber ?? undefined, description: draft.description ?? undefined, remarks: draft.issuer, }; } async function getHashFromFile(file: File | Blob): Promise<number> { return hashStringToInt(await file.text()); } function figureOutSides( documentType: DocumentType, ownerId: string, isOwnerIssuer?: boolean, counterPartyId?: string, ): { creditorId?: string; debtorId?: string } { if (documentType === DocumentType.Unprocessed || documentType === DocumentType.Other) { return {}; } let res: { creditorId: string | undefined; debtorId: string | undefined; } = { creditorId: counterPartyId, debtorId: ownerId, }; if (isOwnerIssuer === true) { res = { creditorId: res.debtorId, debtorId: res.creditorId, }; } if (documentType === DocumentType.CreditInvoice) { res = { creditorId: res.debtorId, debtorId: res.creditorId, }; } return res; } export function getDocumentFromUrlsAndOcrData( fileUrl: string, imageUrl: string, ocrData: OcrData, adminBusinessId: string, chargeId?: string | null, fileHash?: number, ): IInsertDocumentsParams['documents'][number] { const sides = figureOutSides( ocrData.documentType, adminBusinessId, ocrData.isOwnerIssuer, ocrData.counterpartyId, ); const newDocument: IInsertDocumentsParams['documents'][number] = { ownerId: adminBusinessId, image: imageUrl ?? null, file: fileUrl ?? null, documentType: ocrData.documentType, serialNumber: ocrData.serial ?? null, date: ocrData.date ?? null, amount: ocrData.amount ?? null, currencyCode: ocrData.currency ?? null, vat: ocrData.vat ?? null, chargeId: chargeId ?? null, vatReportDateOverride: null, noVatAmount: null, allocationNumber: ocrData.allocationNumber ?? null, exchangeRateOverride: null, fileHash: fileHash?.toString() ?? null, description: ocrData.description ?? null, remarks: ocrData.remarks ?? null, creditorId: sides.creditorId ?? null, debtorId: sides.debtorId ?? null, }; return newDocument; } export async function getDocumentFromFile( injector: Injector, file: File | Blob, chargeId?: string | null, isSensitive?: boolean | null, counterPartyId?: string, hash?: number, ): Promise<IInsertDocumentsParams['documents'][number]> { try { const { ownerId } = await injector.get(AdminContextProvider).getVerifiedAdminContext(); // Buffer the file to allow multiple reads from a stream const buffer = await file.arrayBuffer(); const multiReadableFile = new Blob([buffer], { type: file.type }); const [{ fileUrl, imageUrl }, ocrData, fileHash] = await Promise.all([ uploadToCloudinary(injector, multiReadableFile), getOcrData(injector, multiReadableFile, isSensitive), hash ? Promise.resolve(hash) : getHashFromFile(multiReadableFile), ]); if (!ocrData) { throw new Error('No data returned from Green Invoice'); } if (counterPartyId) { ocrData.counterpartyId = counterPartyId; } return getDocumentFromUrlsAndOcrData(fileUrl, imageUrl, ocrData, ownerId, chargeId, fileHash); } catch (e) { const message = 'Error extracting document data from file'; console.error(`${message}: ${e}`); throw new Error(message, { cause: e }); } }