{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/lexio-rent/json-schema/lexio-rent-quote-schema.json",
  "title": "Quote",
  "description": "Devis : prix garanti, frais et code promo compris, jusqu’à `expiresAt`. Il ne réserve pas le véhicule.",
  "x-generated": "2026-11-10",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/lexio-rent-openapi.json#/components/schemas/Quote",
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "format": "uuid",
      "description": "Identifiant du devis, à passer en `quoteId`."
    },
    "createdAt": {
      "type": "string",
      "format": "date-time",
      "description": "Instant du calcul."
    },
    "expiresAt": {
      "type": "string",
      "format": "date-time",
      "description": "Fin de la garantie de prix, 24 h après `createdAt` ; au-delà, `POST /rentals` répond 409 `QUOTE_EXPIRED`."
    },
    "consumedAt": {
      "anyOf": [
        {
          "type": "string",
          "format": "date-time"
        },
        {
          "type": "null"
        }
      ],
      "description": "Instant où une location a été créée avec ce devis ; null tant qu’il n’a pas servi."
    },
    "rentalId": {
      "anyOf": [
        {
          "type": "string",
          "format": "uuid"
        },
        {
          "type": "null"
        }
      ],
      "description": "Location créée avec ce devis ; null tant qu’il n’a pas servi."
    },
    "currency": {
      "type": "string",
      "enum": [
        "EUR",
        "XPF",
        "CHF",
        "CAD",
        "GBP",
        "USD",
        "XOF"
      ],
      "description": "Devise ISO 4217 : tous les montants de la ressource sont dans cette devise."
    },
    "vehicle": {
      "$ref": "#/$defs/VehicleRef"
    },
    "period": {
      "$ref": "#/$defs/Period"
    },
    "mileage": {
      "$ref": "#/$defs/Mileage"
    },
    "deductible": {
      "anyOf": [
        {
          "$ref": "#/$defs/DeductibleSnapshot"
        },
        {
          "type": "null"
        }
      ],
      "description": "Franchise retenue ; null sans franchise."
    },
    "deposit": {
      "anyOf": [
        {
          "$ref": "#/$defs/DepositSnapshot"
        },
        {
          "type": "null"
        }
      ],
      "description": "Caution retenue ; null sans caution."
    },
    "promoCode": {
      "anyOf": [
        {
          "type": "string",
          "maxLength": 50
        },
        {
          "type": "null"
        }
      ],
      "description": "Code promo appliqué, en majuscules ; null sans code."
    },
    "isVatReverseCharge": {
      "type": "boolean",
      "description": "true : autoliquidation de la TVA, `vatPercent` vaut 0 et `totalTTC` = `totalHT`."
    },
    "vatPercent": {
      "type": "number",
      "minimum": 0,
      "maximum": 100,
      "description": "Taux de TVA appliqué, en pourcentage (20 = 20 %) ; 0 en autoliquidation ou pour un loueur exonéré. Au plus 2 décimales."
    },
    "totalHT": {
      "type": "number",
      "description": "Total hors taxes : somme des `lines[].amountHT`."
    },
    "vatAmount": {
      "type": "number",
      "description": "Montant de la TVA : `totalTTC − totalHT`."
    },
    "totalTTC": {
      "type": "number",
      "description": "Total toutes taxes comprises : somme des `lines[].amountTTC`. Caution et franchise n’y sont jamais comptées."
    },
    "lines": {
      "maxItems": 100,
      "type": "array",
      "items": {
        "$ref": "#/$defs/PriceLine"
      },
      "description": "Détail du prix, une ligne par règle appliquée."
    },
    "pricingBasis": {
      "$ref": "#/$defs/PricingBasis"
    },
    "choices": {
      "type": "object",
      "properties": {
        "mileagePackages": {
          "maxItems": 50,
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "mileage": {
                "$ref": "#/$defs/Mileage"
              },
              "line": {
                "description": "Ligne `MILEAGE` que ce forfait produirait.",
                "$ref": "#/$defs/PriceLine"
              }
            },
            "required": [
              "mileage",
              "line"
            ]
          }
        },
        "deductibles": {
          "maxItems": 50,
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "deductible": {
                "$ref": "#/$defs/DeductibleSnapshot"
              },
              "deposit": {
                "anyOf": [
                  {
                    "$ref": "#/$defs/DepositSnapshot"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "Caution qui serait retenue avec cette franchise ; null sans caution."
              },
              "line": {
                "description": "Ligne `DEDUCTIBLE` que cette franchise produirait.",
                "$ref": "#/$defs/PriceLine"
              }
            },
            "required": [
              "deductible",
              "deposit",
              "line"
            ]
          }
        },
        "options": {
          "maxItems": 100,
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "optionId": {
                "type": "string",
                "format": "uuid"
              },
              "name": {
                "type": "string",
                "maxLength": 200
              },
              "maxBookableQuantity": {
                "type": "integer",
                "minimum": 0,
                "description": "Quantité réservable sur la période : stock restant, borné par la quantité maximale de l’option."
              },
              "line": {
                "description": "Ligne `OPTION` pour une unité.",
                "$ref": "#/$defs/PriceLine"
              }
            },
            "required": [
              "optionId",
              "name",
              "maxBookableQuantity",
              "line"
            ]
          }
        }
      },
      "required": [
        "mileagePackages",
        "deductibles",
        "options"
      ],
      "description": "Prix de chaque forfait, franchise et option possible pour ce véhicule et cette période ; pour en retenir un autre, créez un nouveau devis. Le stock n’est pas réservé."
    }
  },
  "required": [
    "id",
    "createdAt",
    "expiresAt",
    "consumedAt",
    "rentalId",
    "currency",
    "vehicle",
    "period",
    "mileage",
    "deductible",
    "deposit",
    "promoCode",
    "isVatReverseCharge",
    "vatPercent",
    "totalHT",
    "vatAmount",
    "totalTTC",
    "lines",
    "pricingBasis",
    "choices"
  ],
  "$defs": {
    "DeductibleSnapshot": {
      "type": "object",
      "properties": {
        "deductibleId": {
          "anyOf": [
            {
              "type": "string",
              "format": "uuid"
            },
            {
              "type": "null"
            }
          ],
          "description": "Franchise du catalogue (`GET /deductibles`) ; null si elle a été supprimée depuis."
        },
        "name": {
          "anyOf": [
            {
              "type": "string",
              "maxLength": 200
            },
            {
              "type": "null"
            }
          ],
          "description": "Nom de la franchise ; null si elle a été supprimée depuis."
        },
        "description": {
          "anyOf": [
            {
              "type": "string",
              "maxLength": 2000
            },
            {
              "type": "null"
            }
          ],
          "description": "Description de la franchise ; null si absente."
        },
        "excessAmount": {
          "type": "number",
          "minimum": 0,
          "description": "Reste à charge du client en cas de sinistre, tel que fixé par le loueur, sans TVA appliquée. Jamais facturé ni compris dans les totaux : le prix de la franchise retenue est la ligne `DEDUCTIBLE` de `lines`."
        }
      },
      "required": [
        "deductibleId",
        "name",
        "description",
        "excessAmount"
      ],
      "description": "Franchise retenue, figée au moment du calcul."
    },
    "DepositSnapshot": {
      "type": "object",
      "properties": {
        "depositId": {
          "anyOf": [
            {
              "type": "string",
              "format": "uuid"
            },
            {
              "type": "null"
            }
          ],
          "description": "Caution du catalogue (`GET /deposits`) ; null si elle a été supprimée depuis."
        },
        "name": {
          "anyOf": [
            {
              "type": "string",
              "maxLength": 200
            },
            {
              "type": "null"
            }
          ],
          "description": "Nom de la caution ; null si elle a été supprimée depuis."
        },
        "depositAmount": {
          "type": "number",
          "minimum": 0,
          "description": "Caution demandée au client, telle que fixée par le loueur, sans TVA appliquée. Hors `totalTTC` et hors `balance` : elle s’encaisse à part (paiement `DEPOSIT`)."
        }
      },
      "required": [
        "depositId",
        "name",
        "depositAmount"
      ],
      "description": "Caution retenue, figée au moment du calcul."
    },
    "Mileage": {
      "type": "object",
      "properties": {
        "mileagePackageId": {
          "anyOf": [
            {
              "type": "string",
              "format": "uuid"
            },
            {
              "type": "null"
            }
          ],
          "description": "Forfait kilométrique du catalogue (`GET /mileage-packages`) ; null sans forfait."
        },
        "name": {
          "anyOf": [
            {
              "type": "string",
              "maxLength": 100
            },
            {
              "type": "null"
            }
          ],
          "description": "Nom du forfait ; null sans forfait."
        },
        "isUnlimited": {
          "type": "boolean",
          "description": "true : aucun plafond ; `includedKm` et `overageRatePerKmHT` valent alors null."
        },
        "includedKm": {
          "anyOf": [
            {
              "type": "integer",
              "minimum": 0
            },
            {
              "type": "null"
            }
          ],
          "description": "Kilomètres inclus pour toute la période, pas par jour ; null si illimité."
        },
        "overageRatePerKmHT": {
          "anyOf": [
            {
              "type": "number",
              "minimum": 0,
              "maximum": 999999.9999,
              "description": "Au plus 4 décimales."
            },
            {
              "type": "null"
            }
          ],
          "description": "Prix HT de chaque kilomètre au-delà de `includedKm`, facturé au retour ; null si illimité ou sans tarif de dépassement."
        }
      },
      "required": [
        "mileagePackageId",
        "name",
        "isUnlimited",
        "includedKm",
        "overageRatePerKmHT"
      ],
      "description": "Kilométrage inclus et prix du dépassement."
    },
    "Period": {
      "type": "object",
      "properties": {
        "startAt": {
          "type": "string",
          "format": "date-time",
          "description": "Début contractuel, à l’heure du loueur."
        },
        "endAt": {
          "type": "string",
          "format": "date-time",
          "description": "Fin contractuelle, à l’heure du loueur."
        },
        "timezone": {
          "type": "string",
          "minLength": 1,
          "description": "Fuseau horaire IANA du loueur."
        },
        "billedDays": {
          "type": "integer",
          "minimum": 0,
          "description": "Jours facturés : tranches de 24 h entamées, sans compter une dernière tranche d’au plus `toleranceHours` heures (`GET /organization`) ; 1 au minimum."
        },
        "durationMinutes": {
          "type": "integer",
          "minimum": 0,
          "description": "Durée exacte entre `startAt` et `endAt`, en minutes."
        }
      },
      "required": [
        "startAt",
        "endAt",
        "timezone",
        "billedDays",
        "durationMinutes"
      ],
      "description": "Période contractuelle, bornes à l’heure du loueur."
    },
    "PriceLine": {
      "type": "object",
      "properties": {
        "key": {
          "type": "string",
          "enum": [
            "BASE",
            "LEAD_TIME_ADJUSTMENT",
            "DURATION_DISCOUNT",
            "COMMERCIAL_DISCOUNT",
            "WEEKEND_SURCHARGE",
            "SHORT_DURATION_SURCHARGE",
            "MILEAGE",
            "DEDUCTIBLE",
            "OPTION",
            "CHARGE",
            "PROMO",
            "MILEAGE_OVERAGE",
            "MODIFICATION"
          ],
          "description": "Règle qui a produit la ligne : `BASE` (prix de location), `LEAD_TIME_ADJUSTMENT` (délai de réservation), `DURATION_DISCOUNT` (remise de durée), `COMMERCIAL_DISCOUNT` (remise commerciale), `WEEKEND_SURCHARGE` et `SHORT_DURATION_SURCHARGE` (majorations), `MILEAGE` (forfait kilométrique), `DEDUCTIBLE` (prix de la franchise retenue), `OPTION`, `CHARGE` (frais ou remise saisis), `PROMO` (code promo), `MILEAGE_OVERAGE` (dépassement kilométrique au retour), `MODIFICATION` (écart de prix d’une modification de la location)."
        },
        "label": {
          "type": "string",
          "description": "Libellé affichable ; ne pas l’analyser, lire `key`."
        },
        "refId": {
          "anyOf": [
            {
              "type": "string",
              "format": "uuid"
            },
            {
              "type": "null"
            }
          ],
          "description": "Élément à l’origine de la ligne : `optionId` pour `OPTION`, identifiant du frais pour `CHARGE` et `MILEAGE_OVERAGE` d’une location ; null sinon."
        },
        "quantity": {
          "type": "integer",
          "minimum": 1,
          "description": "Nombre d’exemplaires (option) ; 1 pour les autres lignes."
        },
        "unitPriceHT": {
          "type": "number",
          "description": "Prix HT d’un exemplaire pour toute la période (`OPTION`) ; égal à `amountHT` sinon. Indicatif, arrondi à l’unité de la devise : `amountHT` fait foi, `unitPriceHT` × `quantity` peut différer d’une unité."
        },
        "unitPriceTTC": {
          "type": "number",
          "description": "Prix TTC d’un exemplaire pour toute la période (`OPTION`) ; égal à `amountTTC` sinon. Indicatif, arrondi à l’unité de la devise : `amountTTC` fait foi, `unitPriceTTC` × `quantity` peut différer d’une unité."
        },
        "amountHT": {
          "type": "number",
          "description": "Montant HT de la ligne, signé : négatif pour une remise."
        },
        "amountTTC": {
          "type": "number",
          "description": "Montant TTC de la ligne, signé : négatif pour une remise."
        },
        "ratePercent": {
          "anyOf": [
            {
              "type": "number",
              "minimum": 0,
              "maximum": 100,
              "description": "Au plus 2 décimales."
            },
            {
              "type": "null"
            }
          ],
          "description": "Pourcentage appliqué (20 = 20 %) pour une remise de durée, une majoration, une franchise ou un forfait tarifés en pourcentage ; null pour les autres lignes et pour un montant saisi ou offert."
        }
      },
      "required": [
        "key",
        "label",
        "refId",
        "quantity",
        "unitPriceHT",
        "unitPriceTTC",
        "amountHT",
        "amountTTC",
        "ratePercent"
      ],
      "description": "Ligne de prix d’une offre, d’un devis ou d’une location."
    },
    "PricingBasis": {
      "type": "object",
      "properties": {
        "dailyPrices": {
          "maxItems": 1095,
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "date": {
                "type": "string",
                "format": "date",
                "description": "Jour facturé, à l’heure du loueur."
              },
              "seasonSlug": {
                "description": "Saison du jour (`GET /seasons`) ; null sans saison.",
                "type": [
                  "string",
                  "null"
                ]
              },
              "dailyPriceHT": {
                "type": "number",
                "minimum": 0,
                "description": "Prix HT du jour au catalogue, avant facteur de délai."
              }
            },
            "required": [
              "date",
              "seasonSlug",
              "dailyPriceHT"
            ]
          },
          "description": "Un élément par jour facturé."
        },
        "leadTime": {
          "anyOf": [
            {
              "type": "object",
              "properties": {
                "leadTimeTierId": {
                  "type": "string",
                  "format": "uuid",
                  "description": "Palier de délai (`GET /lead-time-tiers`)."
                },
                "leadDays": {
                  "type": "integer",
                  "minimum": 0,
                  "description": "Jours calendaires entre le calcul du prix et le départ."
                },
                "priceFactors": {
                  "maxItems": 10,
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "seasonSlug": {
                        "type": "string"
                      },
                      "priceFactor": {
                        "type": "number",
                        "minimum": 0.1,
                        "maximum": 10,
                        "description": "Facteur du prix journalier (1 = inchangé). Au plus 4 décimales."
                      }
                    },
                    "required": [
                      "seasonSlug",
                      "priceFactor"
                    ]
                  },
                  "description": "Facteur appliqué au prix journalier de chaque saison."
                }
              },
              "required": [
                "leadTimeTierId",
                "leadDays",
                "priceFactors"
              ]
            },
            {
              "type": "null"
            }
          ],
          "description": "Palier de délai de réservation appliqué ; null sans palier."
        }
      },
      "required": [
        "dailyPrices",
        "leadTime"
      ],
      "description": "Explication du prix : base journalière et délai de réservation. Elle explique les lignes sans permettre de recalculer le total, qui se lit dans `lines`."
    },
    "VehicleRef": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "format": "uuid",
          "description": "Identifiant du véhicule."
        },
        "name": {
          "anyOf": [
            {
              "type": "string",
              "maxLength": 100
            },
            {
              "type": "null"
            }
          ],
          "description": "Nom d’usage donné par le loueur (ex. « 208 blanche ») ; null si absent."
        },
        "brand": {
          "type": "string",
          "maxLength": 50,
          "description": "Marque (ex. Peugeot)."
        },
        "model": {
          "type": "string",
          "maxLength": 50,
          "description": "Modèle (ex. 208)."
        },
        "vehicleType": {
          "type": "string",
          "enum": [
            "CITY",
            "COMPACT",
            "CROSSOVER",
            "SEDAN",
            "BREAK",
            "SUV",
            "OFF_ROAD",
            "PICKUP",
            "MINIVAN",
            "VAN",
            "UTILITY",
            "COUPE",
            "CONVERTIBLE",
            "SPORT",
            "LUXURY",
            "MOTORCYCLE",
            "SCOOTER"
          ],
          "description": "Type de véhicule : il fixe la famille et les champs applicables."
        },
        "plate": {
          "description": "Immatriculation, en majuscules ; null si le véhicule n’en a pas. Une plaque ou un numéro de série est toujours présent.",
          "type": [
            "string",
            "null"
          ]
        },
        "serialNumber": {
          "description": "Numéro de série (deux-roues non immatriculé, par exemple) ; null si absent.",
          "type": [
            "string",
            "null"
          ]
        }
      },
      "required": [
        "id",
        "name",
        "brand",
        "model",
        "vehicleType",
        "plate",
        "serialNumber"
      ],
      "description": "Véhicule, vue réduite de `GET /vehicles/{vehicleId}`."
    }
  }
}