Suite 101 · arquitectura

La identidad de un ítem, y el estado de cuenta

Dos apps hablan de la misma pieza de mueble. ¿Qué campo dice que son la misma? La respuesta —el código, no el nombre— decide qué se unifica solo, qué se pregunta y qué se deja en paz. Y de paso: por qué el estado de cuenta de un cliente no se pudo sacar de la pantalla que ya existía.

Al 20 de septiembre de 2026Contrato de la API 0.28.0 y 0.29.0

El error de diseño: creer que el parecido basta

La primera versión del emparejado proponía sola: buscaba por código, luego por nombre, y ofrecía aceptar o rechazar. Acierta casi siempre, así que pareció suficiente.

No lo es, y la razón no tiene nada que ver con qué tan bueno sea el algoritmo. Una propuesta binaria sirve cuando el sistema tiene razón; cuando se equivoca, rechazarla deja a la persona sin nada. El trabajo que de verdad hay que hacer —«éste es aquél»— no se podía hacer desde la pantalla.

La forma correcta es al revés: el desplegable con todos los candidatos siempre, y la sugerencia ya escogida dentro de él. En el caso bueno se teclea igual de rápido —no hay que escoger nada—, y el caso malo tiene solución. El acierto promedio del algoritmo no es una interfaz.

Cuál campo es la identidad

Un ítem tiene código (CAR-01, PT-09), nombre y descripción. Las dos apps llevan código y nombre; la descripción sólo la tiene dash101, porque una pieza en un plano no necesita párrafos.

La pregunta que importa no es «¿qué nombre se queda?». Es cuál de los campos dice que dos renglones son el mismo objeto. Y es el código: es lo que alguien teclea para nombrar una pieza y encontrarla en las dos pantallas. El nombre es una descripción corta, y cada app puede tener la suya sin que nadie se confunda.

Esa respuesta reparte el trabajo en tres, y sólo uno necesita a una persona:

El nombre queda fuera de esa mecánica. Los dos lados siempre lo traen, así que nunca hay hueco que llenar: o cada uno conserva el suyo —lo que está marcado por omisión— o alguien escoge, y entonces queda en los dos lados. La descripción no entra en absoluto.

La regla general, que sirve para cualquier par de sistemas que compartan datos: antes de preguntar cuál valor gana, hay que saber cuál campo es la identidad. La identidad se unifica; lo demás se deja en paz. Tener dos vistas del mismo objeto no es una inconsistencia — es para lo que sirve tener dos apps.

Un guardado que valida a medio camino miente

La primera versión del aplicado hacía esto: escribir la liga, y después revisar que el código no chocara con el de otra pieza. Cuando chocaba, contestaba 409.

El problema no es el 409. Es que la pieza se quedaba ligada de todos modos. El mensaje decía «no se hizo» y sí se había hecho la mitad, que es peor que fallar entero: quien lo recibe cree que puede reintentar desde cero, y no puede.

Ahora son dos pasadas: se valida todo —que las piezas sean de esa obra, que los ítems sean de ese proyecto, que haya cupo, que ningún código choque— y sólo si todo cuadra se escribe. Cuesta una pasada más sobre datos que ya están en memoria.

Y la cuenta del cupo se lleva dentro del envío, no sólo contra la base. Dos piezas asignadas al mismo ítem de cantidad 1 en la misma llamada pasarían las dos: cada una, mirando la base, ve cero piezas ligadas. Es el mismo error de siempre —validar cada renglón contra un estado que los renglones anteriores ya cambiaron— y no lo atrapa ninguna restricción de la base, porque en la base cada fila es legal por separado.

El estado de cuenta, y por qué no salió de lo que ya había

Ya existía /peek: lo que un cliente ve de sí mismo en su portal, con sus proyectos y sus pagos. Parecía la mitad del trabajo hecho.

No lo era, por una línea de SQL. Los pagos de peek salen de un JOIN contra proyectos, así que un cobro que no cuelga de ningún proyecto —un anticipo recibido antes de abrirlo, un pago suelto— no aparece. Para mirar por encima está bien. Para un documento que alguien va a mandarle a su cliente, esa omisión es la diferencia entre cuadrar y que le reclamen: el cliente suma sus transferencias, le da más de lo que dice el papel, y a partir de ahí no le cree a ninguno de los dos números.

La ruta nueva usa un LEFT JOIN con una condición en dos partes: los cobros de los proyectos de ese cliente, y los que traen su nombre como contraparte aunque no cuelguen de ninguno.

Y todo —los totales y cada renglón— sale de una sola consulta. Es la misma lección que peek aprendió a golpes en septiembre: cuando el KPI y la tabla salen de dos consultas, un día se contradicen y las dos se ven ciertas. Aquí el saldo global es, por construcción, la suma de los renglones que se están imprimiendo.

El PDF que no se genera

El estado de cuenta se exporta a PDF con la impresión del navegador, no con una librería en el servidor. Es una decisión, no un atajo.

Un generador de PDF dentro de un Worker es una dependencia grande que hay que mantener, pesa en cada arranque en frío, y obliga a preocuparse por la memoria el día que un cliente traiga cien renglones. La impresión del navegador ya resuelve el problema, sale idéntica en cualquier máquina, y de paso deja escoger tamaño de hoja y márgenes.

Lo que sí es trabajo de la app es que la hoja se lea como un documento y no como una captura de pantalla: sin menú ni botones, sin fondos que se comen la tinta, y con las reglas que impiden que un renglón de tabla se parta entre dos hojas. Eso son veinte líneas de @media print.

Cómo quedó medido

404 pruebas de la API y 143 de dash101. Las que cuidan lo de arriba son cuatro: que un rechazo al emparejar deje las cosas exactamente como estaban; que dos piezas al mismo ítem en un solo envío no pasen ni una; que el saldo global sea renglón por renglón la suma de lo que se enseña; y que los cobros de otro cliente no se cuelen — en una cuenta que se manda por correo, un renglón ajeno no es un detalle de presentación, es una fuga de lo que otro cliente pagó.