/** * Partner Tax Info Service * Servicio para gestion de informacion fiscal de socios comerciales * * @module Partners */ import { DataSource, Repository } from 'typeorm'; import { PartnerTaxInfo } from '../entities'; /** * Service context for multi-tenant operations */ export interface ServiceContext { tenantId: string; userId?: string; } export interface CreatePartnerTaxInfoDto { partnerId: string; taxIdType?: string; taxIdCountry?: string; satRegime?: string; satRegimeName?: string; cfdiUse?: string; cfdiUseName?: string; fiscalZipCode?: string; withholdingIsr?: number; withholdingIva?: number; } export interface UpdatePartnerTaxInfoDto { taxIdType?: string; taxIdCountry?: string; satRegime?: string; satRegimeName?: string; cfdiUse?: string; cfdiUseName?: string; fiscalZipCode?: string; withholdingIsr?: number; withholdingIva?: number; } export interface VerifyTaxInfoDto { verificationSource: string; } // Common SAT Regimes export const SAT_REGIMES = { '601': 'General de Ley Personas Morales', '603': 'Personas Morales con Fines no Lucrativos', '605': 'Sueldos y Salarios e Ingresos Asimilados a Salarios', '606': 'Arrendamiento', '607': 'Regimen de Enajenacion o Adquisicion de Bienes', '608': 'Demas Ingresos', '610': 'Residentes en el Extranjero sin Establecimiento Permanente en Mexico', '611': 'Ingresos por Dividendos (socios y accionistas)', '612': 'Personas Fisicas con Actividades Empresariales y Profesionales', '614': 'Ingresos por Intereses', '615': 'Regimen de los Ingresos por Obtencion de Premios', '616': 'Sin Obligaciones Fiscales', '620': 'Sociedades Cooperativas de Produccion que optan por diferir sus ingresos', '621': 'Incorporacion Fiscal', '622': 'Actividades Agricolas, Ganaderas, Silvicolas y Pesqueras', '623': 'Opcional para Grupos de Sociedades', '624': 'Coordinados', '625': 'Regimen de las Actividades Empresariales con ingresos a traves de Plataformas Tecnologicas', '626': 'Regimen Simplificado de Confianza', }; // Common CFDI Uses export const CFDI_USES = { 'G01': 'Adquisicion de mercancias', 'G02': 'Devoluciones, descuentos o bonificaciones', 'G03': 'Gastos en general', 'I01': 'Construcciones', 'I02': 'Mobiliario y equipo de oficina por inversiones', 'I03': 'Equipo de transporte', 'I04': 'Equipo de computo y accesorios', 'I05': 'Dados, troqueles, moldes, matrices y herramental', 'I06': 'Comunicaciones telefonicas', 'I07': 'Comunicaciones satelitales', 'I08': 'Otra maquinaria y equipo', 'D01': 'Honorarios medicos, dentales y gastos hospitalarios', 'D02': 'Gastos medicos por incapacidad o discapacidad', 'D03': 'Gastos funerales', 'D04': 'Donativos', 'D05': 'Intereses reales efectivamente pagados por creditos hipotecarios', 'D06': 'Aportaciones voluntarias al SAR', 'D07': 'Primas por seguros de gastos medicos', 'D08': 'Gastos de transportacion escolar obligatoria', 'D09': 'Depositos en cuentas para el ahorro', 'D10': 'Pagos por servicios educativos', 'S01': 'Sin efectos fiscales', 'CP01': 'Pagos', 'CN01': 'Nomina', }; export class PartnerTaxInfoService { private readonly repository: Repository; constructor(dataSource: DataSource) { this.repository = dataSource.getRepository(PartnerTaxInfo); } async findByPartnerId(partnerId: string): Promise { return this.repository.findOne({ where: { partnerId }, relations: ['partner'], }); } async findById(id: string): Promise { return this.repository.findOne({ where: { id }, relations: ['partner'], }); } async create( _ctx: ServiceContext, dto: CreatePartnerTaxInfoDto ): Promise { // Check if tax info already exists for partner const existing = await this.findByPartnerId(dto.partnerId); if (existing) { throw new Error('Tax info already exists for this partner. Use update instead.'); } // Validate SAT regime if provided if (dto.satRegime && !SAT_REGIMES[dto.satRegime as keyof typeof SAT_REGIMES]) { throw new Error('Invalid SAT regime code'); } // Validate CFDI use if provided if (dto.cfdiUse && !CFDI_USES[dto.cfdiUse as keyof typeof CFDI_USES]) { throw new Error('Invalid CFDI use code'); } // Auto-fill regime and CFDI use names if codes provided const taxInfo = this.repository.create({ ...dto, taxIdCountry: dto.taxIdCountry ?? 'MEX', satRegimeName: dto.satRegimeName ?? SAT_REGIMES[dto.satRegime as keyof typeof SAT_REGIMES], cfdiUseName: dto.cfdiUseName ?? CFDI_USES[dto.cfdiUse as keyof typeof CFDI_USES], withholdingIsr: dto.withholdingIsr ?? 0, withholdingIva: dto.withholdingIva ?? 0, }); return this.repository.save(taxInfo); } async update( _ctx: ServiceContext, id: string, dto: UpdatePartnerTaxInfoDto ): Promise { const taxInfo = await this.findById(id); if (!taxInfo) { return null; } // Validate SAT regime if provided if (dto.satRegime && !SAT_REGIMES[dto.satRegime as keyof typeof SAT_REGIMES]) { throw new Error('Invalid SAT regime code'); } // Validate CFDI use if provided if (dto.cfdiUse && !CFDI_USES[dto.cfdiUse as keyof typeof CFDI_USES]) { throw new Error('Invalid CFDI use code'); } // Auto-fill names if codes changed const updateData = { ...dto }; if (dto.satRegime && dto.satRegime !== taxInfo.satRegime) { updateData.satRegimeName = dto.satRegimeName ?? SAT_REGIMES[dto.satRegime as keyof typeof SAT_REGIMES]; } if (dto.cfdiUse && dto.cfdiUse !== taxInfo.cfdiUse) { updateData.cfdiUseName = dto.cfdiUseName ?? CFDI_USES[dto.cfdiUse as keyof typeof CFDI_USES]; } Object.assign(taxInfo, updateData); return this.repository.save(taxInfo); } async upsert( ctx: ServiceContext, partnerId: string, dto: Omit ): Promise { const existing = await this.findByPartnerId(partnerId); if (existing) { const updated = await this.update(ctx, existing.id, dto); if (!updated) { throw new Error('Failed to update tax info'); } return updated; } return this.create(ctx, { ...dto, partnerId }); } async verify( _ctx: ServiceContext, id: string, dto: VerifyTaxInfoDto ): Promise { const taxInfo = await this.findById(id); if (!taxInfo) { return null; } taxInfo.isVerified = true; taxInfo.verifiedAt = new Date(); taxInfo.verificationSource = dto.verificationSource; return this.repository.save(taxInfo); } async unverify(_ctx: ServiceContext, id: string): Promise { const taxInfo = await this.findById(id); if (!taxInfo) { return null; } taxInfo.isVerified = false; taxInfo.verifiedAt = null; taxInfo.verificationSource = null; return this.repository.save(taxInfo); } async delete(id: string): Promise { const result = await this.repository.delete({ id }); return result.affected ? result.affected > 0 : false; } getSatRegimes(): Record { return SAT_REGIMES; } getCfdiUses(): Record { return CFDI_USES; } async getWithholdingTotals(partnerId: string): Promise<{ withholdingIsr: number; withholdingIva: number; totalWithholding: number; }> { const taxInfo = await this.findByPartnerId(partnerId); if (!taxInfo) { return { withholdingIsr: 0, withholdingIva: 0, totalWithholding: 0, }; } const isr = Number(taxInfo.withholdingIsr || 0); const iva = Number(taxInfo.withholdingIva || 0); return { withholdingIsr: isr, withholdingIva: iva, totalWithholding: isr + iva, }; } }