JALIL

plano · 03

Proyectos

Cuatro proyectos de mi trabajo en Parfumerie. Tres siguen en producción y uno se discontinuó. Soy desarrollador junior, así que en cada uno cuento cuál era el problema, qué hice yo, qué fue del equipo y cómo terminó. Los números son de la operación real.

01

POS multi-sucursal con facturación electrónica

En producción

Rol
Desarrollo, análisis, capacitación e implementación
Contexto
Backend en Express y frontend en Next.js. Reemplazó al software de caja de terceros que usaba el grupo. Tiene +100 suites de test y +40 migraciones de base de datos.

De qué se trata

Es el sistema con el que las sucursales cobran: junta cobro, facturación electrónica de AFIP, caja y reportes, y se conecta con las terminales de tarjeta, MercadoPago y gift cards. Lo desarrollé yo solo, de punta a punta, para reemplazar al sistema anterior.

El problema

Lo más delicado de un POS es el cobro, y el sistema anterior lo hacía doloroso: venía de un ERP viejo que aún se usa en la empresa, cobrar pedía muchos pasos manuales y era lento. Cuando algo fallaba a mitad del cobro, el cajero no sabía si la venta había salido y quedaba trabado en la caja, con riesgo de cobrar dos veces.

Qué hice

  • Evité los cobros duplicados con idempotencia: cada intento de cobro (carrito + medio de pago + monto) tiene una clave con índice único en la base, así que dos requests simultáneos terminan en un solo cobro.
  • Guardo el intento antes de cobrar, no después. Para eso tuve que separar dónde estaba el riesgo de duplicar (el POST que dispara la terminal) de dónde estaba la espera larga (la consulta, que se puede repetir sin problema).
  • Todo cobro falla limpio en 30 segundos o menos, con un error que el cajero puede entender, y puede ver el estado real del pago sin generar un segundo movimiento.
  • Armé los reportes financieros (planilla de caja, detalle de ventas, Libro IVA Ventas, arqueo y comprobantes emitidos), con exportación a Excel y acceso según el perfil de cada usuario.
  • Sumé ruteo automático entre terminales según el banco emisor de la tarjeta, para aprovechar las promociones bancarias, con integración a SiTef/CliSiTef.
  • Agregué una cola de reintentos para los comprobantes que AFIP rechaza.

Cómo salió

  • Como la protección es un índice único y no depende del timing, un reintento no puede duplicar un cobro, aunque la red o la terminal se porten mal.
  • Les ahorré horas de control a las sucursales y al personal administrativo. Antes, el cierre de caja podía demorar más de 30 minutos y auxiliares administrativos controlaban las cajas de cada sucursal todos los días. Hoy el control lleva muy poco tiempo, porque el margen de error es mínimo.

+500 ventas por día · 7 de 20 sucursales activas (despliegue gradual)

  • Node.js
  • Express
  • MySQL2
  • Prisma/Payway
  • MercadoPago
  • SiTef / CliSiTef
  • SOAP
  • Pino
  • FTP
  • Docker
  • Next.js 15
  • React 19
  • Zustand
  • AFIP
  • Node test runner
  • Playwright

02

Orquestador de pedidos entre Modula, ERP y Magento

En producción

Rol
Desarrollo y análisis, junto a otro desarrollador
Contexto
Monorepo en TypeScript con 104 suites de test, 106 migraciones y 229 modelos de Prisma. La mayor parte de las funcionalidades venían de un backend anterior en PHP/Laravel, así que entré a un proyecto que ya existía.

De qué se trata

Es el sistema que decide cómo se prepara cada pedido online: si sale del armario robotizado Modula, del depósito manual, o si se parte entre los dos. También coordina transportistas, tiendas y seguimiento. Casi todo ya existía cuando entré; yo participé en las partes nuevas y en la migración.

El problema

Cuando llegué, la operación combinaba tres sistemas con lógicas duplicadas y desincronizaciones que costaban plata. El código estaba en PHP/Laravel, con poca cobertura de tests y una base de 229 tablas difícil de tocar sin romper algo.

Qué hice

  • Lo mío: validación de stock contra BigQuery con un colchón de seguridad (stock ≥ cantidad pedida + cantidad comprometida + colchón), todo o nada por pedido. Achica la ventana en la que ERP, depósito y tienda online podían no coincidir.
  • Lo mío: procesos programados con Cloud Scheduler. Ingesta de pedidos cada 3 minutos (con una guarda para que no se pisen), detección de anomalías, monitores de SLA y de cancelaciones, facturación automática y sincronización con el ERP.
  • Con el otro desarrollador: migramos de a poco el código PHP a TypeScript, Turborepo y Prisma, sin frenar la operación.
  • Del equipo: ruteo de pedidos que puede usar un solo motor o los dos, con la posibilidad de revertir una decisión dejando registro.
  • Del equipo: tableros de Fill Rate, OTIF y Perfect Order, con los tiempos de cada tramo del proceso.

Cómo salió

  • Lo que más bajó la sobreventa en retiro en tienda fue achicar esa ventana de inconsistencia entre sistemas.
  • El mayor desafío fue acompañar una migración gradual sin detener la operación.

900 pedidos por día · más de 5.000 en picos (Black Friday, CyberMonday, Día de la Madre, Navidad) · 5% mixtos, 20% full robot, 75% manual con PDA

  • TypeScript
  • Node.js
  • Express
  • Prisma ORM
  • Google BigQuery
  • MySQL / MariaDB
  • Google Cloud Run
  • Cloud Scheduler
  • Cloud Build
  • Docker
  • Next.js 15
  • Turborepo
  • Vitest
  • (base previa: PHP / Laravel)

03

Pipeline ELT multi-fuente hacia el POS

En producción

Rol
Desarrollo
Contexto
Servicio en Python, serverless, sobre Cloud Run.

De qué se trata

Todas las mañanas, antes de que abran las cajas, este servicio trae cinco tipos de datos maestros (clientes, productos, EANs, listas de precios y vendedores) desde BigQuery, FTP, Notion y Google Sheets hacia la base del POS, para que los precios y las promociones estén al día.

El problema

Los datos estaban repartidos: el ERP por FTP, productos y clientes en BigQuery, las campañas en Notion y las excepciones en Sheets. Cada caja terminaba trabajando con una versión distinta de la realidad.

Qué hice

  • La arquitectura es simple: Cloud Scheduler dispara un servicio FastAPI en Cloud Run, que lee de BigQuery, FTP, Notion y Sheets y escribe en Cloud SQL. Entre corridas no consume nada.
  • Para las promociones, el pipeline cruza las campañas (vigencia, canal, sucursal, descuento) con las listas de precios y el catálogo, y deja una tabla plana de producto × sucursal × descuento que la caja lee directo.
  • Cada tipo de dato corre como un job independiente y repetible: si falla el de vendedores, los precios igual se actualizan.
  • Hay un endpoint para reprocesar una corrida que falló, sin esperar a la ventana del día siguiente.

Cómo salió

  • La lógica de las promociones ahora se resuelve antes, en el pipeline, y no en el momento de la venta.
  • Marketing puede cargar campañas y excepciones sin esperar un despliegue ni depender de un desarrollador.

más de 70.000 productos procesados por día · más de 200 promociones por día

  • Python 3
  • FastAPI
  • Pandas
  • google-cloud-bigquery
  • Google Cloud Run
  • Cloud Scheduler
  • Cloud SQL
  • Cloud Logging
  • Notion API
  • Google Sheets API
  • Pytest
  • Docker

04

Motor de picking product-centric con scoring

Discontinuado · decisión de negocio

Rol
Desarrollo
Contexto
Servicio asíncrono en Python.

De qué se trata

Da vuelta la forma de preparar pedidos: el operario escanea cualquier producto que tenga a mano y el sistema le dice para qué pedido es, priorizando el más urgente. La idea era que dejaran de buscar productos mirando una planilla.

El problema

Antes el picking iba por pedido: el operario leía una planilla y salía a buscar el producto. Para invertirlo, el sistema tenía que decidir al instante, dado un SKU escaneado, qué pedido atender primero, y reservarlo de forma que dos operarios no tomaran el mismo.

Qué hice

  • Un sistema de puntaje configurable: puntaje = peso_tiempo × antigüedad + peso_prioridad × método_de_envío. Hay 11 métodos de envío con valores propios, que se ajustan desde la base de datos sin desplegar.
  • Los pedidos se materializan desde BigQuery en una tabla operativa con índice por (sku, puntaje, estado). Así, «dame el pedido más prioritario que necesita este SKU» es una lectura de índice.
  • Bloqueo optimista con una máquina de estados, y escritura doble hacia el circuito anterior para que convivan durante la transición.
  • Todo asíncrono (FastAPI + SQLAlchemy async + aiomysql) para aguantar varios escaneos a la vez sin trabar el event loop.

Cómo salió

  • En pruebas, el tiempo medio por pedido bajó cerca de un 30%.
  • Lo diseñamos pensando desde el primer día en que iba a convivir con el sistema anterior y en poder volver atrás si hacía falta.
  • Se discontinuó por una decisión del negocio, ajena a mi área. El código sigue en el repositorio.
  • Python 3.13
  • FastAPI
  • SQLAlchemy 2 (async)
  • Pydantic Settings
  • google-cloud-bigquery
  • httpx
  • uv
  • Ruff
  • Google Cloud Run
  • Docker