feat+fix: 3 unidades SENDA faltantes + revisión a fondo de PDFs del alta
- SENDA-410-1-G102, SENDA-410-1-G201, SENDA-401-1-L41 agregadas a Monday, seeds y conocimiento IA (inventario 288 unidades; solo existían en el reporte de estatus de venta, todas VENDIDO) - PDFs del alta verificados exhaustivamente (5 documentos × 4 variantes, contenido extraído con pdfplumber + renders visuales): - firma huérfana en página propia (PLD) corregida - encabezados de contacto huérfanos (checklist) corregidos - modelo duplicado y enums crudos en perfil corregidos - Tests pdf 6→16; suites: alta 76, cotizador 71, api 168
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Catálogo de unidades — Desarrollo SENDA
|
||||
|
||||
SENDA es un desarrollo de **casas** de Frasa Desarrollos. Los códigos de unidad tienen la forma `manzana-lote-casa` (ej. 411-1-7 = manzana 411, lote 1, casa 7). Inventario: **139 casas**; al 19-ago-2026 hay **43 DISPONIBLES**, 1 apartada, 56 vendidas y 39 bloqueadas.
|
||||
SENDA es un desarrollo de **casas** de Frasa Desarrollos. Los códigos de unidad tienen la forma `manzana-lote-casa` (ej. 411-1-7 = manzana 411, lote 1, casa 7). Inventario: **142 casas** (139 del cotizador + G102, G201 y L41 que solo aparecían en el reporte de estatus de venta); al 19-ago-2026 hay **43 DISPONIBLES**, 1 apartada, 56 vendidas y 39 bloqueadas.
|
||||
|
||||
## Modelos
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
REQUISITOS_CONTACTO_ENTREGA,
|
||||
REQUISITOS_CONTACTO_CONTRATO,
|
||||
} from '../../data/textosLegales'
|
||||
import { MARGEN, finalY, parrafo, titulo, lineaFirma, numero, type DocumentoPdf } from './helpers'
|
||||
import { MARGEN, finalY, parrafo, titulo, lineaFirma, numero, asegurarEspacio, type DocumentoPdf } from './helpers'
|
||||
|
||||
const vacio = (v: string | undefined | null): string => (v && v.trim() !== '' ? v : '—')
|
||||
|
||||
@@ -97,9 +97,12 @@ export function crearDocumento(alta: AltaCliente): DocumentoPdf {
|
||||
})
|
||||
y = finalY(doc) + 6
|
||||
|
||||
// Encabezado + bloque de contacto van juntos: si no caben, saltan a página nueva
|
||||
y = asegurarEspacio(doc, y, 30)
|
||||
y = parrafo(doc, y, 'ENTREGA DE DOCUMENTOS:', { negritas: true, tamano: 9 })
|
||||
y = parrafo(doc, y, REQUISITOS_CONTACTO_ENTREGA)
|
||||
y += 2
|
||||
y = asegurarEspacio(doc, y, 30)
|
||||
y = parrafo(doc, y, 'FIRMA DE CONTRATO:', { negritas: true, tamano: 9 })
|
||||
y = parrafo(doc, y, REQUISITOS_CONTACTO_CONTRATO)
|
||||
y += 12
|
||||
|
||||
@@ -19,6 +19,15 @@ export interface OpcionesParrafo {
|
||||
alinear?: 'left' | 'center' | 'right'
|
||||
}
|
||||
|
||||
/** Si no caben `altoNecesario` mm antes del pie, salta de página. Devuelve la Y utilizable. */
|
||||
export function asegurarEspacio(doc: jsPDF, y: number, altoNecesario: number): number {
|
||||
if (y + altoNecesario > LIMITE_PAGINA) {
|
||||
doc.addPage()
|
||||
return MARGEN
|
||||
}
|
||||
return y
|
||||
}
|
||||
|
||||
/** Escribe un párrafo con salto de página automático. Devuelve la nueva Y. */
|
||||
export function parrafo(doc: jsPDF, y: number, texto: string, opciones: OpcionesParrafo = {}): number {
|
||||
const tamano = opciones.tamano ?? 8
|
||||
|
||||
@@ -39,7 +39,7 @@ export function crearDocumento(alta: AltaCliente): DocumentoPdf {
|
||||
autoTable(doc, {
|
||||
startY: y,
|
||||
margin: { left: MARGEN, right: MARGEN },
|
||||
styles: { fontSize: 7, cellPadding: 1 },
|
||||
styles: { fontSize: 7, cellPadding: 0.7 },
|
||||
headStyles: { fillColor: [4, 120, 87] },
|
||||
head: [['1 - Nombre y apellidos del cliente']],
|
||||
body: [[g.nombreCompleto]],
|
||||
@@ -48,7 +48,7 @@ export function crearDocumento(alta: AltaCliente): DocumentoPdf {
|
||||
autoTable(doc, {
|
||||
startY: y,
|
||||
margin: { left: MARGEN, right: MARGEN },
|
||||
styles: { fontSize: 7, cellPadding: 1 },
|
||||
styles: { fontSize: 7, cellPadding: 0.7 },
|
||||
headStyles: { fillColor: [4, 120, 87] },
|
||||
head: [['2 - Fecha de Nacimiento', '3 - Lugar de Nacimiento']],
|
||||
body: [[formatoFechaCorta(g.fechaNacimiento), vacio(g.lugarNacimiento)]],
|
||||
@@ -57,7 +57,7 @@ export function crearDocumento(alta: AltaCliente): DocumentoPdf {
|
||||
autoTable(doc, {
|
||||
startY: y,
|
||||
margin: { left: MARGEN, right: MARGEN },
|
||||
styles: { fontSize: 7, cellPadding: 1 },
|
||||
styles: { fontSize: 7, cellPadding: 0.7 },
|
||||
headStyles: { fillColor: [4, 120, 87] },
|
||||
head: [['4 - Nacionalizado o Residente', '4 - País']],
|
||||
body: [[vacio(g.nacionalidad), 'MEXICO']],
|
||||
@@ -66,7 +66,7 @@ export function crearDocumento(alta: AltaCliente): DocumentoPdf {
|
||||
autoTable(doc, {
|
||||
startY: y,
|
||||
margin: { left: MARGEN, right: MARGEN },
|
||||
styles: { fontSize: 7, cellPadding: 1 },
|
||||
styles: { fontSize: 7, cellPadding: 0.7 },
|
||||
headStyles: { fillColor: [4, 120, 87] },
|
||||
head: [['5 - Actividad, Ocupación o Profesión']],
|
||||
body: [[vacio(g.actividadOcupacion)]],
|
||||
@@ -76,7 +76,7 @@ export function crearDocumento(alta: AltaCliente): DocumentoPdf {
|
||||
autoTable(doc, {
|
||||
startY: y,
|
||||
margin: { left: MARGEN, right: MARGEN },
|
||||
styles: { fontSize: 7, cellPadding: 1 },
|
||||
styles: { fontSize: 7, cellPadding: 0.7 },
|
||||
headStyles: { fillColor: [4, 120, 87] },
|
||||
head: [['6 - Domicilio Particular', '']],
|
||||
body: [
|
||||
@@ -90,7 +90,7 @@ export function crearDocumento(alta: AltaCliente): DocumentoPdf {
|
||||
autoTable(doc, {
|
||||
startY: y,
|
||||
margin: { left: MARGEN, right: MARGEN },
|
||||
styles: { fontSize: 7, cellPadding: 1 },
|
||||
styles: { fontSize: 7, cellPadding: 0.7 },
|
||||
headStyles: { fillColor: [4, 120, 87] },
|
||||
head: [['6 - Domicilio Fiscal (Art 29 y 29A CFF)', '']],
|
||||
body: [
|
||||
@@ -105,7 +105,7 @@ export function crearDocumento(alta: AltaCliente): DocumentoPdf {
|
||||
autoTable(doc, {
|
||||
startY: y,
|
||||
margin: { left: MARGEN, right: MARGEN },
|
||||
styles: { fontSize: 7, cellPadding: 1 },
|
||||
styles: { fontSize: 7, cellPadding: 0.7 },
|
||||
headStyles: { fillColor: [4, 120, 87] },
|
||||
head: [['7 - Número Telefónico Casa', '7 - Número Telefónico Celular']],
|
||||
body: [[vacio(g.telCasa), vacio(g.celular)]],
|
||||
@@ -114,7 +114,7 @@ export function crearDocumento(alta: AltaCliente): DocumentoPdf {
|
||||
autoTable(doc, {
|
||||
startY: y,
|
||||
margin: { left: MARGEN, right: MARGEN },
|
||||
styles: { fontSize: 7, cellPadding: 1 },
|
||||
styles: { fontSize: 7, cellPadding: 0.7 },
|
||||
headStyles: { fillColor: [4, 120, 87] },
|
||||
head: [['8 - Correo Electrónico Válido', '9 - CURP']],
|
||||
body: [[g.email, g.curp]],
|
||||
@@ -123,7 +123,7 @@ export function crearDocumento(alta: AltaCliente): DocumentoPdf {
|
||||
autoTable(doc, {
|
||||
startY: y,
|
||||
margin: { left: MARGEN, right: MARGEN },
|
||||
styles: { fontSize: 7, cellPadding: 1 },
|
||||
styles: { fontSize: 7, cellPadding: 0.7 },
|
||||
headStyles: { fillColor: [4, 120, 87] },
|
||||
head: [['10 - RFC (SAT)']],
|
||||
body: [[g.rfc]],
|
||||
@@ -132,7 +132,7 @@ export function crearDocumento(alta: AltaCliente): DocumentoPdf {
|
||||
autoTable(doc, {
|
||||
startY: y,
|
||||
margin: { left: MARGEN, right: MARGEN },
|
||||
styles: { fontSize: 7, cellPadding: 1 },
|
||||
styles: { fontSize: 7, cellPadding: 0.7 },
|
||||
headStyles: { fillColor: [4, 120, 87] },
|
||||
head: [['11 - Datos de la Identificación Oficial Vigente', '']],
|
||||
body: [
|
||||
@@ -153,7 +153,7 @@ export function crearDocumento(alta: AltaCliente): DocumentoPdf {
|
||||
y = parrafo(doc, y, PLD_TEXTO_BENEFICIARIO)
|
||||
y = parrafo(doc, y, PLD_TEXTO_RESPONSABILIDAD)
|
||||
y = parrafo(doc, y, PLD_TEXTO_OTRO_FORMATO)
|
||||
y += 10
|
||||
y += 6
|
||||
y = lineaFirma(doc, y, MARGEN + 40, 100, `NOMBRE Y FIRMA DEL CLIENTE\n${g.nombreCompleto}`, firma.firmaDataUrl)
|
||||
|
||||
// Página 2: beneficiario controlador
|
||||
@@ -166,7 +166,7 @@ export function crearDocumento(alta: AltaCliente): DocumentoPdf {
|
||||
autoTable(doc, {
|
||||
startY: y,
|
||||
margin: { left: MARGEN, right: MARGEN },
|
||||
styles: { fontSize: 7, cellPadding: 1 },
|
||||
styles: { fontSize: 7, cellPadding: 0.7 },
|
||||
headStyles: { fillColor: [4, 120, 87] },
|
||||
head: [['Nombre', 'Nacionalidad']],
|
||||
body: [
|
||||
@@ -177,7 +177,7 @@ export function crearDocumento(alta: AltaCliente): DocumentoPdf {
|
||||
autoTable(doc, {
|
||||
startY: y,
|
||||
margin: { left: MARGEN, right: MARGEN },
|
||||
styles: { fontSize: 7, cellPadding: 1 },
|
||||
styles: { fontSize: 7, cellPadding: 0.7 },
|
||||
headStyles: { fillColor: [4, 120, 87] },
|
||||
head: [['Fecha Nacimiento', 'Lugar Nacimiento']],
|
||||
body: [[formatoFechaCorta(g.fechaNacimiento), vacio(g.lugarNacimiento)]],
|
||||
@@ -186,7 +186,7 @@ export function crearDocumento(alta: AltaCliente): DocumentoPdf {
|
||||
autoTable(doc, {
|
||||
startY: y,
|
||||
margin: { left: MARGEN, right: MARGEN },
|
||||
styles: { fontSize: 7, cellPadding: 1 },
|
||||
styles: { fontSize: 7, cellPadding: 0.7 },
|
||||
headStyles: { fillColor: [4, 120, 87] },
|
||||
head: [['Domicilio']],
|
||||
body: [[`${vacio(g.direccion)} ${vacio(g.codigoPostal)} ${vacio(g.delegacionMunicipio)} BC MEXICO`.trim()]],
|
||||
@@ -195,7 +195,7 @@ export function crearDocumento(alta: AltaCliente): DocumentoPdf {
|
||||
autoTable(doc, {
|
||||
startY: y,
|
||||
margin: { left: MARGEN, right: MARGEN },
|
||||
styles: { fontSize: 7, cellPadding: 1 },
|
||||
styles: { fontSize: 7, cellPadding: 0.7 },
|
||||
headStyles: { fillColor: [4, 120, 87] },
|
||||
head: [['Teléfono Fijo', 'Teléfono Celular']],
|
||||
body: [[vacio(g.telCasa), vacio(g.celular)]],
|
||||
@@ -204,7 +204,7 @@ export function crearDocumento(alta: AltaCliente): DocumentoPdf {
|
||||
autoTable(doc, {
|
||||
startY: y,
|
||||
margin: { left: MARGEN, right: MARGEN },
|
||||
styles: { fontSize: 7, cellPadding: 1 },
|
||||
styles: { fontSize: 7, cellPadding: 0.7 },
|
||||
headStyles: { fillColor: [4, 120, 87] },
|
||||
head: [['Correo Electrónico', 'Ocupación']],
|
||||
body: [[g.email, vacio(g.actividadOcupacion)]],
|
||||
@@ -213,7 +213,7 @@ export function crearDocumento(alta: AltaCliente): DocumentoPdf {
|
||||
autoTable(doc, {
|
||||
startY: y,
|
||||
margin: { left: MARGEN, right: MARGEN },
|
||||
styles: { fontSize: 7, cellPadding: 1 },
|
||||
styles: { fontSize: 7, cellPadding: 0.7 },
|
||||
headStyles: { fillColor: [4, 120, 87] },
|
||||
head: [['C.U.R.P', 'R.F.C']],
|
||||
body: [[g.curp, g.rfc]],
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { jsPDF } from 'jspdf'
|
||||
import type { AltaCliente } from '../../types'
|
||||
import { fixtureAlta } from '../fixture'
|
||||
import * as perfilCliente from './perfilCliente'
|
||||
import * as identificacionCliente from './identificacionCliente'
|
||||
@@ -31,3 +33,297 @@ describe('generadores de PDF (smoke test con fixture)', () => {
|
||||
expect(checklistExpediente.crearDocumento(fixtureAlta).doc.getNumberOfPages()).toBeGreaterThanOrEqual(2)
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests de contenido: jsPDF genera los streams sin comprimir, así que el texto
|
||||
// se puede extraer del output crudo (cadenas "(...) Tj" en cada página).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Bytes 0x80-0x9F de WinAnsiEncoding que no coinciden con latin1
|
||||
const WINANSI: Record<number, string> = {
|
||||
0x80: '€', 0x85: '…', 0x88: 'ˆ', 0x8c: 'Œ', 0x91: '‘', 0x92: '’',
|
||||
0x93: '“', 0x94: '”', 0x95: '•', 0x96: '–', 0x97: '—', 0x98: '˜',
|
||||
0x99: '™', 0x9c: 'œ',
|
||||
}
|
||||
|
||||
/** Decodifica una cadena literal del PDF: escapes octales y WinAnsi */
|
||||
function decodificarCadenaPdf(cadena: string): string {
|
||||
return cadena
|
||||
.replace(/\\([0-7]{1,3})/g, (_, oct: string) => String.fromCharCode(Number.parseInt(oct, 8)))
|
||||
.replace(/\\([\\()nrt])/g, (_, c: string) => ({ n: '\n', r: '\r', t: '\t' })[c as string] ?? c)
|
||||
.replace(/[\x80-\x9f]/g, (c) => WINANSI[c.charCodeAt(0)] ?? c)
|
||||
}
|
||||
|
||||
interface Captura {
|
||||
/** Texto por página (en orden) */
|
||||
paginas: string[]
|
||||
/** Todo el texto del documento */
|
||||
texto: string
|
||||
/** Imágenes XObject embebidas (p. ej. la firma) */
|
||||
numImagenes: number
|
||||
}
|
||||
|
||||
/** Extrae el contenido textual del PDF generado, página por página. */
|
||||
function capturar(doc: jsPDF): Captura {
|
||||
const bin = new TextDecoder('latin1').decode(doc.output('arraybuffer'))
|
||||
const streams = [...bin.matchAll(/stream\r?\n([\s\S]*?)\r?\nendstream/g)]
|
||||
.map((m) => m[1])
|
||||
.filter((s) => s.includes(' Tj'))
|
||||
const paginas = streams.map((stream) => {
|
||||
const partes: string[] = []
|
||||
for (const m of stream.matchAll(/\((?:\\.|[^\\()])*\)/g)) {
|
||||
partes.push(decodificarCadenaPdf(m[0].slice(1, -1)))
|
||||
}
|
||||
return partes.join(' ')
|
||||
})
|
||||
return {
|
||||
paginas,
|
||||
texto: paginas.join('\n'),
|
||||
numImagenes: (bin.match(/\/Subtype\s*\/Image/g) ?? []).length,
|
||||
}
|
||||
}
|
||||
|
||||
const capturarAlta = (crear: (alta: AltaCliente) => { doc: jsPDF }, alta: AltaCliente): Captura =>
|
||||
capturar(crear(alta).doc)
|
||||
|
||||
const PATRON_DEFECTO = /\bundefined\b|\bnull\b|\bNaN\b|\[object Object\]/
|
||||
|
||||
const clonar = (a: AltaCliente): AltaCliente => JSON.parse(JSON.stringify(a)) as AltaCliente
|
||||
|
||||
function conCoacreditado(): AltaCliente {
|
||||
const a = clonar(fixtureAlta)
|
||||
a.datosGenerales.nombreCompleto = 'MARÍA FERNANDA ÑÚÑEZ GÜEMES'
|
||||
a.coacreditado.tieneCoacreditado = true
|
||||
a.coacreditado.generales = {
|
||||
...a.coacreditado.generales,
|
||||
nombreCompleto: 'JOSÉ ÁNGEL MÜLLER IBARGÜENGOITIA',
|
||||
rfc: 'MUIJ850101ABC',
|
||||
curp: 'MUIJ850101HBCLRN09',
|
||||
fechaNacimiento: '1985-01-01',
|
||||
}
|
||||
a.coacreditado.economicos = {
|
||||
ingresosMensuales: 40000.25,
|
||||
otrosIngresos: 500.5,
|
||||
gastosLuz: 300.33,
|
||||
gastosOtros: 1200.67,
|
||||
dependientes: { personas: '2', parentesco: 'HIJOS', edad: '5 Y 8' },
|
||||
}
|
||||
a.referencias = [
|
||||
{
|
||||
nombre: 'REFERENCIA CON NOMBRE EXTREMADAMENTE LARGO PARA PROBAR ANCHO DE CELDA',
|
||||
telefono: '6641887799',
|
||||
parentesco: 'HERMANO',
|
||||
direccion: 'PRIVADA DE LAS BUGAMBILIAS MANZANA 27 LOTE 114 COLONIA PLAYAS DE TIJUANA CÓDIGO POSTAL 22504',
|
||||
},
|
||||
]
|
||||
return a
|
||||
}
|
||||
|
||||
function minimos(): AltaCliente {
|
||||
const a = clonar(fixtureAlta)
|
||||
a.datosGenerales.telCasa = ''
|
||||
a.empleo.ext = ''
|
||||
a.viviendaCredito.enganche = ''
|
||||
a.viviendaCredito.idVigencia = ''
|
||||
a.viviendaCredito.proyecto = 'OTRO'
|
||||
a.viviendaCredito.proyectoOtro = 'RESIDENCIAL LAS ÁGUILAS'
|
||||
a.viviendaCredito.tiposCredito = []
|
||||
a.referencias = []
|
||||
a.economicos = {
|
||||
ingresosMensuales: 0,
|
||||
otrosIngresos: 0,
|
||||
gastosLuz: 0,
|
||||
gastosOtros: 0,
|
||||
dependientes: { personas: '', parentesco: '', edad: '' },
|
||||
}
|
||||
a.firma = { firmaDataUrl: '', asesorNombre: '' }
|
||||
return a
|
||||
}
|
||||
|
||||
describe('contenido de los PDFs', () => {
|
||||
it('ningún generador emite undefined/null/NaN/[object Object] (fixture ni casos borde)', () => {
|
||||
for (const alta of [fixtureAlta, conCoacreditado(), minimos()]) {
|
||||
for (const [, modulo] of generadores) {
|
||||
expect(capturarAlta(modulo.crearDocumento, alta).texto).not.toMatch(PATRON_DEFECTO)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('perfil: datos personales, empleo, económicos, referencias y desarrollo con formato moneda', () => {
|
||||
const t = capturarAlta(perfilCliente.crearDocumento, fixtureAlta).texto
|
||||
for (const frag of [
|
||||
'PASCUAL MARTIN TELLEZ CHIGUINDO',
|
||||
'TECP891124HBCLHS07',
|
||||
'TECP8911246B4',
|
||||
'24/11/1989',
|
||||
'RIO EUFRATES EDIF 1 7145 203',
|
||||
'FISCALIA GENERAL DE LA REPUBLICA',
|
||||
'12 AÑOS 10 MESES',
|
||||
'$54,988.00',
|
||||
'$5,500.00',
|
||||
'$2,700,000.00',
|
||||
'JOSE ARNOLDO MENDOZA',
|
||||
'CYNTHIA ANAHI GARCIA',
|
||||
'CAÑON OASIS',
|
||||
'409-1-11-11',
|
||||
'LAUREL',
|
||||
'Bancario',
|
||||
'ESMERALDA BECERRA DOMINGUEZ',
|
||||
]) {
|
||||
expect(t).toContain(frag)
|
||||
}
|
||||
})
|
||||
|
||||
it('perfil: catálogos con etiqueta legible y modelo sin duplicar en PROYECTO', () => {
|
||||
const t = capturarAlta(perfilCliente.crearDocumento, fixtureAlta).texto
|
||||
for (const crudo of ['VIVIR_AHI', 'E_MEXICANA', 'ASALARIADO', 'SOCIEDAD_CONYUGAL']) {
|
||||
expect(t).not.toContain(crudo)
|
||||
}
|
||||
for (const etiqueta of ['Vivir ahí', 'Economía mexicana', 'Asalariado', 'Propia']) {
|
||||
expect(t).toContain(etiqueta)
|
||||
}
|
||||
expect(t).toContain('11 11 SENDA')
|
||||
expect(t).not.toContain('11 11 SENDA LAUREL')
|
||||
})
|
||||
|
||||
it('perfil: sección co-acreditado solo cuando tieneCoacreditado', () => {
|
||||
const sin = capturarAlta(perfilCliente.crearDocumento, fixtureAlta).texto
|
||||
expect(sin).not.toContain('CO-ACREDITADO')
|
||||
|
||||
const con = capturarAlta(perfilCliente.crearDocumento, conCoacreditado()).texto
|
||||
for (const frag of [
|
||||
'CO-ACREDITADO O CÓNYUGE',
|
||||
'JOSÉ ÁNGEL MÜLLER IBARGÜENGOITIA',
|
||||
'MUIJ850101HBCLRN09',
|
||||
'MUIJ850101ABC',
|
||||
'$40,500.75',
|
||||
'$1,501.00',
|
||||
'MARÍA FERNANDA ÑÚÑEZ GÜEMES',
|
||||
]) {
|
||||
expect(con).toContain(frag)
|
||||
}
|
||||
})
|
||||
|
||||
it('perfil: textos largos de referencias no se truncan', () => {
|
||||
const t = capturarAlta(perfilCliente.crearDocumento, conCoacreditado()).texto
|
||||
for (const palabra of ['EXTREMADAMENTE', 'ANCHO DE CELDA', 'BUGAMBILIAS', 'MANZANA 27', 'LOTE 114', '22504']) {
|
||||
expect(t).toContain(palabra)
|
||||
}
|
||||
})
|
||||
|
||||
it('identificación PLD: campos, textos legales completos y firma no huérfana', () => {
|
||||
const { doc } = identificacionCliente.crearDocumento(fixtureAlta)
|
||||
const c = capturar(doc)
|
||||
for (const frag of [
|
||||
'FORMATO DE IDENTIFICACION DE CLIENTES',
|
||||
'PASCUAL MARTIN TELLEZ CHIGUINDO',
|
||||
'TECP891124HBCLHS07',
|
||||
'TECP8911246B4',
|
||||
'AGENTE DEL MINISTERIO PUBLICO FEDERAL',
|
||||
'FRAY PEDRO DE GANTE',
|
||||
'TIJUANA OTAY',
|
||||
'BAJA CALIFORNIA 22508 MEXICO',
|
||||
'INSTITUTO NACIONAL ELECTORAL',
|
||||
'IDMEX2311049617',
|
||||
'31/12/2032',
|
||||
'Artículos 8, 15, 16, 17',
|
||||
'Artículo 62 de la Ley Federal',
|
||||
'FORMATO PARA LA IDENTIFICACIÓN DEL',
|
||||
'BENEFICIARIO CONTROLADOR',
|
||||
'soy el único Beneficiario Controlador',
|
||||
'en la ciudad de Tijuana, B.C.',
|
||||
]) {
|
||||
expect(c.texto).toContain(frag)
|
||||
}
|
||||
// La firma del formato PLD no debe quedar sola en una página
|
||||
expect(doc.getNumberOfPages()).toBe(2)
|
||||
expect(c.paginas[0]).toContain('Llenar otro formato')
|
||||
expect(c.paginas[0]).toContain('NOMBRE Y FIRMA DEL CLIENTE')
|
||||
// Firma embebida en ambas páginas
|
||||
expect(c.numImagenes).toBeGreaterThanOrEqual(2)
|
||||
})
|
||||
|
||||
it('aviso de privacidad: texto íntegro hasta el último párrafo y firma embebida', () => {
|
||||
const c = capturarAlta(avisoPrivacidad.crearDocumento, fixtureAlta)
|
||||
for (const frag of [
|
||||
'AVISO DE PRIVACIDAD',
|
||||
'Ley Federal de Protección de Datos Personales en Posesión de los Particulares',
|
||||
'Información Confidencial',
|
||||
'cancelación u oposición (ARCO)',
|
||||
'www.frasa.com.mx/aviso-de-privacidad/',
|
||||
'revisar periódicamente el contenido del mismo.',
|
||||
'El suscrito (a) PASCUAL MARTIN TELLEZ CHIGUINDO',
|
||||
'AUTORIZO EL USO DE MIS DATOS',
|
||||
]) {
|
||||
expect(c.texto).toContain(frag)
|
||||
}
|
||||
expect(c.numImagenes).toBeGreaterThanOrEqual(1)
|
||||
})
|
||||
|
||||
it('carta antilavado: cuerpo completo, límite de efectivo y fundamento legal', () => {
|
||||
const c = capturarAlta(cartaAntilavado.crearDocumento, fixtureAlta)
|
||||
for (const frag of [
|
||||
'Estimado (a) PASCUAL MARTIN TELLEZ CHIGUINDO',
|
||||
'proyecto SENDA',
|
||||
'$600,000.00',
|
||||
'SEISCIENTOS MIL PESOS',
|
||||
'Artículo 32, fracción I',
|
||||
'A T E N T A M E N T E',
|
||||
'Frasa Desarrollos SAPI C.V.',
|
||||
'DE CONFORMIDAD',
|
||||
'En mi carácter de “COMPRADOR”',
|
||||
]) {
|
||||
expect(c.texto).toContain(frag)
|
||||
}
|
||||
expect(c.numImagenes).toBeGreaterThanOrEqual(1)
|
||||
})
|
||||
|
||||
it('checklist: listas, firmas de recepción y contactos sin encabezados huérfanos', () => {
|
||||
const c = capturarAlta(checklistExpediente.crearDocumento, fixtureAlta)
|
||||
for (const frag of [
|
||||
'FORMATO DE RECEPCIÓN Y',
|
||||
'CONTROL DE EXPEDIENTE',
|
||||
'PASCUAL MARTIN TELLEZ CHIGUINDO',
|
||||
'$2,700,000.00',
|
||||
'Formatos Generales***',
|
||||
'Buró de crédito',
|
||||
'GLADIS VALENZUELA',
|
||||
'DEYSI LORENA ALEMAN FIGUEROA',
|
||||
'DANIEL ALVARADO LUQUE',
|
||||
'REQUISITOS',
|
||||
'GENERALES',
|
||||
'Solicitud de crédito hipotecario',
|
||||
'3 Referencias en U.S.A.',
|
||||
'Graciela Bautista Velasco',
|
||||
'ALEJANDRA GUERRERO SANCHEZ',
|
||||
'gbautista@frasa.com.mx',
|
||||
'máximo de 48h',
|
||||
]) {
|
||||
expect(c.texto).toContain(frag)
|
||||
}
|
||||
// El encabezado de cada bloque de contacto está en la misma página que su contenido
|
||||
const paginaDe = (pagina: string, frag: string) => pagina.includes(frag)
|
||||
for (const pagina of c.paginas) {
|
||||
if (paginaDe(pagina, 'ENTREGA DE DOCUMENTOS:')) {
|
||||
expect(pagina).toContain('Graciela Bautista Velasco')
|
||||
}
|
||||
if (paginaDe(pagina, 'FIRMA DE CONTRATO:')) {
|
||||
expect(pagina).toContain('ALEJANDRA GUERRERO SANCHEZ')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('casos borde: proyecto OTRO, sin referencias, montos en cero y sin firma', () => {
|
||||
const alta = minimos()
|
||||
for (const [, modulo] of generadores) {
|
||||
expect(() => modulo.crearDocumento(alta)).not.toThrow()
|
||||
}
|
||||
const perfil = capturarAlta(perfilCliente.crearDocumento, alta)
|
||||
expect(perfil.texto).toContain('RESIDENCIAL LAS ÁGUILAS')
|
||||
expect(perfil.texto).toContain('$0.00')
|
||||
// Sin firma no se incrusta ninguna imagen
|
||||
expect(perfil.numImagenes).toBe(0)
|
||||
const carta = capturarAlta(cartaAntilavado.crearDocumento, alta)
|
||||
expect(carta.texto).toContain('proyecto RESIDENCIAL LAS ÁGUILAS')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,11 +3,15 @@ import autoTable from 'jspdf-autotable'
|
||||
import type { AltaCliente } from '../../types'
|
||||
import { formatoMoneda, formatoFechaCorta, nombreArchivoPdf } from '../formatters'
|
||||
import { calcularTotalIngresos, calcularGastosTotal } from '../calculations'
|
||||
import { TIPOS_CREDITO } from '../../data/constantes'
|
||||
import { TIPOS_CREDITO, TIPOS_EMPLEO, TIPOS_ECONOMIA, TIPOS_VIVIENDA, TIPOS_COMPRA, REGIMENES_BIENES } from '../../data/constantes'
|
||||
import { MARGEN, finalY, titulo, lineaFirma, numero, type DocumentoPdf } from './helpers'
|
||||
|
||||
const vacio = (v: string | undefined | null): string => (v && v.trim() !== '' ? v : '—')
|
||||
|
||||
/** Etiqueta legible de un catálogo; si el valor no está en el catálogo se muestra tal cual */
|
||||
const etiqueta = (catalogo: readonly { valor: string; etiqueta: string }[], valor: string): string =>
|
||||
catalogo.find((o) => o.valor === valor)?.etiqueta ?? vacio(valor)
|
||||
|
||||
function seccion(doc: jsPDF, y: number, texto: string): number {
|
||||
if (y > 260) {
|
||||
doc.addPage()
|
||||
@@ -62,7 +66,7 @@ export function crearDocumento(alta: AltaCliente): DocumentoPdf {
|
||||
styles: { fontSize: 7, cellPadding: 1 },
|
||||
headStyles: { fillColor: [4, 120, 87] },
|
||||
head: [['ESTADO CIVIL', 'RÉGIMEN BIENES', 'PESO', 'ESTATURA']],
|
||||
body: [[vacio(g.estadoCivil), vacio(g.regimenBienes), vacio(g.peso), vacio(g.estatura)]],
|
||||
body: [[vacio(g.estadoCivil), etiqueta(REGIMENES_BIENES, g.regimenBienes), vacio(g.peso), vacio(g.estatura)]],
|
||||
})
|
||||
y = finalY(doc)
|
||||
autoTable(doc, {
|
||||
@@ -81,7 +85,7 @@ export function crearDocumento(alta: AltaCliente): DocumentoPdf {
|
||||
headStyles: { fillColor: [4, 120, 87] },
|
||||
head: [['DELEG./MUNICIPIO', 'C.P.', 'ANTIGÜEDAD', 'TIPO DE VIVIENDA', 'TIPO DE COMPRA']],
|
||||
body: [
|
||||
[vacio(g.delegacionMunicipio), vacio(g.codigoPostal), vacio(g.antiguedadDomicilio), vacio(g.tipoVivienda), vacio(g.tipoCompra)],
|
||||
[vacio(g.delegacionMunicipio), vacio(g.codigoPostal), vacio(g.antiguedadDomicilio), etiqueta(TIPOS_VIVIENDA, g.tipoVivienda), etiqueta(TIPOS_COMPRA, g.tipoCompra)],
|
||||
],
|
||||
})
|
||||
y = finalY(doc) + 6
|
||||
@@ -96,8 +100,8 @@ export function crearDocumento(alta: AltaCliente): DocumentoPdf {
|
||||
head: [['TIPO DE EMPLEO', 'TIPO DE ECONOMÍA', 'COMPAÑÍA', 'ANTIGÜEDAD']],
|
||||
body: [
|
||||
[
|
||||
vacio(e.tipoEmpleo),
|
||||
vacio(e.tipoEconomia),
|
||||
etiqueta(TIPOS_EMPLEO, e.tipoEmpleo),
|
||||
etiqueta(TIPOS_ECONOMIA, e.tipoEconomia),
|
||||
vacio(e.compania),
|
||||
`${vacio(e.antiguedadAnios)} AÑOS ${vacio(e.antiguedadMeses)} MESES`,
|
||||
],
|
||||
@@ -207,7 +211,8 @@ export function crearDocumento(alta: AltaCliente): DocumentoPdf {
|
||||
head: [['PROYECTO', 'UBICACIÓN', 'PROTOTIPO']],
|
||||
body: [
|
||||
[
|
||||
[`${vacio(v.manzana)} ${vacio(v.lote)}`.replace(/—/g, '').trim(), proyecto, vacio(v.modelo)].join(' ').trim(),
|
||||
// Manzana/Lote + nombre del proyecto (el modelo va en su propia columna PROTOTIPO)
|
||||
[`${vacio(v.manzana)} ${vacio(v.lote)}`.replace(/—/g, '').trim(), proyecto].filter(Boolean).join(' '),
|
||||
vacio(v.ubicacion),
|
||||
vacio(v.modelo),
|
||||
],
|
||||
|
||||
@@ -110,6 +110,9 @@ export const UNIDADES_SENDA: Unidad[] = [
|
||||
{ codigo: 'SENDA-410-1-F202', modelo: 'ESMERALDA PA', desarrollo: 'SENDA', fechaEntrega: null, supEstacionamiento: null, supConstruccion: 55.78, supExcedente: null, precioSinAcabados: 1850000, precioConAcabados: 1850000, precioLista: 1850000, status: 'DISPONIBLE', mesesParaFirma: null, extras: { prototipo: 'CUADRUPLEX', manzana: 410, lote: 1, cajones: 1 } },
|
||||
{ codigo: 'SENDA-410-1-G101', modelo: 'ESMERALDA PB', desarrollo: 'SENDA', fechaEntrega: null, supEstacionamiento: null, supConstruccion: 55.78, supExcedente: null, precioSinAcabados: 1948000, precioConAcabados: 1948000, precioLista: 1948000, status: 'DISPONIBLE', mesesParaFirma: null, extras: { prototipo: 'CUADRUPLEX', manzana: 410, lote: 1, cajones: 1 } },
|
||||
{ codigo: 'SENDA-410-1-G202', modelo: 'ESMERALDA PA', desarrollo: 'SENDA', fechaEntrega: null, supEstacionamiento: null, supConstruccion: 55.78, supExcedente: null, precioSinAcabados: 1850000, precioConAcabados: 1850000, precioLista: 1850000, status: 'VENDIDO', mesesParaFirma: null, extras: { prototipo: 'CUADRUPLEX', manzana: 410, lote: 1, cajones: 1 } },
|
||||
{ codigo: 'SENDA-410-1-G102', modelo: 'ESMERALDA PB', desarrollo: 'SENDA', fechaEntrega: null, supEstacionamiento: null, supConstruccion: 55.78, supExcedente: null, precioSinAcabados: 1948000, precioConAcabados: 1948000, precioLista: 1948000, status: 'VENDIDO', mesesParaFirma: null, extras: { prototipo: 'CUADRUPLEX', manzana: 410, lote: 1, cajones: 1 } },
|
||||
{ codigo: 'SENDA-410-1-G201', modelo: 'ESMERALDA PA', desarrollo: 'SENDA', fechaEntrega: null, supEstacionamiento: null, supConstruccion: 55.78, supExcedente: null, precioSinAcabados: 1850000, precioConAcabados: 1850000, precioLista: 1850000, status: 'VENDIDO', mesesParaFirma: null, extras: { prototipo: 'CUADRUPLEX', manzana: 410, lote: 1, cajones: 1 } },
|
||||
{ codigo: 'SENDA-401-1-L41', modelo: 'SANTIAGO', desarrollo: 'SENDA', fechaEntrega: null, supEstacionamiento: null, supConstruccion: 63.95, supExcedente: null, precioSinAcabados: 2750000, precioConAcabados: 2750000, precioLista: 2750000, status: 'VENDIDO', mesesParaFirma: null, extras: { prototipo: 'LAUREL', manzana: 401, lote: 1, cajones: 2 } },
|
||||
{ codigo: 'SENDA-409-5-1', modelo: 'SANTIAGO', desarrollo: 'SENDA', fechaEntrega: '2027-11-20', supEstacionamiento: null, supConstruccion: 63.95, supExcedente: null, precioSinAcabados: 2700000, precioConAcabados: 2700000, precioLista: 2700000, status: 'VENDIDO', mesesParaFirma: null, extras: { prototipo: 'LAUREL', manzana: 409, lote: 5, cajones: 2 } },
|
||||
{ codigo: 'SENDA-409-4-1', modelo: 'SANTIAGO', desarrollo: 'SENDA', fechaEntrega: '2027-11-27', supEstacionamiento: null, supConstruccion: 63.95, supExcedente: null, precioSinAcabados: 2750000, precioConAcabados: 2750000, precioLista: 2750000, status: 'VENDIDO', mesesParaFirma: null, extras: { prototipo: 'LAUREL', manzana: 409, lote: 4, cajones: 2 } },
|
||||
{ codigo: 'SENDA-409-1-1', modelo: 'SANTIAGO', desarrollo: 'SENDA', fechaEntrega: '2027-08-28', supEstacionamiento: null, supConstruccion: 63.95, supExcedente: null, precioSinAcabados: 2750000, precioConAcabados: 2750000, precioLista: 2750000, status: 'VENDIDO', mesesParaFirma: null, extras: { prototipo: 'LAUREL', manzana: 409, lote: 1, cajones: 2 } },
|
||||
|
||||
Reference in New Issue
Block a user