- README.md con guía completa de instalación y arquitectura - docs/BACKEND.md — guía detallada del API - docs/FRONTEND.md — guía del SPA React - docs/DATABASE.md — funciones SQL y patrones de datos - docs/API.md — referencia completa de endpoints REST - docs/DEPLOYMENT.md — guía de despliegue en producción - docs/TROUBLESHOOTING.md — solución de problemas comunes - CHANGELOG.md — historial de cambios - CONTRIBUTING.md — guía de contribución
366 lines
10 KiB
Markdown
366 lines
10 KiB
Markdown
# 📦 Documentación del Backend — API Hotel Hacienda
|
|
|
|
## Índice
|
|
1. [Introducción](#introducción)
|
|
2. [Estructura de Carpetas](#estructura-de-carpetas)
|
|
3. [Flujo de una Petición](#flujo-de-una-petición)
|
|
4. [Configuración de Express (`app.js`)](#configuración-de-express-appjs)
|
|
5. [Conexión a Base de Datos](#conexión-a-base-de-datos)
|
|
6. [Controllers](#controllers)
|
|
7. [Rutas](#rutas)
|
|
8. [Middlewares](#middlewares)
|
|
9. [Servicios](#servicios)
|
|
10. [Variables de Entorno](#variables-de-entorno)
|
|
11. [Patrones y Convenciones](#patrones-y-convenciones)
|
|
12. [Guía para Agregar un Nuevo Endpoint](#guía-para-agregar-un-nuevo-endpoint)
|
|
|
|
---
|
|
|
|
## Introducción
|
|
|
|
El backend es una API REST construida con **Node.js** y **Express.js**. No utiliza ORM; la lógica de negocio reside en **funciones SQL de PostgreSQL** que son invocadas desde los controllers.
|
|
|
|
**Entry point:** `index.js` → `src/server.js` → `src/app.js`
|
|
|
|
---
|
|
|
|
## Estructura de Carpetas
|
|
|
|
```
|
|
hotel_hacienda/
|
|
├── index.js # Solo requiere src/server.js
|
|
├── src/
|
|
│ ├── server.js # Levanta el servidor
|
|
│ ├── app.js # Configura Express, CORS, rutas
|
|
│ ├── db/
|
|
│ │ └── connection.js # Pool de PostgreSQL
|
|
│ ├── middlewares/
|
|
│ │ ├── handleValidation.js # Manejo de errores de express-validator
|
|
│ │ └── validators.js # Validadores (login)
|
|
│ ├── routes/ # 16 archivos de rutas
|
|
│ │ ├── auth.routes.js
|
|
│ │ ├── employee.routes.js
|
|
│ │ ├── contract.routes.js
|
|
│ │ ├── product.routes.js
|
|
│ │ ├── expense.routes.js
|
|
│ │ ├── status.routes.js
|
|
│ │ ├── payment.routes.js
|
|
│ │ ├── settings.routes.js
|
|
│ │ ├── mail.routes.js
|
|
│ │ ├── incomes.routes.js
|
|
│ │ ├── purchase.routes.js
|
|
│ │ ├── exchange.routes.js
|
|
│ │ ├── hotelpl.routes.js
|
|
│ │ ├── restaurantpl.routes.js
|
|
│ │ ├── incomehrx.routes.js
|
|
│ │ └── reportcontract.routes.js
|
|
│ ├── controllers/ # 18 archivos de controllers
|
|
│ │ ├── auth.controller.js
|
|
│ │ ├── employee.controller.js
|
|
│ │ ├── contract.controller.js
|
|
│ │ ├── product.controller.js
|
|
│ │ ├── expense.controller.js
|
|
│ │ ├── status.controller.js
|
|
│ │ ├── payment.controller.js
|
|
│ │ ├── settings.controller.js
|
|
│ │ ├── mail.controller.js
|
|
│ │ ├── incomes.controller.js
|
|
│ │ ├── purchase.controller.js
|
|
│ │ ├── exchange.controller.js
|
|
│ │ ├── hotelp.controller.js
|
|
│ │ ├── restaurantpl.controller.js
|
|
│ │ ├── incomehrx.controller.js
|
|
│ │ ├── reportcontract.controller.js
|
|
│ │ └── reporter.controller.js
|
|
│ └── services/
|
|
│ └── mailService.js # Transporte de Nodemailer
|
|
```
|
|
|
|
---
|
|
|
|
## Flujo de una Petición
|
|
|
|
```
|
|
HTTP Request
|
|
│
|
|
▼
|
|
┌─────────────┐
|
|
│ Express │ ← CORS, JSON parser
|
|
│ (app.js) │
|
|
└──────┬──────┘
|
|
│
|
|
▼
|
|
┌─────────────┐
|
|
│ Routes │ ← Prefijo /api/xxx
|
|
│ (.routes) │
|
|
└──────┬──────┘
|
|
│
|
|
▼
|
|
┌─────────────┐
|
|
│ Controllers │ ← Extrae req.params/body
|
|
│ (.controller)│
|
|
└──────┬──────┘
|
|
│
|
|
▼
|
|
┌─────────────┐
|
|
│ Pool │ ← pg (node-postgres)
|
|
│ (connection)│
|
|
└──────┬──────┘
|
|
│
|
|
▼
|
|
┌─────────────┐
|
|
│ PostgreSQL │ ← Funciones SQL
|
|
│ (funciones)│
|
|
└─────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## Configuración de Express (`app.js`)
|
|
|
|
```javascript
|
|
const express = require('express');
|
|
const app = express();
|
|
const cors = require('cors');
|
|
require("dotenv").config();
|
|
|
|
const corsOptions = {
|
|
origin: process.env.URL_CORS,
|
|
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
|
allowedHeaders: ["Content-Type", "Authorization"]
|
|
};
|
|
app.use(cors(corsOptions));
|
|
app.use(express.json());
|
|
```
|
|
|
|
**Observaciones:**
|
|
- `URL_CORS` debe definirse sin slash al final.
|
|
- No hay middleware de autenticación global.
|
|
- Todas las rutas usan prefijo `/api/`.
|
|
|
|
---
|
|
|
|
## Conexión a Base de Datos
|
|
|
|
```javascript
|
|
// src/db/connection.js
|
|
const { Pool } = require('pg');
|
|
require('dotenv').config();
|
|
|
|
const pool = new Pool({
|
|
host: process.env.DB_HOST,
|
|
port: process.env.DB_PORT,
|
|
user: process.env.DB_USER,
|
|
password: process.env.DB_PASSWORD,
|
|
database: process.env.DB_NAME
|
|
});
|
|
|
|
pool.connect()
|
|
.then(() => console.log('Conectado a PostgreSQL'))
|
|
.catch(err => console.error('Error de conexión:', err));
|
|
|
|
module.exports = pool;
|
|
```
|
|
|
|
**Nota:** Se usa un `Pool` reutilizable. Cada controller lo importa directamente.
|
|
|
|
---
|
|
|
|
## Controllers
|
|
|
|
### Patrón General
|
|
|
|
```javascript
|
|
const pool = require('../db/connection');
|
|
|
|
const nombreFuncion = async (req, res) => {
|
|
try {
|
|
const { param1, param2 } = req.body; // o req.params / req.query
|
|
|
|
const result = await pool.query(
|
|
'SELECT nombrefuncionsql($1, $2) AS status',
|
|
[param1, param2]
|
|
);
|
|
|
|
const status = result.rows[0].status;
|
|
|
|
res.json({ message: 'Éxito', status });
|
|
} catch (error) {
|
|
console.error(error);
|
|
res.status(500).json({ message: 'Error interno' });
|
|
}
|
|
};
|
|
|
|
module.exports = { nombreFuncion };
|
|
```
|
|
|
|
### Paginación Manual
|
|
|
|
Algunos endpoints implementan paginación manual:
|
|
|
|
```javascript
|
|
const page = parseInt(req.query.page) || 1;
|
|
const limit = parseInt(req.query.limit) || 500;
|
|
const offset = (page - 1) * limit;
|
|
|
|
const result = await pool.query(
|
|
'SELECT * FROM getemployees() LIMIT $1 OFFSET $2',
|
|
[limit, offset]
|
|
);
|
|
|
|
const totalResult = await pool.query('SELECT COUNT(*) FROM employees');
|
|
const total = parseInt(totalResult.rows[0].count);
|
|
const totalPages = Math.ceil(total / limit);
|
|
|
|
res.json({ page, limit, total, totalPages, data: result.rows });
|
|
```
|
|
|
|
### Manejo de JSONB
|
|
|
|
Para pasar arrays/objetos a PostgreSQL:
|
|
|
|
```javascript
|
|
const categoriesJson = categories ? JSON.stringify(categories) : '[]';
|
|
await pool.query(
|
|
'SELECT newincome($1,$2,$3,$4,$5,$6::jsonb) AS status',
|
|
[param1, param2, param3, param4, param5, categoriesJson]
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
## Rutas
|
|
|
|
### Convención de Nombres
|
|
- Archivos: `nombreplural.routes.js`
|
|
- Prefijo en `app.js`: `/api/nombreplural`
|
|
|
|
### Ejemplo
|
|
|
|
```javascript
|
|
// src/routes/expense.routes.js
|
|
const express = require('express');
|
|
const router = express.Router();
|
|
const expenseController = require('../controllers/expense.controller');
|
|
|
|
router.get('/pendingapproval', expenseController.getPendingAppExpenses);
|
|
router.post('/newexpense', expenseController.newExpense);
|
|
router.put('/updateexpense/:id', expenseController.updateExpense);
|
|
|
|
module.exports = router;
|
|
```
|
|
|
|
### Registro en `app.js`
|
|
|
|
```javascript
|
|
const expenseRoutes = require('./routes/expense.routes');
|
|
app.use('/api/expenses', expenseRoutes);
|
|
```
|
|
|
|
---
|
|
|
|
## Middlewares
|
|
|
|
### Validación de Login
|
|
|
|
```javascript
|
|
// src/middlewares/validators.js
|
|
const { body } = require('express-validator');
|
|
|
|
const loginValidator = [
|
|
body('name_mail_user').notEmpty().withMessage('Usuario requerido'),
|
|
body('user_pass').notEmpty().withMessage('Contraseña requerida')
|
|
];
|
|
|
|
module.exports = { loginValidator };
|
|
```
|
|
|
|
### Manejo de Errores de Validación
|
|
|
|
```javascript
|
|
// src/middlewares/handleValidation.js
|
|
const { validationResult } = require('express-validator');
|
|
|
|
const handleValidation = (req, res, next) => {
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({ errors: errors.array() });
|
|
}
|
|
next();
|
|
};
|
|
|
|
module.exports = handleValidation;
|
|
```
|
|
|
|
**Nota:** Actualmente solo el login tiene validación. Los demás endpoints no validan inputs.
|
|
|
|
---
|
|
|
|
## Servicios
|
|
|
|
### Mail Service (`src/services/mailService.js`)
|
|
|
|
Configura un transporte SMTP reutilizable con Nodemailer:
|
|
|
|
```javascript
|
|
const nodemailer = require('nodemailer');
|
|
|
|
const transporter = nodemailer.createTransport({
|
|
host: process.env.EMAIL_HOST,
|
|
port: process.env.EMAIL_PORT,
|
|
auth: {
|
|
user: process.env.EMAIL_USER,
|
|
pass: process.env.EMAIL_PASS
|
|
}
|
|
});
|
|
|
|
module.exports = transporter;
|
|
```
|
|
|
|
**Uso:** Importado en controllers que envían correos (`auth.controller.js`, `mail.controller.js`).
|
|
|
|
---
|
|
|
|
## Variables de Entorno
|
|
|
|
| Variable | Descripción | Requerida |
|
|
|----------|-------------|-----------|
|
|
| `PORT` | Puerto del servidor | Sí (default 3000) |
|
|
| `URL_CORS` | Origen permitido para CORS | Sí |
|
|
| `DB_HOST` | Host de PostgreSQL | Sí |
|
|
| `DB_PORT` | Puerto de PostgreSQL | Sí |
|
|
| `DB_USER` | Usuario de PostgreSQL | Sí |
|
|
| `DB_PASSWORD` | Contraseña de PostgreSQL | Sí |
|
|
| `DB_NAME` | Nombre de la base de datos | Sí |
|
|
| `EMAIL_HOST` | Servidor SMTP | Sí |
|
|
| `EMAIL_PORT` | Puerto SMTP | Sí |
|
|
| `EMAIL_USER` | Usuario SMTP | Sí |
|
|
| `EMAIL_PASS` | Contraseña SMTP | Sí |
|
|
| `STRIPE_SECRET_KEY` | Clave secreta de Stripe | Para integración Stripe |
|
|
| `FACTURAS_API_URL` | URL API de facturas | Para integración facturas |
|
|
| `FACTURAS_ISSUER_RFC` | RFC del emisor | Para integración facturas |
|
|
| `FACTURAS_TYPE` | Tipo de factura (I/E) | Para integración facturas |
|
|
| `FACTURAS_API_TOKEN` | Token de API facturas | Para integración facturas |
|
|
| `BANXICO_TOKEN` | Token de Banxico | Para tipo de cambio |
|
|
|
|
---
|
|
|
|
## Patrones y Convenciones
|
|
|
|
1. **Un controller por dominio:** Todos los endpoints relacionados con un recurso viven en el mismo controller.
|
|
2. **Parámetros posicionales:** Las funciones SQL usan `$1, $2, $3...` en orden.
|
|
3. **Respuesta consistente:** Siempre retornar JSON con al menos `{ message, ...data }`.
|
|
4. **Errores 500:** Cualquier excepción se captura y responde con status 500.
|
|
5. **Sin ORM:** Las consultas son SQL crudo o llamadas a funciones almacenadas.
|
|
|
|
---
|
|
|
|
## Guía para Agregar un Nuevo Endpoint
|
|
|
|
1. **Crear/actualizar la función SQL** en PostgreSQL.
|
|
2. **Crear el método** en el controller correspondiente.
|
|
3. **Agregar la ruta** en el archivo `.routes.js` apropiado.
|
|
4. **Registrar la ruta** en `src/app.js` con un prefijo `/api/`.
|
|
5. **Probar** la función SQL directamente en PostgreSQL antes de probar el endpoint.
|
|
6. **Actualizar** `docs/API.md` con el nuevo endpoint.
|