{
    "openapi": "3.1.0",
    "info": {
        "title": "Developia API",
        "version": "v1",
        "description": "API commerçant Developia — accès aux données du compte via token Bearer. Chaque token est lié à un commerçant unique et ne peut accéder qu'à ses propres données. Aucun endpoint admin plateforme, aucune donnée cross-commerçant.\n\n**Posture actuelle** : API fermée (pas de doc publique, aucun tiers inscrit). Utilisée uniquement pour tests internes et intégrations custom du super-admin.",
        "contact": {
            "name": "Support Developia",
            "email": "support@developia.fr"
        }
    },
    "servers": [
        {
            "url": "https://api.developia.fr",
            "description": "Production"
        }
    ],
    "components": {
        "securitySchemes": {
            "bearerAuth": {
                "type": "http",
                "scheme": "bearer",
                "bearerFormat": "dvia_...",
                "description": "Token émis depuis `/parametres/api` dans le back-office commerçant. Format `dvia_<60 chars hex>`. Le token est lié à un commerçant unique."
            }
        },
        "schemas": {
            "Error": {
                "type": "object",
                "required": [
                    "error",
                    "message"
                ],
                "properties": {
                    "error": {
                        "type": "string",
                        "example": "not_found",
                        "description": "Code d'erreur snake_case machine-friendly."
                    },
                    "message": {
                        "type": "string",
                        "example": "Endpoint inconnu.",
                        "description": "Explication humaine."
                    }
                }
            },
            "Meta": {
                "type": "object",
                "description": "Métadonnées de pagination cursor-based. Passer `next_cursor` en `?cursor=` pour la page suivante.",
                "properties": {
                    "count": {
                        "type": "integer",
                        "example": 25
                    },
                    "limit": {
                        "type": "integer",
                        "example": 25
                    },
                    "next_cursor": {
                        "type": [
                            "integer",
                            "null"
                        ],
                        "example": 100,
                        "description": "null = plus de résultat."
                    }
                }
            },
            "Client": {
                "type": "object",
                "properties": {
                    "prenom": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "nom": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "email": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "email"
                    },
                    "telephone": {
                        "type": [
                            "string",
                            "null"
                        ]
                    }
                }
            },
            "Prestation": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "integer"
                    },
                    "nom": {
                        "type": "string"
                    },
                    "description": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "duree": {
                        "type": "integer",
                        "description": "Durée en minutes"
                    },
                    "prix": {
                        "type": "number",
                        "format": "float"
                    },
                    "sur_devis": {
                        "type": "boolean"
                    },
                    "acompte_type": {
                        "type": "string",
                        "enum": [
                            "aucun",
                            "fixe",
                            "pourcentage",
                            "empreinte"
                        ]
                    },
                    "acompte": {
                        "type": "number",
                        "format": "float"
                    },
                    "actif": {
                        "type": "boolean"
                    },
                    "visible_clients": {
                        "type": "boolean"
                    },
                    "nb_employes_requis": {
                        "type": "integer"
                    },
                    "created_at": {
                        "type": "string",
                        "format": "date-time"
                    }
                }
            },
            "RendezVous": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "integer"
                    },
                    "date_rdv": {
                        "type": "string",
                        "format": "date"
                    },
                    "heure_debut": {
                        "type": "string",
                        "example": "14:00"
                    },
                    "heure_fin": {
                        "type": "string",
                        "example": "15:00"
                    },
                    "statut": {
                        "type": "string",
                        "enum": [
                            "confirme",
                            "en_attente",
                            "annule",
                            "termine",
                            "no_show"
                        ]
                    },
                    "paiement_statut": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "enum": [
                            "paye",
                            "en_attente",
                            "rembourse",
                            "non_requis",
                            null
                        ]
                    },
                    "montant": {
                        "type": [
                            "number",
                            "null"
                        ],
                        "format": "float"
                    },
                    "montant_acompte": {
                        "type": [
                            "number",
                            "null"
                        ],
                        "format": "float"
                    },
                    "notes": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "Notes internes commerçant (uniquement sur le détail)."
                    },
                    "client": {
                        "$ref": "#/components/schemas/Client"
                    },
                    "prestation": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": [
                                    "integer",
                                    "null"
                                ]
                            },
                            "nom": {
                                "type": [
                                    "string",
                                    "null"
                                ]
                            }
                        }
                    },
                    "employe_id": {
                        "type": [
                            "integer",
                            "null"
                        ]
                    },
                    "created_at": {
                        "type": "string",
                        "format": "date-time"
                    }
                }
            },
            "ClientCRM": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "integer"
                    },
                    "email": {
                        "type": "string",
                        "format": "email"
                    },
                    "prenom": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "nom": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "telephone": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "adresse": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "code_postal": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "ville": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "pays": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "premiere_interaction_at": {
                        "type": "string",
                        "format": "date-time"
                    },
                    "derniere_interaction_at": {
                        "type": "string",
                        "format": "date-time"
                    }
                }
            },
            "Produit": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "integer"
                    },
                    "nom": {
                        "type": "string"
                    },
                    "description": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "prix": {
                        "type": "number",
                        "format": "float"
                    },
                    "stock": {
                        "type": "integer"
                    },
                    "actif": {
                        "type": "boolean"
                    },
                    "categorie": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "created_at": {
                        "type": "string",
                        "format": "date-time"
                    }
                }
            },
            "Commande": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "integer"
                    },
                    "statut": {
                        "type": "string",
                        "enum": [
                            "en_attente",
                            "paye",
                            "expedie",
                            "livre",
                            "annule",
                            "rembourse"
                        ]
                    },
                    "montant_total": {
                        "type": "number",
                        "format": "float"
                    },
                    "frais_livraison": {
                        "type": "number",
                        "format": "float"
                    },
                    "mode_livraison": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "client": {
                        "$ref": "#/components/schemas/Client"
                    },
                    "created_at": {
                        "type": "string",
                        "format": "date-time"
                    }
                }
            },
            "CommandeDetail": {
                "allOf": [
                    {
                        "$ref": "#/components/schemas/Commande"
                    },
                    {
                        "type": "object",
                        "properties": {
                            "adresse_livraison": {
                                "type": "object",
                                "properties": {
                                    "ligne1": {
                                        "type": [
                                            "string",
                                            "null"
                                        ]
                                    },
                                    "code_postal": {
                                        "type": [
                                            "string",
                                            "null"
                                        ]
                                    },
                                    "ville": {
                                        "type": [
                                            "string",
                                            "null"
                                        ]
                                    }
                                }
                            },
                            "lignes": {
                                "type": "array",
                                "items": {
                                    "type": "object",
                                    "properties": {
                                        "produit_id": {
                                            "type": [
                                                "integer",
                                                "null"
                                            ]
                                        },
                                        "nom": {
                                            "type": [
                                                "string",
                                                "null"
                                            ]
                                        },
                                        "quantite": {
                                            "type": "integer"
                                        },
                                        "prix_unitaire": {
                                            "type": "number",
                                            "format": "float"
                                        },
                                        "total": {
                                            "type": "number",
                                            "format": "float"
                                        }
                                    }
                                }
                            },
                            "updated_at": {
                                "type": "string",
                                "format": "date-time"
                            }
                        }
                    }
                ]
            },
            "ReservationHebergement": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "integer"
                    },
                    "hebergement": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": [
                                    "integer",
                                    "null"
                                ]
                            },
                            "nom": {
                                "type": [
                                    "string",
                                    "null"
                                ]
                            }
                        }
                    },
                    "date_arrivee": {
                        "type": "string",
                        "format": "date"
                    },
                    "date_depart": {
                        "type": "string",
                        "format": "date"
                    },
                    "nb_adultes": {
                        "type": "integer"
                    },
                    "nb_enfants": {
                        "type": "integer"
                    },
                    "statut": {
                        "type": "string"
                    },
                    "montant_total_ttc": {
                        "type": "number",
                        "format": "float"
                    },
                    "montant_acompte": {
                        "type": "number",
                        "format": "float"
                    },
                    "client": {
                        "$ref": "#/components/schemas/Client"
                    },
                    "created_at": {
                        "type": "string",
                        "format": "date-time"
                    }
                }
            },
            "ReservationRestaurant": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "integer"
                    },
                    "heure_arrivee": {
                        "type": "string",
                        "format": "date-time"
                    },
                    "duree_min": {
                        "type": "integer"
                    },
                    "nb_personnes": {
                        "type": "integer"
                    },
                    "statut": {
                        "type": "string",
                        "enum": [
                            "confirmee",
                            "annulee",
                            "terminee",
                            "no_show"
                        ]
                    },
                    "client": {
                        "$ref": "#/components/schemas/Client"
                    },
                    "notes_client": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "created_at": {
                        "type": "string",
                        "format": "date-time"
                    }
                }
            },
            "Modules": {
                "type": "object",
                "description": "Modules ouverts par le forfait. Les 31 forfaits commercialisés sont des COMBINAISONS de ces 6 modules : composer une interface en lisant le `nom` du forfait serait fragile, le libellé pouvant changer sans que le périmètre bouge.",
                "properties": {
                    "planning": {
                        "type": "boolean"
                    },
                    "boutique": {
                        "type": "boolean"
                    },
                    "lms": {
                        "type": "boolean"
                    },
                    "hebergement": {
                        "type": "boolean"
                    },
                    "restauration": {
                        "type": "boolean"
                    }
                }
            },
            "Jeton": {
                "type": "object",
                "properties": {
                    "token": {
                        "type": "string",
                        "example": "dvia_a1b2c3…",
                        "description": "Affiché UNE SEULE FOIS. Seul son SHA-256 est conservé côté serveur."
                    },
                    "commercant": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": "integer"
                            },
                            "nom_commerce": {
                                "type": "string"
                            },
                            "email": {
                                "type": "string"
                            },
                            "slug": {
                                "type": "string"
                            }
                        }
                    }
                }
            },
            "DefiOtp": {
                "type": "object",
                "description": "Réponse quand le commerçant a activé la double authentification. Le code à 6 chiffres part par mail ; il faut le présenter à `/v1/auth/otp` avec le `challenge`.",
                "properties": {
                    "otp_required": {
                        "type": "boolean",
                        "example": true
                    },
                    "challenge": {
                        "type": "string",
                        "description": "Jeton de défi. Valide 10 minutes, 5 tentatives maximum."
                    },
                    "email_indice": {
                        "type": "string",
                        "example": "no***@developia.fr"
                    },
                    "expire_dans": {
                        "type": "integer",
                        "example": 600,
                        "description": "Secondes."
                    }
                }
            },
            "Stats": {
                "type": "object",
                "properties": {
                    "periode": {
                        "type": "object",
                        "properties": {
                            "nom": {
                                "type": "string",
                                "enum": [
                                    "jour",
                                    "semaine",
                                    "mois"
                                ]
                            },
                            "debut": {
                                "type": "string",
                                "format": "date"
                            },
                            "fin": {
                                "type": "string",
                                "format": "date"
                            }
                        }
                    },
                    "ca": {
                        "type": "object",
                        "properties": {
                            "ht": {
                                "type": "number",
                                "format": "float"
                            },
                            "tva": {
                                "type": "number",
                                "format": "float"
                            },
                            "ttc": {
                                "type": "number",
                                "format": "float"
                            },
                            "count": {
                                "type": "integer",
                                "description": "Nombre de ventes sur la période."
                            }
                        }
                    },
                    "par_source": {
                        "type": "array",
                        "description": "Ventilation par module d'origine (boutique, planning, hébergement, restaurant, lms, facturation).",
                        "items": {
                            "type": "object",
                            "properties": {
                                "source": {
                                    "type": "string",
                                    "example": "boutique"
                                },
                                "libelle": {
                                    "type": "string",
                                    "example": "Boutique"
                                },
                                "ttc": {
                                    "type": "number",
                                    "format": "float"
                                },
                                "count": {
                                    "type": "integer"
                                }
                            }
                        }
                    },
                    "impayes": {
                        "type": "object",
                        "properties": {
                            "montant": {
                                "type": "number",
                                "format": "float"
                            },
                            "nombre": {
                                "type": "integer"
                            },
                            "en_retard": {
                                "type": "integer",
                                "description": "Factures dont la date d'échéance est dépassée."
                            }
                        }
                    }
                }
            },
            "Notification": {
                "type": "object",
                "description": "Les notifications proviennent de DEUX tables fusionnées par l'API. C'est le couple (`source`, `id`) qui désigne une notification, jamais l'`id` seul : les identifiants se chevauchent d'une source à l'autre.",
                "properties": {
                    "source": {
                        "type": "string",
                        "enum": [
                            "boutique",
                            "planning"
                        ],
                        "description": "Table d'origine. À renvoyer tel quel à `/v1/notifications/lire`."
                    },
                    "id": {
                        "type": "integer"
                    },
                    "module": {
                        "type": "string",
                        "example": "boutique"
                    },
                    "type": {
                        "type": "string",
                        "example": "commande"
                    },
                    "message": {
                        "type": "string"
                    },
                    "ref_id": {
                        "type": "integer",
                        "nullable": true,
                        "description": "Identifiant de l'objet concerné (commande, rendez-vous…)."
                    },
                    "lu": {
                        "type": "boolean"
                    },
                    "created_at": {
                        "type": "string",
                        "format": "date-time"
                    }
                }
            }
        },
        "parameters": {
            "cursor": {
                "name": "cursor",
                "in": "query",
                "description": "ID de dernier élément retourné (pagination cursor-based).",
                "required": false,
                "schema": {
                    "type": "integer"
                }
            },
            "limit": {
                "name": "limit",
                "in": "query",
                "description": "Nombre max de résultats par page (1-100, défaut 25).",
                "required": false,
                "schema": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 100,
                    "default": 25
                }
            },
            "from": {
                "name": "from",
                "in": "query",
                "description": "Date début (inclusive), format YYYY-MM-DD.",
                "required": false,
                "schema": {
                    "type": "string",
                    "format": "date"
                }
            },
            "to": {
                "name": "to",
                "in": "query",
                "description": "Date fin (inclusive), format YYYY-MM-DD.",
                "required": false,
                "schema": {
                    "type": "string",
                    "format": "date"
                }
            }
        },
        "responses": {
            "Unauthorized": {
                "description": "Token manquant, invalide ou révoqué.",
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/Error"
                        },
                        "example": {
                            "error": "invalid_token",
                            "message": "Token inconnu, révoqué ou expiré."
                        }
                    }
                }
            },
            "Forbidden": {
                "description": "Scope insuffisant OU commerçant suspendu.",
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/Error"
                        }
                    }
                }
            },
            "NotFound": {
                "description": "Ressource introuvable ou n'appartenant pas au commerçant.",
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/Error"
                        }
                    }
                }
            }
        }
    },
    "security": [
        {
            "bearerAuth": []
        }
    ],
    "tags": [
        {
            "name": "Méta",
            "description": "Health check + infos du compte"
        },
        {
            "name": "Prestations",
            "description": "Catalogue prestations du commerçant (planning)"
        },
        {
            "name": "Rendez-vous",
            "description": "RDV pris via planning"
        },
        {
            "name": "Clients",
            "description": "Base client CRM (dédupliquée par email)"
        },
        {
            "name": "Produits",
            "description": "Catalogue produits boutique"
        },
        {
            "name": "Commandes",
            "description": "Commandes boutique"
        },
        {
            "name": "Hébergement",
            "description": "Réservations d'hébergement"
        },
        {
            "name": "Restaurant",
            "description": "Réservations de tables restaurant"
        },
        {
            "name": "Google Merchant",
            "description": "Flux XML public pour Google Shopping (aucune auth requise, un flux par commerçant)"
        },
        {
            "name": "Webhooks",
            "description": "Événements POSTés depuis Developia vers l'URL du commerçant. Configuration via back-office /parametres/webhooks. Signés HMAC-SHA256."
        },
        {
            "name": "Authentification",
            "description": "Connexion de l'app Pro. Échange email + mot de passe contre un token `dvia_`, avec gestion de la double authentification."
        },
        {
            "name": "Statistiques",
            "description": "Chiffre d'affaires consolidé et impayés."
        },
        {
            "name": "Notifications",
            "description": "Flux unifié des notifications du commerçant (boutique, annonces, restaurant, rendez-vous)."
        },
        {
            "name": "Push",
            "description": "Enregistrement des appareils pour les notifications push FCM."
        }
    ],
    "webhooks": {
        "commande.payee": {
            "post": {
                "tags": [
                    "Webhooks"
                ],
                "summary": "Une commande boutique vient d'être payée",
                "description": "Envoyé immédiatement après la confirmation de paiement Stripe d'une commande boutique. Payload signé HMAC-SHA256.\n\n**Headers envoyés** :\n- `Content-Type: application/json`\n- `X-Developia-Signature: t=<timestamp>,v1=<hmac>` — signature HMAC\n- `X-Developia-Event: commande.payee` — nom de l'événement\n- `X-Developia-Event-Id: evt_<uuid>` — ID unique (dédup côté récepteur)\n- `X-Developia-Delivery: <id>` — ID de la tentative de livraison\n\n**Vérification signature** (PHP) :\n```php\n$sig = $_SERVER['HTTP_X_DEVELOPIA_SIGNATURE'];\n$body = file_get_contents('php://input');\npreg_match('/t=(\\d+),v1=([a-f0-9]+)/', $sig, $m);\n$expected = hash_hmac('sha256', $m[1] . '.' . $body, $secret);\nif (!hash_equals($expected, $m[2])) http_response_code(401);\n```\n\n**Retry** : 4 tentatives (1min → 5min → 30min → 2h). Le récepteur doit répondre `200-299` pour valider la livraison.",
                "requestBody": {
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "id": {
                                        "type": "string",
                                        "example": "evt_a1b2c3d4e5f6"
                                    },
                                    "event": {
                                        "type": "string",
                                        "example": "commande.payee"
                                    },
                                    "commercant_id": {
                                        "type": "integer",
                                        "example": 42
                                    },
                                    "created_at": {
                                        "type": "string",
                                        "format": "date-time"
                                    },
                                    "data": {
                                        "type": "object",
                                        "properties": {
                                            "commande": {
                                                "type": "object",
                                                "properties": {
                                                    "id": {
                                                        "type": "integer"
                                                    },
                                                    "statut": {
                                                        "type": "string"
                                                    },
                                                    "montant_total": {
                                                        "type": "number"
                                                    },
                                                    "frais_livraison": {
                                                        "type": "number"
                                                    },
                                                    "livraison_type": {
                                                        "type": [
                                                            "string",
                                                            "null"
                                                        ]
                                                    },
                                                    "client": {
                                                        "$ref": "#/components/schemas/Client"
                                                    },
                                                    "lignes": {
                                                        "type": "array",
                                                        "items": {
                                                            "type": "object",
                                                            "properties": {
                                                                "produit_id": {
                                                                    "type": [
                                                                        "integer",
                                                                        "null"
                                                                    ]
                                                                },
                                                                "nom": {
                                                                    "type": [
                                                                        "string",
                                                                        "null"
                                                                    ]
                                                                },
                                                                "quantite": {
                                                                    "type": "integer"
                                                                },
                                                                "prix_unitaire": {
                                                                    "type": "number"
                                                                }
                                                            }
                                                        }
                                                    },
                                                    "created_at": {
                                                        "type": "string",
                                                        "format": "date-time"
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                            },
                            "example": {
                                "id": "evt_a1b2c3d4e5f6",
                                "event": "commande.payee",
                                "commercant_id": 42,
                                "created_at": "2026-08-06T14:32:11+02:00",
                                "data": {
                                    "commande": {
                                        "id": 123,
                                        "statut": "paye",
                                        "montant_total": 45.9,
                                        "frais_livraison": 6.9,
                                        "livraison_type": "livraison",
                                        "client": {
                                            "prenom": "Marie",
                                            "nom": "Dupont",
                                            "email": "marie@example.com",
                                            "telephone": "0612345678"
                                        },
                                        "lignes": [
                                            {
                                                "produit_id": 8,
                                                "nom": "Bougie parfumée",
                                                "quantite": 1,
                                                "prix_unitaire": 19
                                            },
                                            {
                                                "produit_id": 12,
                                                "nom": "Diffuseur",
                                                "quantite": 1,
                                                "prix_unitaire": 20
                                            }
                                        ],
                                        "created_at": "2026-08-06T14:32:10+02:00"
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "2XX": {
                        "description": "Le récepteur a acquitté la livraison. Toute réponse 200-299 marque la delivery comme réussie."
                    },
                    "default": {
                        "description": "Toute autre réponse (ou timeout > 10s) déclenche un retry selon la policy 1min/5min/30min/2h. Après 4 échecs, la livraison est marquée 'failed' et visible dans le dashboard commerçant."
                    }
                }
            }
        },
        "rdv.cree": {
            "post": {
                "tags": [
                    "Webhooks"
                ],
                "summary": "Un nouveau rendez-vous a été pris",
                "description": "Envoyé après création d'un RDV planning — pour les RDV gratuits, immédiatement ; pour les payants, après confirmation Stripe. Signature HMAC-SHA256 identique à commande.payee.\n\nSource possible : `mobile`, `planning_backoffice`, ou omis (web public).",
                "requestBody": {
                    "content": {
                        "application/json": {
                            "example": {
                                "id": "evt_b2c3d4e5f6a7",
                                "event": "rdv.cree",
                                "commercant_id": 42,
                                "created_at": "2026-08-07T10:15:00+02:00",
                                "data": {
                                    "rdv": {
                                        "id": 987,
                                        "date_rdv": "2026-08-15",
                                        "heure_debut": "14:00",
                                        "heure_fin": "15:00",
                                        "statut": "confirme",
                                        "paiement_statut": "paye",
                                        "montant": 65,
                                        "montant_acompte": 20,
                                        "client": {
                                            "prenom": "Marie",
                                            "nom": "Dupont",
                                            "email": "marie@example.com",
                                            "telephone": "0612345678"
                                        },
                                        "prestation": {
                                            "id": 12,
                                            "nom": "Massage relaxant"
                                        },
                                        "employe_id": 7
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "2XX": {
                        "description": "Delivery acknowledgée."
                    }
                }
            }
        },
        "rdv.annule": {
            "post": {
                "tags": [
                    "Webhooks"
                ],
                "summary": "Un rendez-vous a été annulé",
                "description": "Envoyé lorsqu'un RDV passe au statut `annule`, quel que soit l'auteur : client depuis l'app mobile, commerçant depuis son planning.\n\nLe champ `annule_par` indique la source (`client` ou `commercant`).",
                "requestBody": {
                    "content": {
                        "application/json": {
                            "example": {
                                "id": "evt_c3d4e5f6a7b8",
                                "event": "rdv.annule",
                                "commercant_id": 42,
                                "created_at": "2026-08-07T11:20:00+02:00",
                                "data": {
                                    "rdv": {
                                        "id": 987,
                                        "date_rdv": "2026-08-15",
                                        "heure_debut": "14:00",
                                        "heure_fin": "15:00",
                                        "statut": "annule",
                                        "client": {
                                            "prenom": "Marie",
                                            "nom": "Dupont",
                                            "email": "marie@example.com"
                                        },
                                        "prestation": {
                                            "id": 12,
                                            "nom": "Massage relaxant"
                                        },
                                        "annule_par": "client",
                                        "annule_at": "2026-08-07T11:20:00+02:00",
                                        "source": "mobile"
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "2XX": {
                        "description": "Delivery acknowledgée."
                    }
                }
            }
        },
        "resto.reservation.creee": {
            "post": {
                "tags": [
                    "Webhooks"
                ],
                "summary": "Une nouvelle réservation restaurant a été prise",
                "description": "Envoyé après création d'une résa de table restaurant, à la fois pour les résas simples et pour celles avec empreinte CB (dans ce cas dispatch APRÈS validation empreinte).\n\nLe champ `empreinte_active` indique si une empreinte Stripe est active sur cette résa.",
                "requestBody": {
                    "content": {
                        "application/json": {
                            "example": {
                                "id": "evt_d4e5f6a7b8c9",
                                "event": "resto.reservation.creee",
                                "commercant_id": 42,
                                "created_at": "2026-08-07T18:45:00+02:00",
                                "data": {
                                    "reservation": {
                                        "id": 234,
                                        "heure_arrivee": "2026-08-10 20:00:00",
                                        "duree_min": 90,
                                        "nb_personnes": 4,
                                        "statut": "confirmee",
                                        "client": {
                                            "prenom": "Jean",
                                            "nom": "Martin",
                                            "email": "jean@example.com",
                                            "telephone": "0698765432"
                                        },
                                        "notes_client": "sans gluten svp",
                                        "empreinte_active": true,
                                        "created_at": "2026-08-07T18:44:52+02:00"
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "2XX": {
                        "description": "Delivery acknowledgée."
                    }
                }
            }
        },
        "hebergement.reservation.creee": {
            "post": {
                "tags": [
                    "Webhooks"
                ],
                "summary": "Une nouvelle réservation d'hébergement a été prise",
                "description": "Envoyé après création d'une résa hébergement. Deux cas : (1) résa payée en ligne → dispatch après confirmation Stripe ; (2) résa payée sur place → dispatch immédiat.\n\n`mode_paiement` = `integral`, `acompte`, `empreinte` ou `sur_place`.",
                "requestBody": {
                    "content": {
                        "application/json": {
                            "example": {
                                "id": "evt_e5f6a7b8c9d0",
                                "event": "hebergement.reservation.creee",
                                "commercant_id": 42,
                                "created_at": "2026-08-07T14:30:00+02:00",
                                "data": {
                                    "reservation": {
                                        "id": 156,
                                        "hebergement": {
                                            "id": 5,
                                            "nom": "Chambre Océan"
                                        },
                                        "date_arrivee": "2026-08-20",
                                        "date_depart": "2026-08-25",
                                        "nb_adultes": 2,
                                        "nb_enfants": 1,
                                        "statut": "confirme",
                                        "paiement_statut": "acompte_paye",
                                        "mode_paiement": "acompte",
                                        "montant_total_ttc": 750,
                                        "montant_acompte": 150,
                                        "client": {
                                            "prenom": "Sophie",
                                            "nom": "Leroy",
                                            "email": "sophie@example.com",
                                            "telephone": "0623456789"
                                        },
                                        "created_at": "2026-08-07T14:29:47+02:00"
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "2XX": {
                        "description": "Delivery acknowledgée."
                    }
                }
            }
        }
    },
    "paths": {
        "/v1/auth/login": {
            "post": {
                "tags": [
                    "Authentification"
                ],
                "summary": "Connexion — échange identifiants contre un token",
                "description": "Émet un token `dvia_` à partir d'un email et d'un mot de passe commerçant.\n\nRemplace la génération manuelle depuis `/parametres/api` pour les clients qui ne peuvent pas faire coller un token de 65 caractères (app mobile).\n\n**Deux réponses possibles** :\n- `201` — token émis, connexion terminée\n- `200` — le compte a la double authentification : un code part par mail, il faut enchaîner sur `/v1/auth/otp`\n\nLe champ `appareil` sert de libellé au token dans le back-office : il permet au commerçant de révoquer un téléphone perdu sans couper ses autres accès.\n\nLimité en fréquence par email + IP, compteur partagé avec la connexion web.",
                "security": [],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "required": [
                                    "email",
                                    "password"
                                ],
                                "properties": {
                                    "email": {
                                        "type": "string",
                                        "format": "email"
                                    },
                                    "password": {
                                        "type": "string",
                                        "format": "password"
                                    },
                                    "appareil": {
                                        "type": "string",
                                        "example": "Google Pixel 9",
                                        "description": "Libellé du token. Défaut : « App Pro »."
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "201": {
                        "description": "Token émis",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Jeton"
                                }
                            }
                        }
                    },
                    "200": {
                        "description": "Double authentification requise",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/DefiOtp"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Identifiants invalides. Message volontairement indistinct : ne révèle pas si l'adresse existe.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "Compte suspendu ou inactif",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "Email ou mot de passe manquant",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "Trop de tentatives",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/v1/auth/otp": {
            "post": {
                "tags": [
                    "Authentification"
                ],
                "summary": "Double authentification — échange le code contre un token",
                "description": "Second temps de `/v1/auth/login` quand le compte a la 2FA activée.\n\nLe défi vaut 10 minutes et 5 tentatives. Il est **consommé** au premier succès : rejouer la même requête renvoie `401`, ce qui empêche qu'un rejeu produise un second token.",
                "security": [],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "required": [
                                    "challenge",
                                    "code"
                                ],
                                "properties": {
                                    "challenge": {
                                        "type": "string",
                                        "description": "Reçu de `/v1/auth/login`."
                                    },
                                    "code": {
                                        "type": "string",
                                        "example": "123456"
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "201": {
                        "description": "Token émis",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Jeton"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Code incorrect, ou défi inconnu / expiré / déjà utilisé",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "Défi ou code manquant",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "Trop de codes erronés — recommencer la connexion",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/v1/clients": {
            "get": {
                "tags": [
                    "Clients"
                ],
                "summary": "Base client CRM du commerçant",
                "description": "Alimentée automatiquement à chaque commande/RDV/réservation. Déduplication par email.",
                "parameters": [
                    {
                        "$ref": "#/components/parameters/cursor"
                    },
                    {
                        "$ref": "#/components/parameters/limit"
                    },
                    {
                        "name": "q",
                        "in": "query",
                        "description": "Recherche full-text sur email, nom, prénom (LIKE %q%).",
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "data": {
                                            "type": "array",
                                            "items": {
                                                "$ref": "#/components/schemas/ClientCRM"
                                            }
                                        },
                                        "meta": {
                                            "$ref": "#/components/schemas/Meta"
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    }
                }
            }
        },
        "/v1/commandes": {
            "get": {
                "tags": [
                    "Commandes"
                ],
                "summary": "Liste des commandes boutique",
                "parameters": [
                    {
                        "$ref": "#/components/parameters/cursor"
                    },
                    {
                        "$ref": "#/components/parameters/limit"
                    },
                    {
                        "$ref": "#/components/parameters/from"
                    },
                    {
                        "$ref": "#/components/parameters/to"
                    },
                    {
                        "name": "statut",
                        "in": "query",
                        "schema": {
                            "type": "string",
                            "enum": [
                                "en_attente",
                                "paye",
                                "expedie",
                                "livre",
                                "annule",
                                "rembourse"
                            ]
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "data": {
                                            "type": "array",
                                            "items": {
                                                "$ref": "#/components/schemas/Commande"
                                            }
                                        },
                                        "meta": {
                                            "$ref": "#/components/schemas/Meta"
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    }
                }
            }
        },
        "/v1/commandes/{id}": {
            "get": {
                "tags": [
                    "Commandes"
                ],
                "summary": "Détail d'une commande",
                "description": "Inclut adresse livraison + lignes produits.",
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/CommandeDetail"
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "404": {
                        "$ref": "#/components/responses/NotFound"
                    }
                }
            },
            "patch": {
                "tags": [
                    "Commandes"
                ],
                "summary": "Changer le statut d'une commande",
                "description": "Passer une commande Click & Collect à `prete` déclenche le SMS « commande prête » au client, uniquement à la TRANSITION vers ce statut (pas si elle y est déjà) et uniquement si le client a un numéro de téléphone.\n\n`sms_envoye` rapporte l'envoi réel, pas la simple absence d'erreur.",
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "required": [
                                    "statut"
                                ],
                                "properties": {
                                    "statut": {
                                        "type": "string",
                                        "enum": [
                                            "en_attente",
                                            "paye",
                                            "prete",
                                            "expedie",
                                            "livre",
                                            "annule"
                                        ]
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "Statut modifié",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "data": {
                                            "type": "object",
                                            "properties": {
                                                "id": {
                                                    "type": "integer"
                                                },
                                                "statut": {
                                                    "type": "string"
                                                },
                                                "statut_avant": {
                                                    "type": "string",
                                                    "nullable": true
                                                },
                                                "sms_envoye": {
                                                    "type": "boolean"
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "404": {
                        "$ref": "#/components/responses/NotFound"
                    },
                    "422": {
                        "description": "Statut invalide",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/v1/health": {
            "get": {
                "tags": [
                    "Méta"
                ],
                "summary": "Health check public",
                "description": "Endpoint non protégé. Sert aux uptime checks + validation stack.",
                "security": [],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "status": {
                                            "type": "string",
                                            "example": "ok"
                                        },
                                        "version": {
                                            "type": "string",
                                            "example": "v1"
                                        },
                                        "timestamp": {
                                            "type": "string",
                                            "format": "date-time"
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        },
        "/v1/me": {
            "get": {
                "tags": [
                    "Méta"
                ],
                "summary": "Infos du commerçant + forfait + métadonnées du token",
                "description": "Profil du commerçant, forfait et métadonnées du token.\n\n**`forfait.modules` (depuis le 2026-09-15)** indique les modules réellement ouverts. Un client qui compose son interface doit lire ces drapeaux, PAS le nom du forfait.",
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "commercant": {
                                            "type": "object",
                                            "properties": {
                                                "id": {
                                                    "type": "integer"
                                                },
                                                "nom_commerce": {
                                                    "type": "string"
                                                },
                                                "slug": {
                                                    "type": "string"
                                                },
                                                "statut": {
                                                    "type": "string"
                                                }
                                            }
                                        },
                                        "forfait": {
                                            "type": [
                                                "object",
                                                "null"
                                            ],
                                            "properties": {
                                                "id": {
                                                    "type": "integer"
                                                },
                                                "nom": {
                                                    "type": "string"
                                                },
                                                "cycle": {
                                                    "type": "string",
                                                    "enum": [
                                                        "mensuel",
                                                        "annuel"
                                                    ]
                                                }
                                            }
                                        },
                                        "token": {
                                            "type": "object",
                                            "properties": {
                                                "nom": {
                                                    "type": "string"
                                                },
                                                "prefix": {
                                                    "type": "string"
                                                },
                                                "scopes": {
                                                    "type": "array",
                                                    "items": {
                                                        "type": "string"
                                                    }
                                                },
                                                "created_at": {
                                                    "type": "string",
                                                    "format": "date-time"
                                                },
                                                "last_used_at": {
                                                    "type": [
                                                        "string",
                                                        "null"
                                                    ],
                                                    "format": "date-time"
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/Forbidden"
                    }
                }
            }
        },
        "/v1/merchant-feed/{slug}.xml": {
            "get": {
                "tags": [
                    "Google Merchant"
                ],
                "summary": "Flux XML Google Shopping (public, sans auth)",
                "description": "Retourne le catalogue produits actifs du commerçant au format RSS 2.0 + namespace `g:` conforme à la spec Google Merchant Center. À coller dans Google Merchant Center → Produits → Flux → Récupération planifiée.\n\n**Filtres appliqués côté serveur** : seuls les produits `actif=1` et `type='simple'` sont exposés (les annonces classées ne vont pas sur Google Shopping).\n\n**Cache HTTP** : `Cache-Control: public, max-age=3600` — Google recrawle max 1× par heure.",
                "security": [],
                "parameters": [
                    {
                        "name": "slug",
                        "in": "path",
                        "required": true,
                        "description": "Slug public du commerçant (visible dans l'URL de sa vitrine `/p/{slug}`).",
                        "schema": {
                            "type": "string",
                            "example": "developia"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Flux XML Google Merchant",
                        "content": {
                            "application/xml": {
                                "schema": {
                                    "type": "string",
                                    "format": "xml"
                                },
                                "example": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<rss version=\"2.0\" xmlns:g=\"http://base.google.com/ns/1.0\">\n  <channel>\n    <title>Studio Zenith</title>\n    <link>https://developia.fr/p/studio-zenith/boutique</link>\n    <item>\n      <g:id>DVIA-42</g:id>\n      <title>Bougie parfumée</title>\n      <g:price>19.90 EUR</g:price>\n      <g:availability>in_stock</g:availability>\n    </item>\n  </channel>\n</rss>"
                            }
                        }
                    },
                    "404": {
                        "description": "Commerçant introuvable, suspendu, ou sans forfait boutique.",
                        "content": {
                            "application/xml": {
                                "schema": {
                                    "type": "string"
                                },
                                "example": "<?xml version=\"1.0\"?><error>Merchant not found</error>"
                            }
                        }
                    }
                }
            }
        },
        "/v1/notifications": {
            "get": {
                "tags": [
                    "Notifications"
                ],
                "summary": "Flux unifié des notifications",
                "description": "Fusionne les notifications boutique/annonces/restaurant et celles des rendez-vous, triées de la plus récente à la plus ancienne.\n\nUtiliser le couple (`source`, `id`) pour désigner une notification — les identifiants se chevauchent entre les deux sources.",
                "parameters": [
                    {
                        "name": "limit",
                        "in": "query",
                        "schema": {
                            "type": "integer",
                            "default": 30,
                            "minimum": 1,
                            "maximum": 100
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "data": {
                                            "type": "array",
                                            "items": {
                                                "$ref": "#/components/schemas/Notification"
                                            }
                                        },
                                        "meta": {
                                            "type": "object",
                                            "properties": {
                                                "count": {
                                                    "type": "integer"
                                                },
                                                "non_lues": {
                                                    "type": "integer",
                                                    "description": "Total non lues, toutes sources confondues."
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    }
                }
            }
        },
        "/v1/notifications/lire": {
            "post": {
                "tags": [
                    "Notifications"
                ],
                "summary": "Marquer des notifications comme lues",
                "description": "Corps vide ou sans `notifications` : marque TOUT lu. Sinon, seules les notifications listées.",
                "requestBody": {
                    "required": false,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "notifications": {
                                        "type": "array",
                                        "items": {
                                            "type": "object",
                                            "required": [
                                                "source",
                                                "id"
                                            ],
                                            "properties": {
                                                "source": {
                                                    "type": "string",
                                                    "enum": [
                                                        "boutique",
                                                        "planning"
                                                    ]
                                                },
                                                "id": {
                                                    "type": "integer"
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "lu": {
                                            "oneOf": [
                                                {
                                                    "type": "integer"
                                                },
                                                {
                                                    "type": "string",
                                                    "enum": [
                                                        "toutes"
                                                    ]
                                                }
                                            ],
                                            "description": "Nombre de lignes marquées, ou `toutes`."
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    }
                }
            }
        },
        "/v1/prestations": {
            "get": {
                "tags": [
                    "Prestations"
                ],
                "summary": "Liste des prestations du commerçant",
                "parameters": [
                    {
                        "$ref": "#/components/parameters/cursor"
                    },
                    {
                        "$ref": "#/components/parameters/limit"
                    },
                    {
                        "name": "actif",
                        "in": "query",
                        "description": "Filtre : uniquement les actives (1) ou désactivées (0).",
                        "schema": {
                            "type": "integer",
                            "enum": [
                                0,
                                1
                            ]
                        }
                    },
                    {
                        "name": "visible_clients",
                        "in": "query",
                        "description": "Filtre : visibles côté client public (1) ou masquées (0).",
                        "schema": {
                            "type": "integer",
                            "enum": [
                                0,
                                1
                            ]
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "data": {
                                            "type": "array",
                                            "items": {
                                                "$ref": "#/components/schemas/Prestation"
                                            }
                                        },
                                        "meta": {
                                            "$ref": "#/components/schemas/Meta"
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    }
                }
            }
        },
        "/v1/produits": {
            "get": {
                "tags": [
                    "Produits"
                ],
                "summary": "Catalogue produits boutique",
                "parameters": [
                    {
                        "$ref": "#/components/parameters/cursor"
                    },
                    {
                        "$ref": "#/components/parameters/limit"
                    },
                    {
                        "name": "actif",
                        "in": "query",
                        "description": "Filtre : actifs (1) / inactifs (0).",
                        "schema": {
                            "type": "integer",
                            "enum": [
                                0,
                                1
                            ]
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "data": {
                                            "type": "array",
                                            "items": {
                                                "$ref": "#/components/schemas/Produit"
                                            }
                                        },
                                        "meta": {
                                            "$ref": "#/components/schemas/Meta"
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    }
                }
            },
            "post": {
                "tags": [
                    "Produits"
                ],
                "summary": "Créer un produit",
                "description": "**multipart/form-data obligatoire** — les noms de champs sont ceux du formulaire web, car l'endpoint appelle la même validation et le même traitement. Une divergence de nom casserait la création en silence.\n\nLa première image va dans `image` (vignette), les suivantes dans `galerie[]`.\n\n**En-tête `Idempotency-Key` fortement recommandé** : sur réseau instable, un double envoi créerait deux produits. Avec la clé, le second appel renvoie la réponse du premier et l'en-tête `Idempotent-Replay: true`.\n\nNote : `actif` n'est pas coché par défaut (iso formulaire web). Envoyer `actif=1` pour que le produit soit visible immédiatement en vitrine.",
                "parameters": [
                    {
                        "name": "Idempotency-Key",
                        "in": "header",
                        "required": false,
                        "description": "Identifiant unique et stable du geste (UUID). Conservé 24 h.",
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "multipart/form-data": {
                            "schema": {
                                "type": "object",
                                "required": [
                                    "nom",
                                    "prix"
                                ],
                                "properties": {
                                    "nom": {
                                        "type": "string"
                                    },
                                    "prix": {
                                        "type": "number",
                                        "format": "float"
                                    },
                                    "stock": {
                                        "type": "integer"
                                    },
                                    "description": {
                                        "type": "string"
                                    },
                                    "actif": {
                                        "type": "integer",
                                        "enum": [
                                            0,
                                            1
                                        ]
                                    },
                                    "image": {
                                        "type": "string",
                                        "format": "binary",
                                        "description": "Vignette."
                                    },
                                    "galerie[]": {
                                        "type": "array",
                                        "items": {
                                            "type": "string",
                                            "format": "binary"
                                        }
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "201": {
                        "description": "Créé",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "data": {
                                            "$ref": "#/components/schemas/Produit"
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "422": {
                        "description": "Nom vide ou prix invalide",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/v1/produits/{id}": {
            "parameters": [
                {
                    "name": "id",
                    "in": "path",
                    "required": true,
                    "schema": {
                        "type": "integer"
                    }
                }
            ],
            "get": {
                "tags": [
                    "Produits"
                ],
                "summary": "Détail d'un produit",
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "data": {
                                            "$ref": "#/components/schemas/Produit"
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "404": {
                        "$ref": "#/components/responses/NotFound"
                    }
                }
            },
            "patch": {
                "tags": [
                    "Produits"
                ],
                "summary": "Modifier un produit",
                "description": "Modification **partielle** : seuls les champs envoyés changent.\n\nIl n'existe volontairement pas de `PUT` sur cette API — un payload partiel effacerait les champs absents, risque inacceptable depuis un mobile.",
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "nom": {
                                        "type": "string"
                                    },
                                    "description": {
                                        "type": "string"
                                    },
                                    "prix": {
                                        "type": "number",
                                        "format": "float"
                                    },
                                    "stock": {
                                        "type": "integer"
                                    },
                                    "actif": {
                                        "type": "boolean"
                                    },
                                    "click_collect": {
                                        "type": "boolean"
                                    },
                                    "ordre": {
                                        "type": "integer"
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "Modifié",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "data": {
                                            "$ref": "#/components/schemas/Produit"
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "404": {
                        "$ref": "#/components/responses/NotFound"
                    },
                    "422": {
                        "description": "Aucun champ modifiable fourni, ou valeur invalide",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            },
            "delete": {
                "tags": [
                    "Produits"
                ],
                "summary": "Supprimer un produit",
                "description": "Suppression **définitive**, avec ses photos et ses variantes — comportement identique au back-office.\n\nL'historique des commandes n'est pas affecté : chaque ligne de commande conserve une copie du nom, du prix et de la variante au moment de l'achat.",
                "responses": {
                    "200": {
                        "description": "Supprimé",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "supprime": {
                                            "type": "boolean"
                                        },
                                        "id": {
                                            "type": "integer"
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "404": {
                        "$ref": "#/components/responses/NotFound"
                    }
                }
            }
        },
        "/v1/push/register": {
            "post": {
                "tags": [
                    "Push"
                ],
                "summary": "Enregistrer un appareil pour les notifications push",
                "description": "Idempotent : réenregistrer le même token met simplement à jour l'appareil. Si FCM réattribue un token à un autre appareil, il suit son nouveau propriétaire — sans quoi les notifications d'un commerçant partiraient chez un autre.",
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "required": [
                                    "token",
                                    "plateforme"
                                ],
                                "properties": {
                                    "token": {
                                        "type": "string",
                                        "description": "Token FCM de l'appareil."
                                    },
                                    "plateforme": {
                                        "type": "string",
                                        "enum": [
                                            "android",
                                            "ios"
                                        ]
                                    },
                                    "appareil": {
                                        "type": "string",
                                        "example": "Google Pixel 9"
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "201": {
                        "description": "Enregistré"
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "422": {
                        "description": "Token manquant ou plateforme invalide",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            },
            "delete": {
                "tags": [
                    "Push"
                ],
                "summary": "Désinscrire un appareil",
                "description": "À appeler à la déconnexion, ou quand FCM signale un token devenu invalide.",
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "required": [
                                    "token"
                                ],
                                "properties": {
                                    "token": {
                                        "type": "string"
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "Désinscrit"
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "422": {
                        "description": "Token manquant",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/v1/rendez-vous": {
            "get": {
                "tags": [
                    "Rendez-vous"
                ],
                "summary": "Liste des rendez-vous",
                "parameters": [
                    {
                        "$ref": "#/components/parameters/cursor"
                    },
                    {
                        "$ref": "#/components/parameters/limit"
                    },
                    {
                        "$ref": "#/components/parameters/from"
                    },
                    {
                        "$ref": "#/components/parameters/to"
                    },
                    {
                        "name": "statut",
                        "in": "query",
                        "description": "Filtre par statut.",
                        "schema": {
                            "type": "string",
                            "enum": [
                                "confirme",
                                "en_attente",
                                "annule",
                                "termine",
                                "no_show"
                            ]
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "data": {
                                            "type": "array",
                                            "items": {
                                                "$ref": "#/components/schemas/RendezVous"
                                            }
                                        },
                                        "meta": {
                                            "$ref": "#/components/schemas/Meta"
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    }
                }
            }
        },
        "/v1/rendez-vous/{id}": {
            "get": {
                "tags": [
                    "Rendez-vous"
                ],
                "summary": "Détail d'un rendez-vous",
                "description": "Inclut les notes internes commerçant.",
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/RendezVous"
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "404": {
                        "$ref": "#/components/responses/NotFound"
                    }
                }
            }
        },
        "/v1/reservations/hebergement": {
            "get": {
                "tags": [
                    "Hébergement"
                ],
                "summary": "Liste des réservations d'hébergement",
                "parameters": [
                    {
                        "$ref": "#/components/parameters/cursor"
                    },
                    {
                        "$ref": "#/components/parameters/limit"
                    },
                    {
                        "name": "from",
                        "in": "query",
                        "description": "Date d'arrivée min (inclusive).",
                        "schema": {
                            "type": "string",
                            "format": "date"
                        }
                    },
                    {
                        "name": "to",
                        "in": "query",
                        "description": "Date d'arrivée max (inclusive).",
                        "schema": {
                            "type": "string",
                            "format": "date"
                        }
                    },
                    {
                        "name": "statut",
                        "in": "query",
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "data": {
                                            "type": "array",
                                            "items": {
                                                "$ref": "#/components/schemas/ReservationHebergement"
                                            }
                                        },
                                        "meta": {
                                            "$ref": "#/components/schemas/Meta"
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    }
                }
            }
        },
        "/v1/reservations/restaurant": {
            "get": {
                "tags": [
                    "Restaurant"
                ],
                "summary": "Liste des réservations de tables restaurant",
                "parameters": [
                    {
                        "$ref": "#/components/parameters/cursor"
                    },
                    {
                        "$ref": "#/components/parameters/limit"
                    },
                    {
                        "$ref": "#/components/parameters/from"
                    },
                    {
                        "$ref": "#/components/parameters/to"
                    },
                    {
                        "name": "statut",
                        "in": "query",
                        "schema": {
                            "type": "string",
                            "enum": [
                                "confirmee",
                                "annulee",
                                "terminee",
                                "no_show"
                            ]
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "data": {
                                            "type": "array",
                                            "items": {
                                                "$ref": "#/components/schemas/ReservationRestaurant"
                                            }
                                        },
                                        "meta": {
                                            "$ref": "#/components/schemas/Meta"
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    }
                }
            }
        },
        "/v1/stats": {
            "get": {
                "tags": [
                    "Statistiques"
                ],
                "summary": "Chiffre d'affaires et impayés",
                "description": "Calculé par le même service que le cockpit comptable : le chiffre renvoyé ici est celui du back-office, pas une seconde implémentation.",
                "parameters": [
                    {
                        "name": "periode",
                        "in": "query",
                        "description": "Période. Défaut : `jour`.",
                        "schema": {
                            "type": "string",
                            "enum": [
                                "jour",
                                "semaine",
                                "mois"
                            ],
                            "default": "jour"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "data": {
                                            "$ref": "#/components/schemas/Stats"
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    }
                }
            }
        }
    }
}
