Carrera: Ingeniería en Sistemas de Información
Curso: Fundamentos de Programación Web
Profesor: Eddier López
Proyecto: API GraphQL + Aplicación SPA PWA
Fecha de entrega y presentación: miércoles, 10 de junio de 2026
Valor: 30 % de la nota final
| Campo | Información |
|---|---|
| Nombre del proyecto | GreenDeal |
| Modelo de negocio | Gestión de ventas turísticas (proveedores, tours, comisiones e informes) |
| Integrantes | Britney Montero Suárez, Diana Monterrey Castillo, Jenyfer Montano Altamirano, Fabricio Álvarez Rodríguez |
| Repositorio | https://github.com/Fabr-i10/greenDeal.git |
| Frontend (PWA) | http://localhost:5000 |
| API GraphQL | http://localhost:9002/graphql |
| Login REST | http://localhost:9002/login |
Nota: El proyecto está pensado para ejecutarse en local. No se requiere despliegue en la nube para la entrega ni la demostración ante el profesor.
GreenDeal es una plataforma web para agentes o pequeños operadores turísticos que necesitan:
- Registrar proveedores (empresas que ofrecen experiencias).
- Definir tours asociados a un proveedor y un precio por persona en USD.
- Registrar ventas con cálculo automático de comisión (20 %) e IVA incluido en esa comisión.
- Consultar reportes mensuales o por rango de fechas.
| Requisito del enunciado | Cumplimiento en GreenDeal |
|---|---|
| API GraphQL | Apollo Server 5 + Express, esquema en Graph-API/schema.graphql |
| SPA | Una sola página (greendeal/index.html) con navegación por vistas sin recargar |
| PWA instalable | manifest.json, service worker, iconos y modo standalone |
| Funcionamiento offline | Service worker, caché, cola de mutaciones (PouchDB) y sincronización al reconectar |
| Módulo de usuarios | Registro, inicio de sesión, perfil y sesión JWT |
| Al menos 3 módulos adicionales | Proveedores, tours, ventas y reportes (4 módulos) |
| Seguridad | JWT, bcrypt, CORS, validación Zod, aislamiento de datos por usuario |
Los vendedores de experiencias turísticas suelen llevar proveedores, precios y ventas en hojas de cálculo o mensajes. Eso dificulta calcular comisiones, ver totales por periodo y trabajar sin conexión en campo.
GreenDeal centraliza el ciclo:
- El usuario (vendedor) se registra y accede a un panel.
- Da de alta proveedores y tours con precio por persona en dólares.
- Registra ventas indicando tour, fecha y cantidad de personas.
- El sistema calcula montos y muestra reportes de desempeño.
| Regla | Decisión del grupo | Implementación |
|---|---|---|
| Moneda | Trabajar en USD para precios y reportes | formatCurrency con Intl (en-US, USD) |
| Comisión | 20 % sobre el total de la venta | salesservice.js (API) y vista previa en formulario de venta |
| IVA | El 20 % ya incluye 13 % de IVA; se desglosa comisión con/sin IVA | commissionWithoutVAT = commissionTotal / 1.13 |
| Fecha de venta | No puede ser anterior al día actual | Validación Zod (cliente) + reglas en salesservice.js (servidor) |
| Aislamiento de datos | Cada usuario solo ve sus proveedores, tours y ventas | Columna userId en tablas y filtros en servicios |
| Integridad referencial | No eliminar proveedor con tours; no eliminar tour con ventas | Errores de dominio (PROVIDER_HAS_TOURS, TOUR_HAS_SALES) |
flowchart TB
subgraph cliente ["Cliente (navegador)"]
SPA["SPA PWA :5000"]
SW["Service Worker"]
SPA --> SW
end
subgraph local ["Equipo de desarrollo (localhost)"]
FE["npx serve greendeal/"]
API["Graph-API :9002"]
EXP["Express.js"]
GQL["Apollo /graphql"]
WS["WebSocket graphql-ws"]
EXP --> GQL
EXP --> WS
API --> EXP
end
subgraph datos ["Persistencia"]
DB[(SQLite db.sqlite3)]
end
FE --- SPA
SPA -->|HTTP POST /graphql + JWT| GQL
SPA -->|HTTP POST /login| EXP
SPA -->|WS suscripción newSale| WS
SW -->|fetch interceptado| GQL
GQL --> DB
EXP --> DB
| Capa | Responsabilidad | Ubicación |
|---|---|---|
| Transporte | HTTP, CORS, login REST, WebSocket | server.js, auth.js, utils/corsconfig.js |
| GraphQL | Esquema, resolvers, errores GraphQL | schema.graphql, resolvers.js |
| Validación | Entradas con Zod y reportes | utils/domainvalidation.js, utils/reportvalidation.js |
| Negocio | Reglas y acceso a datos | services/*.js |
| Datos | Knex + SQLite | services/connection.js, database/db.sqlite3 |
Decisión del grupo: mantener resolvers delgados y no duplicar lógica de negocio en GraphQL; toda regla de cálculo o validación de dominio vive en services/.
| Capa | Responsabilidad | Ubicación |
|---|---|---|
| Shell | Arranque, sesión, registro SW | index.html, shared/js/app.js |
| Vistas | UI por módulo | features/*/ |
| Servicios API | Queries y mutations GraphQL | js/*service.js |
| Compartido | Utilidades, validación, layout | shared/js/, shared/css/ |
| Offline | Caché y cola de mutaciones | serviceworker.js, sw/* |
proyectoFunda/
├── README.md ← Este documento
├── Graph-API/ ← Backend Node.js + GraphQL
│ ├── server.js
│ ├── schema.graphql
│ ├── resolvers.js
│ ├── auth.js
│ ├── services/
│ ├── utils/
│ ├── init_script/dbscript.js
│ └── database/db.sqlite3
└── greendeal/ ← Frontend SPA PWA
├── index.html
├── manifest.json
├── serviceworker.js
├── sw/ ← Módulos del service worker
├── js/ ← Clientes GraphQL por dominio
├── features/ ← Módulos de interfaz
└── shared/ ← CSS y JS compartidos
| Tecnología | Versión / uso | Motivo de elección |
|---|---|---|
| Node.js | ES modules | Mismo lenguaje que el frontend |
| Express.js | 4.x | Servidor HTTP y endpoint /login |
| Apollo Server | 5.x | Implementación estándar de API GraphQL |
| graphql-ws | 6.x | Suscripciones en tiempo real (newSale) |
| Knex | 3.x | Consultas SQL mantenibles |
| SQLite | better-sqlite3 | Base relacional simple para desarrollo y demo |
| JWT | jsonwebtoken | Sesión stateless para SPA |
| bcrypt | 10 rondas | Hash de contraseñas |
| Zod | 3.x | Validación de entradas en servidor |
| Tecnología | Uso | Motivo de elección |
|---|---|---|
| HTML5 / CSS3 | Estructura y estilos | Requisito base del curso |
| JavaScript ES modules | Sin bundler | Servidor estático local (npx serve) |
| Bootstrap 5 | Grid, modales, utilidades | Rapidez y diseño responsive |
| Boxicons | Iconografía | Consistencia visual |
| Zod (CDN) | Validación de formularios | Misma librería conceptual que el backend |
| PouchDB (CDN) | Cola offline en SW | Persistir mutaciones sin conexión |
| Funcionalidad | Descripción |
|---|---|
| Registro | Alta con nombre, correo, teléfono opcional y contraseña fuerte |
| Login | Autenticación vía POST /login → JWT |
| Perfil | Drawer con correo, teléfono y fecha de registro |
| Sesión | Token en sessionStorage; consulta me en GraphQL |
Decisiones:
- Contraseña con requisitos: mínimo 8 caracteres, mayúscula, minúscula, número y carácter especial; no puede contener el nombre.
- Token con expiración configurable (
JWT_EXPIRES_IN, por defecto 2 horas).
CRUD de empresas proveedoras (nombre, cédula jurídica, teléfono, correo, dirección). Listado paginado en interfaz.
CRUD de tours ligados a un proveedor: nombre, descripción, ubicación, precio por persona (USD). No se puede crear tour sin al menos un proveedor.
Registro de ventas por tour, fecha (YYYY-MM-DD) y cantidad de personas. Vista prevía de totales antes de guardar. Notificación en tiempo real vía suscripción GraphQL al crear una venta.
Reporte mensual o por rango de fechas: totales de ventas, comisiones, IVA, cantidad de ventas y ranking de tours.
- GraphQL HTTP:
POST /graphql - Cabecera:
Authorization: Bearer <token>en operaciones protegidas - Registro público: mutation
register(sin token) - Login:
POST /logincon{ "email", "password" }→{ token, user }
| Tipo | Operaciones |
|---|---|
| Query | me, providers, tours, sales, provider, tour, sale, monthlyReport, dateRangeReport |
| Mutation | register, createProvider, updateProvider, deleteProvider, createTour, updateTour, deleteTour, createSale, deleteSale |
| Subscription | newSale (canal por usuario: NEW_SALE_<userId>) |
Las listas providers, tours y sales aceptan limit (máx. 100, default 10) y offset, y devuelven totalCount.
Decisión del grupo: paginar para mejorar rendimiento en móvil y preparar crecimiento de datos sin cambiar el contrato GraphQL más adelante.
Para precio por persona P y cantidad N:
totalSale = P × NcommissionTotal = totalSale × 0.20commissionWithoutVAT = commissionTotal / 1.13vatAmount = commissionTotal - commissionWithoutVAT
| Característica | Implementación |
|---|---|
| Instalable | manifest.json, display: standalone, iconos 48–512 px |
| Responsive | CSS propio + Bootstrap; tablas como tarjetas en móvil |
| Splash | Pantalla de carga al iniciar |
| Offline | Service worker versión v41 (módulos en greendeal/sw/) |
sequenceDiagram
participant U as Usuario
participant SW as Service Worker
participant API as API local :9002
U->>SW: Mutation sin red
SW->>SW: Guardar en PouchDB + respuesta optimista
U->>U: UI actualizada / aviso offline
Note over U,API: Al volver online
SW->>API: Reenviar mutaciones pendientes
API-->>SW: Confirmación
SW->>SW: Limpiar cola
Decisiones:
- Interceptar solo peticiones GraphQL al origen configurado en
js/env.js. - Caché de lecturas GraphQL por usuario (scope del JWT) para evitar mezclar datos entre cuentas en el mismo navegador.
- Al cerrar sesión, mensaje al SW para limpiar caché GraphQL del usuario (
CLEAR_GRAPHQL_CACHE).
greendeal/js/env.js apunta siempre a la API local:
- GraphQL:
http://localhost:9002/graphql - Login:
http://localhost:9002/login - WebSocket:
ws://localhost:9002/graphql
Decisión del grupo: no usar despliegue en la nube; simplificar configuración para la entrega y la exposición en el laboratorio o en el equipo del estudiante.
| Medida | Descripción |
|---|---|
| Contraseñas | Hash con bcrypt; nunca se devuelve el hash al cliente |
| JWT | Secreto en variable JWT_SECRET (archivo .env, no en Git) |
| Expiración de token | JWT_EXPIRES_IN (ej. 2h) |
| CORS | Orígenes locales permitidos (localhost:5000, :5500); opcional CORS_ORIGINS en .env |
| WebSocket | Misma validación de origen que CORS |
| Autorización | userId del token en todas las operaciones de datos |
| Validación de entrada | Zod en servidor; Zod + errores inline en formularios |
| XSS | escapeHtml en contenido dinámico de tablas |
| Suscripciones | Publicación en canal NEW_SALE_<userId> para no exponer ventas de otros usuarios |
- Motor: SQLite 3
- Archivo:
Graph-API/database/db.sqlite3 - Acceso: Knex (
services/connection.js)
erDiagram
users ||--o{ providers : owns
users ||--o{ tours : owns
users ||--o{ sales : owns
providers ||--o{ tours : offers
tours ||--o{ sales : has
users {
text id PK
text email UK
text password
}
providers {
text id PK
text userId FK
}
tours {
text id PK
text userId FK
text providerId FK
float pricePerPerson
}
sales {
text id PK
text userId FK
text tourId FK
text saleDate
int quantityPeople
}
cd Graph-API
node init_script/dbscript.jsAdvertencia: el script elimina y recrea todas las tablas. Usar solo en desarrollo o antes de una demo controlada.
Se almacena como texto en formato YYYY-MM-DD (fecha local del formulario input type="date").
| Ámbito | Herramienta | Archivo |
|---|---|---|
| Formularios (cliente) | Zod + UI inline | greendeal/shared/js/schemas.js, form-validation.js |
| API (servidor) | Zod | Graph-API/utils/domainvalidation.js |
| Reportes (servidor) | Validadores dedicados | Graph-API/utils/reportvalidation.js |
Decisión del grupo: validar en cliente y servidor; el cliente mejora la experiencia, el servidor garantiza seguridad aunque alguien llame la API directamente.
El profesor o el equipo pueden evaluar el proyecto solo con Node.js, sin Netlify, Render ni otros hostings.
- Node.js 18 o superior
- npm
Copiar Graph-API/.env.example a Graph-API/.env y definir al menos:
| Variable | Descripción |
|---|---|
JWT_SECRET |
Secreto largo y aleatorio (obligatorio) |
JWT_EXPIRES_IN |
Opcional; por defecto 2h |
PORT |
Opcional; por defecto 9002 |
CORS_ORIGINS solo hace falta si el frontend se sirve en un puerto distinto a los ya permitidos (5000 y 5500).
cd Graph-API
npm install
cp .env.example .env
# Editar .env y definir JWT_SECRET
node init_script/dbscript.js
npm start- GraphQL:
http://localhost:9002/graphql - Login:
http://localhost:9002/login
cd greendeal
npx serve -l 5000Abrir en el navegador: http://localhost:5000
- En Chrome/Edge: menú → Instalar aplicación o Añadir a pantalla de inicio (sigue siendo
localhost). - Para probar en el celular en la misma red: usar la IP del PC (
http://192.168.x.x:5000) y agregar ese origen enCORS_ORIGINSdel.env.
- Iniciar API (
npm start). - Iniciar frontend (
npx serve). - Registrar usuario → crear proveedor → tour → venta → reporte.
| Área | Alternativas consideradas | Decisión final |
|---|---|---|
| Framework frontend | React, Vue, vanilla | Vanilla JS — menos tooling, encaje con curso |
| Base de datos | PostgreSQL, SQLite | SQLite — un archivo local, adecuado para el alcance del curso |
| Autenticación | Sesiones en servidor, JWT | JWT — adecuado para SPA desacoplada |
| API | REST puro, GraphQL | GraphQL — requisito del proyecto; una sola URL, tipado por esquema |
| Tiempo real | Polling, WebSocket | graphql-ws — notificación al registrar venta |
| Offline | Solo caché, cola de mutaciones | Caché de queries + cola PouchDB — cumple PWA offline |
| Estilos | Solo Bootstrap, CSS custom | Variables CSS + Bootstrap — identidad GreenDeal oscura |
| Validación | Manual, Zod | Zod en cliente y servidor |
| Ejecución | Nube vs solo local | Solo local — npm start + npx serve en dos terminales |
El enunciado recomienda un sistema de control de versiones. El grupo utiliza Git con historial de commits (features, correcciones de PWA, paginación, seguridad y validación).
Política acordada: no subir .env ni secretos; respaldar el repositorio remoto antes de la entrega del 10 de junio de 2026.
- Problema y modelo de negocio GreenDeal.
- Arquitectura: SPA + API GraphQL + SQLite.
- Demo en vivo: registro → proveedor → tour → venta (mostrar cálculo) → reporte.
- PWA: instalación en móvil o “Añadir a pantalla de inicio”.
- Offline: desconectar red, crear registro, reconectar y sincronizar.
- Seguridad: JWT, validación, datos por usuario.
- Cómo ejecutar en local (dos terminales).
- Limitaciones y trabajo futuro (recordatorios 24 h antes del tour, etc.).
- Recordatorios 24 horas antes del tour con fecha/hora y notificaciones push programadas.
- Despliegue opcional en la nube (no requerido para este curso).
- Optimización de imagen de fondo (
monteverde.jpg) a WebP comprimido. - Unificar cálculo de ventas en un solo módulo compartido front/back.
- Índices SQL en
userIdysaleDate.
- GraphQL — especificación y conceptos
- Apollo Server — documentación del servidor
- MDN: Progressive Web Apps
- Zod — validación de esquemas
- Enunciado del curso: PROYECTO II.pdf (Proyecto Programado II, Fundamentos de Programación Web)