Suite 101 · arquitectura
Cualquiera de la empresa pide una compra desde su teléfono y le cae directa al buzón de quien paga, sin que nadie la autorice antes. Al marcarla pagada, dash101 registra el egreso solo, guarda el comprobante y le avisa por correo a quien la pidió. Encima va la contabilidad fiscal, y su idea manda sobre todo lo demás: no hay dos contabilidades. Hay una sola lista de movimientos, y la fiscal es esa misma lista filtrada por «facturado».
Son cinco estados y ninguno se salta. Lo que hay que entender de esta forma es lo que no tiene: no hay paso de autorización. Mike lo decidió así el 19 de septiembre y cambia la app entera: quien pide, pide, y quien paga decide al pagar. Un paso de autorización habría necesitado un tercer rol, una bandeja más y una razón para que alguien la revise todos los días.
Marcar pagada una orden hace seis cosas: crea el egreso en movimientos con la cuenta que escogió quien paga, le liga el movimiento_id a la orden, guarda el comprobante en archivos, deja el renglón en la bitácora, recalcula los cachés de la partida y del proyecto, y manda el correo. Las cinco primeras ocurren dentro de una transacción del Durable Object de la empresa; el correo va después, cuando el dinero ya quedó registrado.
Que sea una sola transacción no es elegancia: es lo que impide el doble egreso. Un doble clic en un celular con mala señal manda dos veces la misma petición, y sin esto el dinero sale dos veces. La segunda contesta 409 porque la orden ya no está en el buzón, y el saldo de la cuenta no se mueve.
«Contador» es una etiqueta, no un rol: la columna es_contador en personal. Se le pone a quien sea, y si el dueño y quien administra pagan los dos, los dos se marcan. La reparte sólo el dueño, y el servidor la revisa en cada petición: quitársela a alguien surte efecto al momento, aunque tenga el buzón abierto.
No se reusó ve_dinero, que ya existía y parecía servir. Esa marca dice quién ve cifras; no quién saca dinero del banco. Son dos permisos distintos y confundirlos habría dado el poder de pagar a todo el que puede mirar un reporte.
La tentación es tener una lista «real» y otra «fiscal». Se ve ordenado y no funciona, por una razón que pasa casi siempre: la factura llega después del pago.
| Con dos listas | Con una sola lista |
|---|---|
| El pago se captura en la real. Cuando llega la factura, se captura otra vez en la fiscal. | El pago se captura una vez. Cuando llega la factura, se le cuelga al movimiento que ya existe. |
| Las dos listas se despalman y nadie se entera hasta que no cuadran. | No pueden despalmarse: es la misma lista. |
| «Lo facturado contra lo real» hay que reconciliarlo a mano. | «Lo facturado contra lo real» es un filtro. |
Por eso movimientos ganó ocho columnas —facturado, subtotal, iva, tasa_iva, retenciones, uuid_cfdi, fecha_cfdi y forma_pago— en vez de nacer una tabla paralela. Todo lo que ya existía quedó con facturado = 0 y ninguna cifra de dinero cambió: lo viejo no se inventa.
Las facturas viven en cfdi, con el UUID único por empresa, y se ligan a los movimientos por una tabla intermedia que lleva el monto aplicado. Sin ese monto, una factura de $10,000 pagada en tres partes no se puede cuadrar: un CFDI puede cubrir varios pagos y un pago puede cubrir varios CFDI.
La suite guarda centavos enteros; las pantallas de dash101 trabajan en pesos. La conversión vive en un solo lugar de cada lado —lib/ordenes.ts y lib/fiscal.ts— y las pruebas comparan pesos exactos. El motivo es que este error no truena: una pantalla que reciba centavos y los pinte como pesos enseña cien veces de más, se ve perfectamente normal y miente.
El desglose del IVA se calcula desde el total hacia atrás, y el IVA es la resta:
subtotal = redondear(total × 10000 ÷ (10000 + tasa)) · iva = total − subtotal
Así subtotal + iva da el total exacto siempre, que es lo que no pasa cuando se multiplica dos veces. La tasa viaja en puntos base (1600 = 16.00 %) para que no haya decimales en la base, y es editable: hay retención, tasa cero y exento. Si la pantalla no lo toca, lo calcula el servidor, y así la fórmula existe una sola vez en la plataforma.
Una empresa puede tener varios negocios, y el RFC vive en el negocio, no en la empresa. Por eso el buzón, mis órdenes y las cuatro consultas fiscales aceptan negocio_id: con él, la lista y sus totales son de ese negocio.
Que el total salga de la misma consulta que la lista no es un detalle de implementación. Un «hay por pagar» calculado aparte del listado acaba diciendo un número que los renglones de abajo no explican, y en lo fiscal es peor: un IVA del mes que sume dos negocios no es el IVA de ninguno de los dos, y es la cifra con la que se entera al SAT.
es_contador no está en la lista de escritores de personal. Si estuviera, cualquiera con dash101 se marcaría solo como pagador.302 pruebas de la API en verde, más 31 comprobaciones de las migraciones con sqlite3 en memoria que confirman que ninguna cifra vieja se movió. En dash101, 84 pruebas, 18 de ellas contra staging midiendo que lo que llega a las pantallas está en pesos, y un recorrido con navegador a 390 × 844 que pide una compra con foto, la paga y la ve cambiar. El humo contra staging y producción, 200 de 200.
Y la captura de la demo encontró lo que ninguna prueba buscaba: el buzón mezclaba los negocios de la empresa. De ahí salieron los contratos 0.21.2 y 0.21.3.
El humo encontró algo que las pruebas en memoria no: quien abre una empresa como dueño sin ser miembro de ella —el superadmin de la suite— no se podía marcar a sí mismo como quien paga, y en una empresa recién dada de alta eso dejaba la pantalla vacía y a nadie que pudiera pagar. Se corrigió en el contrato 0.21.1, con su caso de prueba.