erp-construccion-backend-v2/src/modules/partners/services/partner-tax-info.service.ts
Adrian Flores Cortes caf9d4f0cb [SPRINT-0] fix: Resolve all 72 TypeScript compilation errors
- billing-usage: Remove unused imports, fix UpdateCouponDto access
- biometrics: Remove unused imports, fix duplicate exports in index.ts
- invoices: Fix undefined vs null type mismatches with ?? null
- partners: Fix duplicate ServiceContext exports, type mismatches
- projects: Remove unused imports
- shared/middleware: Fix TokenPayload type access

Build now passes: npm run build = 0 errors

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 02:31:34 -06:00

272 lines
7.8 KiB
TypeScript

/**
* 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<PartnerTaxInfo>;
constructor(dataSource: DataSource) {
this.repository = dataSource.getRepository(PartnerTaxInfo);
}
async findByPartnerId(partnerId: string): Promise<PartnerTaxInfo | null> {
return this.repository.findOne({
where: { partnerId },
relations: ['partner'],
});
}
async findById(id: string): Promise<PartnerTaxInfo | null> {
return this.repository.findOne({
where: { id },
relations: ['partner'],
});
}
async create(
_ctx: ServiceContext,
dto: CreatePartnerTaxInfoDto
): Promise<PartnerTaxInfo> {
// 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<PartnerTaxInfo | null> {
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<CreatePartnerTaxInfoDto, 'partnerId'>
): Promise<PartnerTaxInfo> {
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<PartnerTaxInfo | null> {
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<PartnerTaxInfo | null> {
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<boolean> {
const result = await this.repository.delete({ id });
return result.affected ? result.affected > 0 : false;
}
getSatRegimes(): Record<string, string> {
return SAT_REGIMES;
}
getCfdiUses(): Record<string, string> {
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,
};
}
}