/** * ════════════════════════════════════════════════════════════ * مُصادَق (Musadaq) — Gemini AI Extraction Service / خدمة استخراج البيانات * ════════════════════════════════════════════════════════════ * This service extracts financial data from invoice images/PDFs using Gemini AI. * It ensures unstructured data is converted into UBL 2.1 compliant JSON. * يقوم باستخراج البيانات المالية من صور/ملفات الفواتير باستخدام ذكاء Gemini الاصطناعي. * يضمن تحويل البيانات غير المهيكلة إلى JSON مطابق لمعايير UBL 2.1. * ════════════════════════════════════════════════════════════ */ import { Injectable, Logger, InternalServerErrorException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { GoogleGenerativeAI } from '@google/generative-ai'; import * as fs from 'fs'; import * as path from 'path'; @Injectable() export class GeminiExtractorService { private readonly logger = new Logger(GeminiExtractorService.name); private genAI: GoogleGenerativeAI; private model: any; constructor(private configService: ConfigService) { const apiKey = this.configService.getOrThrow('GEMINI_API_KEY'); this.genAI = new GoogleGenerativeAI(apiKey); this.model = this.genAI.getGenerativeModel({ model: this.configService.get('GEMINI_MODEL', 'gemini-1.5-flash'), }); } /** * استخراج البيانات من صورة الفاتورة (يدعم فواتير متعددة في ملف واحد) * Extract accounting data from an invoice file (supports multiple invoices per file) */ async extractInvoiceData(filePath: string, storageRoot: string): Promise { try { const fullPath = path.join(storageRoot, filePath); const fileData = fs.readFileSync(fullPath); const prompt = ` أنت الآن "مدقق ضريبي أردني خبير" (Expert Jordanian Tax Auditor). مهمتك هي استخراج البيانات المحاسبية من هذا الملف بدقة متناهية لضمان الامتثال لنظام الفوترة الوطني (JoFotara). قواعد الاستخراج الاستراتيجية: 1. الشخصية: تعامل مع الملف كمدقق يبحث عن أدق التفاصيل المالية والقانونية. 2. العملة: جميع المبالغ بالدينار الأردني (JOD). التزم بدقة 3 خانات عشرية (مثال: 1.250). 3. الضرائب: حدد نسب الضريبة لكل بند بناءً على القوائم المعتمدة: - (16%): النسبة العامة (أي سلع غير مذكورة أدناه). - (10%): حلاوة طحينة، طحينة، أجبان محضرة، أسماك محفوظة، سجق. - (5%): عبوات الألبان والعلب المعدنية والكرتون المطبوع المخصص للتغليف. - (4%): كرتون أطباق البيض، القرطاسية، الزي المدرسي، مدافئ الكاز والغاز، البوتاس والفوسفات. - (2%): الملفوف، الباميا، البازلاء (طازجة أو مبردة). - (0%): اللحوم، الأسماك، لوازم شبكات الري، آلات الزراعة، نباتات الهندباء. - (معفى - Exempt): العدس، الشاي، الحليب، السكر، الأرز، الهواتف الخلوية، الكهرباء. 4. الأطراف: استخرج الرقم الضريبي للمورد (Supplier TIN) والرقم الضريبي أو الوطني للمشتري (Buyer TIN/National ID). 5. التصنيف: - "simplified": إذا كان المشتري فرداً (بدون رقم ضريبي). - "standard": إذا كان المشتري شركة أو منشأة (يوجد رقم ضريبي). 6. الفواتير المتعددة: إذا احتوى الملف على أكثر من فاتورة منفصلة، استخرج بيانات كل واحدة في عنصر مستقل في مصفوفة "invoices". 7. التجاهل: تجاهل الأختام اليدوية التي تغطي النصوص، وحاول استنتاج النص تحتها برمجياً. The output MUST be a strict JSON object with this schema: { "invoices": [ { "invoice_number": "string", "invoice_date": "YYYY-MM-DD", "invoice_type": "cash" | "credit", "ubl_type_code": "388" | "381", "payment_method_code": "013" | "023", "invoice_category": "standard" | "simplified", "supplier_name": "string", "supplier_tin": "string (10 digits)", "buyer_name": "string (optional)", "buyer_tin": "string (optional)", "buyer_national_id": "string (10 digits, optional)", "subtotal": number, "discount_total": number, "tax_amount": number, "grand_total": number, "currency_code": "JOD", "lines": [ { "line_number": number, "description": "string", "quantity": number, "unit_price": number, "discount": number, "tax_rate": number (e.g. 0.16), "line_total": number } ] } ] } Return ONLY the JSON. No markdown formatting. `; // Detect MIME type based on extension const ext = path.extname(filePath).toLowerCase(); let mimeType = 'image/jpeg'; if (ext === '.pdf') mimeType = 'application/pdf'; else if (ext === '.png') mimeType = 'image/png'; else if (ext === '.webp') mimeType = 'image/webp'; const result = await this.model.generateContent([ prompt, { inlineData: { data: fileData.toString('base64'), mimeType: mimeType, }, }, ]); const responseText = result.response.text(); // Clean up markdown if any const cleanedJson = responseText.replace(/```json|```/g, '').trim(); const data = JSON.parse(cleanedJson); return data.invoices || []; } catch (error: any) { this.logger.error(`AI Extraction failed: ${error.message}`); throw new InternalServerErrorException('AI Extraction failed'); } } }