feat: ecosistema FRASA — apps Cotizador, Alta Clientes, Cobranza y Landing

- Cotizador (/quote): 4 propuestas con formulas validadas del Excel, comparador,
  combobox de unidades, vigencia, PDF ANEXO A
- Alta Clientes (/client): wizard 8 pasos, validacion CURP RENAPO, OCR de INE,
  checklist de expediente, firma digital, 5 PDFs legales
- Cobranza (/pay): planes de enganche, mora, vista cobranza del dia, recibos PDF,
  bitacora, dashboard con filtros
- Landing (/): entrada al ecosistema
- Design system StudioBlank (monocromo, acento #1B4332) en las 4 apps
- 123 tests vitest
This commit is contained in:
2026-08-21 22:27:36 +00:00
commit ecd4edac02
147 changed files with 27345 additions and 0 deletions

View File

@@ -0,0 +1,173 @@
import { describe, it, expect } from 'vitest'
import {
datosGeneralesSchema,
coacreditadoSchema,
referenciasSchema,
REGEX_CURP,
REGEX_RFC,
} from './schemas'
import type { DatosGenerales } from '../types'
// Datos válidos base con el fixture del cliente del PDF de referencia
const generalesValidos: DatosGenerales = {
campaniaVenta: '',
fecha: '2026-07-19',
nombreCompleto: 'PASCUAL MARTIN TELLEZ CHIGUINDO',
nacionalidad: 'MEXICANA',
nivelEstudios: 'MAESTRIA',
curp: 'TECP891124HBCLHS07',
rfc: 'TECP8911246B4',
nss: '',
delegacionMunicipio: 'TIJUANA',
direccion: 'RIO EUFRATES EDIF 1 7145 203',
codigoPostal: '22223',
antiguedadDomicilio: '2',
fechaNacimiento: '1989-11-24',
lugarNacimiento: 'TIJUANA',
estadoCivil: 'SOLTERO',
regimenBienes: '',
peso: '87',
estatura: '1.78',
email: 'dantemartinchi@gmail.com',
telCasa: '',
celular: '6646935924',
viviendaAnios: '2',
tipoVivienda: 'PROPIA',
tipoCompra: 'VIVIR_AHI',
actividadOcupacion: 'AGENTE DEL MINISTERIO PUBLICO FEDERAL',
}
describe('regex de identificadores', () => {
it('acepta CURP válida', () => {
expect(REGEX_CURP.test('TECP891124HBCLHS07')).toBe(true)
})
it('rechaza CURP inválida', () => {
expect(REGEX_CURP.test('TECP891124HBCLHS0')).toBe(false) // corta
expect(REGEX_CURP.test('tecp891124hbclhs07')).toBe(false) // minúsculas
expect(REGEX_CURP.test('TECP89112XHBCLHS07')).toBe(false) // fecha inválida
})
it('acepta RFC válido', () => {
expect(REGEX_RFC.test('TECP8911246B4')).toBe(true)
})
it('rechaza RFC inválido', () => {
expect(REGEX_RFC.test('TECP8911246B')).toBe(false) // corto
expect(REGEX_RFC.test('TECP891124!B4')).toBe(false)
expect(REGEX_RFC.test('tecp8911246b4')).toBe(false)
})
})
describe('datosGeneralesSchema', () => {
it('acepta los datos del fixture', () => {
expect(datosGeneralesSchema.safeParse(generalesValidos).success).toBe(true)
})
it('rechaza CURP inválida', () => {
const r = datosGeneralesSchema.safeParse({ ...generalesValidos, curp: 'XXXX000000XXXXXXX0' })
expect(r.success).toBe(false)
})
it('rechaza RFC inválido', () => {
const r = datosGeneralesSchema.safeParse({ ...generalesValidos, rfc: '123' })
expect(r.success).toBe(false)
})
it('rechaza email inválido', () => {
const r = datosGeneralesSchema.safeParse({ ...generalesValidos, email: 'no-es-correo' })
expect(r.success).toBe(false)
})
it('rechaza código postal que no tenga 5 dígitos', () => {
const r = datosGeneralesSchema.safeParse({ ...generalesValidos, codigoPostal: '2223' })
expect(r.success).toBe(false)
})
it('exige régimen de bienes solo cuando el estado civil es CASADO', () => {
const casado = datosGeneralesSchema.safeParse({ ...generalesValidos, estadoCivil: 'CASADO' })
expect(casado.success).toBe(false)
const casadoConRegimen = datosGeneralesSchema.safeParse({
...generalesValidos,
estadoCivil: 'CASADO',
regimenBienes: 'SEPARACION_BIENES',
})
expect(casadoConRegimen.success).toBe(true)
})
})
describe('coacreditadoSchema', () => {
const base = {
generales: { ...generalesValidos, nombreCompleto: '', curp: '', rfc: '', email: '', fechaNacimiento: '', celular: '' },
empleo: {
tipoEmpleo: '' as const,
tipoEconomia: '' as const,
compania: '',
antiguedadAnios: '',
antiguedadMeses: '',
direccion: '',
colonia: '',
numero: '',
codigoPostal: '',
puesto: '',
telefono: '',
ext: '',
delegacion: '',
},
economicos: {
ingresosMensuales: 0,
otrosIngresos: 0,
gastosLuz: 0,
gastosOtros: 0,
dependientes: { personas: '', parentesco: '', edad: '' },
},
}
it('no exige datos del co-acreditado cuando el toggle está apagado', () => {
const r = coacreditadoSchema.safeParse({ ...base, tieneCoacreditado: false })
expect(r.success).toBe(true)
})
it('exige el nombre del co-acreditado cuando el toggle está activo', () => {
const r = coacreditadoSchema.safeParse({ ...base, tieneCoacreditado: true })
expect(r.success).toBe(false)
})
it('acepta co-acreditado con nombre cuando el toggle está activo', () => {
const r = coacreditadoSchema.safeParse({
...base,
tieneCoacreditado: true,
generales: { ...base.generales, nombreCompleto: 'MARIA LOPEZ PEREZ' },
})
expect(r.success).toBe(true)
})
})
describe('referenciasSchema', () => {
const vacia = { nombre: '', telefono: '', parentesco: '', direccion: '' }
it('rechaza cuando ninguna referencia tiene nombre y teléfono', () => {
const r = referenciasSchema.safeParse([vacia, vacia, vacia, vacia])
expect(r.success).toBe(false)
})
it('rechaza referencia con nombre pero sin teléfono', () => {
const r = referenciasSchema.safeParse([
{ ...vacia, nombre: 'JOSE ARNOLDO MENDOZA' },
vacia,
vacia,
vacia,
])
expect(r.success).toBe(false)
})
it('acepta con al menos 1 referencia válida', () => {
const r = referenciasSchema.safeParse([
{ nombre: 'JOSE ARNOLDO MENDOZA', telefono: '6641887799', parentesco: 'AMIGO', direccion: 'ALTAMIRA' },
vacia,
vacia,
vacia,
])
expect(r.success).toBe(true)
})
})