01 / 05Caso de estudio
OMSTA — ERP y app móvil para una agencia de viajes
ERP en producción para una agencia de viajes dominicana —reservas, cobros, contabilidad de doble partida, DGII, nómina y sucursales— con una app móvil nativa para el equipo.
- Mi papel
- Único desarrollador; arquitectura, backend, web, app móvil, despliegue y mantenimiento
- Stack
- Python · Django · Django REST Framework · PostgreSQL · Redis · Django Q2 · React Native · Expo · TypeScript · Railway
- Estado
- En producción diaria · app móvil en desarrollo
Alcance
- 19apps Django en un monolito modular
- 150modelos de dominio
- 54pantallas en la app móvil
El reto
Una agencia vende ocho tipos de producto, cobra en pesos y dólares y declara a la DGII. Venta, dinero y contabilidad necesitaban un solo sistema, sin teclear dos veces el mismo dato.
- Lo que construí
- Diseñé y construí en solitario un monolito Django de 19 apps —de la reserva al reporte fiscal— y una app en React Native y Expo que usa los mismos servicios que la web.
- Decisión técnica clave
- Un hecho económico se captura una sola vez y se propaga: servicios atómicos e idempotentes, invariantes en el modelo y PostgreSQL como única fuente de verdad.
Decisiones de diseño

- Problema
- Una reserva mezcla estado de venta, cobros y saldo, y hay que leerla de un vistazo.
- Decisión
- Escalera de estados, porcentaje cobrado y saldo en la cabecera, con venta y cobro separados.

- Problema
- Fuera de la oficina, un vendedor necesita cobrar sin abrir la web y sin saltarse a contabilidad.
- Decisión
- El cobro desde el teléfono usa el mismo servicio que la web y queda «por verificar» hasta que contabilidad lo aplica.

- Problema
- Editar un pago ya contabilizado descuadraría banco, asientos y saldos.
- Decisión
- El pago contabilizado no se edita; se gestionan comprobantes, se corrige de forma guiada o se reembolsa.

- Problema
- Para quien vende, la contabilidad es una caja negra.
- Decisión
- Cada asiento explica en lenguaje llano qué registra, y la reserva enseña todos sus asientos.

- Problema
- Pagar tarde al hotel pone en riesgo la reserva del cliente.
- Decisión
- La fecha de pago al proveedor se calcula sola desde la entrada y se agenda por vencimiento.

- Problema
- Un error contable descubierto al cerrar el mes llega tarde y sale caro.
- Decisión
- Salud contable de solo lectura y un cierre que se bloquea si alguna identidad no cuadra.

- Problema
- Seis tipos de producto con datos distintos acaban en seis formularios que no se parecen.
- Decisión
- Un mismo patrón de alta para todos, con navegación por secciones y el precio resumido en vivo.

- Problema
- Dar de alta una reserva en el teléfono es largo, y una llamada puede interrumpirla a medias.
- Decisión
- Un asistente por pasos que guarda cada borrador cifrado en el teléfono; la reserva se retoma donde se dejó.
Sistema
Elige un módulo: se enciende su ruta.Elige un módulo: debajo, su decisión.
HTML renderizado en el servidor, sin SPA ni build step, para que la lógica crítica y los permisos vivan sólo en el backend.
Django Templates · Bootstrap 5.3 · jQuery 3.7 · Select2 · HTMX 2.0 · Chart.js 4.4
Un segundo cliente que no calcula saldos ni estados: pinta lo que describe el servidor.
React Native 0.86 · Expo SDK 57 · Expo Router · TypeScript 6.0 · TanStack Query 5 · react-native-mmkv · Expo SecureStore
App Django propia con JWT ligado al dispositivo y un contrato OpenAPI que genera los tipos TypeScript de la app.
Django REST Framework 3.16 · Simple JWT 5.5 · drf-spectacular 0.30 · openapi-typescript 7.13
Acceso cerrado por defecto: login obligatorio, roles efectivos (rol más módulos) y reglas por prefijo de URL en un middleware.
Django 5.2 · django-user-agents · django-crum
Ocho tipos de producto en un dominio que habla con contabilidad sólo por un contrato neutral, protegido por tests de frontera.
Django 5.2 · django-filter · django-select2 · django-countries
Un único servicio de registro para web y móvil; todo pago nace «por verificar», con clave de idempotencia, y contabilidad lo aplica.
Django 5.2 · Django REST Framework · django-storages
La factura publica un paquete de asientos atómico para que la CxC nunca quede acreedora ni los anticipos sin reclasificar.
Django 5.2 · openpyxl 3.1
Capa contable genérica que no importa contabilidad y bloquea la publicación en períodos cerrados.
Django 5.2 · PostgreSQL
ReportLab 4.4 · XlsxWriter 3.2 · Chart.js 4.4
Snapshot fiscal inmutable al contabilizar; genera el TXT oficial y el XLSX, y la presentación a la DGII queda en manos humanas.
Django 5.2 · openpyxl 3.1
Una cuenta bancaria por moneda y la tasa de cambio congelada en cada pago en pesos o dólares.
Chart.js 4.4 · chartjs-plugin-zoom · openpyxl 3.1
Recibos renderizados en vivo; sólo la factura con NCF es inmutable.
WeasyPrint 65 · ReportLab 4.4 · qrcode 8.0 · bleach 6.3
Única fuente de verdad: todo lo contable, auditable o durable vive aquí.
PostgreSQL · psycopg 3.3
Sólo cola de Django Q2, caché y sesiones; puede perderse sin perder estado de negocio.
Redis · redis-py 6.1 · hiredis
Comprobantes y documentos en almacenamiento privado con URL firmada, porque contienen datos personales y financieros.
django-storages 1.14 · boto3 1.42
Cada push a la rama principal despliega producción y demo tras lint, checks y pytest en GitHub Actions.
GitHub Actions · pytest 8.4 · Ruff · Black · Jest
Gunicorn con hilos, reciclado escalonado de procesos y una sonda /health/ exenta de login y de la redirección HTTPS.
Railway · Railpack · Gunicorn 23 · WhiteNoise 6.12
Builds de desarrollo internas probadas en un teléfono Android real; las tiendas esperan a las cuentas del negocio.
EAS Build · Expo Dev Client · expo-doctor
Django Q2 en lugar de Celery; los trabajos se encolan al confirmar la transacción y son idempotentes.
Django Q2 1.9 · Redis
Django email · SMTP
Consulta por IP cacheada 24 horas (la negativa, una) para no pagar ni esperar la geolocalización en cada petición.
IPinfo API · requests
Se consulta desde el servidor, para ubicar hoteles sin exponer la llave en la app.
Google Places API · Google Maps JavaScript API
Cliente
Web Django
HTML renderizado en el servidor, sin SPA ni build step, para que la lógica crítica y los permisos vivan sólo en el backend.
- Tecnologías
- Django Templates · Bootstrap 5.3 · jQuery 3.7 · Select2 · HTMX 2.0 · Chart.js 4.4
- Entrega a
- Railway web
Tecnologías
67 herramientas en 11 áreas. En el sistema, cada módulo dice las suyas.
Lenguajes
- Python 3.12
- TypeScript 6.0
- JavaScript
- HTML y Django Templates
- CSS
Backend
- Django 5.2
- WhiteNoise 6.12
- django-filter 25.1
- django-select2 8.3
- bleach 6.3
Interfaz web
- Bootstrap 5.3
- Bootstrap Icons 1.11
- jQuery 3.7
- Select2 4.1
- HTMX 2.0
- Chart.js 4.4
- flatpickr 4.6
- SweetAlert2 11
App móvil
- React Native 0.86
- React 19.2
- Expo SDK 57
- Expo Router
- TanStack Query 5
- react-native-mmkv 4.3
- Expo SecureStore
- Expo Local Authentication
- Expo Location
- Expo Image Picker
- Expo Document Picker
API y contratos
- Django REST Framework 3.16
- Simple JWT 5.5
- drf-spectacular 0.30
- OpenAPI 3.0
- openapi-typescript 7.13
Datos y colas
- PostgreSQL
- psycopg 3.3
- Redis
- Django Q2 1.9
- django-storages 1.14
- boto3 1.42
PDF y exportación
- WeasyPrint 65
- ReportLab 4.4
- qrcode 8.0
- openpyxl 3.1
- XlsxWriter 3.2
Integraciones
- Google Places API
- Google Maps JavaScript API
- IPinfo
- Correo SMTP
Infraestructura
- Railway
- Railpack
- Gunicorn 23
- Almacenamiento S3 privado
Build móvil
- EAS Build
- Expo Dev Client
- Metro
Calidad y CI
- pytest 8.4
- pytest-django 4.11
- coverage 7.16
- Ruff 0.15
- Black 23.12
- pre-commit
- Jest 29
- ESLint 9
- GitHub Actions
- expo-doctor
- Playwright 1.61
Resultados verificables
- En producción y usado a diario por 15 personas, con despliegue automático en Railway desde la rama principal
- Ciclo completo venta → cobro → factura con NCF → asientos → reportes DGII 606, 607, 608 y 623
- Contabilidad de doble partida con invariantes en el modelo y en la base de datos
- App móvil nativa y su API REST, construidas en 16 días sobre el mismo backend
- Contrato OpenAPI tipado de punta a punta, con CI que falla si app y API se desalinean
- 3.164 pruebas automatizadas con pytest sobre PostgreSQL e integración continua en GitHub Actions
Recorrido por módulos
61 pantallas en 10 módulos.
Panel y reservas10 pantallas

El mes de la agencia de un vistazo, con alertas que llevan a la acción.

Una reserva dice cuánto se vendió, cuánto se cobró y cuánto falta.

Cada reserva con su estado de venta y de cobro.

La fecha de pago al hotel se calcula sola desde la entrada.

La factura con NCF y sus asientos, desde la propia reserva.

Todo lo que le pasó a la reserva, auditado.

Paquetes armados por componentes, cada uno con su costo.

También seguros, cruceros, vuelos y otros servicios.

Alta guiada por secciones, con el precio resumido en vivo.

El mismo patrón de alta para cada tipo de producto.
App móvil15 pantallas

El mes de la agencia, en el bolsillo.

Todas las reservas con su estado de venta y de cobro.

Saldo, vencimiento y acciones rápidas de una reserva.

Cada pago con su recibo, listo para enviar por WhatsApp.

Cobrar fuera de la oficina; contabilidad lo verifica después.

Seis productos y borradores que no se pierden.

Calendario propio: entrada, salida y noches de un toque.

Habitaciones, huéspedes y tarifa, paso a paso.

Descuento, comisión, pago inicial y plazos en un solo paso.

Clientes y empresas, con filtros por estado.

Ficha del cliente: saldo y contacto en un toque.

Avisos cuando contabilidad verifica un pago.

Catálogos de proveedores, vuelos y cruceros.

La administración, también desde el teléfono.

Retenciones de ley configurables desde la app.
Clientes y catálogos3 pantallas
Cobros y pagos6 pantallas

Cada cobro con su método, su estado y su comprobante.

Un pago contabilizado no se edita: se corrige o se reembolsa.

Reembolso con reverso contable y motivo obligatorio.

Cuentas por cobrar derivadas de reservas y pagos.

Lo que se debe a proveedores, ordenado por vencimiento.

Del costo al pago: la cuenta por pagar se salda.
Contabilidad6 pantallas

Cada asiento cuadra y sabe qué documento lo generó.

El asiento explica en español qué registra y por qué.

Toda la contabilidad de una venta en una tabla.

Un auditor interno: identidades contables que deben cuadrar.

Antes de cerrar el mes, el sistema lo revisa todo.

Qué cuenta usa cada operación, y quién la decide.
Fiscal DGII4 pantallas
Banco2 pantallas
Nómina4 pantallas
Administración8 pantallas

Operación en varias sucursales, con permisos por sucursal.

Usuarios con rol, estado y control de acceso.

Cada cambio queda en una bitácora que no se edita.

Reservas registra el pago; contabilidad lo verifica.

Siete documentos para el cliente, configurables.

Correos al cliente que se encienden uno a uno.

Reportes operativos, contables y fiscales en un lugar.

71 tutoriales dentro del sistema, por módulo.
La web en el teléfono3 pantallas
8 min de lectura
El caso completo
Contexto
OMSTA es el sistema con el que opera CristegnoViajes SRL, una agencia de viajes de República Dominicana: vende hoteles, vuelos, paquetes, cruceros, seguros y otros servicios, cobra en pesos y en dólares, paga a sus proveedores, lleva su nómina y declara ante la DGII. Trabaja con dos sucursales, Santo Domingo y Santiago, sobre una misma plataforma.
El primer commit es de abril de 2025. Hoy la web está en producción y la usan a diario 15 personas, y desde septiembre de 2026 el equipo tiene además una app móvil propia.
Problema
Una agencia une tres mundos que suelen vivir separados: la venta, el dinero y la contabilidad. Cuando cada uno vive en una herramienta distinta, el mismo dato se teclea varias veces, los saldos no cuadran y nadie puede reconstruir qué pasó con una reserva.
El reto era que una venta se capturase una sola vez y que de ahí salieran, sin volver a escribir nada, el cobro, la factura con NCF, los asientos, la cuenta por pagar al proveedor, el movimiento bancario y los reportes fiscales. Y que el equipo pudiera hacer su parte también fuera de la oficina.
Mi función
Soy el único desarrollador del sistema: modelé el dominio, diseñé la arquitectura, construí el backend, la interfaz web, la API y la app móvil, y me ocupo del despliegue, del mantenimiento y de los incidentes de producción. El repositorio suma más de mil cien commits.
Usuarios y permisos
Cinco roles base —superadministración, administración, contabilidad, reservas y clientes— y, encima, módulos sueltos que amplían lo que ve cada usuario. Un asesor crea reservas y registra cobros; contabilidad los verifica, factura y cierra; administración gestiona usuarios, sucursales y configuración. Los permisos se imponen en el servidor, por prefijo de URL, y nunca dependen de lo que la interfaz esconda.
Restricciones
- Fiscalidad dominicana: comprobantes NCF por rango autorizado y formatos 606, 607, 608 y 623. El sistema prepara, valida y descarga; la presentación a la DGII la hace una persona.
- Multimoneda: cada pago conserva la tasa con que se cobró y cada capa cuadra contra su propio total.
- Invariantes contables: la cuenta por cobrar nunca queda acreedora, un asiento publicado no se reescribe y un período cerrado no admite asientos.
- Datos sensibles: los comprobantes de pago viven en almacenamiento privado con URL firmada.
- Un solo desarrollador: la infraestructura tenía que ser pequeña y predecible.
Cómo lo construí
Por dominios y por capas: primero reservas y clientes; después los servicios que mueven dinero; luego el libro mayor y, al final, la reportería fiscal. Las vistas orquestan, los servicios abren la transacción y emiten el asiento, los selectores leen y los invariantes viven en el modelo y en restricciones de la base de datos, porque los comandos de reparación no pasan por formularios.
Las fronteras entre dominios están protegidas por tests: el libro mayor no importa contabilidad, reservas habla con contabilidad a través de un contrato neutral y la superficie pública de los modelos más grandes sólo puede bajar. Cada cambio pasa por lint, formato, comprobaciones de Django, migraciones y pytest en GitHub Actions antes de desplegarse.
Decisiones de arquitectura
- Monolito modular, no microservicios. Un equipo de una persona y transacciones que cruzan dominios piden un solo proceso con fronteras claras.
- HTML renderizado en el servidor. Sin SPA ni paso de build: Bootstrap, jQuery y HTMX sobre plantillas de Django, y la lógica sólo en el backend.
- Django Q2 y no Celery. PostgreSQL es la única fuente de verdad, también para el estado de los trabajos; Redis es cola, caché y sesiones, y puede perderse sin perder negocio.
- Paquete contable atómico. Emitir una factura publica venta, costo, anticipos y cobros en un solo servicio.
- Snapshot fiscal inmutable. Lo que se declaró a la DGII no cambia aunque cambien después los datos de origen.
- La app no calcula. El servidor le envía importes formateados, estados con su etiqueta y hasta la forma de los formularios.
La app móvil
La web usaba sesión y CSRF, y no había una API pensada para un cliente fuera del navegador. Construí una API REST propia con JWT ligado a cada dispositivo (rotación y lista negra), un contrato OpenAPI generado con drf-spectacular y los tipos TypeScript de la app generados desde ese contrato: si la API y la app se desalinean, la integración continua falla.
La app está hecha con React Native, Expo y TypeScript. Tiene 54 pantallas: inicio con los indicadores del mes, reservas con su ficha y un asistente de alta por pasos con borradores y calendario propio, clientes, cobros, avisos, catálogos y la configuración administrativa. Se desbloquea con la biometría del teléfono, guarda los tokens en el llavero del sistema, consulta sin conexión desde una caché persistida y registra cobros con foto del comprobante usando el mismo servicio que la web.
La API y la app se construyeron en dieciséis días, entre el 10 y el 25 de septiembre de 2026. Hoy se distribuyen como builds internas de Android por EAS y se prueban en un teléfono real. El proyecto ya está configurado para iOS; la versión para iPhone espera a la cuenta de Apple Developer del negocio.
Desafíos
El dinero cruza tres dominios —reservas, contabilidad y libro mayor— y un pago puede aplicarse, corregirse, revertirse o dejar un excedente. Mantener la idempotencia y la integridad en todos esos caminos fue lo más difícil. Le siguen las reglas fiscales dominicanas y reducir el acoplamiento natural de un monolito grande sin frenar la operación diaria.
Incidentes reales resueltos
- Pagos enviados dos veces: cada pago lleva una clave de idempotencia con una restricción única en la base de datos; un reintento devuelve el pago que ya existía.
- Un reintento de la app devolvía «posible pago duplicado» porque el formulario validaba antes que el servicio. Ahora la clave se resuelve primero.
- PDF rotos en producción por librerías del sistema que WeasyPrint necesitaba: la imagen de despliegue las instala en build y en ejecución.
- Una sonda de salud tras el login hacía que Railway diera por caído un
despliegue sano:
/health/quedó exenta de login y de la redirección HTTPS. - Un descuento aplicado a una reserva ya pagada generó un crédito a favor correcto, pero nadie se enteraba. Corregí el aviso al guardar y el recibo, y añadí un diagnóstico de sólo lectura para auditar excedentes.
Diseño y UX
El panel prioriza lo que mueve el día: reservas, cobros, comisión, saldos y alertas que llevan a la acción. La ficha de reserva separa el estado de venta del de cobro y enseña la escalera de estados, el porcentaje cobrado y el saldo. Los seis tipos de producto comparten el mismo patrón de alta, con navegación por secciones y el precio resumido en vivo.
La contabilidad deja de ser una caja negra: cada asiento explica en lenguaje llano qué registra y la reserva enseña todos los suyos. Un pago contabilizado no se edita, se corrige de forma guiada o se reembolsa. Y bajo 768 px las tablas se convierten en tarjetas.
Sobre las capturas
Todas las capturas salen de una base de datos aislada, sembrada con datos sintéticos por los mismos servicios que usa el sistema: nombres inventados, correos de ejemplo y documentos fiscales ficticios. Ninguna muestra datos de la agencia ni de sus clientes. Las pantallas que presentaban un fallo o no estaban terminadas se dejaron fuera.
Aprendizajes
En software de dinero, comunicar el resultado importa tanto como calcularlo. Los invariantes tienen que vivir en el modelo y en la base de datos, no en los formularios. La idempotencia va antes que la validación: si no, un reintento parece un duplicado. Y reutilizar el servicio de la web desde la API móvil evitó tener dos versiones de la misma regla de negocio.
Estado actual
La web está en producción y se usa a diario. La app móvil está en desarrollo activo: reservas, clientes, cobros y avisos ya funcionan en un teléfono real, y quedan por delante las notificaciones push y la publicación en las tiendas. El trabajo en curso se concentra en una auditoría contable por fases y en seguir pagando deuda técnica sin interrumpir la operación.
Siguiente paso
El repositorio es privado porque es el sistema de un cliente. Si necesitas una plataforma que conecte operación, dinero y contabilidad con trazabilidad real, en la web y en el teléfono, puedes contactarme.
















