🚀 Initialize Musadaq SaaS: Full Backend + AI + React Dashboard + Docker Setup
This commit is contained in:
149
backend/src/modules/invoices/invoice.service.ts
Normal file
149
backend/src/modules/invoices/invoice.service.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* ════════════════════════════════════════════════════════════
|
||||
* مُصادَق (Musadaq) — Invoices Service
|
||||
* ════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
ForbiddenException,
|
||||
Inject,
|
||||
forwardRef,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { InjectQueue } from '@nestjs/bull';
|
||||
import { Queue } from 'bull';
|
||||
import { Invoice, InvoiceStatus } from './entities/invoice.entity';
|
||||
import { LocalStorageService } from '../../services/storage/local-storage.service';
|
||||
import { SubscriptionsService } from '../subscriptions/subscription.service';
|
||||
import { UBLGeneratorService } from './ubl-generator.service';
|
||||
import { JoFotaraGatewayService } from './jofotara-gateway.service';
|
||||
import { CompaniesService } from '../companies/company.service';
|
||||
|
||||
@Injectable()
|
||||
export class InvoicesService {
|
||||
constructor(
|
||||
@InjectRepository(Invoice)
|
||||
private invoiceRepository: Repository<Invoice>,
|
||||
private localStorageService: LocalStorageService,
|
||||
@Inject(forwardRef(() => SubscriptionsService))
|
||||
private subscriptionsService: SubscriptionsService,
|
||||
@InjectQueue('invoice-processing')
|
||||
private invoiceQueue: Queue,
|
||||
private ublGenerator: UBLGeneratorService,
|
||||
private jofotaraGateway: JoFotaraGatewayService,
|
||||
@Inject(forwardRef(() => CompaniesService))
|
||||
private companiesService: CompaniesService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* رفع فاتورة جديدة وبدء المعالجة
|
||||
*/
|
||||
async upload(
|
||||
tenantId: string,
|
||||
companyId: string,
|
||||
file: Express.Multer.File,
|
||||
): Promise<Invoice> {
|
||||
const canUpload = await this.subscriptionsService.checkInvoiceLimit(tenantId);
|
||||
if (!canUpload) {
|
||||
throw new ForbiddenException('Monthly invoice limit reached for your current plan');
|
||||
}
|
||||
|
||||
const fileName = `${Date.now()}-${file.originalname}`;
|
||||
const filePath = await this.localStorageService.saveFile(
|
||||
tenantId,
|
||||
companyId,
|
||||
fileName,
|
||||
file.buffer,
|
||||
);
|
||||
|
||||
const invoice = this.invoiceRepository.create({
|
||||
tenant_id: tenantId,
|
||||
company_id: companyId,
|
||||
original_file_path: filePath,
|
||||
status: InvoiceStatus.UPLOADED,
|
||||
});
|
||||
const savedInvoice = await this.invoiceRepository.save(invoice);
|
||||
|
||||
await this.invoiceQueue.add('extract-data', {
|
||||
invoiceId: savedInvoice.id,
|
||||
tenantId,
|
||||
companyId,
|
||||
filePath,
|
||||
});
|
||||
|
||||
await this.subscriptionsService.incrementInvoiceCount(tenantId);
|
||||
|
||||
return savedInvoice;
|
||||
}
|
||||
|
||||
/**
|
||||
* قائمة الفواتير لشركة محددة
|
||||
*/
|
||||
async findAll(tenantId: string, companyId: string): Promise<Invoice[]> {
|
||||
return this.invoiceRepository.find({
|
||||
where: { tenant_id: tenantId, company_id: companyId },
|
||||
order: { created_at: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* تفاصيل فاتورة
|
||||
*/
|
||||
async findOne(tenantId: string, id: string): Promise<Invoice> {
|
||||
const invoice = await this.invoiceRepository.findOne({
|
||||
where: { id, tenant_id: tenantId },
|
||||
relations: ['lines'],
|
||||
});
|
||||
if (!invoice) throw new NotFoundException('Invoice not found');
|
||||
return invoice;
|
||||
}
|
||||
|
||||
/**
|
||||
* تحديث بيانات الفاتورة يدوياً
|
||||
*/
|
||||
async update(tenantId: string, id: string, updateData: any): Promise<Invoice> {
|
||||
const invoice = await this.findOne(tenantId, id);
|
||||
|
||||
if (invoice.status === InvoiceStatus.APPROVED || invoice.status === InvoiceStatus.SUBMITTING) {
|
||||
throw new ForbiddenException('Cannot edit already approved or submitted invoice');
|
||||
}
|
||||
|
||||
Object.assign(invoice, updateData);
|
||||
return this.invoiceRepository.save(invoice);
|
||||
}
|
||||
|
||||
/**
|
||||
* إرسال الفاتورة إلى بوابة جو فوترة الحكومية
|
||||
*/
|
||||
async submitToJoFotara(tenantId: string, id: string): Promise<any> {
|
||||
const invoice = await this.findOne(tenantId, id);
|
||||
|
||||
if (invoice.status !== InvoiceStatus.VALIDATED && invoice.status !== InvoiceStatus.VALIDATION_FAILED && invoice.status !== InvoiceStatus.EXTRACTED) {
|
||||
throw new ForbiddenException('Invoice must be validated or extracted before submission');
|
||||
}
|
||||
|
||||
const company = await this.companiesService.findOne(tenantId, invoice.company_id);
|
||||
const credentials = await this.companiesService.getDecryptedCredentials(tenantId, invoice.company_id);
|
||||
|
||||
const xml = this.ublGenerator.generateXML(invoice, company);
|
||||
|
||||
await this.invoiceRepository.update(id, { status: InvoiceStatus.SUBMITTING });
|
||||
|
||||
try {
|
||||
const response = await this.jofotaraGateway.submitInvoice(
|
||||
xml,
|
||||
credentials.clientId,
|
||||
credentials.secretKey,
|
||||
);
|
||||
|
||||
await this.invoiceRepository.update(id, { status: InvoiceStatus.APPROVED });
|
||||
return response;
|
||||
} catch (error) {
|
||||
await this.invoiceRepository.update(id, { status: InvoiceStatus.VALIDATION_FAILED });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user