Qué cubre este documento
ZOE administra el ciclo completo de una empresa de servicios: quién es el cliente, qué se le vendió, quién lo ejecuta, cuánto costó de verdad y a quién le toca la utilidad. Este documento describe el núcleo de gestión — la parte que resuelve ese ciclo.
La plataforma tiene más módulos de los que aquí se describen. Este documento cubre el núcleo de gestión: organizaciones, proyectos, dinero, tributario, costos y reparto. Deja fuera de forma deliberada los módulos de producción de contenido. Se dice para que nadie lea esto como el inventario completo del sistema.
El criterio de diseño que atraviesa todo lo que sigue es uno solo: una cifra que se ve confiable y está mal es peor que ninguna cifra. Casi todas las decisiones del modelo de datos que se explican aquí existen porque en algún momento el sistema dio un número creíble y falso.
Multi-tenant con unidades
Cada organización vive aislada, y dentro se divide en unidades de negocio que comparten usuarios, roles y finanzas.
Prospecto → proyecto → cobro
Cotización, conversión a proyecto, fases con entregables, tareas internas y cobros atados a su factura.
Retenciones por reglas, no por tarifa
La tarifa depende del tercero y del año. Todo es dato con vigencia, y nada se da por bueno sin confirmación humana.
Utilidad y reparto
Presupuesto contra gasto real para saber la utilidad de un proyecto en curso, y reparto entre socios con su rastro contable.
Multi-tenant y unidades de negocio
La unidad de aislamiento es el tenant: una organización con su propio nombre, identificador, logotipo y paleta. Cada fila de negocio del sistema lleva su tenantId.
Un usuario, varias organizaciones
La relación entre persona y organización no es un campo sino una tabla, UserTenant, con restricción de unicidad sobre el par usuario–organización. Un usuario puede pertenecer a varias, con un rol distinto en cada una, y puede quedar inactivo en una sin desaparecer de las demás.
isSystem vienen predefinidos y no se editan, para que nadie se quede sin administrador por accidente.Unidades de negocio
Dentro de una organización, la unidad de negocio agrupa proyectos, pagos y órdenes. Cada unidad lleva su color, y una bandera isAcademic que cambia qué campos pide un proyecto. La misma instalación sostiene así líneas de negocio distintas sin duplicar usuarios ni contabilidad.
Los proyectos, los pagos y las órdenes de compra cuelgan de la unidad. Eso permite responder «cuánto dejó esta línea de negocio» sin filtrar a mano, y es lo que hace que el reparto de utilidades pueda calcularse por unidad y no solo por organización.
Del prospecto al cobro
El recorrido comercial es una cadena de estados, y cada salto deja un registro que sobrevive al siguiente.
| Etapa | Estados | Qué decide |
|---|---|---|
| Cliente | PROSPECT · ACTIVE · INACTIVE · COMPLETED | Si entra al embudo comercial o al operativo |
| Interacción | CALL · EMAIL · WHATSAPP · MEETING · NOTE | Historial de contacto, con su fecha |
| Cotización | DRAFT · SENT · APPROVED · REJECTED · EXPIRED | Precio, renglones y tratamiento de IVA |
| Proyecto | PENDING · IN_PROGRESS · IN_REVIEW · CORRECTIONS · COMPLETED · CANCELLED | Ejecución y facturación |
La conversión de cotización a proyecto
Aprobar una cotización crea el proyecto, y el vínculo entre ambos es una llave foránea. Merece explicación porque antes no lo era.
La relación cotización–proyecto vivía como una marca dentro del campo de notas. Editar las notas la borraba, y entonces la cotización volvía a aparecer como convertible: se podía convertir por segunda vez, creando un segundo proyecto y cobrándole la misma deuda dos veces al mismo cliente. La misma lección se aplicó después a los datos estructurados de una propuesta, que también vivían dentro de las notas.
Renglones de cotización
Los renglones se guardan como estructura, no como texto: descripción, cantidad, precio unitario, descuento, tratamiento de IVA, tarifa, total e IVA calculado. El total del renglón es la base, y el IVA va aparte — la misma convención que en la factura y en la orden, para que ninguna suma tenga que adivinar si un número ya trae impuesto dentro.
Fases, entregables y tareas
Un proyecto se descompone en fases, cada fase produce entregables, y el trabajo interno se organiza en tareas. Son tres niveles con estados propios porque responden a preguntas distintas.
| Nivel | Estados | Pregunta que responde |
|---|---|---|
| Fase | PENDING · IN_PROGRESS · COMPLETED · BLOCKED | ¿Por dónde va el proyecto? |
| Entregable | PENDING · IN_PROGRESS · SUBMITTED · APPROVED · REJECTED | ¿Qué se le entregó al cliente y lo aceptó? |
| Tarea | TODO · IN_PROGRESS · IN_REVIEW · DONE | ¿Quién está haciendo qué ahora? |
El entregable tiene SUBMITTED y REJECTED porque entregar no es lo mismo que terminar: un entregable enviado y devuelto es un estado real del negocio, y colapsarlo en «pendiente» borra la información de que ya hubo una vuelta.
Quién trabaja y quién cobra son dos tablas
ProjectMember significa quién trabaja en las tareas. ProjectPartner significa a quién le toca parte de la utilidad, y por la misma fila, quién puede ver el proyecto. Si fueran el mismo modelo, agregar un revisor a un proyecto le daría derecho económico sobre él. Mantener el reparto y el acceso en una sola fuente evita además que se desincronicen.
El dinero y su imputación
Todo movimiento de plata es un Payment con tipo, método, estado y fecha de pago. Los métodos incluyen los locales — transferencia, Nequi, Daviplata — porque un sistema que solo entiende «tarjeta» no sirve para registrar cómo se cobra de verdad.
El tipo que evita contar dos veces
INCOME o EXPENSE, así que un traspaso aparece en los listados y no suma en ningún total.A qué factura se imputa un cobro
Un cobro puede atarse a la factura contra la que se giró. Cuando no lo está, el sistema aplica imputación por antigüedad: lo cobrado va contra la factura más vieja primero.
Con un proyecto facturado en dos partes — un anticipo y un saldo, que es como se cobra una obra — el sistema tenía que adivinar. La imputación por antigüedad es la regla usual del comercio, pero es una suposición: si el cliente giró contra la factura nueva y dejó la vieja pendiente, la pantalla lo decía al revés. Ahora el vínculo es explícito cuando se conoce.
El campo sigue siendo opcional, y no por compatibilidad: hay giros que de verdad no tienen factura todavía. Un anticipo entra antes de que exista el documento, y los ingresos que llegan por importación masiva se registran sin saber contra qué van. Para esos, la antigüedad sigue siendo el respaldo, y existe una operación para atarlos después.
Borrar un papel no devuelve la plata
Las relaciones de Payment usan SetNull y no Cascade. Con borrado en cascada, eliminar un cliente o un proyecto borraba también sus pagos, y el ingreso histórico de meses ya cerrados cambiaba de forma retroactiva e irrecuperable. Anular una factura tampoco puede borrar la plata que el cliente giró: el pago vuelve a quedar sin factura y la imputación por antigüedad lo recoge, que es justo lo que debe pasar.
Cuotas programadas
PaymentSchedule guarda el plan de pagos con estados PENDING · PAID · OVERDUE · CANCELLED. El vencimiento no se calcula al vuelo: se declara, para que «está en mora» sea un hecho registrado y no una inferencia que cambia según cuándo se abra la pantalla.
Motor tributario
El sistema tributario colombiano no tiene «una tarifa»: tiene reglas. El 4 % de servicios se vuelve 6 % si el proveedor no es declarante; el suministro es 2,5 % o 3,5 % por lo mismo; y por debajo de la cuantía mínima en UVT no se retiene nada. Por eso las tarifas son datos y no constantes en el código.
Las cuatro tablas que sostienen el cálculo
| Tabla | Qué guarda | Por qué existe |
|---|---|---|
| TaxConcept | El concepto de retención: servicios generales, transporte de carga, compras | La tarifa no vive aquí, porque depende del tercero |
| TaxConceptRate | Tarifa según la condición del tercero, con cuatro decimales | Un mismo concepto tiene tarifas distintas por declarante o no declarante |
| TaxConceptThreshold | Cuantía mínima en UVT, con vigencia | Estos valores se mueven, y la historia no se puede reescribir |
| UvtValue | Valor del UVT por año | El mínimo se evalúa contra el UVT del año del documento |
El Decreto 572 de 2025 bajó servicios de 4 a 2 UVT y compras de 27 a 10 UVT desde el 1 de junio de 2025. El Consejo de Estado suspendió esos artículos el 7 de mayo de 2026, revocó la suspensión el 2 de junio, y desde el 1 de julio volvieron a regir los valores nuevos. Dos cambios en catorce meses, con el juicio de nulidad todavía abierto. Un campo único obligaría a reescribir la historia en cada vuelta.
La fecha que manda es la del documento
Un costo guarda la fecha del documento del proveedor, distinta de la fecha en que se registró. Registrar hoy una factura de marzo del año pasado debe usar el UVT y los umbrales de marzo del año pasado. Recalcular la historia con valores nuevos destruye la credibilidad de un sistema contable.
El perfil del tercero
TaxParty guarda lo que el motor necesita para escoger la tarifa: tipo de persona, si declara renta, si es responsable de IVA, si es gran contribuyente, si es autorretenedor — a quien no se le retiene — y si es agente de retención de IVA. Sin esa ficha el motor no puede decidir, y lo dice en vez de inventar.
Tarifas, cuantías, valores de UVT y tarifas de ICA llevan una bandera isConfirmed. Ninguna se presenta como definitiva hasta que una persona la aprueba en pantalla. Un número inventado en un motor tributario es peor que ninguno, porque se ve confiable.
Y el motor guarda con cada cálculo las advertencias que emitió — tarifa sin confirmar, tercero sin ficha, UVT sin confirmar. Antes esas advertencias solo vivían en la vista previa del formulario, así que al guardar, el número quedaba pareciendo definitivo justo cuando dejaba de poder revisarse.
ICA es municipal
La tarifa de ICA depende del municipio y de la actividad, y se guarda por milaje en su propia tabla. Sin ella solo queda marcar el valor como provisional, que es exactamente el problema que tiene una hoja de cálculo.
Presupuesto y costo real
Saber si un proyecto deja dinero exige dos cosas separadas: lo que se espera gastar y lo que se gastó. Tenerlas juntas es lo que permite hablar de utilidad antes de cerrar.
Sin partidas de presupuesto, la utilidad de un proyecto en ejecución no se puede calcular — y lo peor es que sí se puede calcular mal: precio menos gastado hasta hoy. En un proyecto de instalación solar eso daba veinte millones de utilidad cuando todavía no se habían comprado ni los paneles ni el inversor. Es la peor clase de número: grande, tranquilizador y falso.
Con las partidas declaradas, las tres cifras salen solas: lo que falta por gastar, lo que costará al final, y la utilidad que queda si el presupuesto aguanta.
Un costo no es un pago
Un ProjectCost es un frente de trabajo — la compra, el alquiler o el servicio que alguien prestó — con su tratamiento fiscal y la carpeta donde viven sus soportes. Los pagos cuelgan de él, en plural.
El gasto que nadie previó
El vínculo entre un gasto y su partida de presupuesto es opcional a propósito. Un gasto que nadie previó es un hecho normal, y esconderlo bajo una partida inventada sería peor que dejarlo aparte: la pantalla los agrupa como «no estaba presupuestado», que es información útil sobre cómo se planeó.
El vínculo con la orden de compra que lo autorizó usa SetNull: si se borra la orden, el gasto sigue existiendo. La plata se gastó de verdad; borrar el papel que la autorizó no la devuelve.
IVA por renglón, no por factura
El tratamiento de IVA va por ítem — GRAVADO · EXCLUIDO · EXENTO — porque en una misma factura conviven renglones excluidos y renglones gravados al 19 %. En un caso real, tratar la factura como un bloque inflaba el IVA en más de un millón de pesos.
Las retenciones también tienen tabla propia, porque cada una tiene su propia base: 4 % sobre lo gravado y 1 % sobre el transporte, no sobre el total. Y los valores quedan congelados: se guarda la tarifa, la base y el UVT que se usaron en ese momento. Si al año siguiente cambia el UVT o una tarifa, esa factura sigue diciendo exactamente lo que dijo.
Reparto entre socios
Un reparto es una decisión autorizada, no un movimiento de caja. Puede haber varios por proyecto, porque la plata se reparte a medida que entra y no solo al cerrar.
La participación de cada socio se guarda con cuatro decimales. Con dos, 33,33 × 3 = 99,99 y la validación de suma lo rechaza: tres socios iguales — el caso más común de una sociedad — no se podían representar. Con 33,3333 la suma da 99,9999 y el desvío cabe holgado en la tolerancia, que además se pudo endurecer.
De la decisión al asiento
Al confirmar un reparto se genera un pago de egreso por socio, de modo que el efectivo disponible baja solo y nadie tiene que acordarse de registrarlo aparte. El vínculo con ese pago es una llave foránea real, no un texto: antes se escribía una vez y nadie garantizaba que apuntara a algo, así que el pago se podía borrar por detrás y el reparto quedaba diciendo que se pagó algo que no existe.
Ese vínculo usa SetNull: si el pago desaparece, el registro de a quién le tocaba cuánto tiene que sobrevivir. Es la decisión, no el movimiento de plata.
Auditoría y borrado
Dos mecanismos sostienen la trazabilidad: un registro de auditoría y un borrado que no destruye.
AuditLog
Registra los cambios sensibles con su autor y su momento. Es lo que permite responder «quién cambió este precio» meses después.
Papelera
El borrado es lógico. Un registro eliminado sale de las pantallas pero sigue existiendo, y con él las relaciones que sostenía.
La regla general del esquema es la misma en todas partes: ninguna eliminación puede borrar hacia atrás un hecho económico. Por eso las relaciones que tocan dinero usan SetNull y no Cascade, y por eso la papelera existe en vez de un DELETE.
Decisiones que costaron caro
Estas quedaron documentadas dentro del propio esquema, donde las encuentra quien vaya a tocar ese código.
| Qué pasó | Qué se aprendió |
|---|---|
| Un comentario de estilo JSDoc dentro del esquema Prisma | El entorno local toleraba /** */, pero el build de producción corre una versión anterior de la herramienta que lo rechaza al validar. El despliegue moría siempre en el mismo punto mientras se seguía sirviendo la build anterior, así que en pantalla no se veía ningún cambio y parecía que el código no llegaba. Solo // y ///. |
| Texto libre dentro de un comentario de esquema | Ese texto se copia tal cual al cliente generado. Escribir ahí la secuencia que cierra un bloque de comentario lo termina antes de tiempo y deja el archivo de tipos corrupto, con cien errores en un archivo que nadie edita. |
| Las pantallas de finanzas ordenaban por fecha de pago | Sin índice, cada carga ordenaba en memoria la tabla completa de la organización. El panel agrupaba por proyecto en cada render, y borrar un proyecto hacía un recorrido secuencial por la llave foránea. |
| Vínculos guardados como marcas dentro de campos de texto | Editar el texto los borraba. Un dato que se pierde al editar otro campo no es un dato. |
Arquitectura y ejecución
Monorepo con dos paquetes, una base de datos y un despliegue.
| Capacidad | Qué cubre |
|---|---|
| Organizaciones | Multi-tenant con roles por organización, unidades de negocio y portal externo de acceso restringido |
| Comercial | Clientes, historial de interacciones, cotizaciones con renglones estructurados y conversión trazable a proyecto |
| Ejecución | Proyectos con fases, entregables y tareas, con estados propios en cada nivel |
| Finanzas | Cobros y gastos, métodos locales, cuotas programadas e imputación explícita a factura |
| Tributario | Retención en la fuente, ICA e IVA con tarifas por condición del tercero, cuantías con vigencia y UVT por año |
| Costos | Presupuesto por partidas contra gasto real, con soportes y retenciones congeladas |
| Reparto | Participaciones con cuatro decimales y generación automática del egreso por socio |
| Control | Registro de auditoría, borrado lógico y comentarios sobre las entidades |
| Inventario | Productos, movimientos de stock, proveedores y órdenes de servicio o compra |
Pruebas
679 pruebas corren en cada cambio, sobre el corredor nativo de Node. Cubren de forma deliberada la lógica que decide plata: imputación de cobros, alcance de rutas por rol, detección de duplicados y la conversión de cobro a factura.