🚀 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,160 @@
/**
* ════════════════════════════════════════════════════════════
* مُصادَق (Musadaq) — Invoice Processor (Queue Consumer)
* ════════════════════════════════════════════════════════════
* المستهلك الرئيسي لطابور معالجة الفواتير (Bull Queue).
* يربط بين الذكاء الاصطناعي، التحقق الضريبي، وتوليد الـ XML.
* ════════════════════════════════════════════════════════════
*/
import { Process, Processor, OnQueueActive, OnQueueCompleted, OnQueueFailed } from '@nestjs/bull';
import { Logger } from '@nestjs/common';
import { Job } from 'bull';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource } from 'typeorm';
import { Invoice, InvoiceStatus } from './entities/invoice.entity';
import { InvoiceLine } from './entities/invoice-line.entity';
import { GeminiExtractorService } from './gemini-extractor.service';
import { TaxValidationService } from '../validation/tax-validation.service';
import { ConfigService } from '@nestjs/config';
@Processor('invoice-processing')
export class InvoiceProcessor {
private readonly logger = new Logger(InvoiceProcessor.name);
constructor(
@InjectRepository(Invoice)
private invoiceRepository: Repository<Invoice>,
@InjectRepository(InvoiceLine)
private lineRepository: Repository<InvoiceLine>,
private geminiExtractor: GeminiExtractorService,
private taxValidation: TaxValidationService,
private configService: ConfigService,
private dataSource: DataSource,
) {}
@OnQueueActive()
onActive(job: Job) {
this.logger.log(`Processing job ${job.id} of type ${job.name}...`);
}
@OnQueueCompleted()
onComplete(job: Job, result: any) {
this.logger.log(`Completed job ${job.id} for invoice ${job.data.invoiceId}`);
}
@OnQueueFailed()
onError(job: Job, error: Error) {
this.logger.error(`Job ${job.id} failed: ${error.message}`);
}
/**
* الخطوة الأولى: استخراج البيانات باستخدام AI
*/
@Process('extract-data')
async handleExtraction(job: Job<{ invoiceId: string; filePath: string; tenantId: string }>) {
const { invoiceId, filePath } = job.data;
const storageRoot = this.configService.get<string>('STORAGE_PATH', './uploads');
try {
// 1. Update status to EXTRACTING
await this.invoiceRepository.update(invoiceId, { status: InvoiceStatus.EXTRACTING });
// 2. Extract data via Gemini
const data = await this.geminiExtractor.extractInvoiceData(filePath, storageRoot);
// 3. Save extracted data in a transaction
await this.saveExtractedData(invoiceId, data);
this.logger.log(`Extraction successful for invoice ${invoiceId}`);
} catch (error) {
await this.invoiceRepository.update(invoiceId, {
status: InvoiceStatus.VALIDATION_FAILED,
// Optional: Save error message in a notes column
});
throw error;
}
}
/**
* حفظ البيانات المستخرجة في قاعدة البيانات
*/
private async saveExtractedData(invoiceId: string, data: any) {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// 1. Update Invoice Header
await queryRunner.manager.update(Invoice, invoiceId, {
invoice_number: data.invoice_number,
invoice_date: data.invoice_date,
invoice_type: data.invoice_type || 'cash',
supplier_name: data.supplier_name,
supplier_tin: data.supplier_tin,
buyer_name: data.buyer_name,
buyer_tin: data.buyer_tin,
subtotal: data.subtotal,
discount_total: data.discount_total,
tax_amount: data.tax_amount,
grand_total: data.grand_total,
currency_code: data.currency_code || 'JOD',
status: InvoiceStatus.EXTRACTED,
});
// 2. Clear old lines if any (shouldn't happen on first extract)
await queryRunner.manager.delete(InvoiceLine, { invoice_id: invoiceId });
// 3. Create new lines
if (data.lines && Array.isArray(data.lines)) {
const lines = data.lines.map((l: any) =>
queryRunner.manager.create(InvoiceLine, {
invoice_id: invoiceId,
line_number: l.line_number,
description: l.description,
quantity: l.quantity,
unit_price: l.unit_price,
discount: l.discount || 0,
tax_rate: l.tax_rate,
line_total: l.line_total,
})
);
await queryRunner.manager.save(InvoiceLine, lines);
}
await queryRunner.commitTransaction();
// 4. Trigger Auto-Validation (Internal Step)
await this.autoValidate(invoiceId);
} catch (error) {
await queryRunner.rollbackTransaction();
throw error;
} finally {
await queryRunner.release();
}
}
/**
* الخطوة الثانية: التحقق الضريبي التلقائي
*/
private async autoValidate(invoiceId: string) {
const invoice = await this.invoiceRepository.findOne({
where: { id: invoiceId },
relations: ['lines'],
});
if (!invoice) return;
const result = this.taxValidation.validateInvoice(invoice);
if (result.isValid) {
await this.invoiceRepository.update(invoiceId, { status: InvoiceStatus.VALIDATED });
} else {
await this.invoiceRepository.update(invoiceId, {
status: InvoiceStatus.VALIDATION_FAILED,
// Optional: Save detailed error list
});
}
}
}