✨ Feat: Invoice viewing, JoFotara UBL refinements, delete functionality, and tax rate validation
This commit is contained in:
@@ -63,8 +63,13 @@ export class GeminiExtractorService {
|
||||
}
|
||||
]
|
||||
}
|
||||
Pay close attention to Jordanian Tax Rules:
|
||||
- Standard Rate: 0.16 (16%)
|
||||
- Reduced Rate: 0.04 (4%)
|
||||
- Zero Rate: 0.00 (0%)
|
||||
- Exempt: null or 0 with explanation.
|
||||
- The formula MUST hold: Subtotal - Discount + Tax = Grand Total.
|
||||
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).
|
||||
`;
|
||||
|
||||
// Detect MIME type based on extension
|
||||
|
||||
@@ -84,4 +84,24 @@ export class InvoicesController {
|
||||
async submit(@CurrentUser() user: any, @Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.invoicesService.submitToJoFotara(user.tenantId, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* حذف الفاتورة نهائياً
|
||||
*/
|
||||
@Post(':id/delete') // Using POST for delete to match frontend request style or use standard DELETE
|
||||
@Roles(UserRole.ADMIN, UserRole.ACCOUNTANT)
|
||||
async remove(@CurrentUser() user: any, @Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.invoicesService.remove(user.tenantId, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* الحصول على الملف الأصلي للفاتورة
|
||||
*/
|
||||
@Get(':id/file')
|
||||
async getFile(
|
||||
@CurrentUser() user: any,
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
) {
|
||||
return this.invoicesService.getFile(user.tenantId, id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,13 @@ import {
|
||||
ForbiddenException,
|
||||
Inject,
|
||||
forwardRef,
|
||||
InternalServerErrorException,
|
||||
StreamableFile,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { InjectQueue } from '@nestjs/bull';
|
||||
import { Queue } from 'bull';
|
||||
import { Invoice, InvoiceStatus } from './entities/invoice.entity';
|
||||
@@ -157,4 +161,39 @@ export class InvoicesService {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* حذف الفاتورة والملف التابع لها
|
||||
*/
|
||||
async remove(tenantId: string, id: string): Promise<void> {
|
||||
const invoice = await this.findOne(tenantId, id);
|
||||
|
||||
// 1. Delete file if exists
|
||||
if (invoice.original_file_path) {
|
||||
await this.localStorageService.deleteFile(invoice.original_file_path);
|
||||
}
|
||||
|
||||
// 2. Delete from DB (lines will be deleted via CASCADE)
|
||||
await this.invoiceRepository.delete(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* الحصول على الملف كـ Stream
|
||||
*/
|
||||
async getFile(tenantId: string, id: string): Promise<StreamableFile> {
|
||||
const invoice = await this.findOne(tenantId, id);
|
||||
if (!invoice.original_file_path) {
|
||||
throw new NotFoundException('Invoice file not found');
|
||||
}
|
||||
|
||||
const storageRoot = this.localStorageService['storageRoot']; // Accessing private for path resolve
|
||||
const fullPath = path.join(storageRoot, invoice.original_file_path);
|
||||
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
throw new NotFoundException('File does not exist on disk');
|
||||
}
|
||||
|
||||
const file = fs.createReadStream(fullPath);
|
||||
return new StreamableFile(file);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,11 +35,15 @@ export class JoFotaraGatewayService {
|
||||
const url = this.currentEnv === 'production' ? this.prodUrl : this.sandboxUrl;
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
invoice: Buffer.from(xmlContent).toString('base64'),
|
||||
};
|
||||
|
||||
this.logger.debug(`Submitting to JoFotara: ${url}/submit`);
|
||||
|
||||
const response = await axios.post(
|
||||
`${url}/submit`,
|
||||
{
|
||||
invoice: Buffer.from(xmlContent).toString('base64'),
|
||||
},
|
||||
payload,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -49,6 +53,7 @@ export class JoFotaraGatewayService {
|
||||
},
|
||||
);
|
||||
|
||||
this.logger.log(`JoFotara Response: ${JSON.stringify(response.data)}`);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
this.logger.error(`JoFotara API Error: ${error.response?.data || error.message}`);
|
||||
|
||||
@@ -85,6 +85,19 @@ export class UBLGeneratorService {
|
||||
.ele('cac:Price')
|
||||
.ele('cbc:PriceAmount', { currencyID: invoice.currency_code }).txt(line.unit_price.toString()).up()
|
||||
.up()
|
||||
.ele('cac:TaxTotal')
|
||||
.ele('cbc:TaxAmount', { currencyID: invoice.currency_code }).txt((line.line_total * line.tax_rate).toFixed(3)).up()
|
||||
.ele('cac:TaxSubtotal')
|
||||
.ele('cbc:TaxableAmount', { currencyID: invoice.currency_code }).txt(line.line_total.toString()).up()
|
||||
.ele('cbc:TaxAmount', { currencyID: invoice.currency_code }).txt((line.line_total * line.tax_rate).toFixed(3)).up()
|
||||
.ele('cac:TaxCategory')
|
||||
.ele('cbc:ID').txt(line.tax_rate > 0 ? 'S' : 'Z').up()
|
||||
.ele('cbc:Percent').txt((line.tax_rate * 100).toString()).up()
|
||||
.ele('cac:TaxScheme')
|
||||
.ele('cbc:ID').txt('VAT').up()
|
||||
.up()
|
||||
.up()
|
||||
.up()
|
||||
.up();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user