🚀 Initialize Musadaq SaaS: Full Backend + AI + React Dashboard + Docker Setup
This commit is contained in:
90
backend/src/modules/invoices/gemini-extractor.service.ts
Normal file
90
backend/src/modules/invoices/gemini-extractor.service.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* ════════════════════════════════════════════════════════════
|
||||
* مُصادَق (Musadaq) — Gemini AI Extraction Service
|
||||
* ════════════════════════════════════════════════════════════
|
||||
* يقوم باستخراج البيانات من صور/ملفات الفواتير باستخدام 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'),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* استخراج البيانات من صورة الفاتورة
|
||||
*/
|
||||
async extractInvoiceData(filePath: string, storageRoot: string): Promise<any> {
|
||||
try {
|
||||
const fullPath = path.join(storageRoot, filePath);
|
||||
const fileData = fs.readFileSync(fullPath);
|
||||
|
||||
const prompt = `
|
||||
You are a Jordanian tax expert. Extract all details from this invoice image for the Jordan National Invoicing System (JoFotara / JoInvoice).
|
||||
The output MUST be a strict JSON object following this schema:
|
||||
{
|
||||
"invoice_number": "string",
|
||||
"invoice_date": "YYYY-MM-DD",
|
||||
"invoice_type": "cash" | "credit",
|
||||
"supplier_name": "string",
|
||||
"supplier_tin": "string (10 digits)",
|
||||
"buyer_name": "string (optional)",
|
||||
"buyer_tin": "string (optional)",
|
||||
"subtotal": number (before discount and tax),
|
||||
"discount_total": number (total discount),
|
||||
"tax_amount": number (total tax),
|
||||
"grand_total": number (final amount),
|
||||
"currency_code": "JOD",
|
||||
"lines": [
|
||||
{
|
||||
"line_number": number,
|
||||
"description": "string",
|
||||
"quantity": number,
|
||||
"unit_price": number,
|
||||
"discount": number,
|
||||
"tax_rate": number (e.g. 0.16 for 16%),
|
||||
"line_total": number (quantity * unit_price - discount)
|
||||
}
|
||||
]
|
||||
}
|
||||
Return ONLY the JSON. No markdown formatting. If a field is missing, return null.
|
||||
Pay close attention to Jordanian Tax Rules (subtotal - discount + tax = grand_total).
|
||||
`;
|
||||
|
||||
const result = await this.model.generateContent([
|
||||
prompt,
|
||||
{
|
||||
inlineData: {
|
||||
data: fileData.toString('base64'),
|
||||
mimeType: 'image/jpeg', // Adjusted based on file extension in prod
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const responseText = result.response.text();
|
||||
// Clean up markdown if any
|
||||
const cleanedJson = responseText.replace(/```json|```/g, '').trim();
|
||||
|
||||
return JSON.parse(cleanedJson);
|
||||
} catch (error) {
|
||||
this.logger.error(`AI Extraction failed: ${error.message}`);
|
||||
throw new InternalServerErrorException('AI Extraction failed');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user