# Diseño de API — Barbería SaaS

Todas las rutas bajo `/api/*` pasan por `requireAuth` + `requireTenant`
(ver `middleware-auth.js`), excepto las marcadas **público**.

**Convenciones:**
- Respuestas de error: `{ "error": "mensaje claro" }` con el status HTTP correspondiente
- Listados: paginados con `?page=1&limit=50`, nunca traer todo sin límite
- Fechas: ISO 8601, siempre en horario `America/Santiago`

---

## Auth y Onboarding

| Método | Ruta | Roles | Descripción |
|---|---|---|---|
| POST | `/api/auth/login` | público | email + password → JWT |
| POST | `/api/auth/logout` | cualquiera | invalida sesión |
| POST | `/api/onboarding/paso-1` | público | crea tenant + usuario dueño |
| PATCH | `/api/onboarding/paso-2` | dueño | datos de negocio + slug |
| PATCH | `/api/onboarding/paso-3` | dueño | sucursales |
| PATCH | `/api/onboarding/paso-4` | dueño | servicios iniciales |
| PATCH | `/api/onboarding/paso-5` | dueño | modelo de comisión general |
| PATCH | `/api/onboarding/paso-6` | dueño | citas + modalidad de pago |
| PATCH | `/api/onboarding/paso-7` | dueño | boleta/factura/control interno |
| POST | `/api/onboarding/confirmar` | dueño | activa plan trial, cierra el wizard |
| GET | `/api/onboarding/slug-disponible?slug=` | público | check en tiempo real |

## Invitaciones

| Método | Ruta | Roles | Descripción |
|---|---|---|---|
| POST | `/api/invitaciones` | dueño, admin_local | crea invitación + envía link |
| GET | `/api/invitaciones/:token` | público | valida token, muestra a qué se une |
| POST | `/api/invitaciones/:token/aceptar` | público | crea el usuario real |
| POST | `/api/invitaciones/:id/reenviar` | dueño, admin_local | regenera token/expiración |
| DELETE | `/api/invitaciones/:id` | dueño, admin_local | cancela |

## Equipo

| Método | Ruta | Roles | Descripción |
|---|---|---|---|
| GET | `/api/usuarios` | dueño, admin_local | lista del tenant |
| PATCH | `/api/usuarios/:id` | dueño, admin_local, (propio) | editar datos |
| PATCH | `/api/usuarios/:id/estado` | dueño, admin_local | suspender/activar |
| GET/PUT | `/api/usuarios/:id/comision` | dueño, admin_local | personal_comision_config |
| GET/PUT | `/api/usuarios/:id/horario` | dueño, admin_local, (propio) | horarios_barbero |
| POST | `/api/usuarios/:id/excepciones` | dueño, admin_local, (propio) | vacaciones/licencia/día especial |

## Catálogo

| Método | Ruta | Roles | Descripción |
|---|---|---|---|
| GET/POST | `/api/servicios` | dueño, admin_local | filtrable por categoría |
| PATCH/DELETE | `/api/servicios/:id` | dueño, admin_local | |
| GET/POST | `/api/productos` | dueño, admin_local | |
| PATCH/DELETE | `/api/productos/:id` | dueño, admin_local | |

## Ventas

| Método | Ruta | Roles | Descripción |
|---|---|---|---|
| POST | `/api/ventas` | barbero, cajero, dueño, admin_local | crea venta + items, calcula monto_barbero/monto_local |
| GET | `/api/ventas` | todos (barbero: solo las suyas) | |
| GET | `/api/ventas/:id` | todos (con scope) | |
| PATCH | `/api/ventas/:id/anular` | dueño, admin_local | |

## Citas

| Método | Ruta | Roles | Descripción |
|---|---|---|---|
| GET | `/api/citas` | todos (barbero: solo las suyas) | |
| POST | `/api/citas` | dueño, admin_local, barbero (para sí mismo) | |
| PATCH | `/api/citas/:id` | dueño, admin_local, (barbero dueño) | |
| DELETE | `/api/citas/:id` | dueño, admin_local, (barbero dueño) | |
| GET | `/api/citas/disponibilidad?fecha=&barbero_id=&servicio_id=` | dueño, admin_local | cruza horario + excepciones + citas ya tomadas |
| POST | `/api/citas/:id/concretar-venta` | barbero, cajero | genera la venta enlazada |

## Público (link de agendamiento del cliente — sin auth)

| Método | Ruta | Descripción |
|---|---|---|
| GET | `/api/public/:slug` | datos del negocio, barberos activos |
| GET | `/api/public/:slug/servicios` | agrupados por categoría, con precio_online si aplica |
| GET | `/api/public/:slug/disponibilidad?fecha=&barbero_id=` | mismo cálculo que el interno |
| POST | `/api/public/:slug/citas` | crea cita con `origen = 'online'` |
| POST | `/api/public/:slug/pago` | inicia cobro con la pasarela activa (Mercado Pago/Flow) |
| POST | `/api/public/:slug/pago/webhook` | callback de la pasarela, marca `pago_estado` |

## Finanzas

| Método | Ruta | Roles | Descripción |
|---|---|---|---|
| GET/POST | `/api/gastos` | dueño, admin_local | monto_neto + monto_iva separados |
| GET/POST | `/api/otros-ingresos` | dueño, admin_local | incluye arriendo fijo de silla |
| GET | `/api/categorias-gasto` | dueño, admin_local | |
| GET | `/api/liquidaciones` | dueño, admin_local, (barbero: las suyas) | |
| POST | `/api/liquidaciones/generar` | dueño, admin_local | cierra un período, congela detalle |
| PATCH | `/api/liquidaciones/:id/pagar` | dueño, admin_local | |
| GET | `/api/reportes/resultados?desde=&hasta=` | dueño, admin_local | ingresos reales (monto_local) − gastos |
| GET | `/api/reportes/gastos-por-categoria` | dueño, admin_local | |
| GET | `/api/reportes/credito-fiscal?desde=&hasta=` | dueño, admin_local | suma monto_iva donde tiene_factura=TRUE |

## Configuración

| Método | Ruta | Roles | Descripción |
|---|---|---|---|
| GET/PUT | `/api/config/pagos` | dueño | modalidad + credenciales Mercado Pago/Flow (para cobrarle a SUS clientes) |
| GET/PUT | `/api/config/sii` | dueño | pendiente hasta integrar Lioren |
| GET/PUT | `/api/config/features` | dueño | citas_habilitadas y futuros módulos |

## Planes y suscripción SaaS (lo que el cliente te paga a ti)

| Método | Ruta | Roles | Descripción |
|---|---|---|---|
| GET | `/api/planes` | público | planes activos, con precio_neto por periodicidad |
| GET | `/api/complementos` | público | ej: integración SII, con precio_uf |
| POST | `/api/suscripcion/cotizar` | dueño | calcula total (plan + complementos + IVA) sin generar el pago |
| POST | `/api/suscripcion/pagar` | dueño | genera el link de pago (Mercado Pago/Flow) con las credenciales de la plataforma |
| POST | `/api/suscripcion/pagar/webhook` | público (webhook) | confirma el pago → activa plan, contrata complementos |
| GET | `/api/superadmin/plataforma-config-pagos` | superadmin | ver credenciales propias |
| PUT | `/api/superadmin/plataforma-config-pagos` | superadmin | actualizar credenciales |

**`/api/suscripcion/cotizar` — cómo se calcula el total:**

Los precios en `planes` y `complementos` ya incluyen IVA — es lo que el
cliente ve y paga, sin sorpresas. El neto se calcula hacia atrás, solo
para tus propios registros contables.

```
meses_del_periodo   = 1 (mensual) | 6 (semestral) | 12 (anual)
monto_total          = planes.precio_final + (complementos.precio_mensual_final × meses_del_periodo, si contrató SII)
monto_neto           = monto_total / 1.19        (para tu declaración de IVA, no lo ve el cliente)
monto_iva            = monto_total - monto_neto
```

**Ejemplo real — Plan Semestral + integración SII:**
```
monto_total  = $90.000 + ($18.000 × 6) = $198.000   (esto paga el cliente, IVA incluido)
monto_neto   = $198.000 / 1.19 = $166.386,55
monto_iva    = $198.000 - $166.386,55 = $31.613,45
```
El límite de 2.500 documentos/mes se mantiene fijo sin importar la
periodicidad del plan — es un tope MENSUAL, no uno que se multiplique
por los meses contratados.

Este mismo cálculo es el que se guarda como snapshot en `pagos_suscripcion`
al momento de pagar — igual que en `venta_items`, nunca se recalcula
retroactivamente si el precio del plan cambia después. Si editas el
precio de un plan o complemento (`PATCH /api/superadmin/planes/:id` o
`/complementos/:id`), lo pagado antes queda intacto — solo cambia lo
que se cobra en la PRÓXIMA renovación.

| Método | Ruta | Roles | Descripción |
|---|---|---|---|
| GET | `/api/tenant/suscripcion` | dueño, admin_local | estado actual, para decidir qué banner mostrar |

**Respuesta de `/api/tenant/suscripcion`:**
```json
{
  "estado_alerta": "por_vencer",
  "es_trial": false,
  "plan_actual": "Plan Semestral",
  "fecha_vencimiento": "2026-08-14",
  "dias_restantes": 5
}
```

**Lógica del cálculo (backend, no frontend):**
- `planes.es_trial = true` → `estado_alerta = 'comprar'`
- Plan pagado, `dias_restantes > 7` → `'ninguna'` (sin banner)
- Plan pagado, `dias_restantes` entre 0 y 7 → `'por_vencer'`
- `fecha_vencimiento_plan` ya pasó → `'vencido'`

**Recomendación — período de gracia antes de suspender:** que `'vencido'`
no bloquee de inmediato. Un margen de 2-3 días donde el sistema sigue
funcionando pero con el banner urgente, y recién después de eso el job que
ya diseñamos pasa `tenants.estado = 'suspendido'`. Evita que alguien pierda
acceso a mitad de una venta por un pago que se demoró un día en procesarse.

## Superadmin

| Método | Ruta | Roles | Descripción |
|---|---|---|---|
| GET | `/api/superadmin/tenants` | superadmin | todos, con filtro por estado |
| PATCH | `/api/superadmin/tenants/:id/suspender` | superadmin | queda logueado en superadmin_logs |
| DELETE | `/api/superadmin/tenants/:id` | superadmin | soft delete + respaldo JSON automático |
| PATCH | `/api/superadmin/usuarios/:id/reset-password` | superadmin | queda logueado |
| GET/PUT | `/api/superadmin/tenants/:id/sii` | superadmin | |
| GET/POST | `/api/superadmin/planes` | superadmin | listar (incluye inactivos) / crear |
| PATCH/DELETE | `/api/superadmin/planes/:id` | superadmin | editar precio; DELETE desactiva, no borra |
| GET/POST | `/api/superadmin/complementos` | superadmin | mismo patrón que planes |
| PATCH | `/api/superadmin/complementos/:id` | superadmin | acá se ajusta el precio del SII cuando cambie |
| POST | `/api/superadmin/tenants/:id/limpiar-datos-prueba` | superadmin | ver detalle abajo |

### `/limpiar-datos-prueba` — convertir un trial en cuenta real

**No borra el tenant.** Deja intacta toda la configuración (usuarios,
servicios, sucursales, comisiones, horarios, pasarela de pago, slug) y
solo vacía lo transaccional que el cliente generó probando.

**Orden de borrado (obligatorio, por integridad referencial):**
1. `liquidaciones` (cascada automática a `liquidacion_detalle`)
2. `citas` (cascada a `cita_servicios`)
3. `ventas` (cascada a `venta_items` — recién ahora es seguro, porque
   `liquidacion_detalle` ya no apunta a ningún `venta_item`)
4. `clientes`
5. `gastos`
6. `otros_ingresos`

Si se borra `ventas` antes que `liquidaciones`, MySQL rechaza la operación
por la FK de `liquidacion_detalle.venta_item_id` (no tiene `ON DELETE
CASCADE` a propósito, para que nunca se pueda perder el rastro de un pago
ya liquidado sin querer).

**Además:**
- Genera respaldo JSON automático antes de borrar (mismo patrón export/
  import que ya usaste en SetecREST)
- Resetea `tenant_config_sii.ultimo_folio_boleta` a 0, si el cliente
  emitió boletas de prueba en modo certificación
- `UPDATE tenants SET fecha_conversion_real = CURDATE()`
- Queda registrado en `superadmin_logs` con `accion = 'limpiar_datos_prueba'`
  y el detalle del respaldo generado

---

## Nota sobre `/api/citas/disponibilidad`

Es el endpoint más importante de todo el módulo de citas — el que usa el
admin cuando atiende la llamada. Lógica:

1. Traer `horarios_barbero` del día de semana pedido
2. Restar bloques de `excepciones_horario` de esa fecha exacta
3. Restar bloques ya ocupados en `citas` (estado != 'cancelada') ese día
4. Cortar el resultado en slots según `servicios.duracion_min` del servicio pedido

Este cálculo es el mismo tanto para la vista interna como para el link
público — un solo endpoint, dos consumidores.
