# Especificación: App móvil de cliente — Clínica Egurrola

**Para quién es este archivo:** otra IA (o un desarrollador) que debe **armar la app móvil** y, si hace falta, **APIs nuevas en Laravel** sin romper el sistema actual (SPA Vue 3 + Laravel 9).

**Regla de oro:** extrae lógica a servicios y **añade rutas nuevas**. No cambies firmas ni comportamiento de las rutas que ya usa el SPA.

Producción actual del SPA:

- API: `https://spa.clinicaegurrola.com.mx/api`
- Sitio: `https://spa.clinicaegurrola.com.mx`
- Imágenes: `{APP_URL}/storage/...` (el path en BD viene como `public/images/...`; sustituir `public` por `storage`).

Repo Laravel: `cork-vue3-laravel`. Auth: Laravel Sanctum, header `Authorization: Bearer {token}`.

---

## 0. Cómo usar este documento

1. Implementar primero un **prefijo paralelo** `/api/app/v1/*` (o reutilizar endpoints existentes **solo de lectura / ya usados por el cliente**).
2. La app **nunca** debe llamar endpoints de staff (`/ventas/create` de mostrador, `/ventas/page`, `/clientes/all`, fusionar, inventario, cortes, etc.).
3. Copiar **todas** las reglas de negocio de las secciones 2–10. Si una regla parece “rara”, **igual se respeta**: es lo que hace hoy el sistema.
4. Frontend móvil: Flutter / React Native / Kotlin / Swift — da igual. El contrato es JSON + multipart.
5. Idioma UI: español (México).

### Qué NO hacer

- No modificar `POST /api/tokens/create`, `POST /api/tiendas/pagar`, `POST /api/sesiones/create-cliente` salvo extraer código interno a un Service y dejar el controlador SPA llamando al mismo Service.
- No exigir campos nuevos en esas rutas viejas (rompería el Vue).
- No usar cookies/CSRF; la app usa Bearer token (Sanctum personal access token). CORS ya permite `api/*`.

---

## 1. Arquitectura recomendada (sin afectar el SPA)

```
/api/...                    ← existente, lo usa Vue. NO tocar contratos.
/api/app/v1/...             ← NUEVO, solo app. Middleware auth:sanctum + rol Cliente.
```

Las rutas nuevas deben:

- Comprobar `user->hasRole('Cliente')` y `user->cliente_id`.
- Devolver `{ data, message? }` coherente.
- Usar URLs **absolutas** de imágenes/comprobantes.
- Tener throttle propio (el grupo `api` actual limita 60 req/min y tumba el POS si la app hace polling).

### Bootstrap (recomendado, nuevo)

`GET /api/app/v1/bootstrap`

Devuelve de un golpe lo que el SPA pide en 4–5 llamadas:

- `user` (id, name, email, telefono, cliente_id, tipo_cliente, autorizado, estatus)
- `cliente` (id, razonSocial, rfc, celular, email, tipo, estatus)
- `roles`, `tipo_cliente`
- `config`: `exige_inventario_ventas`, `dias_max_cancelacion_venta_linea_tras_pago`, `dias_habiles_max_devolucion_aviso`
- `sucursales` activas
- `cuenta_transferencia_principal` (banco, titular, clabe, cuenta)
- `capacidades` (ver más abajo)

Mientras no exista, la app puede armarlo con los endpoints actuales: `GET /users` + `GET /tokens/permissions` + `GET /config-empresa` + `GET /sucursales/all` + `GET /opciones-pago-transferencia/principal`.

---

## 2. Dos tipos de cliente (OBLIGATORIO)

Hay **exactamente dos** tipos. Toda la navegación de la app depende de esto.

| Valor `users.tipo_cliente` | Alias | Qué es |
|---|---|---|
| `cliente_linea` | En línea | Se registra solo en `/auth/register`. Cuenta **activa de inmediato**. Tienda sí, **citas NO**. |
| `cliente_local` | Local | Lo crea el **staff**. Puede tienda **y** agendar cita con comprobante. |

Regla PHP (`app/Helpers/ClienteHelper.php`):

- Es local si rol `Cliente` **y** (`users.tipo_cliente === 'cliente_local'` **o** `clientes.tipo` (case-insensitive) === `cliente_local`).
- `clienteLocalPuedeAgendarCitaConPago` = solo local.
- `clienteDebeOcultarPreciosTratamientos` = Cliente que **no** es local (la app de linea **no muestra precios de tratamientos**; de todos modos linea no agenda citas).

### Mapa de pantallas

| Pantalla | `cliente_linea` | `cliente_local` |
|---|---|---|
| Login / registro / recuperar contraseña | Sí | Sí (registro público siempre crea linea) |
| Catálogo / tienda | Sí | Sí |
| Carrito / pagar / mis compras / cancelar pedido | Sí | Sí |
| Perfil, direcciones, datos fiscales | Sí | Sí |
| Agendar cita + mis citas | **NO** (ocultar menú; si llama API → 403) | **Sí** |
| Monitor de sala (público) | Opcional | Opcional |
| POS / inventario / staff | Nunca | Nunca |

Tras login, si es `cliente_linea` el SPA redirige a `/productos/presentacion`. La app debe abrir **Tienda**, no un dashboard de citas.

---

## 3. Autenticación

### 3.1 Login — `POST /api/tokens/create` (existente, público)

Body JSON:

```json
{ "email": "correo@dominio.com", "password": "secret" }
```

Validación: `email` required + exists `users.email`; `password` required.

Login interno (`Auth::once`) **además exige**:

- `users.estatus = 'activo'`
- `users.autorizado = 'si'`

Si el password es correcto pero no está autorizado o está inactivo → **falla igual** (422, `errors.email` = credenciales incorrectas).

Respuesta 200:

```json
{ "data": "ID|plaintexttoken" }
```

Guardar el string completo. Enviar en todas las peticiones autenticadas:

```
Authorization: Bearer ID|plaintexttoken
```

El token **no caduca** hoy. Nombre del token = `user.name`.

### 3.2 Usuario actual — `GET /api/users` (Sanctum)

**Ojo:** responde el User **en la raíz**, no dentro de `{ data }`.

Carga: `roles`, `cliente`, `tiendas` (carrito), `cliente.direcciones` (solo principal `es_principal=si`), `cliente.fiscales` (solo principal).

Campos clave: `id`, `name`, `email`, `telefono`, `cliente_id`, `tipo_cliente`, `autorizado`, `estatus`.

### 3.3 Permisos — `GET /api/tokens/permissions` (Sanctum)

```json
{
  "roles": ["Cliente"],
  "permissions": ["...nombres Spatie..."],
  "tipo_cliente": "cliente_linea"
}
```

La app de cliente **no necesita** el array de permissions de Spatie para menú (el staff sí). Basta `roles[0] === "Cliente"` + `tipo_cliente`.

### 3.4 Logout — `DELETE /api/tokens/{idToken}` (Sanctum)

El `{idToken}` es la parte **antes** del `|` del token (`"12|abc..."` → id `12`). Pone `expires_at = now()`.

### 3.5 Registro público — `POST /api/registro` (público)

El formulario web **siempre** manda `tipo_cliente: "cliente_linea"`. La app de cliente debe hacer lo mismo.

Body:

```json
{
  "name": "Ana",
  "paternal_surname": "López",
  "maternal_surname": "Ruiz",
  "telefono": "4421234567",
  "tipo_cliente": "cliente_linea",
  "email": "ana@mail.com",
  "password": "********",
  "password_confirmation": "********",
  "nota": ""
}
```

Validación:

- name, paternal_surname, maternal_surname: required string max 255
- telefono: max 45, **no required**
- tipo_cliente: required
- email: required, email, unique `users.email`
- password: required, confirmed, Laravel `Password::defaults()` (mín. 8)

Efectos si `tipo_cliente == cliente_linea`:

- Crea `clientes`: rfc `''`, razonSocial = nombre completo, celular = telefono, email, observacion = nota, tipo = `cliente_linea`
- Crea `users`: name completo, email, telefono, password hash, **autorizado = 'si'**, cliente_id, tipo_cliente
- Rol Spatie `Cliente`
- Correo al usuario `AutoAvisoNuevoClienteLocal` (asunto “Registro Completado”)
- Correo al admin `AvisoNuevoCliente`
- Si el mail falla, **el registro igual se guarda**

Si alguien manda otro `tipo_cliente` (no hacer esto en la app): User con `autorizado=no`, **sin** Cliente, no puede loguear hasta que staff autorice.

Mensaje UI linea: cuenta **activada**. Login inmediato.

### 3.6 Recuperar contraseña (público)

1. `POST /api/tokens/pw` `{ "email": "..." }` — debe existir. Genera `users.uuid` y manda correo con link **web**:
   `{APP_URL}/auth/changepw/{uuid}`
2. `POST /api/tokens/getpwuuid` `{ "uuid": "..." }`
3. `POST /api/tokens/savepw` `{ "uuid", "password", "password_confirmation" }` — password confirmed + defaults; limpia uuid.

La app puede abrir el link en WebView o pedir uuid si implementan deep link **nuevo** (`egurrola://reset?uuid=`). **No quitar** el correo web.

### 3.7 Cambiar contraseña logueado

SPA usa `PUT /api/users/{userId}/password` con `{ password, password_confirmation }`.

**Hueco de seguridad actual:** no comprueba que `{userId}` sea el autenticado. La app **debe enviar siempre su propio id**. API nueva recomendada: `PUT /api/app/v1/password` sin id en la URL.

`PUT /api/users/perfil` solo cambia `name` y `email` del autenticado. El perfil Vue **no lo usa**; edita con `PUT /users/{id}/edit` (pide también `tipo_cliente` y `role` — frágil). Para la app, preferir endpoint nuevo de perfil (ver §8) sin romper el viejo.

---

## 4. Tienda (catálogo)

Catálogo **público** (se puede ver sin login). Agregar al carrito **requiere token**.

### 4.1 Listar productos — `POST /api/productos/page?page=1&perPage=12` (público)

Query: `page`, `perPage` (default 15, min 1).  
Body JSON:

```json
{
  "search": "",
  "tipo": null,
  "tratamiento": null,
  "marca": null,
  "estatus": "activo",
  "sucursal_id": null,
  "solo_con_existencia": true
}
```

- `tipo` = `tipo_producto_id`
- `tratamiento` = `tipo_servicio_id`
- `marca` = `marca_id`
- `estatus`: `activo` | `inactivo` | `todos` (cliente: siempre `activo`)
- `sucursal_id`: si vacío, el backend usa la **primera sucursal activa por id**
- `solo_con_existencia`: si `ConfigEmpresa.exige_inventario_ventas` es true, la app **debe** mandar `true` (el SPA lo hace). Oculta stock ≤ 0.

Respuesta: paginator Laravel (`data.data`, `data.current_page`, `data.last_page`, `data.total`) más `exige_inventario_ventas`.

Cada producto incluye: id, nombre, descripcion, precio (`total` = precio), iva, ieps, `path` imagen, marca, tipo_producto, tipo_tratamiento, `cantidad_existente`.

URL imagen:

```
{APP_URL}/ + path.replace("public", "storage")
```

Ejemplo path `public/images/productos/xyz.jpg` → `https://spa.clinicaegurrola.com.mx/storage/images/productos/xyz.jpg`

Search filtra: id, nombre, descripcion, precio, marca.

Lector de barras (SPA): 8–14 dígitos en search.

### 4.2 Filtros públicos

- `POST /api/marcas/all` → marcas `estatus=activo`
- `POST /api/tipos-productos/all` → tipos producto activos
- `POST /api/tipos-servicios/all` → tipos de servicio/tratamiento activos (filtro “tratamiento” del catálogo)

### 4.3 Cantidad al agregar

- Si `exige_inventario_ventas`: máximo = existencia (tope interno SPA 1000).
- Si no exige: tope 1000.

### 4.4 Config inventario

`GET /api/config-empresa` (Sanctum) → `exige_inventario_ventas` (bool). También viene en la paginación de productos.

---

## 5. Carrito y crear compra

Modelo carrito: tabla `tiendas` (`user_id`, `producto_id`, `cantidad`). **No es del cliente_id**, es del user.

### 5.1 Endpoints (Sanctum)

| Método | Ruta | Qué hace |
|---|---|---|
| GET | `/tiendas/count` | Número de líneas del user |
| GET | `/tiendas/all` | Líneas con nombre, precio, total, existencia + `datasum` + `exige_inventario_ventas` |
| POST | `/tiendas/create` | `{ producto_id, user_id, cantidad }` — si ya existe la línea, **suma** cantidad |
| PUT | `/tiendas/{id}/edit` | Reemplaza cantidad |
| DELETE | `/tiendas/{id}` | Quita línea |
| GET | `/tiendas/delete-by-user` | Vacía carrito del user autenticado |
| POST | `/tiendas/pagar` | Checkout multipart |
| POST | `/tiendas/{venta}/comprobante` | Subir/reemplazar comprobante |

`user_id` en create: el del usuario logueado (`GET /users`.id).

Stock al agregar/editar/pagar: solo si `exige_inventario_ventas`. Sucursal de stock = **primera sucursal activa** (`orderBy id` en listado; en pagar es `first()` sin order — mismo espíritu: una sucursal “default”).

### 5.2 Checkout — crear compra `POST /api/tiendas/pagar`

`Content-Type: multipart/form-data`

Campos (lo que manda el SPA):

| Campo | Obligatorio | Notas |
|---|---|---|
| `cliente_id` | Sí en la práctica (Vue lo manda) | Debe ser `user.cliente_id`. El backend **hoy no valida** que coincida ni que el carrito no esté vacío. |
| `direccion_id` | Sí | exists `direcciones_entrega` |
| `datos_fiscales_id` | No | exists `datos_fiscales` |
| `opcion_pago_transferencia_id` | Sí | Vue manda el **id de `opciones_pago_transferencia`**. El validador actual hace `exists:tipos_pagos,id` (inconsistencia histórica). **API nueva debe validar `exists:opciones_pago_transferencia,id`** sin cambiar la ruta vieja. |
| `comprobante_pago` | No | file pdf,jpg,jpeg,png máx **2048 KB** |

UI Vue: el botón Pagar se habilita solo con dirección + opción de transferencia. Solo rol Cliente.

Flujo backend al pagar (NO inventar otro):

1. Sucursal = primera activa.
2. Tipo de pago de la venta: siempre el `tipos_pagos` cuya `descripcion = 'Transferencias'` (no el id del formulario).
3. Si exige inventario: cada línea vs stock; 422 `errors.tienda`.
4. `total` = suma `cantidad * producto.precio`. `iva` venta = 0. `subtotal` = total.
5. Crea `Venta`:
   - `es_pago_tienda = 'no'` (pendiente de verificar por staff; **el Vue manda 'si' y el backend lo ignora**)
   - `es_tienda_online = 'si'`
   - `direccion_id`, `opcion_pago_transferencia_id`, `comprobante_pago` path o null
   - `datos_fiscales_id` si viene
   - `cancelado` default BD (`no`)
6. Crea **un** `Pago` (tipo Transferencias, pago=total, cambio 0).
7. Un `VentaDetalle` por línea (`tratamiento_id` y `vale_id` null, `es_pago_tienda='no'`).
8. **Borra todo el carrito** de ese user.
9. **No descuenta inventario** aquí. El stock se mueve cuando staff hace `PUT /ventas/{id}/autorizar` con `es_pago_tienda=si` y pone `fecha_pago_verificado`.
10. Mails admin+cliente **solo si hubo archivo comprobante**.
11. Respuesta actual: `{ "data": [] }` — la app debería luego ir a mis compras. API nueva debería devolver `{ data: { id: ventaId } }`.

Pantalla post-compra: `/ventas/mis-compras`.

Subir comprobante después: `POST /tiendas/{ventaId}/comprobante` file required mismos mimes.

### 5.3 Datos para el checkout (antes de pagar)

1. `GET /users` → precargar dirección principal y fiscales principales + `cliente_id`.
2. `GET /opciones-pago-transferencia/principal` → cuenta a mostrar (banco, titular, clabe, cuenta) y su `id`.
3. Listar direcciones: `GET /direcciones-entrega/for-select/{clienteId}` (solo activas).
4. Listar fiscales: `GET /datos-fiscales/for-select/{clienteId}`.
5. CRUD direcciones/fiscales: ver §8 (el cliente **debe poder crear** dirección antes de pagar).

---

## 6. Mis compras y cancelar pedido

### 6.1 Listar — `POST /api/ventas/mis-compras` (Sanctum)

Filtra `ventas.estatus=activo`, `cliente_id = user.cliente_id`. Trae direccion, datosFiscales, envio.

Flags importantes por venta:

- `cancelado`: `si` | `no`
- `es_tienda_online`: debe ser `si` para cancelar
- `es_pago_tienda`: `si` = pago **ya verificado por administración**
- `es_cancelacion_cliente`: `si` = ya hay solicitud de cancelación
- `comprobante_pago` / `comprobante_pago_path`
- `puede_cancelar_cliente`: `si` | `no`
- `motivo_no_cancelacion_cliente`: texto para UI
- `dias_max_cancelacion_venta_linea_tras_pago` y `dias_habiles_max_devolucion_aviso`: **informativos** en pantalla de devoluciones. **No se usan** en el if de cancelación (hoy).

Estados de UI sugeridos:

| Condición | Etiqueta |
|---|---|
| `cancelado=si` | Cancelado |
| `es_cancelacion_cliente=si` y `cancelado=no` | Solicitud de cancelación en revisión |
| `es_pago_tienda=si` | Pago verificado |
| `es_pago_tienda=no` | Pendiente de verificar pago |
| hay `envio` | Enviado |

### 6.2 Cancelar — `POST /api/ventas/{venta}/cancelar-linea`

Body: `{ "observacion_cancelado": "opcional, max 1000" }`

403 si `user.cliente_id !== venta.cliente_id`.

#### Reglas **en este orden** (`resolverPuedeCancelarLineaCliente`)

1. Ya `cancelado=si` → no. “La venta ya está cancelada.”
2. Ya `es_cancelacion_cliente=si` → no. “Ya registramos su solicitud…”
3. `es_tienda_online !== 'si'` → no. Solo compras de tienda en línea (no ventas de mostrador).
4. Existe fila en `ventas_cortes` → no. Ya está en corte de caja.
5. Pago verificado (`es_pago_tienda` trim/lower === `si`) **Y** existe registro en `envios` → no. “El pago ya fue verificado y se registró el envío.”
6. Si **solo** está verificado **o** solo hay envío (no ambos) → **sí puede**.
7. Los días de ConfigEmpresa **no bloquean** (código actual).

Si puede:

**A) Sin comprobante** (`comprobante_pago` vacío):

- `cancelado = si` + observación
- Correos admin y cliente (cancelación directa)
- Resultado: cancelada de verdad

**B) Con comprobante:**

- **No** pone `cancelado=si`
- `es_cancelacion_cliente = si`, `fecha_cancelacion_cliente = now()`
- Correo solo a admin (solicitud de revisión)
- El cliente espera; staff confirma después con otro flujo

UI: si `puede_cancelar_cliente === 'no'`, mostrar `motivo_no_cancelacion_cliente`. No reintentar.

---

## 7. Citas (solo `cliente_local`)

`cliente_linea` que llame estas APIs recibe 403. Ocultar el módulo.

### 7.1 Sucursales — `GET /api/sucursales/all`

Solo sucursales `estatus=activo`.

### 7.2 Horarios del día — `POST /api/horarios/disponibles`

Body: `{ "sucursal_id": 1, "fecha": "2026-08-17" }` (`fecha` date required).

Slots fijos **08:00–19:00**. Respeta `hora_entrada`/`hora_salida` de `horarios` para `DAYOFWEEK(fecha)` y esa sucursal. Terapistas rol 2 activos **excepto id 36**. Devuelve `{ id: "HH:MM:SS", name: "h AM/PM" }`.

### 7.3 Tratamientos para armar la cita — `POST /api/tratamientos/all`

Si el user no es local, el backend **anula precios**. El local sí los ve.

El SPA manda los tratamientos elegidos como `tratamientos_vistas` (array JSON de objetos con `id`).

### 7.4 Agendar — `POST /api/sesiones/create-cliente` (multipart)

Validación:

- `sucursal_id` required exists sucursales
- `cliente_id` required exists clientes **y debe ser el del user**
- `duracion` required (el SPA la calcula según tratamientos)
- `fecha_asignada` required date (datetime del slot)
- `opcion_pago_transferencia_id` required exists **`opciones_pago_transferencia`**
- `comprobante_pago` **required** file pdf/jpg/jpeg/png max 2048 KB
- `tratamientos_vistas`: JSON string o array; cada ítem `{ "id": tratamientoId }`
- opcionales: `observaciones`, `observacion_cliente`, `preferencia_terepista`, `celular` (si viene, actualiza `clientes.celular`)

Más reglas:

- Solo `ClienteHelper::clienteLocalPuedeAgendarCitaConPago`
- Horario debe caer dentro del de la sucursal ese día; si no → 422 “Solo se permite un horario válido…”
- `terapista_id` se guarda **fijo = 36** (“no asignada”). La app **no** deja elegir terapista.
- `aprobado = 'NO'`, `es_pago_cita = 'no'`, asistencia NO, vale NO
- Comprobante se guarda en `public/images/productos`
- Mail `AvisoNuevaCita`

La cita **no está confirmada** hasta que staff ejecuta `PUT /sesiones/{id}/autorizar-pago-cita` con `es_pago_cita=si` → entonces `aprobado=SI` y `fecha_pago_cita_verificado`.

### 7.5 Calendario SPA (opcional en app)

- `POST /sesiones/all` — el cliente solo ve **sus** citas; días pasados “No disponible”
- `POST /sesiones/fecha`
- `GET /sesiones/{id}` — precios de tratamientos ocultos si no es local

### 7.6 Mis citas (ya existe para móvil, Vue no lo usa)

Requieren rol Cliente + `cliente_id`. Solo sus sesiones, cliente activo.

- `GET /api/sesiones/mis-citas/resumen?sucursal_id=`
- `POST /api/sesiones/mis-citas`
  ```json
  { "tipo": "asistidas|no_asistidas|pendientes|todas", "page": 1, "per_page": 20, "sucursal_id": null }
  ```
  `per_page` máx 50.
- `GET /api/sesiones/mis-citas/{sesion}` — 403 si no es suya

Estados (`estado_cliente`):

| Código | Condición | Label |
|---|---|---|
| `asistida` | asistencia=SI **o** asistencia_cumplida=SI | Asistida |
| `no_asistida` | fecha_asignada &lt; now y ambas asistencia NO | No asistida |
| `pendiente_aprobacion` | aprobado=NO | Pendiente de aprobación |
| `pendiente` | fecha ≥ now y aprobado=SI | Cita programada |
| `en_proceso` | resto | (en proceso) |

El filtro `pendientes` incluye: pendiente + pendiente_aprobacion + en_proceso.

El cliente **no** autoriza pagos de cita. No hay cancelar-cita en el código actual: **no inventar cancelación de cita** salvo que se pida como feature nueva aparte.

### 7.7 Monitor (público, opcional)

`GET /api/sesiones/monitor?sucursal_id=&fecha=` sin login. Solo citas `aprobado=SI` de **hoy**. No es flujo del cliente para agendar.

---

## 8. Perfil, direcciones, datos fiscales

### 8.1 Ver perfil

Combinar `GET /users` + su `cliente`. Mostrar nombre, email, teléfono, tipo_cliente (label “En línea” / “Local”), direcciones, fiscales.

### 8.2 Editar perfil (recomendado nuevo)

Hoy el SPA usa `PUT /users/{id}/edit` con campos de staff (`role`, `tipo_cliente`). **No uses eso desde la app.**

Crear `PUT /api/app/v1/perfil`:

- name, email, telefono
- opcional: sincronizar `clientes.razonSocial`, `clientes.celular`, `clientes.email` del `cliente_id` del user

Sin cambiar `tipo_cliente` ni `autorizado` ni rol.

Cambiar password: `PUT /api/app/v1/password` `{ password, password_confirmation, password_actual? }`.

### 8.3 Direcciones — `/api/direcciones-entrega`

Hoy **no hay policy de dueño** (cualquier autenticado podría tocar cualquier cliente_id). La app **solo** debe mandar su `cliente_id`.

**POST /create:**

- required: `cliente_id`, `calle`, `numero_exterior`, `colonia`, `codigo_postal`, `ciudad`, `estado`, `es_principal` (`si`|`no`)
- nullable: `nombre`, `numero_interior`, `referencia`
- `pais` sometimes
- Si `es_principal=si`, el backend desmarca las otras del mismo cliente
- 201

**PUT /{id}/edit:** same fields sometimes + `estatus` activo|inactivo

**DELETE /{id}:** pasa a inactivo. **400** si es la **única** dirección principal activa.

**GET /all/{clienteId}** todas (principal primero).  
**GET /for-select/{clienteId}** solo activas.

Al menos una dirección principal hace falta para pagar.

### 8.4 Datos fiscales — `/api/datos-fiscales`

**POST /create** multipart:

- `razon_social` required
- `rfc` max 13
- `correo` email
- `uso_cfdi`, `regimen_fiscal`, `metodo_pago` max 10
- **`documento_fiscal_file` required PDF**
- `es_principal` si|no
- Path: `public/documentos/documentos_fiscales`
- Principal único por cliente

**POST /{id}/edit:** PDF opcional (si llega, reemplaza y borra el anterior).

**DELETE:** deja `estatus=inactivo`. 400 si es el único principal activo.

Append `documento_fiscal_path` para abrir el PDF.

Fiscales son **opcionales** en el checkout.

### 8.5 Cuenta para transferir

`GET /api/opciones-pago-transferencia/principal`  
`GET /api/opciones-pago-transferencia/for-select` (activas)

Campos: `id`, `banco`, `titular`, `clabe`, `cuenta`, `es_principal`, `estatus`.

El cliente **no** da de alta cuentas; las carga administración.

---

## 9. Configuración que afecta al cliente

`GET /api/config-empresa` (singleton).

| Campo | Efecto real |
|---|---|
| `exige_inventario_ventas` | Catálogo con stock, tope de cantidad, rechazo al pagar si no hay existencia |
| `dias_max_cancelacion_venta_linea_tras_pago` | Se **muestra** en mis compras; **no** bloquea cancelar (código actual) |
| `dias_habiles_max_devolucion_aviso` | Texto de devoluciones manuales |

No hay endpoint de “versión mínima de app”: se puede añadir `GET /api/app/v1/status` `{ min_version, maintenance, message }` sin tocar el SPA.

---

## 10. Correos que el sistema ya dispara (no reimplementar en la app)

| Evento | Mails |
|---|---|
| Registro linea | Cliente “Registro Completado” + aviso admin |
| Recuperar password | Link web changepw |
| Compra con comprobante | Admin + cliente |
| Cancelación sin comprobante | Admin + cliente |
| Solicitud cancelación con comprobante | Solo admin |
| Nueva cita (create-cliente) | Aviso nueva cita |
| Staff verifica pago cita | Aviso activación cita |
| Staff verifica pago tienda | (flujo staff, no app) |

La app no envía correos; solo dispara las APIs.

---

## 11. Errores y códigos HTTP

Usar los mismos que Laravel:

- 401: token ausente/inválido
- 403: no es dueño / no es cliente_local para citas
- 422: `{ "message": "...", "errors": { "campo": ["texto"] } }` — mostrar el primer mensaje de `errors`
- 429: throttle 60/min del grupo `api` — no spamear ping
- 500: no reintentar crear compra a ciegas (riesgo de duplicar); primero consultar mis-compras

Multipart: no mandar `Content-Type: application/json` al subir archivos.

---

## 12. Inventario de endpoints para la app

### Usables hoy (existentes)

Públicos: `tokens/create`, `tokens/pw`, `tokens/savepw`, `tokens/getpwuuid`, `registro`, `productos/page`, `marcas/all`, `tipos-productos/all`, `tipos-servicios/all`, `sesiones/monitor`.

Sanctum cliente: `users` GET, `tokens/permissions`, `tokens/{id}` DELETE, `users/{id}/password`, carrito `tiendas/*`, `tiendas/pagar`, `tiendas/{venta}/comprobante`, `ventas/mis-compras`, `ventas/{id}`, `ventas/{id}/cancelar-linea`, sucursales, horarios/disponibles, `sesiones/create-cliente`, `sesiones/all`, `sesiones/fecha`, `sesiones/{id}`, `sesiones/mis-citas*`, tratamientos/all, config-empresa, direcciones-entrega/*, datos-fiscales/*, opciones-pago-transferencia/principal y for-select.

### Nuevos recomendados (paralelos, no rompen Vue)

```
GET    /api/app/v1/bootstrap
GET    /api/app/v1/status
PUT    /api/app/v1/perfil
PUT    /api/app/v1/password
POST   /api/app/v1/compras                 // wrapper de tiendas/pagar que valida dueño, carrito no vacío, opciones_pago_transferencia, y responde venta.id
GET    /api/app/v1/compras/{id}/comprobante-publico  // opcional: imagen/PDF del ticket generado en servidor (el SPA usa html2canvas)
POST   /api/app/v1/dispositivos            // push FCM (opcional fase 2)
```

Al implementar wrappers, **llamar los mismos servicios** (o el mismo código interno) que `TiendaController::pagar` y `SesionController::storeCliente`.

### Prohibidos para la app de cliente

Cualquier cosa de: ventas/create (mostrador), ventas/page, ventas/autorizar, clientes/fusionar, inventarios, cortes, users/all, print-jobs, config-impresiones, recálculo existencias, impersonar otros cliente_id.

---

## 13. Modelo mental de “compra en línea”

```
[Catálogo] → [Carrito] → [Dirección + cuenta CLABE + comprobante opcional]
    → POST tiendas/pagar
    → Venta es_tienda_online=si, es_pago_tienda=no  (pendiente)
    → Staff verifica → es_pago_tienda=si + fecha_pago_verificado + descuenta stock
    → Staff arma envío → tabla envios
    → Cliente puede cancelar según §6.2
```

La app **no** marca el pago como verificado.

---

## 14. Criterios de aceptación de la app

- Login rechaza no autorizados igual que la web.
- Registro linea entra de inmediato a Tienda, no a Citas.
- Linea no ve módulo Citas; Local sí, con comprobante obligatorio y terapista no elegible.
- Sin token se puede ver catálogo, no comprar.
- Pagar exige dirección + cuenta de transferencia; fiscales opcionales.
- Tras pagar, el carrito queda vacío y la compra aparece en mis compras como no verificada.
- Cancelar con comprobante deja “en revisión”; sin comprobante cancela ya.
- Imágenes y PDFs se abren con URL absoluta `/storage/...`.
- Cero llamadas a APIs de mostrador.

---

## 15. Archivos del repo a copiar comportamiento (fuente de verdad)

| Tema | Archivo |
|---|---|
| Rutas | `routes/api.php` |
| Tipos de cliente | `app/Helpers/ClienteHelper.php` |
| Login / pw | `app/Http/Controllers/TokenController.php` |
| Registro | `app/Http/Controllers/UserController.php` `registro()` |
| Catálogo | `app/Http/Controllers/ProductoController.php` `paginacion()` |
| Carrito / pagar | `app/Http/Controllers/TiendaController.php` |
| Mis compras / cancelar | `app/Http/Controllers/VentaController.php` `misVentas`, `cancelarLineaCliente`, `resolverPuedeCancelarLineaCliente` |
| Citas cliente | `app/Http/Controllers/SesionController.php` `storeCliente` |
| Portal citas | `app/Http/Controllers/Api/ClientePortalSesionController.php`, `app/Services/ClienteSesionPortalService.php` |
| Menú Vue (qué ve cada tipo) | `resources/js/src/components/layout/header.vue`, `resources/js/src/router/index.js` |
| Checkout UI | `resources/js/src/views/carrito/index.vue` |
| Tienda UI | `resources/js/src/views/productos/presentacion.vue` |
| Citas UI | `resources/js/src/views/citas/reservar.vue` |

Si el código y este documento discrepan, **gana el código PHP**.
