{
  "openapi": "3.0.3",
  "info": {
    "title": "UPTEP · API du bot Discord",
    "version": "1.0.0",
    "description": "API privée pour le bot Discord de l’alliance UPTEP sur le serveur 5.\n\n### Authentification\nToutes les opérations nécessitent `Authorization: Bearer VOTRE_SECRET`. Chaque endpoint vérifie sa permission. Aucun secret n’est fourni par cette documentation. Le bot est un client de confiance : il doit transmettre l’ID Discord réel de l’auteur de l’interaction. Les associations, pseudos et droits sont résolus par le serveur.\n\n### Parcours\n1. Créer une demande → `attente`.\n2. Un responsable chaîne la valide → `en_cours`.\n3. Enregistrer les convois partiels.\n4. Les deux restants atteignent zéro → `terminee`.\n\n### Fiabilité\n`commande_id` est unique pour toutes les écritures. Un retry identique renvoie la réponse initiale, qui peut refléter un état plus ancien ; ne pas changer l’ID entre tentatives. Un même ID avec un autre contenu renvoie 409. Les droits sont revérifiés, même pour un retry. Les ID et quantités sont des chaînes JSON pour conserver leur précision (maximum 9223372036854775807).\n\n### Pagination et limites\nLes listes sont paginées (100 éléments maximum). Réutiliser `suite_apres` dans le paramètre `apres` jusqu’à obtenir null. Limite actuelle : 30 requêtes par minute et par IP, rafale de 10. Une réponse 429 peut être en HTML.\n\n### Données réelles\nCes endpoints écrivent en base et sont indépendants du simulateur de ressources. Les valeurs d’exemple sont fictives."
  },
  "servers": [
    {
      "url": "https://uptep.top/api/bot/v1",
      "description": "API de production UPTEP"
    }
  ],
  "tags": [
    {
      "name": "Disponibilité"
    },
    {
      "name": "Demandes"
    },
    {
      "name": "Convois"
    }
  ],
  "security": [
    {
      "BotBearer": []
    }
  ],
  "paths": {
    "/health": {
      "get": {
        "tags": [
          "Disponibilité"
        ],
        "summary": "Vérifier que l’API répond",
        "description": "**Permission du secret : `health:read`.**\n\nContrôle de disponibilité de l’API ; ne teste pas PostgreSQL.",
        "responses": {
          "200": {
            "description": "Succès.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "service": {
                      "type": "string",
                      "example": "uptep-bot-api"
                    },
                    "version": {
                      "type": "string",
                      "example": "1"
                    },
                    "status": {
                      "type": "string",
                      "example": "healthy"
                    },
                    "time": {
                      "type": "string",
                      "format": "date-time"
                    }
                  },
                  "required": [
                    "ok",
                    "service",
                    "version",
                    "status",
                    "time"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Secret absent, invalide, désactivé ou expiré.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          },
          "403": {
            "description": "Permission du secret insuffisante, association Discord invalide ou droit utilisateur absent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          },
          "405": {
            "description": "Méthode HTTP refusée.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          },
          "429": {
            "description": "Limite de requêtes dépassée (réponse Nginx HTML).",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "503": {
            "description": "Service momentanément indisponible.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          }
        },
        "operationId": "get__health"
      }
    },
    "/demandes": {
      "get": {
        "tags": [
          "Demandes"
        ],
        "summary": "Lister les demandes ouvertes",
        "description": "**Permission du secret : `demandes:read`.**\n\nDemandes non terminées, ordonnées par ID croissant. Suivre `suite_apres` pour récupérer toutes les pages.",
        "responses": {
          "200": {
            "description": "Succès.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "demandes": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Demande"
                      }
                    },
                    "suite_apres": {
                      "type": "string",
                      "pattern": "^[1-9][0-9]{0,18}$",
                      "maxLength": 19,
                      "description": "Entier exact transmis comme chaîne JSON, maximum 9223372036854775807.",
                      "example": "123456789012345678",
                      "nullable": true
                    }
                  },
                  "required": [
                    "ok",
                    "demandes",
                    "suite_apres"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Secret absent, invalide, désactivé ou expiré.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          },
          "403": {
            "description": "Permission du secret insuffisante, association Discord invalide ou droit utilisateur absent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          },
          "405": {
            "description": "Méthode HTTP refusée.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          },
          "429": {
            "description": "Limite de requêtes dépassée (réponse Nginx HTML).",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "503": {
            "description": "Service momentanément indisponible.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          },
          "404": {
            "description": "Route ou demande introuvable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          },
          "422": {
            "description": "Valeurs, champs ou paramètres invalides.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "etat",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "attente",
                "en_cours",
                "les_deux"
              ],
              "default": "les_deux"
            },
            "description": "attente : non validées ; en_cours : validées non terminées ; les_deux : les deux catégories."
          },
          {
            "name": "limite",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 100
            },
            "description": "Nombre maximal de résultats par page."
          },
          {
            "name": "apres",
            "in": "query",
            "schema": {
              "type": "string",
              "pattern": "^(0|[1-9][0-9]{0,18})$",
              "default": "0"
            },
            "description": "Curseur suite_apres de la réponse précédente. Continuer jusqu’à null."
          }
        ],
        "operationId": "get__demandes"
      },
      "post": {
        "tags": [
          "Demandes"
        ],
        "summary": "Créer une demande de ressources",
        "description": "**Permission du secret : `demandes:create`.**\n\nLe demandeur doit avoir une association Discord unique dans la chaîne et être actuellement membre UPTEP sur s5. Les quantités restantes sont initialisées aux quantités originales. Au moins une ressource doit être supérieure à zéro. Pseudo et dates sont déterminés par le serveur.",
        "responses": {
          "201": {
            "description": "Succès.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "demande": {
                      "$ref": "#/components/schemas/Demande"
                    }
                  },
                  "required": [
                    "ok",
                    "demande"
                  ]
                }
              }
            },
            "headers": {
              "Idempotency-Replayed": {
                "description": "true si la réponse initiale est rejouée, false pour une nouvelle écriture.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "true",
                    "false"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Secret absent, invalide, désactivé ou expiré.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          },
          "403": {
            "description": "Permission du secret insuffisante, association Discord invalide ou droit utilisateur absent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          },
          "405": {
            "description": "Méthode HTTP refusée.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          },
          "429": {
            "description": "Limite de requêtes dépassée (réponse Nginx HTML).",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "503": {
            "description": "Service momentanément indisponible.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          },
          "400": {
            "description": "JSON invalide.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          },
          "404": {
            "description": "Route ou demande introuvable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          },
          "409": {
            "description": "Conflit : commande réutilisée avec un autre contenu, validation déjà faite, demande non validée/terminée ou dépassement du restant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          },
          "415": {
            "description": "Content-Type application/json requis.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          },
          "422": {
            "description": "Valeurs, champs ou paramètres invalides.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreationDemande"
              }
            }
          }
        },
        "operationId": "post__demandes"
      }
    },
    "/demandes/{id}/validation": {
      "post": {
        "tags": [
          "Demandes"
        ],
        "summary": "Valider une demande",
        "description": "**Permission du secret : `demandes:validate`.**\n\nLe validateur doit avoir une association Discord unique, être membre UPTEP et posséder un unique compte du site lié à son joueur, actif, avec le droit **chaine** et son mot de passe initial changé. Une validation déjà effectuée est refusée, sauf retry identique de la même commande.",
        "responses": {
          "200": {
            "description": "Succès.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "demande": {
                      "$ref": "#/components/schemas/Demande"
                    }
                  },
                  "required": [
                    "ok",
                    "demande"
                  ]
                }
              }
            },
            "headers": {
              "Idempotency-Replayed": {
                "description": "true si la réponse initiale est rejouée, false pour une nouvelle écriture.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "true",
                    "false"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Secret absent, invalide, désactivé ou expiré.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          },
          "403": {
            "description": "Permission du secret insuffisante, association Discord invalide ou droit utilisateur absent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          },
          "405": {
            "description": "Méthode HTTP refusée.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          },
          "429": {
            "description": "Limite de requêtes dépassée (réponse Nginx HTML).",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "503": {
            "description": "Service momentanément indisponible.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          },
          "400": {
            "description": "JSON invalide.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          },
          "404": {
            "description": "Route ou demande introuvable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          },
          "409": {
            "description": "Conflit : commande réutilisée avec un autre contenu, validation déjà faite, demande non validée/terminée ou dépassement du restant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          },
          "415": {
            "description": "Content-Type application/json requis.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          },
          "422": {
            "description": "Valeurs, champs ou paramètres invalides.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[1-9][0-9]{0,18}$",
              "maxLength": 19,
              "description": "Entier exact transmis comme chaîne JSON, maximum 9223372036854775807.",
              "example": "1"
            },
            "description": "ID de la demande."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Validation"
              }
            }
          }
        },
        "operationId": "post__demandes_id_validation"
      }
    },
    "/demandes/{id}/convois": {
      "get": {
        "tags": [
          "Convois"
        ],
        "summary": "Lister les convois d’une demande",
        "description": "**Permission du secret : `convois:read`.**\n\nFonctionne aussi pour les demandes terminées. Ordre d’enregistrement (ID croissant). Une demande existante sans convoi renvoie une liste vide ; une demande absente renvoie 404. Suivre `suite_apres` pour obtenir tous les convois.",
        "responses": {
          "200": {
            "description": "Succès.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "iddemande": {
                      "$ref": "#/components/schemas/ID"
                    },
                    "convois": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Convoi"
                      }
                    },
                    "suite_apres": {
                      "type": "string",
                      "pattern": "^[1-9][0-9]{0,18}$",
                      "maxLength": 19,
                      "description": "Entier exact transmis comme chaîne JSON, maximum 9223372036854775807.",
                      "example": "123456789012345678",
                      "nullable": true
                    }
                  },
                  "required": [
                    "ok",
                    "iddemande",
                    "convois",
                    "suite_apres"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Secret absent, invalide, désactivé ou expiré.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          },
          "403": {
            "description": "Permission du secret insuffisante, association Discord invalide ou droit utilisateur absent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          },
          "405": {
            "description": "Méthode HTTP refusée.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          },
          "429": {
            "description": "Limite de requêtes dépassée (réponse Nginx HTML).",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "503": {
            "description": "Service momentanément indisponible.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          },
          "404": {
            "description": "Route ou demande introuvable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          },
          "422": {
            "description": "Valeurs, champs ou paramètres invalides.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[1-9][0-9]{0,18}$",
              "maxLength": 19,
              "description": "Entier exact transmis comme chaîne JSON, maximum 9223372036854775807.",
              "example": "1"
            },
            "description": "ID de la demande."
          },
          {
            "name": "limite",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 100
            },
            "description": "Nombre maximal de résultats par page."
          },
          {
            "name": "apres",
            "in": "query",
            "schema": {
              "type": "string",
              "pattern": "^(0|[1-9][0-9]{0,18})$",
              "default": "0"
            },
            "description": "Curseur suite_apres de la réponse précédente. Continuer jusqu’à null."
          }
        ],
        "operationId": "get__demandes_id_convois"
      },
      "post": {
        "tags": [
          "Convois"
        ],
        "summary": "Enregistrer un convoi effectué",
        "description": "**Permission du secret : `convois:create`.**\n\nL’expéditeur doit être un membre UPTEP associé à son Discord, différent du demandeur. La demande doit être validée et non terminée. Chaque quantité doit être inférieure ou égale au restant correspondant. L’enregistrement et la soustraction des restants sont atomiques. Les deux restants à zéro clôturent automatiquement la demande. Ce signalement ne déclenche aucun envoi dans le jeu et ne prouve pas l’exécution du convoi.",
        "responses": {
          "201": {
            "description": "Succès.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "demande": {
                      "$ref": "#/components/schemas/Demande"
                    },
                    "convoi": {
                      "$ref": "#/components/schemas/Convoi"
                    }
                  },
                  "required": [
                    "ok",
                    "demande",
                    "convoi"
                  ]
                }
              }
            },
            "headers": {
              "Idempotency-Replayed": {
                "description": "true si la réponse initiale est rejouée, false pour une nouvelle écriture.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "true",
                    "false"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Secret absent, invalide, désactivé ou expiré.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          },
          "403": {
            "description": "Permission du secret insuffisante, association Discord invalide ou droit utilisateur absent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          },
          "405": {
            "description": "Méthode HTTP refusée.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          },
          "429": {
            "description": "Limite de requêtes dépassée (réponse Nginx HTML).",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "503": {
            "description": "Service momentanément indisponible.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          },
          "400": {
            "description": "JSON invalide.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          },
          "404": {
            "description": "Route ou demande introuvable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          },
          "409": {
            "description": "Conflit : commande réutilisée avec un autre contenu, validation déjà faite, demande non validée/terminée ou dépassement du restant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          },
          "415": {
            "description": "Content-Type application/json requis.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          },
          "422": {
            "description": "Valeurs, champs ou paramètres invalides.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Erreur"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[1-9][0-9]{0,18}$",
              "maxLength": 19,
              "description": "Entier exact transmis comme chaîne JSON, maximum 9223372036854775807.",
              "example": "1"
            },
            "description": "ID de la demande."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DeclarationConvoi"
              },
              "example": {
                "commande_id": "EXEMPLE-remplacer-par-interaction-id",
                "discord_id": "123456789012345678",
                "nbpomme": "3000",
                "nbmateriaux": "5000"
              }
            }
          }
        },
        "operationId": "post__demandes_id_convois"
      }
    }
  },
  "components": {
    "securitySchemes": {
      "BotBearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "Secret dédié au bot. Saisir uniquement le token, sans le préfixe Bearer."
      }
    },
    "schemas": {
      "ID": {
        "type": "string",
        "pattern": "^[1-9][0-9]{0,18}$",
        "maxLength": 19,
        "description": "Entier exact transmis comme chaîne JSON, maximum 9223372036854775807.",
        "example": "123456789012345678"
      },
      "Quantite": {
        "type": "string",
        "pattern": "^(0|[1-9][0-9]{0,18})$",
        "maxLength": 19,
        "description": "Quantité entière positive ou nulle, maximum 9223372036854775807.",
        "example": "10000"
      },
      "Erreur": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "enum": [
              false
            ]
          },
          "error": {
            "type": "string"
          }
        },
        "required": [
          "ok",
          "error"
        ]
      },
      "Demande": {
        "type": "object",
        "properties": {
          "id": {
            "$ref": "#/components/schemas/ID"
          },
          "serveur": {
            "type": "string",
            "enum": [
              "s5"
            ]
          },
          "iddemandeur": {
            "$ref": "#/components/schemas/ID"
          },
          "pseudo_demandeur": {
            "type": "string"
          },
          "discord_demandeur": {
            "$ref": "#/components/schemas/ID"
          },
          "nbpomme_original": {
            "$ref": "#/components/schemas/Quantite"
          },
          "nbpomme_restant": {
            "$ref": "#/components/schemas/Quantite"
          },
          "nbmateriaux_original": {
            "$ref": "#/components/schemas/Quantite"
          },
          "nbmateriaux_restant": {
            "$ref": "#/components/schemas/Quantite"
          },
          "valide": {
            "type": "boolean"
          },
          "idvalidateur": {
            "type": "string",
            "pattern": "^[1-9][0-9]{0,18}$",
            "maxLength": 19,
            "description": "Entier exact transmis comme chaîne JSON, maximum 9223372036854775807.",
            "example": "123456789012345678",
            "nullable": true
          },
          "pseudo_validateur": {
            "type": "string",
            "nullable": true
          },
          "discord_validateur": {
            "type": "string",
            "pattern": "^[1-9][0-9]{0,18}$",
            "maxLength": 19,
            "description": "Entier exact transmis comme chaîne JSON, maximum 9223372036854775807.",
            "example": "123456789012345678",
            "nullable": true
          },
          "date_demande": {
            "type": "string",
            "description": "Date PostgreSQL avec fuseau horaire (UTC).",
            "example": "2026-10-10 18:00:00+00"
          },
          "date_validation": {
            "type": "string",
            "description": "Date PostgreSQL avec fuseau horaire (UTC).",
            "example": "2026-10-10 18:00:00+00",
            "nullable": true
          },
          "fini": {
            "type": "boolean"
          },
          "date_fin": {
            "type": "string",
            "description": "Date PostgreSQL avec fuseau horaire (UTC).",
            "example": "2026-10-10 18:00:00+00",
            "nullable": true
          },
          "etat": {
            "type": "string",
            "enum": [
              "attente",
              "en_cours",
              "terminee"
            ]
          }
        },
        "required": [
          "id",
          "serveur",
          "iddemandeur",
          "pseudo_demandeur",
          "discord_demandeur",
          "nbpomme_original",
          "nbpomme_restant",
          "nbmateriaux_original",
          "nbmateriaux_restant",
          "valide",
          "idvalidateur",
          "pseudo_validateur",
          "discord_validateur",
          "date_demande",
          "date_validation",
          "fini",
          "date_fin",
          "etat"
        ]
      },
      "Convoi": {
        "type": "object",
        "properties": {
          "id": {
            "$ref": "#/components/schemas/ID"
          },
          "iddemande": {
            "$ref": "#/components/schemas/ID"
          },
          "idenvoyeur": {
            "$ref": "#/components/schemas/ID"
          },
          "pseudo_envoyeur": {
            "type": "string"
          },
          "discord_envoyeur": {
            "$ref": "#/components/schemas/ID"
          },
          "nbpomme": {
            "$ref": "#/components/schemas/Quantite"
          },
          "nbmateriaux": {
            "$ref": "#/components/schemas/Quantite"
          },
          "date_convoi": {
            "type": "string",
            "description": "Date PostgreSQL avec fuseau horaire (UTC).",
            "example": "2026-10-10 18:00:00+00"
          },
          "date_enregistrement": {
            "type": "string",
            "description": "Date PostgreSQL avec fuseau horaire (UTC).",
            "example": "2026-10-10 18:00:00+00"
          }
        },
        "required": [
          "id",
          "iddemande",
          "idenvoyeur",
          "pseudo_envoyeur",
          "discord_envoyeur",
          "nbpomme",
          "nbmateriaux",
          "date_convoi"
        ]
      },
      "Validation": {
        "type": "object",
        "properties": {
          "commande_id": {
            "type": "string",
            "pattern": "^[A-Za-z0-9_.:-]{1,100}$",
            "maxLength": 100,
            "description": "Identifiant unique de l’interaction Discord. Réutiliser le même identifiant et le même contenu lors des retries.",
            "example": "EXEMPLE-remplacer-par-interaction-id"
          },
          "discord_id": {
            "type": "string",
            "pattern": "^[1-9][0-9]{0,18}$",
            "maxLength": 19,
            "description": "ID Discord de l’auteur réel de la commande. Le bot doit le lire depuis l’interaction Discord, jamais depuis un champ libre.",
            "example": "123456789012345678"
          }
        },
        "required": [
          "commande_id",
          "discord_id"
        ],
        "additionalProperties": false
      },
      "CreationDemande": {
        "type": "object",
        "properties": {
          "commande_id": {
            "type": "string",
            "pattern": "^[A-Za-z0-9_.:-]{1,100}$",
            "maxLength": 100,
            "description": "Identifiant unique de l’interaction Discord. Réutiliser le même identifiant et le même contenu lors des retries.",
            "example": "EXEMPLE-remplacer-par-interaction-id"
          },
          "discord_id": {
            "type": "string",
            "pattern": "^[1-9][0-9]{0,18}$",
            "maxLength": 19,
            "description": "ID Discord de l’auteur réel de la commande. Le bot doit le lire depuis l’interaction Discord, jamais depuis un champ libre.",
            "example": "123456789012345678"
          },
          "nbpomme": {
            "$ref": "#/components/schemas/Quantite"
          },
          "nbmateriaux": {
            "$ref": "#/components/schemas/Quantite"
          }
        },
        "required": [
          "commande_id",
          "discord_id",
          "nbpomme",
          "nbmateriaux"
        ],
        "additionalProperties": false
      },
      "DeclarationConvoi": {
        "type": "object",
        "properties": {
          "commande_id": {
            "type": "string",
            "pattern": "^[A-Za-z0-9_.:-]{1,100}$",
            "maxLength": 100,
            "description": "Identifiant unique de l’interaction Discord. Réutiliser le même identifiant et le même contenu lors des retries.",
            "example": "EXEMPLE-remplacer-par-interaction-id"
          },
          "discord_id": {
            "type": "string",
            "pattern": "^[1-9][0-9]{0,18}$",
            "maxLength": 19,
            "description": "ID Discord de l’auteur réel de la commande. Le bot doit le lire depuis l’interaction Discord, jamais depuis un champ libre.",
            "example": "123456789012345678"
          },
          "nbpomme": {
            "$ref": "#/components/schemas/Quantite"
          },
          "nbmateriaux": {
            "$ref": "#/components/schemas/Quantite"
          },
          "date_convoi": {
            "type": "string",
            "format": "date-time",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(Z|[+-]\\d{2}:\\d{2})$",
            "description": "Facultatif, heure serveur par défaut. ISO 8601 avec secondes et fuseau ; pas de date future au-delà de 60 secondes de tolérance.",
            "example": "2026-10-10T20:00:00+02:00"
          }
        },
        "required": [
          "commande_id",
          "discord_id",
          "nbpomme",
          "nbmateriaux"
        ],
        "additionalProperties": false
      }
    }
  }
}
