openapi: 3.1.0
info:
  title: Route
  version: 1.0.0
  description: "# Transcend Route API - Documentación Completa\n\n## Descripción General\n\
    \nLa API de Transcend Route calcula rutas optimizadas para transporte de mercancías\
    \ por carretera, considerando restricciones de vehículos, normativa EU de tiempos\
    \ de conducción, y puntos negros de tráfico.\n\n## Autenticación\n\nLa API requiere\
    \ autenticación mediante:\n- **JWT Bearer Token**: `Authorization: Bearer <token>`\n\
    - **API Key**: `x-api-key: <api-key>`\n\n## Características principales\n- Cálculo\
    \ de rutas optimizadas para camiones\n- Gestión automática de paradas de descanso\
    \ según normativa EU\n- Integración con puntos negros de tráfico\n- Isocronas\
    \ para análisis de cobertura\n- Exportación de rutas en formato GPX\n\n## Normativa\
    \ EU de tiempos de conducción\nLa API calcula automáticamente las paradas obligatorias\
    \ según la normativa europea:\n- **Parada corta**: 45 minutos después de 4.5 horas\
    \ de conducción\n- **Descanso diario**: 11 horas (puede ser 9h en descanso reducido)\n\
    - **Descanso semanal**: 45 horas (puede ser 24h en descanso reducido)\n- **Máximo\
    \ semanal**: 56 horas de conducción\n- **Máximo bisemanal**: 90 horas de conducción\n\
    \n## Endpoints Principales\n\n### 1. Cálculo de Ruta (`GET /api/route`)\n\n**Descripción**:\
    \ Calcula una ruta optimizada entre origen y destino.\n\n**Parámetros principales**:\n\
    - `origin_lat`, `origin_lon`: Coordenadas de origen (requerido)\n- `destiny_lat`,\
    \ `destiny_lon`: Coordenadas de destino (requerido)\n- `waypoints`: Array JSON\
    \ de paradas intermedias\n- `vehicle`: Información del vehículo (dimensiones,\
    \ peso)\n- `driver`: Estado de horas del conductor\n- `date`: Fecha de salida/llegada\n\
    - `map_layers`: Opciones de ruta (evitar peajes, etc.)\n\n#### Ejemplo Básico\n\
    \n```bash\ncurl -X GET \"https://api.transcend.es/route?origin_lat=42.2406&origin_lon=-8.7207&destiny_lat=41.3874&destiny_lon=2.1686\"\
    \ \\\n  -H \"Authorization: Bearer YOUR_JWT_TOKEN\"\n```\n\n#### Ejemplo Completo\
    \ con Vehículo y Conductor\n\n```bash\ncurl -X GET \"https://api.transcend.es/route?\\\
    \norigin_lat=42.2406&\\\norigin_lon=-8.7207&\\\ndestiny_lat=41.3874&\\\ndestiny_lon=2.1686&\\\
    \nvehicle=%7B%22width%22%3A2.55%2C%22height%22%3A4.0%2C%22weight%22%3A40000%2C%22consumption%22%3A32%7D&\\\
    \ndriver=%7B%22hoursStatus%22%3A%7B%22remainingWeeklyHours%22%3A56%2C%22remainingDayHours%22%3A9%7D%7D&\\\
    \ndate=%7B%22type%22%3A%22departure%22%2C%22date%22%3A%222025-01-15T08%3A00%3A00Z%22%7D\"\
    \ \\\n  -H \"Authorization: Bearer YOUR_JWT_TOKEN\"\n```\n\n#### Ejemplo con Waypoints\
    \ y Mercancía ADR\n\n```bash\ncurl -X GET \"https://api.transcend.es/route?\\\n\
    origin_lat=40.4168&\\\norigin_lon=-3.7038&\\\ndestiny_lat=41.3851&\\\ndestiny_lon=2.1734&\\\
    \nwaypoints=%5B%7B%22position%22%3A%5B40.5%2C-3.5%5D%2C%22customBreakMinutes%22%3A30%7D%5D&\\\
    \nmerchandise=%7B%22hsCode%22%3A%223301%22%2C%22weight%22%3A20000%2C%22isADR%22%3Atrue%7D&\\\
    \nmap_layers=%7B%22avoidTolls%22%3Afalse%2C%22calculateStops%22%3Atrue%7D\" \\\
    \n  -H \"Authorization: Bearer YOUR_JWT_TOKEN\"\n```\n\n### 2. Puntos Negros de\
    \ Tráfico (`POST /api/traffic/blackspots/path`)\n\n**Descripción**: Obtiene zonas\
    \ de alta peligrosidad a lo largo de una ruta.\n\n```bash\ncurl -X POST \"https://api.transcend.es/api/traffic/blackspots/path\"\
    \ \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Authorization: Bearer\
    \ YOUR_JWT_TOKEN\" \\\n  -d '{\n    \"points\": [\n      [-3.7038, 40.4168],\n\
    \      [-3.7138, 40.4268],\n      [-3.7238, 40.4368]\n    ],\n    \"minDangerLevel\"\
    : 30\n  }'\n```\n\n### 3. Isocronas (`GET /api/isochrone`)\n\n**Descripción**:\
    \ Calcula áreas alcanzables desde un punto.\n\n```bash\ncurl -X GET \"https://api.transcend.es/api/isochrone?lat=40.4168&lon=-3.7038&km=100&isTime=false\"\
    \ \\\n  -H \"Authorization: Bearer YOUR_JWT_TOKEN\"\n```\n\n## Casos de Uso Empresarial\n\
    \n### 1. Distribución Urbana con Múltiples Paradas\n\n**Escenario**: Camión de\
    \ reparto que debe visitar 5 clientes en Madrid.\n\n```bash\ncurl -X GET \"https://api.transcend.es/api/route?\\\
    \norigin_lat=40.4168&\\\norigin_lon=-3.7038&\\\ndestiny_lat=40.4168&\\\ndestiny_lon=-3.7038&\\\
    \nwaypoints=%5B\\\n%7B%22position%22%3A%5B40.421%2C-3.692%5D%2C%22customBreakMinutes%22%3A15%7D%2C\\\
    \n%7B%22position%22%3A%5B40.430%2C-3.710%5D%2C%22customBreakMinutes%22%3A20%7D%2C\\\
    \n%7B%22position%22%3A%5B40.415%2C-3.725%5D%2C%22customBreakMinutes%22%3A10%7D%2C\\\
    \n%7B%22position%22%3A%5B40.405%2C-3.715%5D%2C%22customBreakMinutes%22%3A25%7D\\\
    \n%5D&\\\nvehicle=%7B%22width%22%3A2.3%2C%22height%22%3A3.5%2C%22weight%22%3A12000%7D&\\\
    \ndate=%7B%22type%22%3A%22departure%22%2C%22date%22%3A%222025-01-15T08%3A00%3A00Z%22%7D\"\
    \ \\\n  -H \"x-api-key: YOUR_API_KEY\"\n```\n\n### 2. Transporte Internacional\
    \ con ADR\n\n**Escenario**: Mercancía peligrosa desde España a Francia.\n\n```bash\n\
    curl -X GET \"https://api.transcend.es/api/route?\\\norigin_lat=41.3851&\\\norigin_lon=2.1734&\\\
    \ndestiny_lat=43.7102&\\\ndestiny_lon=7.2620&\\\nmerchandise=%7B%22hsCode%22%3A%221201%22%2C%22weight%22%3A24000%2C%22isADR%22%3Atrue%7D&\\\
    \nmap_layers=%7B%22avoidTunnels%22%3Atrue%2C%22avoidHighways%22%3Afalse%2C%22trafficDensity%22%3Atrue%7D&\\\
    \nvehicle=%7B%22weight%22%3A44000%2C%22height%22%3A4.2%7D&\\\nshow_black_points=true\"\
    \ \\\n  -H \"x-api-key: YOUR_API_KEY\"\n```\n\n### 3. Ruta con Llegada Programada\n\
    \n**Escenario**: Conductor debe llegar a las 17:00 para descarga.\n\n```bash\n\
    curl -X GET \"https://api.transcend.es/api/route?\\\norigin_lat=37.3891&\\\norigin_lon=-5.9845&\\\
    \ndestiny_lat=40.4168&\\\ndestiny_lon=-3.7038&\\\ndate=%7B%22type%22%3A%22arrival%22%2C%22date%22%3A%222025-01-15T17%3A00%3A00Z%22%7D&\\\
    \ndriver=%7B%22hoursStatus%22%3A%7B%22remainingWeeklyHours%22%3A48%2C%22remainingDayHours%22%3A8%7D%7D\"\
    \ \\\n  -H \"x-api-key: YOUR_API_KEY\"\n```\n\n## Gestión de Rutas Guardadas\n\
    \n### Crear Ruta Guardada\n\n```bash\ncurl -X POST \"https://api.transcend.es/api/saved-routes\"\
    \ \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Authorization: Bearer\
    \ YOUR_JWT_TOKEN\" \\\n  -d '{\n    \"title\": \"Madrid - Barcelona semanal\"\
    ,\n    \"description\": \"Ruta habitual con parada en Zaragoza\",\n    \"origin\"\
    : {\n      \"coordinates\": [40.4168, -3.7038],\n      \"locality\": \"Madrid\"\
    \n    },\n    \"destination\": {\n      \"coordinates\": [41.3851, 2.1734],\n\
    \      \"locality\": \"Barcelona\"\n    },\n    \"waypoints\": [\n      {\n  \
    \      \"coordinates\": [41.6561, -0.8773],\n        \"locality\": \"Zaragoza\"\
    \n      }\n    ]\n  }'\n```\n\n### Listar Rutas Guardadas\n\n```bash\ncurl -X\
    \ GET \"https://api.transcend.es/api/saved-routes\" \\\n  -H \"Authorization:\
    \ Bearer YOUR_JWT_TOKEN\"\n```\n\n### Exportar Ruta como GPX\n\n```bash\ncurl\
    \ -X GET \"https://api.transcend.es/api/saved-routes/60f1b2b3c4d5e6f7g8h9i0j1/gpx\"\
    \ \\\n  -H \"Authorization: Bearer YOUR_JWT_TOKEN\" \\\n  --output route.gpx\n\
    ```\n\n## Gestión de Ubicaciones\n\n### Crear Ubicación\n\n```bash\ncurl -X POST\
    \ \"https://api.transcend.es/api/locations\" \\\n  -H \"Content-Type: application/json\"\
    \ \\\n  -H \"Authorization: Bearer YOUR_JWT_TOKEN\" \\\n  -d '{\n    \"name\"\
    : \"Almacén Central Madrid\",\n    \"address\": \"Calle Industrial 25\",\n   \
    \ \"city\": \"Madrid\",\n    \"province\": \"Madrid\",\n    \"latitude\": \"40.4168\"\
    ,\n    \"longitude\": \"-3.7038\",\n    \"coordinates\": [40.4168, -3.7038],\n\
    \    \"openingTime\": \"08:00\",\n    \"closingTime\": \"18:00\",\n    \"phone\"\
    : \"+34 91 123 4567\",\n    \"email\": \"almacen.madrid@empresa.com\"\n  }'\n\
    ```\n\n## Códigos de Error Comunes\n\n| Código | Descripción | Solución |\n|--------|-------------|----------|\n\
    | 400 | Parámetros inválidos | Verificar formato de coordenadas y JSON |\n| 401\
    \ | No autorizado | Verificar token JWT o API key |\n| 404 | Ruta no encontrada\
    \ | Verificar coordenadas y conectividad |\n| 429 | Too Many Requests | Esperar\
    \ antes de reintentar |\n| 500 | Error interno del servidor | Contactar soporte\
    \ técnico |\n\n## Límites de Uso\n\n- **Rutas por hora**: 1000 (por usuario)\n\
    - **Waypoints por ruta**: 50 máximo\n- **Distancia máxima**: 2000 km\n- **Tiempo\
    \ de respuesta**: < 30 segundos (normal), < 60 segundos (con optimización)\n\n\
    ## Formatos de Datos\n\n### Coordenadas\n- **Formato**: `[longitud, latitud]`\
    \ (GeoJSON standard)\n- **Rango**: Longitud -180 a 180, Latitud -90 a 90\n- **Precisión**:\
    \ Hasta 6 decimales (centímetros)\n\n### Fechas\n- **Formato**: ISO 8601 (`2025-01-15T08:00:00Z`)\n\
    - **Zona horaria**: UTC preferido\n\n### JSON Encoding\nLos parámetros complejos\
    \ deben estar URL-encoded:\n\n```javascript\n// JavaScript\nconst params = {\n\
    \  vehicle: { width: 2.55, height: 4.0 },\n  driver: { hoursStatus: { remainingWeeklyHours:\
    \ 56 } }\n};\nconst encoded = encodeURIComponent(JSON.stringify(params));\n//\
    \ Resultado: %7B%22vehicle%22%3A%7B%22width%22%3A2.55%2C%22height%22%3A4.0%7D%2C%22driver%22%3A%7B%22hoursStatus%22%3A%7B%22remainingWeeklyHours%22%3A56%7D%7D%7D\n\
    ```\n\n## SDKs y Librerías\n\n### JavaScript/TypeScript\n\n```javascript\nclass\
    \ TranscendRouteAPI {\n  constructor(apiKey, baseUrl = 'https://api.transcend.es')\
    \ {\n    this.apiKey = apiKey;\n    this.baseUrl = baseUrl;\n  }\n\n  async calculateRoute(origin,\
    \ destination, options = {}) {\n    const params = new URLSearchParams({\n   \
    \   origin_lat: origin.lat,\n      origin_lon: origin.lon,\n      destiny_lat:\
    \ destination.lat,\n      destiny_lon: destination.lon,\n      ...this.encodeOptions(options)\n\
    \    });\n\n    const response = await fetch(`${this.baseUrl}/api/route`, {\n\
    \      headers: {\n        'x-api-key': this.apiKey\n      }\n    });\n\n    return\
    \ response.json();\n  }\n\n  encodeOptions(options) {\n    const encoded = {};\n\
    \    Object.keys(options).forEach(key => {\n      if (typeof options[key] ===\
    \ 'object') {\n        encoded[key] = encodeURIComponent(JSON.stringify(options[key]));\n\
    \      } else {\n        encoded[key] = options[key];\n      }\n    });\n    return\
    \ encoded;\n  }\n}\n\n// Uso\nconst api = new TranscendRouteAPI('your-api-key');\n\
    const route = await api.calculateRoute(\n  { lat: 42.2406, lon: -8.7207 },\n \
    \ { lat: 41.3874, lon: 2.1686 },\n  {\n    vehicle: { width: 2.55, height: 4.0,\
    \ weight: 40000 },\n    driver: { hoursStatus: { remainingWeeklyHours: 56 } }\n\
    \  }\n);\n```\n\n### Python\n\n```python\nimport requests\nimport json\nfrom urllib.parse\
    \ import urlencode\n\nclass TranscendRouteAPI:\n    def __init__(self, api_key:\
    \ str, base_url='https://api.transcend.es'):\n        self.api_key = api_key\n\
    \        self.base_url = base_url\n        self.session = requests.Session()\n\
    \        self.session.headers.update({'x-api-key': api_key})\n\n    def calculate_route(self,\
    \ origin, destination, **options):\n        params = {\n            'origin_lat':\
    \ origin['lat'],\n            'origin_lon': origin['lon'],\n            'destiny_lat':\
    \ destination['lat'],\n            'destiny_lon': destination['lon']\n       \
    \ }\n\n        # Encode complex objects as JSON strings\n        for key, value\
    \ in options.items():\n            if isinstance(value, (dict, list)):\n     \
    \           params[key] = json.dumps(value)\n            else:\n             \
    \   params[key] = value\n\n        response = self.session.get(f'{self.base_url}/api/route',\
    \ params=params)\n        response.raise_for_status()\n        return response.json()\n\
    \n# Uso\napi = TranscendRouteAPI('your-api-key')\nroute = api.calculate_route(\n\
    \    {'lat': 42.2406, 'lon': -8.7207},\n    {'lat': 41.3874, 'lon': 2.1686},\n\
    \    vehicle={'width': 2.55, 'height': 4.0, 'weight': 40000},\n    driver={'hoursStatus':\
    \ {'remainingWeeklyHours': 56}}\n)\n```\n\n## Webhooks y Integraciones\n\n###\
    \ Eventos Disponibles\n- `route.calculated`: Nueva ruta calculada\n- `route.saved`:\
    \ Ruta guardada por usuario\n- `location.created`: Nueva ubicación creada\n\n\
    ### Configuración de Webhooks\n\n```bash\ncurl -X POST \"https://api.transcend.es/api/webhooks\"\
    \ \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Authorization: Bearer\
    \ YOUR_JWT_TOKEN\" \\\n  -d '{\n    \"url\": \"https://your-app.com/webhooks/route-events\"\
    ,\n    \"events\": [\"route.calculated\", \"route.saved\"],\n    \"secret\": \"\
    your-webhook-secret\"\n  }'\n```\n\n## Soporte y Contacto\n\n- **Documentación\
    \ completa**: [OpenAPI Spec](./openapi.yaml)\n- **Email soporte**: support@transcend.es\n\
    - **Portal desarrolladores**: https://developers.transcend.es\n- **Status page**:\
    \ https://status.transcend.es\n\n## Changelog\n\n### v1.0.0 (2025-01-15)\n- ✅\
    \ Cálculo de rutas con paradas automáticas\n- ✅ Soporte para tipos de fecha (departure/arrival)\n\
    - ✅ Integración con puntos negros de tráfico\n- ✅ Gestión de rutas guardadas\n\
    - ✅ API de ubicaciones\n- ✅ Exportación GPX\n- ✅ Gamificación básica\n- ✅ Rutas\
    \ temporales compartidas\n\n## Testing y Validación\n\n### Estrategia de Testing\n\
    \n#### Niveles de Testing\n\n##### 1. Unit Tests\n- **Cobertura**: Funciones individuales\
    \ y métodos\n- **Herramientas**: Jest, Mocha, Chai\n- **Enfoque**: Lógica de negocio,\
    \ cálculos, validaciones\n\n##### 2. Integration Tests\n- **Cobertura**: APIs\
    \ externas, bases de datos, servicios\n- **Herramientas**: Supertest, Testcontainers\n\
    - **Enfoque**: Flujos completos, integraciones\n\n##### 3. End-to-End Tests\n\
    - **Cobertura**: Flujos completos de usuario\n- **Herramientas**: Cypress, Playwright\n\
    - **Enfoque**: Experiencia completa del usuario\n\n##### 4. Performance Tests\n\
    - **Cobertura**: Carga, estrés, volumetría\n- **Herramientas**: k6, Artillery,\
    \ JMeter\n- **Enfoque**: Rendimiento bajo carga\n\n### Métricas de Calidad\n\n\
    ```yaml\n# Objetivos de calidad\ncoverage:\n  statements: 85%\n  branches: 80%\n\
    \  functions: 90%\n  lines: 85%\n\nperformance:\n  response_time_p95: 2000ms \
    \ # Percentil 95\n  throughput: 100 req/sec\n  error_rate: 0.1%          # Máximo\
    \ 0.1%\n\nreliability:\n  uptime: 99.9%\n  mttr: 15min               # Mean Time\
    \ To Recovery\n  mtbf: 720h               # Mean Time Between Failures\n```\n\n\
    ### Scripts de Testing Automatizado\n\n#### Script de CI/CD\n\n```yaml\n# .github/workflows/test.yml\n\
    name: Tests\n\non:\n  push:\n    branches: [ main, develop ]\n  pull_request:\n\
    \    branches: [ main ]\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n\n    services:\n\
    \      mongodb:\n        image: mongo:5.0\n        ports:\n          - 27017:27017\n\
    \n    steps:\n    - uses: actions/checkout@v3\n\n    - name: Setup Node.js\n \
    \     uses: actions/setup-node@v3\n      with:\n        node-version: '18'\n \
    \       cache: 'npm'\n\n    - name: Install dependencies\n      run: npm ci\n\n\
    \    - name: Run linting\n      run: npm run lint\n\n    - name: Run unit tests\n\
    \      run: npm run test:unit\n      env:\n        NODE_ENV: test\n\n    - name:\
    \ Run integration tests\n      run: npm run test:integration\n      env:\n   \
    \     NODE_ENV: test\n        MONGODB_URI: mongodb://localhost:27017/transcend_test\n\
    \n    - name: Generate coverage report\n      run: npm run coverage\n\n    - name:\
    \ Upload coverage to Codecov\n      uses: codecov/codecov-action@v3\n      with:\n\
    \        file: ./coverage/lcov.info\n\n  performance-test:\n    runs-on: ubuntu-latest\n\
    \    if: github.ref == 'refs/heads/main'\n\n    steps:\n    - uses: actions/checkout@v3\n\
    \n    - name: Setup Node.js\n      uses: actions/setup-node@v3\n      with:\n\
    \        node-version: '18'\n\n    - name: Install dependencies\n      run: npm\
    \ ci\n\n    - name: Run performance tests\n      run: npm run test:performance\n\
    \      env:\n        BASE_URL: ${{ secrets.PERF_TEST_URL }}\n        API_KEY:\
    \ ${{ secrets.PERF_TEST_API_KEY }}\n\n  security-test:\n    runs-on: ubuntu-latest\n\
    \    if: github.ref == 'refs/heads/main'\n\n    steps:\n    - uses: actions/checkout@v3\n\
    \n    - name: Run security scan\n      uses: securecodewarrior/github-actions-gosec@master\n\
    \      with:\n        args: './...'\n\n    - name: Run dependency check\n    \
    \  uses: dependency-check/Dependency-Check_Action@main\n      with:\n        project:\
    \ 'Transcend Route API'\n        path: '.'\n        format: 'ALL'\n```\n\n####\
    \ Script de Smoke Tests\n\n```bash\n#!/bin/bash\n# scripts/smoke-test.sh\n\nset\
    \ -e\n\necho \"\U0001F680 Running smoke tests for Transcend Route API\"\n\nBASE_URL=\"\
    ${BASE_URL:-http://localhost:8086}\"\nAPI_KEY=\"${API_KEY:-test-key}\"\n\necho\
    \ \"\U0001F4CD Testing basic route calculation...\"\ncurl -f -s \"${BASE_URL}/api/route?origin_lat=40.4168&origin_lon=-3.7038&destiny_lat=41.3851&destiny_lon=2.1734\"\
    \ \\\n  -H \"x-api-key: ${API_KEY}\" > /dev/null\necho \"✅ Route calculation OK\"\
    \n\necho \"\U0001F4CD Testing health check...\"\ncurl -f -s \"${BASE_URL}/health\"\
    \ > /dev/null\necho \"✅ Health check OK\"\n\necho \"\U0001F4CD Testing traffic\
    \ health...\"\ncurl -f -s \"${BASE_URL}/api/traffic/health\" \\\n  -H \"x-api-key:\
    \ ${API_KEY}\" > /dev/null\necho \"✅ Traffic health OK\"\n\necho \"\U0001F4CD\
    \ Testing black spots...\"\ncurl -f -s -X POST \"${BASE_URL}/api/traffic/blackspots/path\"\
    \ \\\n  -H \"Content-Type: application/json\" \\\n  -H \"x-api-key: ${API_KEY}\"\
    \ \\\n  -d '{\"points\":[[-3.7038,40.4168],[-3.7138,40.4268]],\"minDangerLevel\"\
    :30}' > /dev/null\necho \"✅ Black spots OK\"\n\necho \"\U0001F389 All smoke tests\
    \ passed!\"\n```\n\n### Tests de Integración Detallados\n\n#### Test Básico de\
    \ Ruta\n\n```javascript\n// test/integration/route.integration.test.js\nconst\
    \ request = require('supertest');\nconst app = require('../../src/index');\n\n\
    describe('Route API Integration Tests', () => {\n  describe('GET /api/route',\
    \ () => {\n    it('should calculate a basic route successfully', async () => {\n\
    \      const response = await request(app)\n        .get('/api/route')\n     \
    \   .query({\n          origin_lat: 40.4168,\n          origin_lon: -3.7038,\n\
    \          destiny_lat: 41.3851,\n          destiny_lon: 2.1734\n        })\n\
    \        .set('x-api-key', process.env.TEST_API_KEY)\n        .expect(200);\n\n\
    \      // Validar estructura de respuesta\n      expect(response.body).toHaveProperty('main');\n\
    \      expect(response.body).toHaveProperty('stops');\n      expect(response.body.main).toHaveProperty('summary');\n\
    \      expect(response.body.main.summary).toHaveProperty('time');\n      expect(response.body.main.summary).toHaveProperty('length');\n\
    \n      // Validar tipos de datos\n      expect(typeof response.body.main.summary.time).toBe('string');\n\
    \      expect(typeof response.body.main.summary.length).toBe('number');\n    \
    \  expect(response.body.main.summary.length).toBeGreaterThan(0);\n    });\n\n\
    \    it('should handle invalid coordinates', async () => {\n      const response\
    \ = await request(app)\n        .get('/api/route')\n        .query({\n       \
    \   origin_lat: 91,  // Latitud inválida\n          origin_lon: -3.7038,\n   \
    \       destiny_lat: 41.3851,\n          destiny_lon: 2.1734\n        })\n   \
    \     .set('x-api-key', process.env.TEST_API_KEY)\n        .expect(400);\n\n \
    \     expect(response.body).toHaveProperty('message');\n      expect(response.body.message).toMatch(/coordenadas/i);\n\
    \    });\n  });\n});\n```\n\n#### Test de Puntos Negros\n\n```javascript\n// test/integration/blackspots.integration.test.js\n\
    describe('Black Spots API Integration Tests', () => {\n  describe('POST /api/traffic/blackspots/path',\
    \ () => {\n    it('should return black spots along a route', async () => {\n \
    \     const routePoints = [\n        [-3.7038, 40.4168], // Madrid\n        [-3.7138,\
    \ 40.4268],\n        [-3.7238, 40.4368]\n      ];\n\n      const response = await\
    \ request(app)\n        .post('/api/traffic/blackspots/path')\n        .send({\n\
    \          points: routePoints,\n          minDangerLevel: 30\n        })\n  \
    \      .set('x-api-key', process.env.TEST_API_KEY)\n        .expect(200);\n\n\
    \      expect(response.body).toHaveProperty('blackSpots');\n      expect(response.body).toHaveProperty('metadata');\n\
    \      expect(Array.isArray(response.body.blackSpots)).toBe(true);\n    });\n\
    \  });\n});\n```\n\n## Integración con Sistemas ERP\n\n### Sincronización de Ubicaciones\n\
    \n```bash\n#!/bin/bash\n# Script para sincronizar ubicaciones desde ERP\n\nERP_API_URL=\"\
    https://erp.empresa.com/api\"\nTRANSCEND_API_URL=\"https://api.transcend.es\"\n\
    API_KEY=\"your-api-key\"\n\n# Obtener ubicaciones del ERP\nlocations=$(curl -s\
    \ \"${ERP_API_URL}/locations\" -H \"Authorization: Bearer ${ERP_TOKEN}\")\n\n\
    # Convertir y enviar a Transcend\necho \"$locations\" | jq -r '.[] | @json' |\
    \ while read -r location; do\n  # Transformar formato ERP a Transcend\n  transcend_location=$(echo\
    \ \"$location\" | jq '{\n    name: .name,\n    address: .address,\n    city: .city,\n\
    \    province: .province,\n    latitude: (.coordinates.lat | tostring),\n    longitude:\
    \ (.coordinates.lon | tostring),\n    coordinates: [.coordinates.lon, .coordinates.lat],\n\
    \    openingTime: .businessHours.open,\n    closingTime: .businessHours.close,\n\
    \    customerId: .id\n  }')\n\n  # Crear en Transcend\n  curl -X POST \"${TRANSCEND_API_URL}/api/locations\"\
    \ \\\n    -H \"Content-Type: application/json\" \\\n    -H \"x-api-key: ${API_KEY}\"\
    \ \\\n    -d \"$transcend_location\"\ndone\n```\n\n### Cálculo Automático de Rutas\
    \ para Pedidos\n\n```javascript\n// Node.js - Integración con sistema de pedidos\n\
    \nconst axios = require('axios');\n\nclass ERPRouteIntegration {\n  constructor(transcendApiKey)\
    \ {\n    this.transcend = axios.create({\n      baseURL: 'https://api.transcend.es',\n\
    \      headers: { 'x-api-key': transcendApiKey }\n    });\n  }\n\n  async calculateRouteForOrder(order)\
    \ {\n    // Extraer información del pedido\n    const origin = await this.getWarehouseLocation(order.warehouseId);\n\
    \    const destination = await this.getCustomerLocation(order.customerId);\n \
    \   const vehicle = await this.getVehicleForOrder(order);\n\n    // Calcular ruta\n\
    \    const routeParams = {\n      origin_lat: origin.lat,\n      origin_lon: origin.lon,\n\
    \      destiny_lat: destination.lat,\n      destiny_lon: destination.lon,\n  \
    \    vehicle: JSON.stringify(vehicle),\n      merchandise: JSON.stringify({\n\
    \        weight: order.totalWeight,\n        isADR: order.isHazardous\n      }),\n\
    \      date: JSON.stringify({\n        type: 'departure',\n        date: order.requestedDeliveryDate\n\
    \      })\n    };\n\n    const response = await this.transcend.get('/api/route',\
    \ { params: routeParams });\n\n    // Actualizar pedido con información de ruta\n\
    \    await this.updateOrderWithRoute(order.id, response.data);\n\n    return response.data;\n\
    \  }\n\n  async getWarehouseLocation(warehouseId) {\n    // Consultar ubicación\
    \ del almacén desde ERP\n    const response = await this.erp.get(`/warehouses/${warehouseId}`);\n\
    \    return {\n      lat: response.data.latitude,\n      lon: response.data.longitude\n\
    \    };\n  }\n\n  async getCustomerLocation(customerId) {\n    // Buscar ubicación\
    \ del cliente en Transcend o ERP\n    try {\n      const response = await this.transcend.get(`/api/locations/${customerId}`);\n\
    \      return {\n        lat: parseFloat(response.data.latitude),\n        lon:\
    \ parseFloat(response.data.longitude)\n      };\n    } catch (error) {\n     \
    \ // Si no existe en Transcend, buscar en ERP\n      const erpLocation = await\
    \ this.erp.get(`/customers/${customerId}/location`);\n      return {\n       \
    \ lat: erpLocation.data.lat,\n        lon: erpLocation.data.lon\n      };\n  \
    \  }\n  }\n}\n\n// Uso\nconst integration = new ERPRouteIntegration('your-api-key');\n\
    \n// Procesar pedido nuevo\nconst order = {\n  id: 'ORD-2025-001',\n  warehouseId:\
    \ 'WH-MAD',\n  customerId: 'CUST-123',\n  totalWeight: 15000,\n  isHazardous:\
    \ false,\n  requestedDeliveryDate: '2025-01-20T10:00:00Z'\n};\n\nconst route =\
    \ await integration.calculateRouteForOrder(order);\nconsole.log('Ruta calculada:',\
    \ route.summary);\n```\n\n## Optimización de Flotas\n\n### Asignación Óptima de\
    \ Vehículos\n\n```python\nimport requests\nimport json\nfrom typing import List,\
    \ Dict, Any\n\nclass FleetOptimizer:\n    def __init__(self, api_key: str):\n\
    \        self.api_key = api_key\n        self.base_url = \"https://api.transcend.es\"\
    \n        self.session = requests.Session()\n        self.session.headers.update({\"\
    x-api-key\": api_key})\n\n    def optimize_fleet_assignment(self, orders: List[Dict],\
    \ vehicles: List[Dict], depot: Dict) -> Dict:\n        assignments = {}\n\n  \
    \      # Ordenar pedidos por urgencia y distancia\n        sorted_orders = sorted(orders,\
    \ key=lambda x: (x.get('priority', 1), x['distance']))\n\n        for vehicle\
    \ in vehicles:\n            if not sorted_orders:\n                break\n\n \
    \           # Encontrar pedidos que este vehículo puede atender\n            suitable_orders\
    \ = [\n                order for order in sorted_orders\n                if self.vehicle_can_handle_order(vehicle,\
    \ order)\n            ]\n\n            if suitable_orders:\n                #\
    \ Asignar pedido más cercano\n                assignment = self.assign_order_to_vehicle(vehicle,\
    \ suitable_orders[0], depot)\n                assignments[vehicle['id']] = assignment\n\
    \                sorted_orders.remove(suitable_orders[0])\n\n        return {\n\
    \            'assignments': assignments,\n            'unassigned_orders': sorted_orders\n\
    \        }\n\n    def vehicle_can_handle_order(self, vehicle: Dict, order: Dict)\
    \ -> bool:\n        # Verificar capacidad de peso\n        if vehicle['max_weight']\
    \ < order['weight']:\n            return False\n\n        # Verificar restricciones\
    \ ADR\n        if order.get('isADR') and not vehicle.get('adr_certified', False):\n\
    \            return False\n\n        # Verificar dimensiones\n        if (vehicle['max_width']\
    \ < order.get('width', 0) or\n            vehicle['max_height'] < order.get('height',\
    \ 0)):\n            return False\n\n        return True\n\n    def assign_order_to_vehicle(self,\
    \ vehicle: Dict, order: Dict, depot: Dict) -> Dict:\n        # Calcular ruta para\
    \ este vehículo y pedido\n        route_params = {\n            'origin_lat':\
    \ depot['lat'],\n            'origin_lon': depot['lon'],\n            'destiny_lat':\
    \ order['destination']['lat'],\n            'destiny_lon': order['destination']['lon'],\n\
    \            'vehicle': json.dumps({\n                'width': vehicle['width'],\n\
    \                'height': vehicle['height'],\n                'weight': vehicle['weight']\
    \ + order['weight'],\n                'consumption': vehicle['consumption']\n\
    \            }),\n            'merchandise': json.dumps({\n                'weight':\
    \ order['weight'],\n                'isADR': order.get('isADR', False)\n     \
    \       }),\n            'date': json.dumps({\n                'type': 'departure',\n\
    \                'date': order.get('deadline', '2025-01-15T08:00:00Z')\n     \
    \       })\n        }\n\n        response = self.session.get('/api/route', params=route_params)\n\
    \        route_data = response.json()\n\n        return {\n            'vehicle_id':\
    \ vehicle['id'],\n            'order_id': order['id'],\n            'route': route_data,\n\
    \            'estimated_cost': route_data['main']['summary']['cost'],\n      \
    \      'estimated_time': route_data['main']['summary']['timeWithBreaks']\n   \
    \     }\n\n# Ejemplo de uso\noptimizer = FleetOptimizer(\"your-api-key\")\n\n\
    orders = [\n    {\n        'id': 'ORD-001',\n        'weight': 8000,\n       \
    \ 'width': 2.2,\n        'height': 2.8,\n        'isADR': False,\n        'priority':\
    \ 1,\n        'distance': 150,\n        'destination': {'lat': 40.5, 'lon': -3.5},\n\
    \        'deadline': '2025-01-15T17:00:00Z'\n    }\n]\n\nvehicles = [\n    {\n\
    \        'id': 'VEH-001',\n        'max_weight': 12000,\n        'max_width':\
    \ 2.5,\n        'max_height': 3.0,\n        'width': 2.3,\n        'height': 2.8,\n\
    \        'weight': 4000,  # Tara\n        'consumption': 28,\n        'adr_certified':\
    \ True\n    }\n]\n\ndepot = {'lat': 40.4168, 'lon': -3.7038}\n\nresult = optimizer.optimize_fleet_assignment(orders,\
    \ vehicles, depot)\nprint(\"Asignaciones óptimas:\", json.dumps(result, indent=2))\n\
    ```\n\n## Conclusión\n\nEsta documentación completa proporciona todo lo necesario\
    \ para integrar la API de Transcend Route en sistemas empresariales. Desde ejemplos\
    \ básicos hasta implementaciones avanzadas de optimización de flotas, la API está\
    \ diseñada para escalar con las necesidades del negocio del transporte.\n\nPara\
    \ más información, contacta al equipo de soporte en support@transcend.es.\n"
  contact:
    name: Transcend Support
    email: support@transcend.es
  license:
    name: Proprietary
    url: https://transcend.es/license
servers:
- url: https://api.pro.cargoffer.com
  description: Producción
tags:
- name: Route
  description: Cálculo y optimización de rutas
- name: Isochrone
  description: Cálculo de isocronas (áreas alcanzables)
- name: Traffic
  description: Información de tráfico y puntos negros
- name: Saved Routes
  description: Gestión de rutas guardadas por usuario
- name: Locations
  description: Gestión de ubicaciones guardadas
- name: Gamification
  description: Métricas de gamificación del usuario
- name: Temp Code
  description: Rutas temporales compartidas por código
- name: Health
  description: Endpoints de salud del servicio
security:
- bearerAuth: []
- apiKeyAuth: []
paths:
  /health:
    get:
      summary: Health check del servicio
      description: Verifica que el servicio esté funcionando correctamente
      tags:
      - Health
      responses:
        '200':
          description: Servicio saludable
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ok
                  timestamp:
                    type: string
                    format: date-time
  /test:
    get:
      summary: Alias de health check
      tags:
      - Health
      responses:
        '200':
          description: Servicio saludable
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ok
  /api/route:
    get:
      summary: Calcular ruta optimizada
      description: "Calcula una ruta optimizada para camiones entre origen y destino,\
        \ \nincluyendo waypoints intermedios y paradas de descanso obligatorias.\n\
        \n## Tipos de fecha\n- `departure`: La fecha proporcionada es la hora de salida\
        \ (default)\n- `arrival`: La fecha proporcionada es la hora de llegada deseada.\
        \ El sistema\n  calcula automáticamente la hora de salida necesaria.\n\n##\
        \ Cálculo de paradas\nEl sistema calcula automáticamente las paradas necesarias\
        \ según:\n- Horas de conducción restantes del conductor\n- Normativa EU de\
        \ tiempos de conducción\n- Waypoints definidos por el usuario\n"
      tags:
      - Route
      parameters:
      - name: origin_lat
        in: query
        required: true
        description: Latitud del punto de origen
        schema:
          type: number
          format: double
          minimum: -90
          maximum: 90
        example: 42.2406
      - name: origin_lon
        in: query
        required: true
        description: Longitud del punto de origen
        schema:
          type: number
          format: double
          minimum: -180
          maximum: 180
        example: -8.7207
      - name: destiny_lat
        in: query
        required: true
        description: Latitud del punto de destino
        schema:
          type: number
          format: double
          minimum: -90
          maximum: 90
        example: 41.3874
      - name: destiny_lon
        in: query
        required: true
        description: Longitud del punto de destino
        schema:
          type: number
          format: double
          minimum: -180
          maximum: 180
        example: 2.1686
      - name: waypoints
        in: query
        required: false
        description: 'Array de waypoints intermedios (JSON-encoded).

          Cada waypoint puede tener un tiempo de parada personalizado.

          '
        schema:
          type: string
        example: '[{"position":[42.0,-7.5],"customBreakMinutes":30,"stopType":"WAYPOINT"}]'
      - name: avoid_points
        in: query
        required: false
        description: 'Array de puntos a evitar (JSON-encoded).

          Cada punto se define como [latitud, longitud].

          El sistema creará zonas de evitación de 1km de radio alrededor de cada punto.

          '
        schema:
          type: string
        example: '[[42.0259,-7.185],[41.5,-2.3]]'
      - name: vehicle
        in: query
        required: false
        description: 'Información del vehículo (JSON-encoded).

          Usado para restricciones de altura, peso, anchura en la ruta.

          '
        schema:
          type: string
        example: '{"width":2.55,"height":4.0,"weight":40000,"length":16.5,"axleLoad":11500,"tankSize":600,"consumption":32,"avgSpeed":80}'
      - name: driver
        in: query
        required: false
        description: 'Estado de horas del conductor (JSON-encoded).

          Usado para calcular cuándo necesita paradas de descanso.

          '
        schema:
          type: string
        example: '{"hoursStatus":{"remainingWeeklyHours":56,"remainingBiweeklyHours":90,"remainingDayHours":9}}'
      - name: merchandise
        in: query
        required: false
        description: 'Información de la mercancía (JSON-encoded).

          Usado para rutas ADR (mercancías peligrosas).

          '
        schema:
          type: string
        example: '{"hsCode":"8703","weight":15000,"isADR":false}'
      - name: date
        in: query
        required: false
        description: 'Fecha y tipo de fecha (JSON-encoded).

          - `type: "departure"`: La fecha es hora de salida

          - `type: "arrival"`: La fecha es hora de llegada deseada

          '
        schema:
          type: string
        example: '{"type":"departure","date":"2025-01-15T08:00:00Z"}'
      - name: map_layers
        in: query
        required: false
        description: 'Capas del mapa y opciones de ruta (JSON-encoded).

          Controla qué evitar y qué POIs mostrar.

          '
        schema:
          type: string
        example: '{"avoidTolls":false,"avoidHighways":false,"calculateStops":true,"shortestRoute":false}'
      - name: show_black_points
        in: query
        required: false
        description: Incluir puntos negros de peligrosidad en la respuesta
        schema:
          type: boolean
          default: false
      responses:
        '200':
          description: Ruta calculada exitosamente
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RouteResponse'
        '400':
          description: Parámetros inválidos
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: No se encontró ruta
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Error interno del servidor
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/route/count:
    get:
      summary: Contar rutas en intervalo de tiempo
      description: Obtiene el conteo de rutas calculadas en un intervalo de fechas
      tags:
      - Route
      parameters:
      - name: startDate
        in: query
        required: true
        schema:
          type: string
          format: date-time
      - name: endDate
        in: query
        required: true
        schema:
          type: string
          format: date-time
      responses:
        '200':
          description: Conteo de rutas
          content:
            application/json:
              schema:
                type: object
                properties:
                  count:
                    type: integer
                    example: 150
  /api/isochrone:
    get:
      summary: Calcular isocronas
      description: 'Calcula el área alcanzable desde un punto dado en un tiempo o
        distancia determinada.

        Útil para análisis de cobertura de servicios.

        '
      tags:
      - Isochrone
      parameters:
      - name: lat
        in: query
        required: true
        description: Latitud del punto central
        schema:
          type: number
          format: double
        example: 40.4168
      - name: lon
        in: query
        required: true
        description: Longitud del punto central
        schema:
          type: number
          format: double
        example: -3.7038
      - name: km
        in: query
        required: false
        description: 'Distancia en km (si isTime=false) o minutos (si isTime=true).

          Máximo: 1000km o 120 minutos.

          '
        schema:
          type: number
          default: 100
          maximum: 1000
        example: 50
      - name: isTime
        in: query
        required: false
        description: Si true, km representa minutos en lugar de kilómetros
        schema:
          type: boolean
          default: false
      - name: vehicle
        in: query
        required: false
        description: Información del vehículo (JSON-encoded)
        schema:
          type: string
      responses:
        '200':
          description: Isocrona calculada
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IsochroneResponse'
        '400':
          description: Parámetros inválidos
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/traffic/blackspots/path:
    post:
      summary: Obtener puntos negros a lo largo de una ruta
      description: 'Retorna los puntos negros (zonas de alta peligrosidad) que se
        encuentran

        a lo largo de una ruta definida por una serie de puntos.

        '
      tags:
      - Traffic
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - points
              properties:
                points:
                  type: array
                  description: Array de coordenadas [longitud, latitud] que definen
                    la ruta
                  items:
                    type: array
                    items:
                      type: number
                    minItems: 2
                    maxItems: 2
                  minItems: 2
                  example:
                  - - -3.7038
                    - 40.4168
                  - - -3.7138
                    - 40.4268
                  - - -3.7238
                    - 40.4368
                minDangerLevel:
                  type: integer
                  description: Nivel mínimo de peligrosidad (0-100)
                  minimum: 0
                  maximum: 100
                  default: 30
      responses:
        '200':
          description: Puntos negros encontrados
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BlackSpotsResponse'
        '400':
          description: Parámetros inválidos
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/traffic/blackspots/area:
    get:
      summary: Obtener puntos negros en un área
      description: 'Retorna los puntos negros dentro de un radio alrededor de un punto
        central.

        '
      tags:
      - Traffic
      parameters:
      - name: lat
        in: query
        required: true
        description: Latitud del centro del área
        schema:
          type: number
          format: double
        example: 40.4168
      - name: lon
        in: query
        required: true
        description: Longitud del centro del área
        schema:
          type: number
          format: double
        example: -3.7038
      - name: radius
        in: query
        required: false
        description: Radio de búsqueda en metros (1000-25000)
        schema:
          type: integer
          minimum: 1000
          maximum: 25000
          default: 25000
        example: 10000
      - name: minDangerLevel
        in: query
        required: false
        description: Nivel mínimo de peligrosidad (0-100)
        schema:
          type: integer
          minimum: 0
          maximum: 100
          default: 30
      responses:
        '200':
          description: Puntos negros en el área
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BlackSpotsResponse'
  /api/traffic/health:
    get:
      summary: Health check del servicio de tráfico
      tags:
      - Traffic
      - Health
      responses:
        '200':
          description: Servicio de tráfico saludable
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum:
                    - healthy
                    - unhealthy
                  service:
                    type: string
                    example: traffic-controller
                  trafficService:
                    type: string
                    enum:
                    - connected
                    - disconnected
                  timestamp:
                    type: string
                    format: date-time
        '503':
          description: Servicio no disponible
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: unhealthy
                  error:
                    type: string
  /api/saved-routes:
    post:
      summary: Crear ruta guardada
      description: Guarda una ruta calculada para el usuario autenticado
      tags:
      - Saved Routes
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SavedRouteCreate'
      responses:
        '200':
          description: Ruta guardada exitosamente
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SavedRoute'
        '400':
          description: Datos inválidos o User ID requerido
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    get:
      summary: Listar rutas guardadas
      description: Obtiene todas las rutas guardadas del usuario autenticado
      tags:
      - Saved Routes
      responses:
        '200':
          description: Lista de rutas guardadas
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/SavedRoute'
        '400':
          description: User ID requerido
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/saved-routes/{id}:
    get:
      summary: Obtener ruta guardada por ID
      tags:
      - Saved Routes
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: ID de la ruta guardada (MongoDB ObjectId)
      responses:
        '200':
          description: Ruta guardada
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SavedRoute'
        '404':
          description: Ruta no encontrada
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    put:
      summary: Actualizar ruta guardada
      tags:
      - Saved Routes
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SavedRouteUpdate'
      responses:
        '200':
          description: Ruta actualizada
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SavedRoute'
        '404':
          description: Ruta no encontrada
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    delete:
      summary: Eliminar ruta guardada
      tags:
      - Saved Routes
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: Ruta eliminada
          content:
            application/json:
              schema:
                type: object
                properties:
                  deleted:
                    type: boolean
                    example: true
        '404':
          description: Ruta no encontrada
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/saved-routes/{id}/gpx:
    get:
      summary: Exportar ruta como GPX
      description: Descarga la ruta guardada en formato GPX para dispositivos GPS
      tags:
      - Saved Routes
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: Archivo GPX
          content:
            application/gpx+xml:
              schema:
                type: string
                format: binary
        '404':
          description: Ruta no encontrada
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/locations:
    post:
      summary: Crear ubicación
      description: Guarda una ubicación frecuente para el usuario (cliente, almacén,
        etc.)
      tags:
      - Locations
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LocationCreate'
      responses:
        '200':
          description: Ubicación creada
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Location'
        '400':
          description: Datos inválidos
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    get:
      summary: Listar ubicaciones
      description: Obtiene todas las ubicaciones del usuario autenticado
      tags:
      - Locations
      responses:
        '200':
          description: Lista de ubicaciones
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Location'
  /api/locations/{id}:
    get:
      summary: Obtener ubicación por ID
      tags:
      - Locations
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: Ubicación
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Location'
        '404':
          description: Ubicación no encontrada
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    put:
      summary: Actualizar ubicación
      tags:
      - Locations
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LocationUpdate'
      responses:
        '200':
          description: Ubicación actualizada
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Location'
        '404':
          description: Ubicación no encontrada
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    delete:
      summary: Eliminar ubicación
      tags:
      - Locations
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: Ubicación eliminada
          content:
            application/json:
              schema:
                type: object
                properties:
                  deleted:
                    type: boolean
                    example: true
        '404':
          description: Ubicación no encontrada
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/tempCode/{id}:
    get:
      summary: Obtener ruta por código temporal
      description: 'Obtiene una ruta compartida mediante un código temporal.

        Las rutas temporales expiran después de un tiempo configurado.

        '
      tags:
      - Temp Code
      parameters:
      - name: id
        in: path
        required: true
        description: Código temporal de la ruta
        schema:
          type: string
      responses:
        '200':
          description: Ruta temporal
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TempRoute'
        '404':
          description: Ruta temporal no encontrada o expirada
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/gamification/metrics:
    get:
      summary: Obtener métricas de gamificación
      description: Obtiene las métricas de gamificación del usuario autenticado
      tags:
      - Gamification
      responses:
        '200':
          description: Métricas de gamificación
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GamificationMetrics'
        '400':
          description: User ID requerido
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/gamification/summary:
    get:
      summary: Obtener resumen de gamificación
      description: Obtiene un resumen de los logros y estadísticas del usuario
      tags:
      - Gamification
      responses:
        '200':
          description: Resumen de gamificación
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GamificationSummary'
  /api/gamification/metrics/refresh:
    post:
      summary: Refrescar métricas de gamificación
      description: Fuerza una actualización de las métricas de gamificación
      tags:
      - Gamification
      responses:
        '200':
          description: Métricas actualizadas
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GamificationMetrics'
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: JWT token obtenido del servicio IAM
    apiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
      description: API Key proporcionada por Transcend
  schemas:
    Error:
      type: object
      properties:
        message:
          type: string
          example: Error description
    Vehicle:
      type: object
      description: 'Información del vehículo para cálculo de restricciones de ruta.

        Las dimensiones determinan qué carreteras puede usar el vehículo.

        '
      properties:
        width:
          type: number
          description: Ancho del vehículo en metros
          example: 2.55
        height:
          type: number
          description: Alto del vehículo en metros
          example: 4.0
        weight:
          type: number
          description: Peso total del vehículo en kg
          example: 40000
        length:
          type: number
          description: Longitud del vehículo en metros
          example: 16.5
        axleLoad:
          type: number
          description: Carga máxima por eje en kg
          example: 11500
        tankSize:
          type: number
          description: Capacidad del tanque en litros
          example: 600
        consumption:
          type: number
          description: Consumo en litros/100km
          example: 32
        avgSpeed:
          type: number
          description: Velocidad media estimada en km/h
          example: 80
    Driver:
      type: object
      description: 'Estado de horas del conductor según normativa EU.

        Se usa para calcular cuándo necesita paradas de descanso.

        '
      properties:
        hoursStatus:
          type: object
          properties:
            remainingWeeklyHours:
              type: number
              description: Horas restantes de conducción esta semana (máx 56h)
              example: 45
            remainingBiweeklyHours:
              type: number
              description: Horas restantes de conducción en 2 semanas (máx 90h)
              example: 80
            remainingDayHours:
              type: number
              description: Horas restantes de conducción hoy (máx 9h, ext 10h)
              example: 9
            remainingDrivingHours:
              type: number
              description: Horas hasta la próxima parada obligatoria (4.5h)
              example: 4.5
            lastRestStart:
              type: string
              format: date-time
              description: Inicio del último descanso
    Merchandise:
      type: object
      description: Información de la mercancía transportada
      properties:
        hsCode:
          type: string
          description: Código arancelario HS de la mercancía
          example: '8703'
        weight:
          type: number
          description: Peso de la mercancía en kg
          example: 15000
        isADR:
          type: boolean
          description: Si la mercancía es peligrosa (ADR)
          default: false
    MapLayers:
      type: object
      description: 'Opciones de ruta y capas del mapa.

        Controla qué tipo de carreteras evitar y qué POIs mostrar.

        '
      properties:
        gasStations:
          type: boolean
          description: Mostrar gasolineras
          default: false
        parkings:
          type: boolean
          description: Mostrar parkings para camiones
          default: false
        showers:
          type: boolean
          description: Mostrar duchas/áreas de descanso
          default: false
        supermarkets:
          type: boolean
          description: Mostrar supermercados
          default: false
        cafeterias:
          type: boolean
          description: Mostrar cafeterías
          default: false
        workshops:
          type: boolean
          description: Mostrar talleres
          default: false
        weather:
          type: boolean
          description: Mostrar información meteorológica
          default: false
        avoidTolls:
          type: boolean
          description: Evitar peajes
          default: false
        avoidHighways:
          type: boolean
          description: Evitar autopistas
          default: false
        allowSecondary:
          type: boolean
          description: Permitir carreteras secundarias
          default: false
        shortestRoute:
          type: boolean
          description: Calcular ruta más corta en lugar de más rápida
          default: false
        avoidTunnels:
          type: boolean
          description: Evitar túneles
          default: false
        calculateStops:
          type: boolean
          description: Calcular paradas de descanso automáticamente
          default: true
        trafficDensity:
          type: boolean
          description: Considerar densidad de tráfico
          default: false
        radars:
          type: boolean
          description: Mostrar radares
          default: false
    DateType:
      type: object
      description: 'Configuración de fecha para la ruta.


        - `departure`: La fecha indica cuándo sale el conductor

        - `arrival`: La fecha indica cuándo debe llegar (el sistema calcula la salida)

        '
      required:
      - type
      - date
      properties:
        type:
          type: string
          enum:
          - departure
          - arrival
          description: Tipo de fecha
          example: departure
        date:
          type: string
          format: date-time
          description: Fecha y hora (ISO 8601)
          example: '2025-01-15T08:00:00Z'
    Waypoint:
      type: object
      description: Punto intermedio en la ruta definido por el usuario
      properties:
        position:
          type: array
          items:
            type: number
          minItems: 2
          maxItems: 2
          description: Coordenadas [latitud, longitud]
          example:
          - 42.0
          - -7.5
        customBreakMinutes:
          type: integer
          description: Tiempo de parada en minutos
          default: 0
          example: 30
        stopType:
          type: string
          enum:
          - WAYPOINT
          - SHORT_BREAK
          - LONG_BREAK
          - WEEKLY_REST
          description: Tipo de parada
          default: WAYPOINT
    UnifiedStop:
      type: object
      description: 'Parada unificada en la ruta (puede ser waypoint del usuario o
        parada calculada).

        Incluye información completa de tiempos y estado de horas.

        '
      properties:
        coordinates:
          type: array
          items:
            type: number
          minItems: 2
          maxItems: 2
          description: Coordenadas [latitud, longitud]
        stopType:
          type: string
          enum:
          - WAYPOINT
          - SHORT_BREAK
          - LONG_BREAK
          - WEEKLY_REST
          description: 'Tipo de parada:

            - WAYPOINT: Parada definida por usuario

            - SHORT_BREAK: Descanso corto (45 min)

            - LONG_BREAK: Descanso diario (11h)

            - WEEKLY_REST: Descanso semanal (45h)

            '
        durationMinutes:
          type: integer
          description: Duración de la parada en minutos
        arrivalDate:
          type: string
          format: date-time
          description: Fecha/hora de llegada a la parada
        departureDate:
          type: string
          format: date-time
          description: Fecha/hora de salida de la parada
        isWaypoint:
          type: boolean
          description: Si es un waypoint definido por usuario
        isUserDefined:
          type: boolean
          description: Si fue definido explícitamente por el usuario
        pureDrivingMinutes:
          type: number
          description: Minutos de conducción pura hasta esta parada
        accumulatedMinutes:
          type: number
          description: Minutos acumulados (conducción + descansos anteriores)
        distanceFromStart:
          type: number
          description: Distancia desde el inicio en km
        hoursStatus:
          type: object
          description: Estado de horas de conducción en este punto
          properties:
            remainingDaily:
              type: number
              description: Horas restantes del día
            remainingWeekly:
              type: number
              description: Horas restantes de la semana
            needsBreak:
              type: boolean
              description: Si necesita un descanso
            exceedsDaily:
              type: boolean
              description: Si excede el límite diario
    RouteSummary:
      type: object
      description: Resumen de la ruta calculada
      properties:
        has_time_restrictions:
          type: boolean
          description: Si la ruta tiene restricciones horarias
        has_toll:
          type: boolean
          description: Si la ruta tiene peajes
        has_highway:
          type: boolean
          description: Si la ruta usa autopistas
        has_ferry:
          type: boolean
          description: Si la ruta usa ferry
        min_lat:
          type: number
          description: Latitud mínima del bounding box
        min_lon:
          type: number
          description: Longitud mínima del bounding box
        max_lat:
          type: number
          description: Latitud máxima del bounding box
        max_lon:
          type: number
          description: Longitud máxima del bounding box
        time:
          type: string
          description: Tiempo de conducción puro (formato HH:MM)
          example: 06:30
        length:
          type: number
          description: Distancia total en km
          example: 620.5
        cost:
          type: number
          description: Coste estimado del viaje (combustible + peajes)
          example: 450.0
        timeInSeconds:
          type: integer
          description: Tiempo de conducción puro en segundos
          example: 23400
        timeWithBreaksInSeconds:
          type: integer
          description: Tiempo total incluyendo descansos en segundos
          example: 37800
        timeWithBreaks:
          type: string
          description: Tiempo total con descansos (formato HH:MM)
          example: '10:30'
        departureDate:
          type: string
          format: date-time
          description: Fecha/hora de salida (calculada si type=arrival)
        arrivalDate:
          type: string
          format: date-time
          description: Fecha/hora de llegada
        remainingWeeklyDrivingHours:
          type: number
          description: Horas de conducción restantes en la semana al finalizar
        remainingWeeklyDrivingHoursFormatted:
          type: string
          description: Horas restantes formateadas
          example: '45:30'
        distance:
          type: number
          description: Distancia total en km (alias de length)
    MainRoute:
      type: object
      description: Ruta principal recomendada
      properties:
        shape:
          type: string
          description: Polyline encoded de la geometría de la ruta
        summary:
          $ref: '#/components/schemas/RouteSummary'
        maneuvers:
          type: array
          description: Lista de maniobras de navegación
          items:
            $ref: '#/components/schemas/Maneuver'
        stops:
          type: array
          items:
            $ref: '#/components/schemas/UnifiedStop'
    Maneuver:
      type: object
      description: Instrucción de navegación
      properties:
        type:
          type: integer
          description: Tipo de maniobra (código Valhalla)
        instruction:
          type: string
          description: Instrucción de texto
          example: Gire a la derecha en Calle Mayor
        length:
          type: number
          description: Distancia del tramo en km
        time:
          type: number
          description: Tiempo del tramo en segundos
        begin_shape_index:
          type: integer
          description: Índice inicial en el polyline
        end_shape_index:
          type: integer
          description: Índice final en el polyline
        street_names:
          type: array
          items:
            type: string
          description: Nombres de calles
        toll:
          type: boolean
          description: Si este tramo tiene peaje
        highway:
          type: boolean
          description: Si este tramo es autopista
    RouteResponse:
      type: object
      description: Respuesta completa del cálculo de ruta
      properties:
        main:
          $ref: '#/components/schemas/MainRoute'
        alternatives:
          type: array
          description: Rutas alternativas
          items:
            $ref: '#/components/schemas/MainRoute'
        stops:
          type: array
          description: Todas las paradas de la ruta principal
          items:
            $ref: '#/components/schemas/UnifiedStop'
        black_points:
          type: array
          description: Puntos negros a lo largo de la ruta
          items:
            $ref: '#/components/schemas/BlackSpot'
        temp_code:
          type: string
          description: Código temporal para compartir la ruta
          example: ABC123DEF
        has_route_limit:
          type: number
          description: Límite de rutas mensuales del usuario
          example: 100
    BlackSpot:
      type: object
      description: Punto negro (zona de alta peligrosidad)
      properties:
        id:
          type: string
          description: ID único del punto negro
        coordinates:
          type: array
          items:
            type: number
          minItems: 2
          maxItems: 2
          description: Coordenadas [latitud, longitud]
        dangerLevel:
          type: integer
          description: Nivel de peligrosidad (0-100)
          minimum: 0
          maximum: 100
          example: 75
        description:
          type: string
          description: Descripción del punto negro
    BlackSpotsResponse:
      type: object
      properties:
        blackSpots:
          type: array
          items:
            $ref: '#/components/schemas/BlackSpot'
        metadata:
          type: object
          properties:
            total:
              type: integer
              description: Total de puntos negros encontrados
            averageDangerLevel:
              type: integer
              description: Nivel de peligrosidad promedio
            maxDangerLevel:
              type: integer
              description: Nivel de peligrosidad máximo
        request:
          type: object
          properties:
            pointsCount:
              type: integer
              description: Número de puntos en la solicitud
            minDangerLevel:
              type: integer
              description: Nivel mínimo solicitado
            areaCovered:
              type: number
              description: Área cubierta en km²
    IsochroneResponse:
      type: object
      description: Respuesta de cálculo de isocrona
      properties:
        type:
          type: string
          example: FeatureCollection
        features:
          type: array
          items:
            type: object
            properties:
              type:
                type: string
                example: Feature
              geometry:
                type: object
                properties:
                  type:
                    type: string
                    example: Polygon
                  coordinates:
                    type: array
                    description: Coordenadas del polígono
              properties:
                type: object
                properties:
                  contour:
                    type: number
                    description: Valor del contorno (km o minutos)
    LocationPoint:
      type: object
      description: Punto de ubicación con metadatos
      properties:
        coordinates:
          type: array
          items:
            type: number
          minItems: 2
          maxItems: 2
          description: Coordenadas [latitud, longitud]
        postalCode:
          type: string
          description: Código postal
        locality:
          type: string
          description: Localidad/ciudad
    SavedRoute:
      type: object
      description: Ruta guardada por el usuario
      properties:
        _id:
          type: string
          description: ID único de la ruta
        title:
          type: string
          description: Título de la ruta
          example: Madrid - Barcelona semanal
        description:
          type: string
          description: Descripción de la ruta
        origin:
          $ref: '#/components/schemas/LocationPoint'
        destination:
          $ref: '#/components/schemas/LocationPoint'
        waypoints:
          type: array
          items:
            $ref: '#/components/schemas/LocationPoint'
        stops:
          type: array
          items:
            $ref: '#/components/schemas/LocationPoint'
        polyline:
          type: object
          description: Datos de la polyline de la ruta
          properties:
            main:
              type: object
              description: Ruta principal
            alternatives:
              type: array
              description: Rutas alternativas
            black_points:
              type: array
              items:
                $ref: '#/components/schemas/BlackSpot'
            selectedRouteId:
              type: string
        driver:
          type: object
          properties:
            _id:
              type: string
            hoursStatus:
              type: object
              properties:
                remainingWeeklyHours:
                  type: number
                remainingBiweeklyHours:
                  type: number
        vehicle:
          type: object
          properties:
            _id:
              type: string
            brand:
              type: string
            model:
              type: string
            fuelType:
              type: string
            consumption:
              type: number
            weight:
              type: number
            width:
              type: number
            height:
              type: number
        merchandise:
          $ref: '#/components/schemas/Merchandise'
        mapLayers:
          $ref: '#/components/schemas/MapLayers'
        date:
          $ref: '#/components/schemas/DateType'
        optimizedStops:
          type: boolean
        owner:
          type: string
          description: ID del usuario propietario
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    SavedRouteCreate:
      type: object
      required:
      - title
      - origin
      - destination
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 200
        description:
          type: string
          maxLength: 1000
        origin:
          $ref: '#/components/schemas/LocationPoint'
        destination:
          $ref: '#/components/schemas/LocationPoint'
        waypoints:
          type: array
          items:
            $ref: '#/components/schemas/LocationPoint'
        polyline:
          type: object
        vehicle:
          type: object
        driver:
          type: object
        merchandise:
          $ref: '#/components/schemas/Merchandise'
        mapLayers:
          $ref: '#/components/schemas/MapLayers'
        date:
          $ref: '#/components/schemas/DateType'
    SavedRouteUpdate:
      type: object
      properties:
        title:
          type: string
        description:
          type: string
        origin:
          $ref: '#/components/schemas/LocationPoint'
        destination:
          $ref: '#/components/schemas/LocationPoint'
        waypoints:
          type: array
          items:
            $ref: '#/components/schemas/LocationPoint'
        polyline:
          type: object
        vehicle:
          type: object
        driver:
          type: object
        merchandise:
          $ref: '#/components/schemas/Merchandise'
        mapLayers:
          $ref: '#/components/schemas/MapLayers'
        date:
          $ref: '#/components/schemas/DateType'
    Location:
      type: object
      description: Ubicación guardada (cliente, almacén, etc.)
      properties:
        _id:
          type: string
        name:
          type: string
          description: Nombre de la ubicación
          example: Almacén Central Madrid
        address:
          type: string
          description: Dirección completa
          example: Calle Industrial 25
        city:
          type: string
          example: Madrid
        province:
          type: string
          example: Madrid
        country:
          type: string
          example: España
        zipcode:
          type: string
          example: '28001'
        latitude:
          type: string
          description: Latitud como string
          example: '40.4168'
        longitude:
          type: string
          description: Longitud como string
          example: '-3.7038'
        coordinates:
          type: array
          items:
            type: number
          minItems: 2
          maxItems: 2
        openingTime:
          type: string
          description: Hora de apertura (HH:MM)
          example: 08:00
        closingTime:
          type: string
          description: Hora de cierre (HH:MM)
          example: '18:00'
        stopTime:
          type: string
          description: Tiempo de parada habitual
        phone:
          type: string
          description: Teléfono de contacto
        email:
          type: string
          format: email
          description: Email de contacto
        contactPerson:
          type: string
          description: Persona de contacto
        customerId:
          type: string
          description: ID del cliente (para integración)
        owner:
          type: string
          description: ID del usuario propietario
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    LocationCreate:
      type: object
      required:
      - name
      - address
      - city
      - province
      - latitude
      - longitude
      - coordinates
      - openingTime
      - closingTime
      properties:
        name:
          type: string
          minLength: 1
        address:
          type: string
          minLength: 1
        city:
          type: string
          minLength: 1
        province:
          type: string
          minLength: 1
        country:
          type: string
        zipcode:
          type: string
        latitude:
          type: string
        longitude:
          type: string
        coordinates:
          type: array
          items:
            type: number
          minItems: 2
          maxItems: 2
        openingTime:
          type: string
          pattern: ^([01]?[0-9]|2[0-3]):[0-5][0-9]$
          description: Formato HH:MM
        closingTime:
          type: string
          pattern: ^([01]?[0-9]|2[0-3]):[0-5][0-9]$
          description: Formato HH:MM
        phone:
          type: string
        email:
          type: string
          format: email
        contactPerson:
          type: string
        customerId:
          type: string
    LocationUpdate:
      type: object
      properties:
        name:
          type: string
        address:
          type: string
        city:
          type: string
        province:
          type: string
        country:
          type: string
        zipcode:
          type: string
        latitude:
          type: string
        longitude:
          type: string
        coordinates:
          type: array
          items:
            type: number
          minItems: 2
          maxItems: 2
        openingTime:
          type: string
        closingTime:
          type: string
        phone:
          type: string
        email:
          type: string
          format: email
        contactPerson:
          type: string
        customerId:
          type: string
    TempRoute:
      type: object
      description: Ruta temporal compartida por código
      properties:
        _id:
          type: string
        code:
          type: string
          description: Código único para compartir
        userId:
          type: string
          description: ID del usuario que creó la ruta (opcional)
        origin:
          $ref: '#/components/schemas/LocationPoint'
        destination:
          $ref: '#/components/schemas/LocationPoint'
        waypoints:
          type: array
          items:
            $ref: '#/components/schemas/LocationPoint'
        stops:
          type: array
          items:
            $ref: '#/components/schemas/UnifiedStop'
        polyline:
          type: object
        merchandise:
          $ref: '#/components/schemas/Merchandise'
        date:
          $ref: '#/components/schemas/DateType'
        optimizedStops:
          type: boolean
        createdAt:
          type: string
          format: date-time
    GamificationMetrics:
      type: object
      description: Métricas de gamificación del usuario
      properties:
        totalRoutes:
          type: integer
          description: Total de rutas calculadas
        totalDistance:
          type: number
          description: Distancia total recorrida en km
        totalTime:
          type: number
          description: Tiempo total de conducción en horas
        fuelSaved:
          type: number
          description: Combustible ahorrado estimado en litros
        co2Saved:
          type: number
          description: CO2 ahorrado estimado en kg
        achievements:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              name:
                type: string
              description:
                type: string
              unlockedAt:
                type: string
                format: date-time
    GamificationSummary:
      type: object
      description: Resumen de gamificación
      properties:
        level:
          type: integer
          description: Nivel actual del usuario
        points:
          type: integer
          description: Puntos acumulados
        rank:
          type: string
          description: Rango del usuario
          example: Road Master
        nextLevelPoints:
          type: integer
          description: Puntos necesarios para el siguiente nivel
        stats:
          $ref: '#/components/schemas/GamificationMetrics'
