# Importación de estaciones

## Propósito

Este flujo permite reemplazar masivamente el catálogo de estaciones desde un archivo Excel provisto por operación.

El diseño actual prioriza:

- validación estricta del archivo antes de tocar datos vigentes
- confirmación explícita del mapeo de columnas antes de importar
- reemplazo transaccional para evitar estados parciales
- match contra ciudades normalizadas en base a `ciudad + departamento`, incluyendo aliases persistidos en base de datos y tolerancia defensiva a abreviaturas y variantes razonables dentro del mismo departamento
- respuesta resumida para que la interfaz explique filas omitidas sin exponer detalles internos

## Endpoint

`POST /api/admin/estaciones/import`

## Endpoint auxiliar para omitidas corregidas

`POST /api/admin/estaciones/import/skipped`

## Endpoints para omitidas persistidas

- `GET /api/admin/estaciones-omitidas`
- `GET /api/admin/estaciones-omitidas/{id}/edit`
- `PUT /api/admin/estaciones-omitidas/{id}`
- `POST /api/admin/estaciones-omitidas/{id}/resolve`

## Permisos y roles

Requiere pasar por los siguientes controles:

- `auth:sanctum`
- `abilities:admin`
- `uuid-check`
- `rootUser`

En la práctica, sólo un admin root autenticado puede ejecutar la importación.

## Input

`multipart/form-data`

Campos:

- `file`: requerido, archivo Excel `.xlsx` o `.xls`, máximo 20 MB
- `mapping[direccion]`: requerido, columna origen que contiene la dirección
- `mapping[coordenadas]`: requerido, columna origen que contiene latitud y longitud
- `mapping[ciudad]`: requerido, columna origen para ciudad
- `mapping[departamento]`: requerido, columna origen para departamento

### Formato esperado del Excel

La fila de encabezados válida debe estar en la fila `1`.

No es obligatorio que los nombres visibles coincidan exactamente con los campos del sistema porque la interfaz permite mapear columnas manualmente. Aun así, el archivo debe incluir al menos columnas equivalentes a:

1. dirección o localidad
2. coordenadas
3. ciudad
4. departamento

La UI propone automáticamente coincidencias similares en base a heurísticas de texto, pero la confirmación final siempre la hace la persona usuaria.

`titulo` no forma parte del archivo ni se genera a partir del Excel. Para estaciones importadas, la referencia visible en admin y web pública se resuelve desde `direccion`.

## Output

### Respuesta exitosa `200 OK`

```json
{
  "message": "Importación de estaciones finalizada.",
  "total_rows": 352,
  "imported_rows": 349,
  "skipped_rows": 3,
  "skipped": [
    {
      "row": 121,
      "direccion": "Dirección inválida",
      "coordenadas": "-25.25595, -57.57174",
      "ciudad": "Ciudad no encontrada",
      "departamento": "Central",
      "reason": "No se encontró una ciudad normalizada que coincida con la fila.",
      "row_data": {
        "no": "121",
        "direccion_yo_localidad": "Dirección inválida",
        "coordenadas": "-25.25595, -57.57174",
        "ciudad": "Ciudad no encontrada",
        "departamento": "Central"
      }
    }
  ]
}
```

### Respuesta exitosa de carga de omitidas corregidas `200 OK`

```json
{
  "message": "Las filas corregidas se cargaron correctamente.",
  "total_rows": 3,
  "imported_rows": 2,
  "skipped_rows": 1,
  "skipped": [
    {
      "row": 21,
      "direccion": "Ruta 5, Km. 1",
      "coordenadas": "dato inválido",
      "reason": "Las coordenadas no tienen un formato válido."
    }
  ]
}
```

### Error de validación `422 Unprocessable Entity`

```json
{
  "message": "The given data was invalid.",
  "errors": {
    "file": [
      "Debe seleccionar un archivo de Excel."
    ]
  }
}
```

### Error de negocio controlado `422 Unprocessable Entity`

```json
{
  "message": "El archivo no tiene el formato esperado para importar estaciones."
}
```

### Error inesperado `500 Internal Server Error`

```json
{
  "message": "Ocurrió un error al importar las estaciones."
}
```

## Ejemplo de request

```bash
curl -X POST "http://localhost/api/admin/estaciones/import" \
  -H "Accept: application/json" \
  -H "Authorization: Bearer TOKEN" \
  -H "uuid: DEVICE-UUID" \
  -F "file=@/ruta/estaciones-activas.xlsx" \
  -F "mapping[direccion]=direccion_yo_localidad" \
  -F "mapping[coordenadas]=coordenadas" \
  -F "mapping[ciudad]=ciudad" \
  -F "mapping[departamento]=departamento"
```

## Comportamiento de negocio

1. La persona usuaria selecciona el archivo y revisa un modal de mapeo.
2. La interfaz sugiere columnas similares y permite corregirlas manualmente.
3. Se valida el archivo, el shape del mapping y la existencia de las columnas seleccionadas.
4. Se parsean las filas de estaciones.
5. Se intenta resolver `ciudad_id` por nombre normalizado de ciudad y departamento.
Si no hay match exacto, se consultan aliases persistidos en `ciudades_aliases` y luego coincidencias parciales y por similitud alta, siempre dentro del mismo departamento y sólo cuando quede un candidato inequívoco.
6. Las filas inválidas se omiten y se reportan en el resumen.
La respuesta conserva también la fila original normalizada para que la UI pueda descargar una planilla de corrección con la misma estructura que el Excel de entrada.
7. Si existe al menos una fila válida, se ejecuta una transacción que:
   - elimina `promociones_estaciones`
   - elimina `estaciones_servicios`
   - elimina `estaciones`
   - inserta el nuevo catálogo de estaciones
8. Las filas omitidas que después se corrigen manualmente pueden cargarse por `POST /api/admin/estaciones/import/skipped` sin volver a reemplazar el catálogo ya importado.
9. Además, cada omitida queda persistida en `estaciones_import_omitidas` para revisión manual posterior desde el admin, donde se puede editar el dato fallido, asignar una ciudad oficial y resolverla individualmente.

## Notas de seguridad

- El endpoint está restringido a admins root autenticados.
- El request acepta únicamente archivos Excel válidos.
- El backend rechaza mapeos incompletos o columnas seleccionadas que no existan en el archivo.
- La corrección manual de omitidas valida que `ciudad_id` exista en el catálogo real.
- El servicio devuelve mensajes controlados y evita filtrar excepciones internas.
- El match de ciudades se hace sobre texto normalizado, aliases administrables en BD, abreviaturas conocidas y similitud alta acotada al mismo departamento para reducir errores operativos sin relajar la consistencia del catálogo.

## Notas de performance

- El lookup de ciudades usa Query Builder con columnas explícitas.
- El lookup incorpora ciudades oficiales y aliases desde consultas separadas, ambas con columnas mínimas explícitas.
- El reemplazo de estaciones se ejecuta dentro de una sola transacción.
- La inserción final es masiva (`insert`) en vez de una creación por modelo.
- Se impone un límite defensivo de filas (`1000`) para evitar cargas accidentales desproporcionadas.
- La UI de mapeo sólo lee encabezados y unas pocas filas de vista previa para evitar parsear toda la hoja antes de confirmar el import.

## Cómo correr los tests relacionados

Desde `copetrol-api-and-public`:

```bash
php8.1 artisan test --filter=ImportEstacionesTest
php8.1 artisan test --filter=EstacionesImportServiceTest
php8.1 artisan test --filter=CiudadesAdminTest
php8.1 artisan test --filter=EstacionesOmitidasAdminTest
```
