import * as plugins from '../../plugins.js'; import type { TAccountingDocItem } from '@tsclass/tsclass/dist_ts/finance/index.js'; import type { EInvoice } from '../../einvoice.js'; import { CurrencyCalculator } from '../utils/currency.utils.js'; import type { ValidationResult } from './validation.types.js'; /** * VAT Category codes according to UNCL5305 */ export enum VATCategory { S = 'S', // Standard rate Z = 'Z', // Zero rated E = 'E', // Exempt from tax AE = 'AE', // VAT Reverse Charge K = 'K', // VAT exempt for EEA intra-community supply G = 'G', // Free export outside EU O = 'O', // Services outside scope of tax L = 'L', // Canary Islands general indirect tax M = 'M' // Tax for production, services and importation in Ceuta and Melilla } /** * Extended VAT information for EN16931 */ export interface VATBreakdown { category: VATCategory; rate: number; taxableAmount: number; taxAmount: number; exemptionReason?: string; exemptionReasonCode?: string; } /** * Comprehensive VAT Category Rules Validator * Implements all EN16931 VAT category-specific business rules */ export class VATCategoriesValidator { private results: ValidationResult[] = []; private currencyCalculator?: CurrencyCalculator; /** * Validate VAT categories according to EN16931 */ public validate(invoice: EInvoice): ValidationResult[] { this.results = []; // Initialize currency calculator if currency is available if (invoice.currency) { this.currencyCalculator = new CurrencyCalculator(invoice.currency); } // Group items by VAT category const itemsByCategory = this.groupItemsByVATCategory(invoice.items || []); const breakdownsByCategory = this.groupBreakdownsByCategory(invoice.taxBreakdown || []); // Validate each VAT category this.validateStandardRate(itemsByCategory.get('S'), breakdownsByCategory.get('S'), invoice); this.validateZeroRated(itemsByCategory.get('Z'), breakdownsByCategory.get('Z'), invoice); this.validateExempt(itemsByCategory.get('E'), breakdownsByCategory.get('E'), invoice); this.validateReverseCharge(itemsByCategory.get('AE'), breakdownsByCategory.get('AE'), invoice); this.validateIntraCommunity(itemsByCategory.get('K'), breakdownsByCategory.get('K'), invoice); this.validateExport(itemsByCategory.get('G'), breakdownsByCategory.get('G'), invoice); this.validateOutOfScope(itemsByCategory.get('O'), breakdownsByCategory.get('O'), invoice); // Cross-category validation this.validateCrossCategoryRules(invoice, itemsByCategory, breakdownsByCategory); return this.results; } /** * Validate Standard Rate VAT (BR-S-*) */ private validateStandardRate( items?: TAccountingDocItem[], breakdown?: any, invoice?: EInvoice ): void { if (!items || items.length === 0) return; // BR-S-01: Invoice with standard rated items must have standard rated breakdown if (!breakdown) { this.addError('BR-S-01', 'Invoice with standard rated items must have a standard rated VAT breakdown', 'taxBreakdown' ); return; } // BR-S-02: Standard rate VAT category taxable amount const expectedTaxable = this.calculateTaxableAmount(items); if (!this.areAmountsEqual(breakdown.netAmount, expectedTaxable)) { this.addError('BR-S-02', `Standard rate VAT taxable amount mismatch`, 'taxBreakdown.netAmount', breakdown.netAmount, expectedTaxable ); } // BR-S-03: Standard rate VAT category tax amount const rate = breakdown.taxPercent || 0; const expectedTax = this.calculateVATAmount(expectedTaxable, rate); if (!this.areAmountsEqual(breakdown.taxAmount, expectedTax)) { this.addError('BR-S-03', `Standard rate VAT tax amount mismatch`, 'taxBreakdown.taxAmount', breakdown.taxAmount, expectedTax ); } // BR-S-04: Standard rate VAT category code must be "S" if (breakdown.categoryCode && breakdown.categoryCode !== 'S') { this.addError('BR-S-04', 'Standard rate VAT category code must be "S"', 'taxBreakdown.categoryCode', breakdown.categoryCode, 'S' ); } // BR-S-05: Standard rate VAT rate must be greater than zero if (rate <= 0) { this.addError('BR-S-05', 'Standard rate VAT rate must be greater than zero', 'taxBreakdown.taxPercent', rate, '> 0' ); } // BR-S-08: No exemption reason for standard rate if (breakdown.exemptionReason) { this.addError('BR-S-08', 'Standard rate VAT must not have an exemption reason', 'taxBreakdown.exemptionReason' ); } } /** * Validate Zero Rated VAT (BR-Z-*) */ private validateZeroRated( items?: TAccountingDocItem[], breakdown?: any, invoice?: EInvoice ): void { if (!items || items.length === 0) return; // BR-Z-01: Invoice with zero rated items must have zero rated breakdown if (!breakdown) { this.addError('BR-Z-01', 'Invoice with zero rated items must have a zero rated VAT breakdown', 'taxBreakdown' ); return; } // BR-Z-02: Zero rate VAT category taxable amount const expectedTaxable = this.calculateTaxableAmount(items); if (!this.areAmountsEqual(breakdown.netAmount, expectedTaxable)) { this.addError('BR-Z-02', 'Zero rate VAT taxable amount mismatch', 'taxBreakdown.netAmount', breakdown.netAmount, expectedTaxable ); } // BR-Z-03: Zero rate VAT tax amount must be zero if (breakdown.taxAmount !== 0) { this.addError('BR-Z-03', 'Zero rate VAT tax amount must be zero', 'taxBreakdown.taxAmount', breakdown.taxAmount, 0 ); } // BR-Z-04: Zero rate VAT category code must be "Z" if (breakdown.categoryCode && breakdown.categoryCode !== 'Z') { this.addError('BR-Z-04', 'Zero rate VAT category code must be "Z"', 'taxBreakdown.categoryCode', breakdown.categoryCode, 'Z' ); } // BR-Z-05: Zero rate VAT rate must be zero if (breakdown.taxPercent !== 0) { this.addError('BR-Z-05', 'Zero rate VAT rate must be zero', 'taxBreakdown.taxPercent', breakdown.taxPercent, 0 ); } } /** * Validate Exempt from Tax (BR-E-*) */ private validateExempt( items?: TAccountingDocItem[], breakdown?: any, invoice?: EInvoice ): void { if (!items || items.length === 0) return; // BR-E-01: Invoice with exempt items must have exempt breakdown if (!breakdown) { this.addError('BR-E-01', 'Invoice with tax exempt items must have an exempt VAT breakdown', 'taxBreakdown' ); return; } // BR-E-02: Exempt VAT category taxable amount const expectedTaxable = this.calculateTaxableAmount(items); if (!this.areAmountsEqual(breakdown.netAmount, expectedTaxable)) { this.addError('BR-E-02', 'Exempt VAT taxable amount mismatch', 'taxBreakdown.netAmount', breakdown.netAmount, expectedTaxable ); } // BR-E-03: Exempt VAT tax amount must be zero if (breakdown.taxAmount !== 0) { this.addError('BR-E-03', 'Exempt VAT tax amount must be zero', 'taxBreakdown.taxAmount', breakdown.taxAmount, 0 ); } // BR-E-04: Exempt VAT category code must be "E" if (breakdown.categoryCode && breakdown.categoryCode !== 'E') { this.addError('BR-E-04', 'Exempt VAT category code must be "E"', 'taxBreakdown.categoryCode', breakdown.categoryCode, 'E' ); } // BR-E-05: Exempt VAT rate must be zero if (breakdown.taxPercent !== 0) { this.addError('BR-E-05', 'Exempt VAT rate must be zero', 'taxBreakdown.taxPercent', breakdown.taxPercent, 0 ); } // BR-E-06: Exempt VAT must have exemption reason if (!breakdown.exemptionReason && !breakdown.exemptionReasonCode) { this.addError('BR-E-06', 'Exempt VAT must have an exemption reason or exemption reason code', 'taxBreakdown.exemptionReason' ); } } /** * Validate VAT Reverse Charge (BR-AE-*) */ private validateReverseCharge( items?: TAccountingDocItem[], breakdown?: any, invoice?: EInvoice ): void { if (!items || items.length === 0) return; // BR-AE-01: Invoice with reverse charge items must have reverse charge breakdown if (!breakdown) { this.addError('BR-AE-01', 'Invoice with reverse charge items must have a reverse charge VAT breakdown', 'taxBreakdown' ); return; } // BR-AE-02: Reverse charge VAT category taxable amount const expectedTaxable = this.calculateTaxableAmount(items); if (!this.areAmountsEqual(breakdown.netAmount, expectedTaxable)) { this.addError('BR-AE-02', 'Reverse charge VAT taxable amount mismatch', 'taxBreakdown.netAmount', breakdown.netAmount, expectedTaxable ); } // BR-AE-03: Reverse charge VAT tax amount must be zero if (breakdown.taxAmount !== 0) { this.addError('BR-AE-03', 'Reverse charge VAT tax amount must be zero', 'taxBreakdown.taxAmount', breakdown.taxAmount, 0 ); } // BR-AE-04: Reverse charge VAT category code must be "AE" if (breakdown.categoryCode && breakdown.categoryCode !== 'AE') { this.addError('BR-AE-04', 'Reverse charge VAT category code must be "AE"', 'taxBreakdown.categoryCode', breakdown.categoryCode, 'AE' ); } // BR-AE-05: Reverse charge VAT rate must be zero if (breakdown.taxPercent !== 0) { this.addError('BR-AE-05', 'Reverse charge VAT rate must be zero', 'taxBreakdown.taxPercent', breakdown.taxPercent, 0 ); } // BR-AE-06: Reverse charge must have exemption reason if (!breakdown.exemptionReason && !breakdown.exemptionReasonCode) { this.addError('BR-AE-06', 'Reverse charge VAT must have an exemption reason', 'taxBreakdown.exemptionReason' ); } // BR-AE-08: Buyer must have VAT identifier for reverse charge if (!invoice?.metadata?.buyerTaxId) { this.addError('BR-AE-08', 'Buyer must have a VAT identifier for reverse charge invoices', 'metadata.buyerTaxId' ); } } /** * Validate Intra-Community Supply (BR-K-*) */ private validateIntraCommunity( items?: TAccountingDocItem[], breakdown?: any, invoice?: EInvoice ): void { if (!items || items.length === 0) return; // BR-K-01: Invoice with intra-community items must have intra-community breakdown if (!breakdown) { this.addError('BR-K-01', 'Invoice with intra-community supply must have corresponding VAT breakdown', 'taxBreakdown' ); return; } // BR-K-02: Intra-community VAT category taxable amount const expectedTaxable = this.calculateTaxableAmount(items); if (!this.areAmountsEqual(breakdown.netAmount, expectedTaxable)) { this.addError('BR-K-02', 'Intra-community VAT taxable amount mismatch', 'taxBreakdown.netAmount', breakdown.netAmount, expectedTaxable ); } // BR-K-03: Intra-community VAT tax amount must be zero if (breakdown.taxAmount !== 0) { this.addError('BR-K-03', 'Intra-community VAT tax amount must be zero', 'taxBreakdown.taxAmount', breakdown.taxAmount, 0 ); } // BR-K-04: Intra-community VAT category code must be "K" if (breakdown.categoryCode && breakdown.categoryCode !== 'K') { this.addError('BR-K-04', 'Intra-community VAT category code must be "K"', 'taxBreakdown.categoryCode', breakdown.categoryCode, 'K' ); } // BR-K-05: Intra-community VAT rate must be zero if (breakdown.taxPercent !== 0) { this.addError('BR-K-05', 'Intra-community VAT rate must be zero', 'taxBreakdown.taxPercent', breakdown.taxPercent, 0 ); } // BR-K-06: Must have exemption reason if (!breakdown.exemptionReason && !breakdown.exemptionReasonCode) { this.addError('BR-K-06', 'Intra-community supply must have an exemption reason', 'taxBreakdown.exemptionReason' ); } // BR-K-08: Both seller and buyer must have VAT identifiers if (!invoice?.metadata?.sellerTaxId) { this.addError('BR-K-08', 'Seller must have a VAT identifier for intra-community supply', 'metadata.sellerTaxId' ); } if (!invoice?.metadata?.buyerTaxId) { this.addError('BR-K-09', 'Buyer must have a VAT identifier for intra-community supply', 'metadata.buyerTaxId' ); } // BR-K-10: Must be in different EU member states if (invoice?.from?.address?.countryCode === invoice?.to?.address?.countryCode) { this.addWarning('BR-K-10', 'Intra-community supply should be between different EU member states', 'address.countryCode' ); } } /** * Validate Export Outside EU (BR-G-*) */ private validateExport( items?: TAccountingDocItem[], breakdown?: any, invoice?: EInvoice ): void { if (!items || items.length === 0) return; // BR-G-01: Invoice with export items must have export breakdown if (!breakdown) { this.addError('BR-G-01', 'Invoice with export items must have an export VAT breakdown', 'taxBreakdown' ); return; } // BR-G-02: Export VAT category taxable amount const expectedTaxable = this.calculateTaxableAmount(items); if (!this.areAmountsEqual(breakdown.netAmount, expectedTaxable)) { this.addError('BR-G-02', 'Export VAT taxable amount mismatch', 'taxBreakdown.netAmount', breakdown.netAmount, expectedTaxable ); } // BR-G-03: Export VAT tax amount must be zero if (breakdown.taxAmount !== 0) { this.addError('BR-G-03', 'Export VAT tax amount must be zero', 'taxBreakdown.taxAmount', breakdown.taxAmount, 0 ); } // BR-G-04: Export VAT category code must be "G" if (breakdown.categoryCode && breakdown.categoryCode !== 'G') { this.addError('BR-G-04', 'Export VAT category code must be "G"', 'taxBreakdown.categoryCode', breakdown.categoryCode, 'G' ); } // BR-G-05: Export VAT rate must be zero if (breakdown.taxPercent !== 0) { this.addError('BR-G-05', 'Export VAT rate must be zero', 'taxBreakdown.taxPercent', breakdown.taxPercent, 0 ); } // BR-G-06: Must have exemption reason if (!breakdown.exemptionReason && !breakdown.exemptionReasonCode) { this.addError('BR-G-06', 'Export must have an exemption reason', 'taxBreakdown.exemptionReason' ); } // BR-G-08: Buyer should be outside EU const buyerCountry = invoice?.to?.address?.countryCode; if (buyerCountry && this.isEUCountry(buyerCountry)) { this.addWarning('BR-G-08', 'Export category should be used for buyers outside EU', 'to.address.countryCode', buyerCountry, 'non-EU' ); } } /** * Validate Out of Scope Services (BR-O-*) */ private validateOutOfScope( items?: TAccountingDocItem[], breakdown?: any, invoice?: EInvoice ): void { if (!items || items.length === 0) return; // BR-O-01: Invoice with out of scope items must have out of scope breakdown if (!breakdown) { this.addError('BR-O-01', 'Invoice with out of scope items must have corresponding VAT breakdown', 'taxBreakdown' ); return; } // BR-O-02: Out of scope VAT category taxable amount const expectedTaxable = this.calculateTaxableAmount(items); if (!this.areAmountsEqual(breakdown.netAmount, expectedTaxable)) { this.addError('BR-O-02', 'Out of scope VAT taxable amount mismatch', 'taxBreakdown.netAmount', breakdown.netAmount, expectedTaxable ); } // BR-O-03: Out of scope VAT tax amount must be zero if (breakdown.taxAmount !== 0) { this.addError('BR-O-03', 'Out of scope VAT tax amount must be zero', 'taxBreakdown.taxAmount', breakdown.taxAmount, 0 ); } // BR-O-04: Out of scope VAT category code must be "O" if (breakdown.categoryCode && breakdown.categoryCode !== 'O') { this.addError('BR-O-04', 'Out of scope VAT category code must be "O"', 'taxBreakdown.categoryCode', breakdown.categoryCode, 'O' ); } // BR-O-05: Out of scope VAT rate must be zero if (breakdown.taxPercent !== 0) { this.addError('BR-O-05', 'Out of scope VAT rate must be zero', 'taxBreakdown.taxPercent', breakdown.taxPercent, 0 ); } // BR-O-06: Must have exemption reason if (!breakdown.exemptionReason && !breakdown.exemptionReasonCode) { this.addError('BR-O-06', 'Out of scope services must have an exemption reason', 'taxBreakdown.exemptionReason' ); } } /** * Cross-category validation rules */ private validateCrossCategoryRules( invoice: EInvoice, itemsByCategory: Map, breakdownsByCategory: Map ): void { // BR-CO-17: VAT category tax amount = Σ(VAT category taxable amount × VAT rate) breakdownsByCategory.forEach((breakdown, category) => { if (category === 'S' && breakdown.taxPercent > 0) { const expectedTax = this.calculateVATAmount(breakdown.netAmount, breakdown.taxPercent); if (!this.areAmountsEqual(breakdown.taxAmount, expectedTax)) { this.addError('BR-CO-17', `VAT tax amount calculation error for category ${category}`, 'taxBreakdown.taxAmount', breakdown.taxAmount, expectedTax ); } } }); // BR-CO-18: Invoice with mixed VAT categories const categoriesUsed = new Set(); itemsByCategory.forEach((items, category) => { if (items.length > 0) categoriesUsed.add(category); }); // BR-IC-01: Supply to EU countries without VAT ID should use standard rate if (categoriesUsed.has('K') && !invoice.metadata?.buyerTaxId) { this.addError('BR-IC-01', 'Intra-community supply requires buyer VAT identifier', 'metadata.buyerTaxId' ); } // BR-IC-02: Reverse charge requires specific conditions if (categoriesUsed.has('AE')) { // Check for service codes that qualify for reverse charge const hasQualifyingServices = invoice.items?.some(item => this.isReverseChargeService(item) ); if (!hasQualifyingServices) { this.addWarning('BR-IC-02', 'Reverse charge should only be used for qualifying services', 'items' ); } } // BR-CO-19: Sum of VAT breakdown taxable amounts must equal invoice tax exclusive total let totalTaxable = 0; breakdownsByCategory.forEach(breakdown => { totalTaxable += breakdown.netAmount || 0; }); const declaredTotal = invoice.totalNet || 0; if (!this.areAmountsEqual(totalTaxable, declaredTotal)) { this.addError('BR-CO-19', 'Sum of VAT breakdown taxable amounts must equal invoice total without VAT', 'totalNet', declaredTotal, totalTaxable ); } } // Helper methods private groupItemsByVATCategory(items: TAccountingDocItem[]): Map { const groups = new Map(); items.forEach(item => { const category = this.determineVATCategory(item); if (!groups.has(category)) { groups.set(category, []); } groups.get(category)!.push(item); }); return groups; } private groupBreakdownsByCategory(breakdowns: any[]): Map { const groups = new Map(); breakdowns.forEach(breakdown => { const category = breakdown.categoryCode || this.inferCategoryFromRate(breakdown.taxPercent); groups.set(category, breakdown); }); return groups; } private determineVATCategory(item: TAccountingDocItem): string { // Determine VAT category from item metadata or rate const metadata = (item as any).metadata; if (metadata?.vatCategory) { return metadata.vatCategory; } // Infer from rate if (item.vatPercentage === undefined || item.vatPercentage === null) { return 'S'; // Default to standard } else if (item.vatPercentage > 0) { return 'S'; // Standard rate } else if (item.vatPercentage === 0) { // Could be Z, E, AE, K, G, or O - need more context if (metadata?.exemptionReason) { if (metadata.exemptionReason.includes('reverse')) return 'AE'; if (metadata.exemptionReason.includes('intra')) return 'K'; if (metadata.exemptionReason.includes('export')) return 'G'; if (metadata.exemptionReason.includes('scope')) return 'O'; return 'E'; // Default exempt } return 'Z'; // Default zero-rated } return 'S'; // Default } private inferCategoryFromRate(rate?: number): string { if (!rate || rate === 0) return 'Z'; if (rate > 0) return 'S'; return 'S'; } private calculateTaxableAmount(items: TAccountingDocItem[]): number { const total = items.reduce((sum, item) => { const lineNet = (item.unitNetPrice || 0) * (item.unitQuantity || 0); return sum + (this.currencyCalculator ? this.currencyCalculator.round(lineNet) : lineNet); }, 0); return this.currencyCalculator ? this.currencyCalculator.round(total) : total; } private calculateVATAmount(taxableAmount: number, rate: number): number { const vat = taxableAmount * (rate / 100); return this.currencyCalculator ? this.currencyCalculator.round(vat) : vat; } private areAmountsEqual(value1: number, value2: number): boolean { if (this.currencyCalculator) { return this.currencyCalculator.areEqual(value1, value2); } return Math.abs(value1 - value2) < 0.01; } private isEUCountry(countryCode: string): boolean { const euCountries = [ 'AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR', 'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL', 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE' ]; return euCountries.includes(countryCode); } private isReverseChargeService(item: TAccountingDocItem): boolean { // Check if item qualifies for reverse charge // This would typically check service codes const metadata = (item as any).metadata; if (metadata?.serviceCode) { // Construction services, telecommunication, etc. const reverseChargeServices = ['44', '45', '61', '62']; return reverseChargeServices.some(code => metadata.serviceCode.startsWith(code) ); } return false; } private addError( ruleId: string, message: string, field?: string, value?: any, expected?: any ): void { this.results.push({ ruleId, source: 'EN16931', severity: 'error', message, field, value, expected, btReference: this.getBTReference(ruleId), bgReference: 'BG-23' // VAT breakdown }); } private addWarning( ruleId: string, message: string, field?: string, value?: any, expected?: any ): void { this.results.push({ ruleId, source: 'EN16931', severity: 'warning', message, field, value, expected, btReference: this.getBTReference(ruleId), bgReference: 'BG-23' }); } private getBTReference(ruleId: string): string | undefined { const btMap: Record = { 'BR-S-': 'BT-118', // VAT category rate 'BR-Z-': 'BT-118', 'BR-E-': 'BT-120', // VAT exemption reason 'BR-AE-': 'BT-120', 'BR-K-': 'BT-120', 'BR-G-': 'BT-120', 'BR-O-': 'BT-120', 'BR-CO-17': 'BT-117', // VAT category tax amount 'BR-CO-18': 'BT-118', 'BR-CO-19': 'BT-116' // VAT category taxable amount }; for (const [prefix, bt] of Object.entries(btMap)) { if (ruleId.startsWith(prefix)) { return bt; } } return undefined; } } /** * Get VAT category name */ export function getVATCategoryName(category: VATCategory): string { const names: Record = { [VATCategory.S]: 'Standard rate', [VATCategory.Z]: 'Zero rated', [VATCategory.E]: 'Exempt from tax', [VATCategory.AE]: 'VAT Reverse Charge', [VATCategory.K]: 'VAT exempt for EEA intra-community supply', [VATCategory.G]: 'Free export outside EU', [VATCategory.O]: 'Services outside scope of tax', [VATCategory.L]: 'Canary Islands general indirect tax', [VATCategory.M]: 'Tax for production, services and importation in Ceuta and Melilla' }; return names[category] || 'Unknown'; }