130 lines
6.9 KiB
TypeScript
130 lines
6.9 KiB
TypeScript
/**
|
|
* ════════════════════════════════════════════════════════════
|
|
* مُصادَق (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<string>('GEMINI_API_KEY');
|
|
this.genAI = new GoogleGenerativeAI(apiKey);
|
|
this.model = this.genAI.getGenerativeModel({
|
|
model: this.configService.get<string>('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<any> {
|
|
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');
|
|
}
|
|
}
|
|
}
|