Mi Cancha API
Backend de la app Flutter mi_cancha, que sirve para reservar canchas deportivas en Colombia.
Stack: NestJS 12 (ESM), PostgreSQL y Prisma 7, con JWT propio y validación con class-validator.
Puesta en marcha
npm install # genera el cliente de Prisma e instala el panel (admin/)
cp .env.example .env # y define un JWT_SECRET propio (mín. 32 caracteres)
npm run db:up # Postgres 17 en Docker (puerto 5433)
npm run db:migrate # aplica las migraciones
npm run db:seed # datos de demo
npm run build:admin # compila el panel en public/admin
npm run start:dev
- API:
http://localhost:3000/api/v1
- Swagger:
http://localhost:3000/docs (deshabilitado si NODE_ENV=production)
- Panel de super admin:
http://localhost:3000/admin
- Usuarios demo (seed): super admin
[email protected] / SuperAdmin2026! · negocio [email protected] / MiCancha2026!
Panel de super admin (admin/)
SPA en React + Vite + Tailwind servida en /admin: se compila en public/admin, que en Vercel sirve el CDN y en local sirve Nest. Solo entran cuentas SUPER_ADMIN.
- Dashboard: clientes, usuarios por rol, ingreso mensual recurrente, canchas, altas por mes.
- Clientes: listado con búsqueda; alta de cliente (usuario de negocio + negocio + plan); cambio de plan y suspensión.
- Usuarios: listado de todas las cuentas con filtro por rol.
- Planes: crear, editar y eliminar (solo si no tiene clientes).
- Catálogos: servicios y métodos de pago editables; enums del sistema en solo lectura.
Desarrollo con recarga en caliente: npm run start:dev y, en otra terminal, npm run admin:dev → http://localhost:5173/admin/ (Vite reenvía /api a Nest).
Producción (VPS con Easypanel)
Todo corre en un VPS de Hostinger (179.66.129.62) con Easypanel, proyecto mi_cancha_app:
| Servicio | Qué es | Dominios |
|---|
api | Esta API + landing + /admin + /reservar (imagen del Dockerfile) | app.micanchaapp.com, micanchaapp.com, www.micanchaapp.com |
db | PostgreSQL 18, solo red interna (mi_cancha_app_db:5432) | — |
negocio | Nginx con la app Flutter web (repo mi-cacha-app) | negocio.micanchaapp.com |
- Despliegue: Easypanel compila la rama
main con el Dockerfile. Las variables de producción se editan en Easypanel → api → Environment y se aplican al darle a Deploy.
- Migraciones: no corren en el build. Aplícalas antes de hacer push de código que las necesite (si borras columnas: primero el push, luego la migración):
scripts/migrate-vps.sh status # qué falta (no cambia nada)
scripts/migrate-vps.sh deploy # backup a Koofr y aplica las pendientes de tu prisma/ local
- Backups: cron diario a las 3:00 a. m. (Colombia),
pg_dump a Koofr (mi-cancha-backups, 30 días) y copia local en el VPS (/var/backups/mi-cancha, 7 días). Script y pasos para restaurar: /usr/local/bin/mi-cancha-backup.sh en el VPS; log en /var/log/mi-cancha-backup.log.
Neon (histórico)
Hasta el 2026-11-02 la base estuvo en Neon (proyecto mi-cancha, empty-mud-04894416); quedó como copia congelada de ese día. .env.neon sigue sirviendo para el desarrollo contra esa copia (npm run start:neon), no para producción.
start:dev usa la base local de Docker (.env) y start:neon usa Neon (.env.neon). Las dos usan el puerto 3000: corre una a la vez, o cambia el puerto con PORT=3001 npm run start:neon.
SEED_MODE=minimal exige SEED_SUPER_ADMIN_PASSWORD (mín. 12 caracteres). Sin SEED_MODE, el seed carga los datos demo: úsalo solo en local.
Diagrama de la base de datos
docs/database/ERD.md (Mermaid) y docs/database/erd.html (para abrir en el navegador).
Los genera prisma/erd/generator.ts, que es un generador más del schema. Se actualizan solos cada vez que corre prisma generate, prisma migrate dev o npm install.
Para regenerarlos a mano: npm run db:erd. No los edites directamente.
Endpoints
Documentación completa en docs/api/: guía de uso, referencia de endpoints, especificación OpenAPI y colección de Postman (docs/api/mi-cancha.postman_collection.json). Se regeneran con npm run docs:api.
| Método | Ruta | Descripción |
|---|
| GET | /health | Estado de la API y de la BD |
| POST | /auth/business/register | Registro de administrador de negocio |
| POST | /auth/business/login | Login de negocio (email + contraseña) |
| POST | /auth/google · /auth/apple | Login de jugador con el token del proveedor |
| GET | /auth/me | Usuario autenticado |
| GET · PUT | /business/profile | Perfil del negocio (PUT crea o actualiza) |
| PUT · DELETE | /business/profile/image | Foto de portada del negocio (multipart image, JPG/PNG ≤ 4 MB) |
| GET · POST · PATCH · DELETE | /business/courts · /:id | Canchas con horarios y tarifas (respeta el límite del plan; DELETE desactiva) |
| PUT · DELETE | /business/courts/:id/photos/:slot | Fotos de la cancha por posición: MAIN, SURFACE, LIGHTING (obligatorias), EXTRA_1, EXTRA_2 |
| GET | /business/availability?date=YYYY-MM-DD&sport=FUTBOL | Turnos del día de todas las canchas reservables del deporte (una tarjeta por cancha, con availableCount) |
| GET | /business/courts/:id/availability?date=YYYY-MM-DD | Turnos del día de una cancha: AVAILABLE, BOOKED o PAST, con su precio |
| GET | /business/reservations · /:id | Con date=YYYY-MM-DD, la agenda de ese día (lista). Sin date, todas paginadas (page, pageSize), de la más reciente a la más antigua. Filtros: from, to, status, courtId, customerId, search (nombre, teléfono o cédula) |
| POST |
Las rutas /business/* requieren rol ADMIN_NEGOCIO, y las /admin/*, rol SUPER_ADMIN.
Plan inactivo = solo lectura. Si la suscripción del negocio está suspendida o no tiene plan, el ADMIN_NEGOCIO puede hacer GET de todo, pero cualquier POST, PUT, PATCH o DELETE responde 403 con code: SUBSCRIPTION_INACTIVE. Lo aplica un guard global (ActiveSubscriptionGuard), así que cubre también los endpoints futuros. El super admin no se ve afectado; la app puede saber el estado con GET /business/account (subscription.isActive).
Convenciones
- Dinero: precios de canchas y reservas con hasta 2 decimales, en la moneda de la cancha (
currency, ISO 4217, por defecto COP). Planes y pagos siguen en enteros COP.
- Canchas: 7 horarios (uno por día,
MONDAY…SUNDAY); si closeTime < openTime cierra al día siguiente. Las tarifas (rates) no se pueden cruzar; donde ninguna aplica rige basePricePerHour y una reserva que cruza franjas se cobra por fracciones. Solo reciben reservas si isActive y tienen las fotos MAIN, SURFACE y LIGHTING (isBookable). Con el límite del plan alcanzado se permite una sola cancha inactiva más; el error es 403 con code: PLAN_LIMIT_REACHED.
- Fechas: ISO 8601 en UTC. La agenda por día usa la zona del negocio (
APP_UTC_OFFSET, por defecto -05:00).
- Enums fijos (
UserRole, Sport, DayOfWeek, CourtPhotoSlot, ReservationStatus, PaymentStatus, PaymentConcept): en MAYÚSCULAS_CON_GUION_BAJO. La app Flutter los mapea a sus enums.
- Catálogos editables (
Amenity = servicios, PaymentMethod = métodos de pago): la API recibe y devuelve su code (PARQUEADERO, NEQUI…). Los nombres visibles salen de GET /catalogs. Si un método tiene requiresReference, el pago exige referencia.
- Reservas: el valor es
subtotal (tarifas) − discount = totalPrice; amountPaid suma los pagos aprobados y balance es lo que falta. Un pago al reservar es TOTAL si cubre el total y ANTICIPO si no; por defecto en EFECTIVO.
- Clientes: cada negocio tiene los suyos; teléfono y cédula son únicos por negocio. Si al reservar llega un teléfono o cédula ya registrados, se usa ese cliente y se conservan sus datos. Los teléfonos de 10 dígitos se guardan con
+57.
- Nivel de fidelidad (automático): por reservas no canceladas:
NEW 0–1, RECURRENT 2–4, FREQUENT 5–19, VIP 20+. Asistencia = jugadas ÷ (jugadas + no asistió). Saldo = total − pagado de reservas confirmadas y jugadas (no cuenta las de no asistió).
Pruebas
npm test # unitarias (Prisma mockeado)
npm run test:e2e # e2e contra la BD local; requiere el seed
npm run lint
Login social
Define GOOGLE_CLIENT_IDS y APPLE_CLIENT_IDS (separados por coma) con los client IDs o bundle IDs de la app.
Si no están definidos, esos endpoints responden 503.