# Administración web de prestadores

## Alcance y autoridad

El panel de `web_mycode` permite a administradores autorizados listar,
inspeccionar, moderar y verificar prestadores, revisar documentos protegidos y
mantener los catálogos de servicios. No publica un directorio, mapa, locales ni
puntos para usuarios finales.

Laravel actúa como BFF. La interfaz decide qué navegación y botones mostrar,
pero `api_mycode` sigue siendo la autoridad para permisos, transiciones de
estado, verificación y consistencia de catálogos. Ocultar una acción en la UI no
reemplaza la autorización del API.

## Sesión y rutas web

El login usa `POST /admin/session/login`. El JWT recibido desde `api_mycode` se
guarda sólo en la sesión Laravel; no se entrega al HTML, JSON o JavaScript ni se
guarda en `localStorage` o `sessionStorage`. `admin.web` protege todas las
pantallas excepto el login y `admin.bridge` reemplaza cualquier
`Authorization` del navegador por el token de la sesión del servidor.

Con el panel bajo el sitio principal:

| Método | Ruta | Pantalla |
| --- | --- | --- |
| GET | `/admin/service-providers` | Lista y filtros |
| GET | `/admin/service-providers/{uuid}` | Detalle, historial y moderación |
| GET | `/admin/service-provider-verifications/{uuid}` | Documento y revisión |
| GET | `/admin/service-catalogs` | Catálogos administrativos |
| GET | `/admin/service-location-reports` | Bandeja de reportes y filtros |
| GET | `/admin/service-location-reports/{uuid}` | Detalle y resolución de un reporte |
| GET | `/admin/service-location-report-appeals` | Bandeja de apelaciones |
| GET | `/admin/service-location-report-appeals/{uuid}` | Detalle y resolución de una apelación |

Si `ADMIN_PANEL_DOMAIN` está configurado y coincide con el host, las mismas
pantallas se sirven sin el prefijo `/admin`: `/service-providers`,
`/service-provider-verifications/{uuid}` y `/service-catalogs`.

## Permisos

| Permiso | Capacidades |
| --- | --- |
| `view_service_providers` | Ver lista, filtros y detalle de prestadores |
| `review_service_providers` | Aprobar/rechazar prestadores, iniciar/revisar reportes y resolver reportes o apelaciones no suspensivas |
| `suspend_service_providers` | Suspender/restaurar y aplicar o revertir una suspensión mediante resolución/apelación |
| `verify_service_providers` | Cambiar nivel, abrir/revisar verificaciones y documentos |
| `manage_service_provider_catalogs` | Leer y mutar categorías, servicios, especialidades y características |

Un `401` en el bridge significa sesión ausente o vencida y activa el flujo
actual de cierre de sesión. Un `403` significa que el API rechazó la operación;
la pantalla conserva el contexto y muestra el error de permiso.

## Allowlist del BFF

El navegador sólo llama rutas del mismo origen bajo
`/api/bridge/admin/{path}`. El registro acepta exactamente:

| Método | `path` permitido | Destino upstream |
| --- | --- | --- |
| GET | `service-providers` | `/api/v1/admin/service-providers` |
| GET | `service-providers/{uuid}` | `/api/v1/admin/service-providers/{uuid}` |
| POST | `service-providers/{uuid}/{approve\|reject\|suspend\|restore\|verify}` | Igual bajo `/api/v1/admin/` |
| GET | `service-provider-verifications/{uuid}` | Detalle administrativo directo |
| POST | `service-provider-verifications/{uuid}/review` | Revisión administrativa |
| GET | `service-provider-verifications/{uuid}/document` | Documento protegido |
| GET, POST | `service-catalogs/{categories\|services\|specialties\|features}` | Lista administrativa o creación |
| PUT, DELETE | `service-catalogs/{tipo}/{uuid}` | Actualización o desactivación |
| GET | `public/service-catalogs/{tipo}` | `/api/v1/public/service-catalogs/{tipo}` |
| GET | `service-location-reports[/{uuid}]` | Lista o detalle administrativo de reportes |
| POST | `service-location-reports/{uuid}/start-review` | Inicia revisión |
| POST | `service-location-reports/{uuid}/resolve` | Emite resolución |
| GET | `service-location-report-appeals[/{uuid}]` | Lista o detalle administrativo de apelaciones |
| POST | `service-location-report-appeals/{uuid}/resolve` | Acepta o rechaza una apelación |

La lectura `public/service-catalogs/*` se mantiene para compatibilidad con
formularios privados. La pantalla administrativa usa la lectura
`service-catalogs/*`, porque necesita ver registros activos e inactivos.

Las rutas heredadas de usuarios, estadísticas y errores de aplicación
conservan su validación previa. Cualquier método, catálogo, UUID o subruta no
incluido en la allowlist responde `404` sin contactar al API. Los payloads se
validan en el BFF antes de enviarse; los errores `409` y `422` del API se
conservan para que el formulario no pierda sus valores.

Las rutas de reportes usan una proyección de errores adicional: conservan el
estado HTTP y únicamente los nombres de campos conocidos, sustituyendo mensajes
del upstream por textos neutros. Así un `422` puede marcar el campo correcto sin
entregar SQL, trazas, rutas del servidor ni otros diagnósticos al navegador.

## Reportes, medidas y apelaciones

La bandeja de reportes consume el contrato real de `api_mycode` y permite
filtrar por estado, motivo, medida, presencia de apelación, fechas UTC y
búsqueda segura. El detalle muestra el contexto autorizado, la auditoría y las
acciones disponibles según permisos.

- Un reporte `pending` puede pasar a `under_review`.
- La resolución admite `upheld`, `rejected` o `duplicate` y exige justificación
  interna. Ésta permanece sólo en administración.
- `warning` y `suspension` sólo acompañan `upheld`; `provider_closed` acogido
  exige suspensión. Para `duplicate`, la interfaz carga un selector de reportes
  no duplicados de la misma sucursal, excluye el reporte actual y envía el UUID
  canónico seleccionado. Los candidatos se cargan en páginas de hasta 100 y el
  control «Cargar más reportes» permite recorrer todas las páginas sin descargar
  un conjunto ilimitado de una vez; el moderador no copia identificadores manualmente.
- Una suspensión retira inmediatamente la ubicación de resultados públicos.
- Un `409` cancela la edición local, vuelve a consultar el detalle y explica
  que otro moderador cambió el estado.
- Los formularios bloquean doble envío, conservan valores ante `422` y tienen
  confirmación explícita y foco de teclado contenido.

La apelación administrativa admite `accepted` o `rejected` con justificación
interna. Aceptar una advertencia requiere revisión; aceptar una suspensión exige
además `suspend_service_providers`. La API decide si otra suspensión vigente
impide reactivar la ubicación.

## Contrato de catálogos

Los cuatro nombres válidos son `categories`, `services`, `specialties` y
`features`.

- `GET /api/v1/admin/service-catalogs/{tipo}` requiere
  `manage_service_provider_catalogs`, devuelve una colección plana y contiene
  registros activos e inactivos con `is_active`.
- `GET /api/v1/public/service-catalogs/{tipo}` devuelve sólo registros activos.
  En categorías incluye sólo raíces activas y sus hijos activos.
- `DELETE` no borra físicamente: cambia `is_active` a `false` e invalida el
  caché público.
- `PUT` mantiene el `slug` estable. La UI lo presenta como sólo lectura durante
  la edición.
- Categorías aceptan `parent_id`, `icon` y `sort_order`; el API valida igualdad
  de `scope`, descendientes y ausencia de ciclos dentro de una transacción.
- Características aceptan `data_type` e `is_filterable`. Todos los tipos
  aceptan `scope`, `name`, `slug`, `description` e `is_active`.

La UI muestra el estado activo/inactivo, permite editar ambos y sólo ofrece
desactivar cuando el registro está activo.

## Documentos y respuestas

La pantalla nunca recibe `document_path`, metadata privada ni una URL directa
de almacenamiento. Construye únicamente el enlace same-origin del bridge. El
contrato de detalle puede omitir un indicador de disponibilidad; en ese caso
se ofrece el enlace protegido y el API responde `404` si no existe archivo.

El BFF permite PDF, imágenes y `application/octet-stream`, conserva sólo
`Content-Type` y `Content-Disposition`, y reemplaza siempre la política de
caché por `Cache-Control: private, no-store`. No reenvía cookies, rutas ni
headers de caché del upstream. Las respuestas JSON administrativas eliminan
recursivamente claves de credenciales, Bearer, tokens, JWT y contraseñas antes
de llegar al navegador, sin ocultar los diagnósticos del visor interno de
errores.

## Configuración

Configurar en el `.env` del servidor, nunca en Git:

| Variable | Uso |
| --- | --- |
| `API_MYCODE_URL` | Origen HTTPS de `api_mycode` |
| `API_MYCODE_ADMIN_JWT_SECRET` | Validación del JWT admin; `JWT_SECRET` es fallback |
| `ADMIN_PANEL_DOMAIN` | Host dedicado opcional, por ejemplo `admin.mycode.cl` |
| `SESSION_DRIVER`, `SESSION_DOMAIN` | Persistencia y alcance correcto de sesión |
| `SESSION_SECURE_COOKIE=true` | Cookie sólo por HTTPS en producción |
| `APP_URL`, `APP_KEY` | URL canónica y cifrado Laravel |

El API debe desplegarse primero con las migraciones aplicadas y las rutas
administrativas de prestadores, verificación directa y catálogos disponibles.

## Gate de build

Las dependencias de desarrollo son necesarias para las pruebas:

```bash
composer install --prefer-dist --no-interaction
npm ci
php artisan test
for test_file in tests/js/*.test.js; do node "$test_file"; done
npm run production
php artisan architecture:audit --strict
git diff --check
```

`npm run production` es el gate del artefacto minificado. En el repositorio se
ejecuta después `npm run development` y se confirma que
`public/js/admin/app.js` sea la concatenación exacta, legible y en el orden de
`webpack.mix.js`. El despliegue productivo vuelve a ejecutar `npm run
production`; no debe servir el bundle de desarrollo por accidente.

## Despliegue

Desde la raíz de `web_mycode` en el servidor:

```bash
composer install --no-dev --prefer-dist --no-interaction --optimize-autoloader
npm ci
npm run production
php artisan optimize:clear
php artisan config:cache
php artisan view:cache
php artisan route:clear
```

Se usa `route:clear` porque el proyecto todavía contiene una ruta Closure. No
ejecutar PHPUnit después de instalar Composer con `--no-dev`.

## Smoke checks

Sin sesión, ajustar el host según el despliegue:

```bash
curl -sS -o /dev/null -w '%{http_code}\n' https://admin.mycode.cl/login
curl -sS -o /dev/null -w '%{http_code} %{redirect_url}\n' \
  https://admin.mycode.cl/service-providers
curl -sS https://admin.mycode.cl/session/status
curl -sS -o /dev/null -w '%{http_code}\n' \
  https://mycode.cl/api/bridge/admin/service-providers
```

Se espera login `200`, pantalla privada `302` hacia `/login`, estado
`{"authenticated":false}` y bridge `401`.

En staging, con cuentas de prueba que cubran los permisos representativos:

1. aplicar todos los filtros y paginar la lista;
2. abrir detalle, sucursales, operaciones e historial;
3. aprobar/rechazar y suspender/restaurar un prestador de prueba;
4. cambiar nivel, abrir el documento same-origin y aprobar/rechazar una
   verificación;
5. crear, editar y desactivar una entrada de cada catálogo;
6. comprobar navegación/acciones ocultas sin permiso;
7. enviar una petición directa sin permiso y confirmar `403`;
8. confirmar `Cache-Control: private, no-store` en documentos.

No usar prestadores ni documentos reales en smoke tests mutables.

## Política de versión

La entrega integrada de moderación, resoluciones y apelaciones corresponde a
`VERSION=1.8.0`. En commits posteriores se evalúa el diálogo del hook según el
alcance real: mantener para documentación o correcciones internas, patch para
arreglos compatibles y minor para capacidades nuevas visibles.
