/** * FiscalCalculationService - Servicio de Calculos Fiscales * * Proporciona funciones de calculo de impuestos, retenciones y totales fiscales. * * @module Fiscal */ import { Repository } from 'typeorm'; import { TaxCategory, TaxNature } from '../entities/tax-category.entity'; import { WithholdingType } from '../entities/withholding-type.entity'; interface ServiceContext { tenantId: string; userId?: string; } export interface TaxLine { taxCategoryId: string; taxCategoryCode: string; taxCategoryName: string; rate: number; baseAmount: number; taxAmount: number; nature: TaxNature; } export interface WithholdingLine { withholdingTypeId: string; withholdingTypeCode: string; withholdingTypeName: string; rate: number; baseAmount: number; withholdingAmount: number; } export interface TaxCalculationResult { subtotal: number; taxes: TaxLine[]; totalTaxes: number; withholdings: WithholdingLine[]; totalWithholdings: number; total: number; breakdown: { transferredTaxes: number; retainedTaxes: number; netPayable: number; }; } export interface TaxCalculationInput { subtotal: number; taxRates: Array<{ taxCategoryId: string; rate: number; includeInTotal?: boolean; }>; withholdingRates?: Array<{ withholdingTypeId: string; rate?: number; // If not provided, uses default rate }>; } export class FiscalCalculationService { private taxCategoryRepository: Repository; private withholdingTypeRepository: Repository; constructor( taxCategoryRepository: Repository, withholdingTypeRepository: Repository ) { this.taxCategoryRepository = taxCategoryRepository; this.withholdingTypeRepository = withholdingTypeRepository; } /** * Calcular impuestos y retenciones para un monto */ async calculateTaxes( _ctx: ServiceContext, input: TaxCalculationInput ): Promise { const { subtotal, taxRates, withholdingRates = [] } = input; // Calculate taxes const taxes: TaxLine[] = []; let totalTaxes = 0; let transferredTaxes = 0; let retainedTaxes = 0; for (const taxRate of taxRates) { const taxCategory = await this.taxCategoryRepository.findOne({ where: { id: taxRate.taxCategoryId }, }); if (!taxCategory) { throw new Error(`Tax category ${taxRate.taxCategoryId} not found`); } const taxAmount = this.roundCurrency(subtotal * (taxRate.rate / 100)); taxes.push({ taxCategoryId: taxCategory.id, taxCategoryCode: taxCategory.code, taxCategoryName: taxCategory.name, rate: taxRate.rate, baseAmount: subtotal, taxAmount, nature: taxCategory.taxNature, }); if (taxRate.includeInTotal !== false) { totalTaxes += taxAmount; if (taxCategory.taxNature === TaxNature.TAX || taxCategory.taxNature === TaxNature.BOTH) { transferredTaxes += taxAmount; } } } // Calculate withholdings const withholdings: WithholdingLine[] = []; let totalWithholdings = 0; for (const wRate of withholdingRates) { const withholdingType = await this.withholdingTypeRepository.findOne({ where: { id: wRate.withholdingTypeId }, }); if (!withholdingType) { throw new Error(`Withholding type ${wRate.withholdingTypeId} not found`); } const rate = wRate.rate ?? Number(withholdingType.defaultRate); const withholdingAmount = this.roundCurrency(subtotal * (rate / 100)); withholdings.push({ withholdingTypeId: withholdingType.id, withholdingTypeCode: withholdingType.code, withholdingTypeName: withholdingType.name, rate, baseAmount: subtotal, withholdingAmount, }); totalWithholdings += withholdingAmount; retainedTaxes += withholdingAmount; } const total = this.roundCurrency(subtotal + totalTaxes); const netPayable = this.roundCurrency(total - totalWithholdings); return { subtotal, taxes, totalTaxes: this.roundCurrency(totalTaxes), withholdings, totalWithholdings: this.roundCurrency(totalWithholdings), total, breakdown: { transferredTaxes: this.roundCurrency(transferredTaxes), retainedTaxes: this.roundCurrency(retainedTaxes), netPayable, }, }; } /** * Calcular IVA estandar (16%) */ calculateStandardIVA(_ctx: ServiceContext, amount: number): { base: number; iva: number; total: number } { const iva = this.roundCurrency(amount * 0.16); return { base: amount, iva, total: this.roundCurrency(amount + iva), }; } /** * Calcular IVA con tasa personalizada */ calculateIVA(_ctx: ServiceContext, amount: number, rate: number): { base: number; iva: number; total: number } { const iva = this.roundCurrency(amount * (rate / 100)); return { base: amount, iva, total: this.roundCurrency(amount + iva), }; } /** * Calcular retencion ISR */ calculateISRWithholding( _ctx: ServiceContext, amount: number, rate: number = 10 ): { base: number; withholding: number; net: number } { const withholding = this.roundCurrency(amount * (rate / 100)); return { base: amount, withholding, net: this.roundCurrency(amount - withholding), }; } /** * Calcular retencion IVA */ calculateIVAWithholding( _ctx: ServiceContext, amount: number, rate: number = 10.6667 ): { base: number; withholding: number; net: number } { const withholding = this.roundCurrency(amount * (rate / 100)); return { base: amount, withholding, net: this.roundCurrency(amount - withholding), }; } /** * Calcular total para factura con IVA y retenciones */ calculateInvoiceTotal( _ctx: ServiceContext, subtotal: number, ivaRate: number = 16, isrWithholdingRate: number = 0, ivaWithholdingRate: number = 0 ): InvoiceTotalResult { const iva = this.roundCurrency(subtotal * (ivaRate / 100)); const isrWithholding = this.roundCurrency(subtotal * (isrWithholdingRate / 100)); const ivaWithholding = this.roundCurrency(subtotal * (ivaWithholdingRate / 100)); const total = this.roundCurrency(subtotal + iva); const totalWithholdings = this.roundCurrency(isrWithholding + ivaWithholding); const netPayable = this.roundCurrency(total - totalWithholdings); return { subtotal, iva, ivaRate, total, isrWithholding, isrWithholdingRate, ivaWithholding, ivaWithholdingRate, totalWithholdings, netPayable, }; } /** * Obtener base imponible desde total con IVA */ getBaseFromTotalWithIVA(_ctx: ServiceContext, totalWithIVA: number, ivaRate: number = 16): number { return this.roundCurrency(totalWithIVA / (1 + ivaRate / 100)); } /** * Redondear a 2 decimales (moneda) */ private roundCurrency(amount: number): number { return Math.round(amount * 100) / 100; } /** * Validar que los calculos fiscales sean correctos */ validateCalculation( _ctx: ServiceContext, subtotal: number, expectedTotal: number, ivaRate: number = 16, tolerance: number = 0.01 ): { valid: boolean; expectedTotal: number; difference: number } { const calculated = this.calculateIVA(_ctx, subtotal, ivaRate); const difference = Math.abs(calculated.total - expectedTotal); return { valid: difference <= tolerance, expectedTotal: calculated.total, difference, }; } } export interface InvoiceTotalResult { subtotal: number; iva: number; ivaRate: number; total: number; isrWithholding: number; isrWithholdingRate: number; ivaWithholding: number; ivaWithholdingRate: number; totalWithholdings: number; netPayable: number; }