🚀 Initialize Musadaq SaaS: Full Backend + AI + React Dashboard + Docker Setup
This commit is contained in:
14
backend/src/modules/validation/tax-validation.module.ts
Normal file
14
backend/src/modules/validation/tax-validation.module.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* ════════════════════════════════════════════════════════════
|
||||
* مُصادَق (Musadaq) — Tax Validation Module
|
||||
* ════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TaxValidationService } from './tax-validation.service';
|
||||
|
||||
@Module({
|
||||
providers: [TaxValidationService],
|
||||
exports: [TaxValidationService],
|
||||
})
|
||||
export class TaxValidationModule {}
|
||||
103
backend/src/modules/validation/tax-validation.service.ts
Normal file
103
backend/src/modules/validation/tax-validation.service.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* ════════════════════════════════════════════════════════════
|
||||
* مُصادَق (Musadaq) — Tax Validation Service
|
||||
* ════════════════════════════════════════════════════════════
|
||||
* محرك التحقق من القواعد الضريبية الأردنية (ISTD Rules).
|
||||
* يضمن دقة الحسابات قبل إرسالها إلى "جو فوترة".
|
||||
* ════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Invoice } from '../invoices/entities/invoice.entity';
|
||||
|
||||
export interface ValidationResult {
|
||||
isValid: boolean;
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class TaxValidationService {
|
||||
private readonly logger = new Logger(TaxValidationService.name);
|
||||
private readonly PRECISION = 0.005; // السماحية في الفروقات العشرية البسيطة
|
||||
|
||||
/**
|
||||
* التحقق الشامل من الفاتورة
|
||||
*/
|
||||
validateInvoice(invoice: Invoice): ValidationResult {
|
||||
const errors: string[] = [];
|
||||
|
||||
// 1. Rule 001: التحقق من مجموع بنود الفاتورة (Subtotal)
|
||||
this.checkRule001(invoice, errors);
|
||||
|
||||
// 2. Rule 002: التحقق من قيمة الضريبة (Tax Amount)
|
||||
this.checkRule002(invoice, errors);
|
||||
|
||||
// 3. Rule 003: التحقق من الخصومات (Discounts)
|
||||
this.checkRule003(invoice, errors);
|
||||
|
||||
// 4. Rule 004: التحقق من المجموع النهائي (Grand Total)
|
||||
this.checkRule004(invoice, errors);
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Rule 001: Σ (Quantity * UnitPrice) = Subtotal (Before Tax/Discount)
|
||||
*/
|
||||
private checkRule001(invoice: Invoice, errors: string[]) {
|
||||
const calculatedSubtotal = invoice.lines.reduce(
|
||||
(sum, line) => sum + Number(line.quantity) * Number(line.unit_price),
|
||||
0,
|
||||
);
|
||||
|
||||
if (Math.abs(calculatedSubtotal - Number(invoice.subtotal)) > this.PRECISION) {
|
||||
errors.push(`خطأ في القاعدة 001: مجموع البنود (${calculatedSubtotal}) لا يطابق المجموع الفرعي المسجل (${invoice.subtotal})`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rule 002: TaxAmount = (Subtotal - Discount) * TaxRate
|
||||
* ملاحظة: يجب التحقق لكل بند بشكل منفصل أو للمجموع حسب نوع الفاتورة
|
||||
*/
|
||||
private checkRule002(invoice: Invoice, errors: string[]) {
|
||||
const calculatedTax = invoice.lines.reduce(
|
||||
(sum, line) => {
|
||||
const lineBeforeTax = (Number(line.quantity) * Number(line.unit_price)) - Number(line.discount);
|
||||
return sum + (lineBeforeTax * Number(line.tax_rate));
|
||||
},
|
||||
0,
|
||||
);
|
||||
|
||||
if (Math.abs(calculatedTax - Number(invoice.tax_amount)) > this.PRECISION) {
|
||||
errors.push(`خطأ في القاعدة 002: قيمة الضريبة المحسوبة (${calculatedTax.toFixed(3)}) لا تطابق القيمة المسجلة (${invoice.tax_amount})`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rule 003: Σ Line Discounts = Total Discount
|
||||
*/
|
||||
private checkRule003(invoice: Invoice, errors: string[]) {
|
||||
const totalLineDiscounts = invoice.lines.reduce(
|
||||
(sum, line) => sum + Number(line.discount),
|
||||
0,
|
||||
);
|
||||
|
||||
if (Math.abs(totalLineDiscounts - Number(invoice.discount_total)) > this.PRECISION) {
|
||||
errors.push(`خطأ في القاعدة 003: مجموع خصومات البنود (${totalLineDiscounts}) لا يطابق إجمالي الخصم (${invoice.discount_total})`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rule 004: GrandTotal = Subtotal - Discount + Tax
|
||||
*/
|
||||
private checkRule004(invoice: Invoice, errors: string[]) {
|
||||
const calculatedGrandTotal = Number(invoice.subtotal) - Number(invoice.discount_total) + Number(invoice.tax_amount);
|
||||
|
||||
if (Math.abs(calculatedGrandTotal - Number(invoice.grand_total)) > this.PRECISION) {
|
||||
errors.push(`خطأ في القاعدة 004: المجموع النهائي المحسوب (${calculatedGrandTotal.toFixed(3)}) لا يطابق المسجل (${invoice.grand_total})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user