calcula-rfc
Version:
Librería para calcular el RFC (Registro Federal de Contribuyentes) mexicano con homoclave de personas físicas
432 lines (398 loc) • 13.8 kB
JavaScript
var dayjs = require('dayjs');
var customParseFormat = require('dayjs/plugin/customParseFormat');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var dayjs__default = /*#__PURE__*/_interopDefaultLegacy(dayjs);
var customParseFormat__default = /*#__PURE__*/_interopDefaultLegacy(customParseFormat);
dayjs__default["default"].extend(customParseFormat__default["default"]);
/**
* Librería para calcular el RFC (Registro Federal de Contribuyentes) mexicano
* incluyendo la homoclave de personas físicas basado en el algoritmo oficial.
*
* Basado en el documento "IFAI 0610100135506 065 Algoritmo" del SAT.
*
* @author Gerardo Lucero (basado en trabajo de Javier Islas García)
* @version 2.0.0
*/
// Constantes del algoritmo
const VOCALES = ['A', 'E', 'I', 'O', 'U'];
const PALABRAS_OBSCENAS = ['BUEI', 'BATO', 'BOFE', 'BUEY', 'CACA', 'CACO', 'CAGO', 'CAKO', 'CAGA', 'CAKA', 'COGI', 'COJA', 'COJI', 'COJO', 'COLA', 'CULO', 'COGE', 'COJE', 'FALO', 'FOCA', 'FETO', 'GATA', 'GETA', 'GUEI', 'GUEY', 'JETA', 'JOTO', 'KAKA', 'KAGA', 'KACA', 'KOGE', 'KOGI', 'KOJA', 'KOJE', 'KOJI', 'KACO', 'KOLA', 'KAGO', 'KOJO', 'KULO', 'LILO', 'LOBA', 'LOCA', 'LOKA', 'LOKO', 'LORA', 'LORO', 'LOCO', 'MALA', 'MAMA', 'MEAR', 'MEON', 'MIAR', 'MOCO', 'MOKO', 'MULA', 'MULO', 'MAMO', 'MAME', 'MEAS', 'MION', 'NACA', 'NACO', 'PEDA', 'PIPI', 'PITO', 'POPO', 'PEDO', 'PUTA', 'QULO', 'RUIN', 'PENE', 'PUTO', 'RATA', 'ROBA', 'ROBE', 'ROBO', 'SAPO', 'SENO', 'SOPE', 'TETA', 'VACA', 'VAGA', 'VAGO', 'VUEI', 'VUEY', 'WUEI', 'WUEY'];
const SUFIJOS_NOMBRES = ['MARIA', 'JOSE', 'DE', 'DEL', 'LOS', 'LAS', 'LA', 'MA', 'MA.', 'J.', 'J'];
const SUFIJOS_APELLIDOS = ['DE', 'LA', 'LAS', 'MC', 'VON', 'DEL', 'LOS', 'Y', 'MAC', 'VAN'];
const EQUIVALENCIAS_NOMBRE_NUMERICO = {
'A': '11',
'B': '12',
'C': '13',
'D': '14',
'E': '15',
'F': '16',
'G': '17',
'H': '18',
'I': '19',
'J': '21',
'K': '22',
'L': '23',
'M': '24',
'N': '25',
'O': '26',
'P': '27',
'Q': '28',
'R': '29',
'S': '32',
'T': '33',
'U': '34',
'V': '35',
'W': '36',
'X': '37',
'Y': '38',
'Z': '39',
'&': '10',
'%': '10'
};
const EQUIVALENCIAS_COCIENTE_RESIDUO = ['1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
/**
* Normaliza texto removiendo acentos y caracteres especiales
* @param {string} str - Texto a normalizar
* @returns {string} Texto normalizado
*/
function normalizeText(str) {
const map = {
'a': 'á|à|ã|â|À|Á|Ã|Â',
'e': 'é|è|ê|É|È|Ê',
'i': 'í|ì|î|Í|Ì|Î',
'o': 'ó|ò|ô|õ|Ó|Ò|Ô|Õ',
'u': 'ú|ù|û|ü|Ú|Ù|Û|Ü',
'c': 'ç|Ç',
'n': 'ñ|Ñ'
};
let normalized = str.toLowerCase();
for (const [replacement, pattern] of Object.entries(map)) {
normalized = normalized.replace(new RegExp(pattern, 'g'), replacement);
}
return normalized;
}
/**
* Limpia y formatea texto para el cálculo del RFC
* @param {string} value - Texto a limpiar
* @returns {string} Texto limpio en mayúsculas
*/
function cleanText(value) {
if (!value || typeof value !== 'string') return '';
return normalizeText(value).replace(/[/\-Ü]/g, ' ').replace(/\s+/g, ' ').trim().toUpperCase();
}
/**
* Valida los parámetros de entrada
* @param {string} nombres - Nombres de la persona
* @param {string} apellidoPaterno - Apellido paterno
* @param {string} apellidoMaterno - Apellido materno
* @param {string} fechaNacimiento - Fecha de nacimiento
* @throws {Error} Si algún parámetro es inválido
*/
function validarParametros(nombres, apellidoPaterno, apellidoMaterno, fechaNacimiento) {
if (!nombres || typeof nombres !== 'string' || nombres.trim() === '') {
throw new Error('El parámetro [nombres] es requerido y no puede estar vacío');
}
if (!apellidoPaterno && !apellidoMaterno) {
throw new Error('Al menos uno de los apellidos (paterno o materno) debe ser proporcionado');
}
if (!fechaNacimiento || typeof fechaNacimiento !== 'string') {
throw new Error('El parámetro [fechaNacimiento] es requerido');
}
// Validar formato de fecha - probar múltiples formatos
const formatos = ['MM/DD/YYYY', 'YYYY-MM-DD', 'DD/MM/YYYY', 'MM-DD-YYYY', 'DD-MM-YYYY'];
let fecha = null;
for (const formato of formatos) {
fecha = dayjs__default["default"](fechaNacimiento, formato, true);
if (fecha.isValid()) {
break;
}
}
// Si ningún formato funciona, intentar parsing automático
if (!fecha || !fecha.isValid()) {
fecha = dayjs__default["default"](fechaNacimiento);
}
if (!fecha.isValid()) {
throw new Error('La fecha de nacimiento debe tener un formato válido (ej: MM/DD/YYYY, YYYY-MM-DD, DD/MM/YYYY)');
}
}
/**
* Obtiene la primera letra de una cadena
* @param {string} value - Cadena de texto
* @returns {string} Primera letra o 'X' si está vacía
*/
function getPrimerLetra(value) {
return value && value.length > 0 ? value.charAt(0) : 'X';
}
/**
* Obtiene la segunda letra de una cadena
* @param {string} value - Cadena de texto
* @returns {string} Segunda letra o 'X' si no existe
*/
function getSegundaLetra(value) {
return value && value.length > 1 ? value.charAt(1) : 'X';
}
/**
* Encuentra la primera vocal interna de una palabra
* @param {string} value - Palabra a analizar
* @returns {string} Primera vocal interna o 'X' si no existe
*/
function getPrimerVocalInterna(value) {
if (!value || value.length <= 1) return 'X';
for (let i = 1; i < value.length; i++) {
if (VOCALES.includes(value.charAt(i))) {
return value.charAt(i);
}
}
return 'X';
}
/**
* Verifica si una palabra es obscena según la lista del SAT
* @param {string} palabra - Palabra a verificar
* @returns {boolean} True si es obscena
*/
function esPalabraObscena(palabra) {
return PALABRAS_OBSCENAS.includes(palabra);
}
/**
* Remueve sufijos de nombres que no deben considerarse
* @param {string} nombres - Nombres completos
* @returns {string} Nombres sin sufijos
*/
function removerSufijosNombres(nombres) {
let nombre = nombres;
while (nombre.includes(' ')) {
const posicion = nombre.indexOf(' ');
const palabra = nombre.substring(0, posicion);
if (!SUFIJOS_NOMBRES.includes(palabra)) {
break;
}
nombre = nombre.substring(posicion + 1);
}
return nombre;
}
/**
* Remueve sufijos de apellidos que no deben considerarse
* @param {string} apellidos - Apellidos completos
* @returns {string} Apellidos sin sufijos
*/
function removerSufijosApellidos(apellidos) {
let apellido = apellidos;
while (apellido.includes(' ')) {
const posicion = apellido.indexOf(' ');
const palabra = apellido.substring(0, posicion);
if (!SUFIJOS_APELLIDOS.includes(palabra)) {
break;
}
apellido = apellido.substring(posicion + 1);
}
return apellido;
}
/**
* Genera las primeras 4 letras del RFC
* @param {string} nombre - Nombre limpio
* @param {string} apellidoPaterno - Apellido paterno limpio
* @param {string} apellidoMaterno - Apellido materno limpio
* @returns {string} Primeras 4 letras del RFC
*/
function generarLetrasRFC(nombre, apellidoPaterno, apellidoMaterno) {
let letras = '';
if (!apellidoMaterno) {
// Solo apellido paterno
letras += getPrimerLetra(apellidoPaterno);
letras += getSegundaLetra(apellidoPaterno);
letras += getPrimerLetra(nombre);
letras += getSegundaLetra(nombre);
} else if (!apellidoPaterno) {
// Solo apellido materno
letras += getPrimerLetra(apellidoMaterno);
letras += getSegundaLetra(apellidoMaterno);
letras += getPrimerLetra(nombre);
letras += getSegundaLetra(nombre);
} else {
// Ambos apellidos
letras += getPrimerLetra(apellidoPaterno);
if (apellidoPaterno.length <= 2) {
letras += getPrimerLetra(apellidoMaterno);
letras += getPrimerLetra(nombre);
letras += getSegundaLetra(nombre);
} else {
letras += getPrimerVocalInterna(apellidoPaterno);
letras += getPrimerLetra(apellidoMaterno);
letras += getPrimerLetra(nombre);
}
}
// Verificar si es palabra obscena
if (esPalabraObscena(letras)) {
letras = `${letras.substring(0, 3)}X`;
}
return letras;
}
/**
* Convierte fecha de nacimiento al formato del RFC (YYMMDD)
* @param {string} fechaNacimiento - Fecha de nacimiento
* @returns {string} Fecha en formato YYMMDD
*/
function generarFechaRFC(fechaNacimiento) {
// Usar la misma lógica de validación que en validarParametros
const formatos = ['MM/DD/YYYY', 'YYYY-MM-DD', 'DD/MM/YYYY', 'MM-DD-YYYY', 'DD-MM-YYYY'];
let fecha = null;
for (const formato of formatos) {
fecha = dayjs__default["default"](fechaNacimiento, formato, true);
if (fecha.isValid()) {
break;
}
}
// Si ningún formato funciona, intentar parsing automático
if (!fecha || !fecha.isValid()) {
fecha = dayjs__default["default"](fechaNacimiento);
}
return fecha.format('YYMMDD');
}
/**
* Obtiene equivalencia numérica de un carácter
* @param {string} caracter - Carácter a convertir
* @returns {string} Equivalencia numérica
*/
function getEquivalenciaNumerica(caracter) {
if (EQUIVALENCIAS_NOMBRE_NUMERICO[caracter]) {
return EQUIVALENCIAS_NOMBRE_NUMERICO[caracter];
}
if (caracter >= '0' && caracter <= '9') {
return caracter.padStart(2, '0');
}
if (caracter === ' ') {
return '00';
}
return null;
}
/**
* Calcula la homoclave del RFC
* @param {string} nombres - Nombres originales
* @param {string} apellidoPaterno - Apellido paterno original
* @param {string} apellidoMaterno - Apellido materno original
* @returns {string} Homoclave de 2 caracteres
*/
function calcularHomoclave(nombres, apellidoPaterno, apellidoMaterno) {
const nombreCompleto = `${apellidoPaterno || ''} ${apellidoMaterno || ''} ${nombres}`.trim();
// Convertir a equivalencia numérica
let nombreNumerico = '0';
for (let i = 0; i < nombreCompleto.length; i++) {
const equivalencia = getEquivalenciaNumerica(nombreCompleto.charAt(i));
if (equivalencia !== null) {
nombreNumerico += equivalencia;
}
}
// Calcular suma ponderada
let suma = 0;
for (let i = 0; i < nombreNumerico.length - 1; i++) {
const digito1 = parseInt(nombreNumerico.charAt(i), 10);
const digito2 = parseInt(nombreNumerico.charAt(i + 1), 10);
suma += (digito1 * 10 + digito2) * digito2;
}
// Calcular homoclave
const base = suma % 1000;
const cociente = Math.floor(base / 34);
const residuo = base % 34;
return EQUIVALENCIAS_COCIENTE_RESIDUO[cociente] + EQUIVALENCIAS_COCIENTE_RESIDUO[residuo];
}
/**
* Calcula el dígito verificador del RFC
* @param {string} rfc - RFC sin dígito verificador (12 caracteres)
* @returns {string} Dígito verificador
*/
function calcularDigitoVerificador(rfc) {
const valores = {
'0': 0,
'1': 1,
'2': 2,
'3': 3,
'4': 4,
'5': 5,
'6': 6,
'7': 7,
'8': 8,
'9': 9,
'A': 10,
'B': 11,
'C': 12,
'D': 13,
'E': 14,
'F': 15,
'G': 16,
'H': 17,
'I': 18,
'J': 19,
'K': 20,
'L': 21,
'M': 22,
'N': 23,
'O': 25,
'P': 26,
'Q': 27,
'R': 28,
'S': 29,
'T': 30,
'U': 31,
'V': 32,
'W': 33,
'X': 34,
'Y': 35,
'Z': 36,
' ': 37
};
let suma = 0;
for (let i = 0; i < 12; i++) {
const caracter = i < rfc.length ? rfc.charAt(i) : ' ';
const valor = valores[caracter] || 0;
suma += valor * (13 - i);
}
const residuo = suma % 11;
if (residuo === 0) return '0';
if (residuo === 1) return 'A';
return (11 - residuo).toString();
}
/**
* Calcula el RFC completo de una persona física
* @param {string} nombres - Nombres de la persona
* @param {string} apellidoPaterno - Apellido paterno
* @param {string} apellidoMaterno - Apellido materno
* @param {string} fechaNacimiento - Fecha de nacimiento (formatos: MM/DD/YYYY, YYYY-MM-DD, etc.)
* @returns {string} RFC completo con homoclave y dígito verificador
* @throws {Error} Si los parámetros son inválidos
*
* @example
* // RFC con ambos apellidos
* calculaRFC('JUAN CARLOS', 'PEREZ', 'GOMEZ', '01/15/1985');
* // Retorna: 'PEGJ850115AB1'
*
* @example
* // RFC con un solo apellido
* calculaRFC('MARIA', 'LOPEZ', '', '12/25/1990');
* // Retorna: 'LOMA901225XY2'
*/
function calculaRFC(nombres, apellidoPaterno = '', apellidoMaterno = '', fechaNacimiento) {
try {
// Validar parámetros
validarParametros(nombres, apellidoPaterno, apellidoMaterno, fechaNacimiento);
// Limpiar y normalizar datos
const nombreLimpio = cleanText(nombres);
const apellidoPaternoLimpio = cleanText(apellidoPaterno);
const apellidoMaternoLimpio = cleanText(apellidoMaterno);
// Remover sufijos
const nombreSinSufijos = removerSufijosNombres(nombreLimpio);
const apellidoPaternoSinSufijos = removerSufijosApellidos(apellidoPaternoLimpio);
const apellidoMaternoSinSufijos = removerSufijosApellidos(apellidoMaternoLimpio);
// Generar componentes del RFC
const letras = generarLetrasRFC(nombreSinSufijos, apellidoPaternoSinSufijos, apellidoMaternoSinSufijos);
const fecha = generarFechaRFC(fechaNacimiento);
const homoclave = calcularHomoclave(nombreLimpio, apellidoPaternoLimpio, apellidoMaternoLimpio);
// RFC sin dígito verificador
const rfcSinDigito = letras + fecha + homoclave;
// Calcular dígito verificador
const digitoVerificador = calcularDigitoVerificador(rfcSinDigito);
return rfcSinDigito + digitoVerificador;
} catch (error) {
throw new Error(`Error al calcular RFC: ${error.message}`);
}
}
module.exports = calculaRFC;
//# sourceMappingURL=index.cjs.map