erp-construccion-backend-v2/src/modules/fiscal/services/fiscal-calculation.service.ts
Adrian Flores Cortes 100c5a6588 feat(modules): implement 13 backend modules for 100% completion
Implemented modules:
- audit: 8 services (GDPR compliance, retention policies, sensitive data)
- billing-usage: 8 services, 6 controllers (subscription management, usage tracking)
- biometrics: 3 services, 3 controllers (offline auth, device sync, lockout)
- core: 6 services (sequence, currency, UoM, payment-terms, geography)
- feature-flags: 3 services, 3 controllers (rollout strategies, A/B testing)
- fiscal: 7 services, 7 controllers (SAT/Mexican tax compliance)
- mobile: 4 services, 4 controllers (offline-first, sync queue, device management)
- partners: 6 services, 6 controllers (unified customers/suppliers, credit limits)
- profiles: 5 services, 3 controllers (avatar upload, preferences, completion)
- warehouses: 3 services, 3 controllers (zones, hierarchical locations)
- webhooks: 5 services, 5 controllers (HMAC signatures, retry logic)
- whatsapp: 5 services, 5 controllers (business API integration, templates)

Total: 154 files, ~43K lines of new backend code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 01:54:23 -06:00

302 lines
7.8 KiB
TypeScript

/**
* 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<TaxCategory>;
private withholdingTypeRepository: Repository<WithholdingType>;
constructor(
taxCategoryRepository: Repository<TaxCategory>,
withholdingTypeRepository: Repository<WithholdingType>
) {
this.taxCategoryRepository = taxCategoryRepository;
this.withholdingTypeRepository = withholdingTypeRepository;
}
/**
* Calcular impuestos y retenciones para un monto
*/
async calculateTaxes(
_ctx: ServiceContext,
input: TaxCalculationInput
): Promise<TaxCalculationResult> {
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;
}