{
  "openapi": "3.1.0",
  "info": { "title": "Estimaciones SAVA", "version": "1.0.0", "description": "Precio orientativo calculado por SAVA. Sin reserva, contratación ni correo. Citar fecha, moneda, IVA, gasóleo y supuestos. No inventar un precio cuando no esté disponible." },
  "servers": [{ "url": "https://www.savaexpress.com" }],
  "paths": {
    "/api/agentes/v1/estimaciones": {
      "get": {
        "operationId": "consultarEstimacionSava",
        "summary": "Consultar una estimación pública actual sin cuenta",
        "description": "Origen España. Hasta 10 metros facturables y fecha entre hoy y 30 días. Los alias Barcelona y Milan/Milano/Milán completan códigos de referencia 08001 y 20121; otras ciudades requieren códigos postales. Sin peso se utiliza metros × 1750 kg; sin fecha se utiliza hoy en Madrid. Mostrar todos los supuestos. Respetar Retry-After; nunca eludir cuotas ni enumerar tarifas.",
        "security": [],
        "parameters": [
          { "name": "origen", "in": "query", "schema": { "type": "string", "enum": ["Barcelona"] } },
          { "name": "destino", "in": "query", "schema": { "type": "string", "enum": ["Milan", "Milano", "Milán"] } },
          { "name": "cpOrigen", "in": "query", "schema": { "type": "string", "pattern": "^[0-9]{5}$" } },
          { "name": "cpDestino", "in": "query", "schema": { "type": "string", "maxLength": 12 } },
          { "name": "paisDestino", "in": "query", "schema": { "type": "string", "pattern": "^[A-Z]{2}$" } },
          { "name": "metros", "in": "query", "required": true, "schema": { "type": "number", "exclusiveMinimum": 0, "maximum": 10 } },
          { "name": "pesoKg", "in": "query", "schema": { "type": "number", "exclusiveMinimum": 0, "maximum": 24000 } },
          { "name": "fecha", "in": "query", "schema": { "type": "string", "format": "date" } }
        ],
        "responses": {
          "200": { "description": "Estimación con supuestos, o revisión manual sin precio", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Resultado" } } } },
          "400": { "$ref": "#/components/responses/Error" },
          "414": { "$ref": "#/components/responses/Error" },
          "422": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/Limite" },
          "503": { "$ref": "#/components/responses/NoDisponible" }
        }
      }
    },
    "/api/agentes/v1/cotizaciones": {
      "post": {
        "operationId": "crearCotizacionIntegracion",
        "summary": "Canal opcional para integraciones autorizadas por SAVA",
        "security": [{ "credencialSava": [] }],
        "parameters": [{ "name": "Idempotency-Key", "in": "header", "required": true, "schema": { "type": "string", "pattern": "^[A-Za-z0-9_-]{16,128}$" } }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Peticion" } } } },
        "responses": {
          "201": { "description": "Resultado creado; vigencia orientativa de 15 minutos", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Resultado" } } } },
          "200": { "description": "Repetición idempotente del mismo envío, sin nuevo cálculo", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Resultado" } } } },
          "400": { "$ref": "#/components/responses/Error" }, "401": { "$ref": "#/components/responses/Error" },
          "408": { "$ref": "#/components/responses/Error" }, "409": { "$ref": "#/components/responses/Error" },
          "410": { "$ref": "#/components/responses/Error" }, "413": { "$ref": "#/components/responses/Error" },
          "415": { "$ref": "#/components/responses/Error" }, "422": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/Limite" }, "503": { "$ref": "#/components/responses/NoDisponible" }
        }
      }
    },
    "/api/agentes/v1/cotizaciones/{id}": {
      "get": {
        "operationId": "recuperarCotizacionIntegracion",
        "summary": "Recuperar el resultado propio; caducado devuelve 410 y borrado devuelve 404",
        "security": [{ "credencialSava": [] }],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }],
        "responses": {
          "200": { "description": "Resultado del propietario autenticado", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Resultado" } } } },
          "401": { "$ref": "#/components/responses/Error" }, "404": { "$ref": "#/components/responses/Error" }, "410": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/Limite" }, "503": { "$ref": "#/components/responses/NoDisponible" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": { "credencialSava": { "type": "http", "scheme": "bearer", "description": "Credencial opaca sava_ emitida por SAVA para una empresa, cliente e integración. No es necesaria para las estimaciones públicas." } },
    "schemas": {
      "Peticion": {
        "type": "object", "additionalProperties": false,
        "required": ["paisDestino", "cpOrigen", "cpDestino", "ldm", "pesoKg", "fechaCarga", "servicio", "adr"],
        "properties": {
          "paisDestino": { "type": "string", "pattern": "^[A-Z]{2}$" }, "cpOrigen": { "type": "string", "pattern": "^[0-9]{5}$" },
          "cpDestino": { "type": "string", "minLength": 1, "maxLength": 12, "pattern": "^[A-Za-z0-9 -]+$" },
          "ldm": { "type": "number", "exclusiveMinimum": 0, "maximum": 10 }, "pesoKg": { "type": "number", "exclusiveMinimum": 0, "maximum": 24000 },
          "fechaCarga": { "type": "string", "format": "date" }, "servicio": { "const": "STANDARD_LTL" }, "adr": { "const": false }
        }
      },
      "Resultado": {
        "type": "object", "required": ["id", "estado", "emitido", "caduca", "fechaCarga"],
        "properties": {
          "id": { "type": "string", "format": "uuid" }, "estado": { "enum": ["estimacion", "revision_manual"] },
          "emitido": { "type": "string", "format": "date-time" }, "caduca": { "type": "string", "format": "date-time" }, "fechaCarga": { "type": "string", "format": "date" },
          "moneda": { "const": "EUR" }, "total": { "type": "number", "description": "Sólo presente cuando estado=estimacion. Importe sin IVA, incluye gasóleo." },
          "transporte": { "type": "number" }, "gasoleo": { "type": "number" }, "gasoleoProvisional": { "type": "boolean" }, "semanaGasoleo": { "type": "string" },
          "ldmDeclarado": { "type": "number" }, "ldmFacturable": { "type": "number" }, "motivoLdm": { "type": ["string", "null"] },
          "condiciones": { "type": "string" }, "mensaje": { "type": "string" }, "fuente": { "const": "SAVA" }, "siguientePaso": { "type": "string" },
          "supuestos": {
            "type": "object", "description": "Presente en la consulta pública. Se debe mostrar al usuario junto al importe.",
            "properties": {
              "origenPais": { "const": "ES" }, "paisDestino": { "type": "string" }, "cpOrigen": { "type": "string" }, "cpDestino": { "type": "string" },
              "origenDeReferencia": { "type": "boolean" }, "destinoDeReferencia": { "type": "boolean" }, "pesoNoIndicado": { "type": "boolean" },
              "pesoUtilizadoKg": { "type": "number" }, "limitePesoSupuestoKg": { "type": ["number", "null"] }, "fechaNoIndicada": { "type": "boolean" },
              "fechaCarga": { "type": "string", "format": "date" }, "mercancia": { "type": "string" }
            }
          }
        }
      }
    },
    "responses": {
      "Error": { "description": "Solicitud rechazada. No hay precio utilizable." },
      "Limite": { "description": "Cuota agotada. Esperar; no rotar IP o credencial para evitarla.", "headers": { "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Segundos mínimos antes de reintentar" } } },
      "NoDisponible": { "description": "No se puede obtener un precio actual. No reutilizar un importe como si acabara de calcularse.", "headers": { "Retry-After": { "schema": { "type": "integer", "minimum": 1 } } } }
    }
  }
}
