Facundo Cornejo.
Volver a proyectos

Case study

Punto Natural — POS/ERP en Go

Un punto de venta donde la plata y el stock no pueden estar mal: transacciones atómicas, idempotencia real y una auditoría de seguridad que cerró en 0 hallazgos altos.

Sin demo publica: corre en la infraestructura del cliente
GoPostgreSQLReactDocker

Contexto

Punto Natural es un comercio de barrio que vendía con cuaderno: fiado anotado a mano, stock de memoria y cierre de caja al ojo. Me contrataron para reemplazar eso por un sistema propio, corriendo en su mostrador. Lo construí de cero en Go, con el frontend React embebido en el mismo binario para que el deploy sea un solo archivo.

Problema

Un POS es un sistema donde los bugs se pagan en pesos. Si dos ventas concurrentes descuentan el mismo stock, hay faltante real en la góndola. Si el vendedor hace doble clic en Cobrar, el cliente paga dos veces. Si el cierre de caja no da, alguien pone la diferencia de su bolsillo. El desafío no era hacer pantallas: era garantizar que esas tres cosas no pudieran pasar.

Solución y decisiones técnicas

  • Stock con decremento atómico en la base (UPDATE ... WHERE stock >= cantidad), nunca read-modify-write. La venta entera va en una transacción: si algo falla, no queda media venta.
  • Idempotencia con huella del payload: la misma clave con un carrito distinto devuelve 422 en vez de repetir el cobro. Verificado con requests concurrentes reales — una sola fila, un solo descuento de saldo.
  • La plata siempre en centavos enteros, jamás en float. El reparto de descuentos por combo usa multiplicación de 128 bits para que un total grande no desborde int64, y reparte el residuo en trozos equivalentes para que ninguna línea quede negativa.
  • El monto esperado del cierre de caja es una función pura con tests propios, separada de la base. El arqueo se puede probar sin levantar Postgres, y la diferencia da 0 en los cuatro medios de pago.
  • Migraciones con goose embebido como librería (go:embed), ejecutadas por un subcomando del propio binario en vez de un CLI aparte. En el servidor migrar es un paso manual explícito, nunca automático al arrancar.
  • sqlc para el acceso a datos: el SQL se escribe a mano y el tipado lo genera el compilador. El 100% de las consultas queda parametrizado por construcción.
  • Performance medida, no supuesta: sembré 4 años sintéticos (~116 mil ventas) y ajusté los índices con EXPLAIN (ANALYZE, BUFFERS) hasta conseguir Index Only Scan en los caminos calientes.

Seguridad

  • Auditoría de seguridad integral: 0 hallazgos altos, 0 medios y 5 bajos, los cinco corregidos y verificados end-to-end en la misma sesión.
  • Mapeé las 54 rutas de la API para confirmar que ningún DTO filtrara datos según el rol: un empleado nunca ve el costo de un producto.
  • Cacé una fuga de tiempo en el login: el hash señuelo usaba un costo de bcrypt distinto al real, así que el tiempo de respuesta delataba si el usuario existía.
  • Sesiones del lado del servidor con expiración deslizante y tope de vida absoluto. La cookie no lleva Expires: la vigencia la decide el server, no el navegador.
  • Headers de seguridad y chequeo de origen en las rutas que mutan, con HSTS solo en producción.
  • Backup cifrado con ensayo de restore automatizado: la prueba no es que el archivo exista, es que sobre la base restaurada se pueda hacer una venta real.

Mi rol

Todo: relevamiento con el dueño, modelo de datos, backend, frontend, infraestructura y las revisiones. Trabajé con un agente de IA como ejecutor y yo como arquitecto y revisor, con un informe por sprint que después auditaba contra el código con verificación independiente. Varios de los hallazgos de arriba salieron de esas revisiones, no de los tests.

Estado y resultado

La aplicación está completa y verificada, con una instancia de staging provisionada en mi propio VPS y CI corriendo migraciones up/down/up contra un Postgres real en cada push. La salida a producción está pendiente de la validación visual del cliente. Las tres invariantes de negocio que definí al principio — stock negativo, saldos descuadrados y caja abierta huérfana — dan 0 en cada corrida.

Aprendizajes

  • Los tests verdes no son el criterio de cierre. El criterio es la corrida real contra el sistema de verdad: un mock nunca me reprodujo el comportamiento que después me rompió.
  • La incertidumbre se modela como estado explícito, no como un booleano. Una operación irreversible necesita poder decir "no sé si salió".
  • Verificar el efecto observable y no el valor intermedio: el DOM puede estar perfecto y el total salir en $0.
  • Sembrar datos a escala real antes de tocar índices evita optimizar lo que no dolía.

En pantalla

Pantalla de punto de venta