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>
302 lines
7.8 KiB
TypeScript
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;
|
|
}
|