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