🚀 Initialize Musadaq SaaS: Full Backend + AI + React Dashboard + Docker Setup

This commit is contained in:
Hamza-Ayed
2026-04-16 23:26:32 +03:00
commit d66891ba0f
221 changed files with 13079 additions and 0 deletions

View 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');
}
}
}