openapi: 3.1.0
info:
  title: Belive Connect — API partenaires
  version: "1.0"
  description: |
    API destinée aux établissements qui ont leur propre système de réservation (channel manager interne).
    Elle permet de **bloquer chez Belive 360 les tables réservées chez vous**, de **lire les réservations** de votre
    établissement (toutes sources) et de **recevoir nos événements par webhook**.

    **Authentification** : `Authorization: Bearer blv_live_…` (clé créée dans le portail vendeur → Finance → Intégrations & API ;
    une clé = un établissement ; affichée une seule fois). Environnement de test : clés `blv_test_…` sur `https://preprod.belive360.com`.

    **Erreurs** : `{ "error": "<message FR>", "error_en": "<message EN>", "code": "<CODE>" }`.
    **Limite** : 60 requêtes / minute / clé → `429` + `Retry-After: 60`.
    **Conflits de table** : premier arrivé, premier servi (`409 TABLE_TAKEN`).
    **Idempotence** : `POST /v1/reservations` exige `idempotencyKey` ; rejouer renvoie la même réservation en `200`.

    **Contrôle à l'entrée** : chaque réservation partenaire reçoit un code `BLV-XXXX` et un lien `guestAccessUrl`
    qui affiche au client un QR personnel signé, accepté par Belive Terminal. Belive envoie l'email au client
    (désactivable avec `notifyCustomer: false` pour intégrer `guestAccessUrl` vous-même).

    **Webhooks** : corps JSON signé `X-Belive-Signature: t=<timestamp>,v1=<hex HMAC-SHA256(secret, "<timestamp>.<corps brut>")>`,
    5 tentatives (1 min, 5 min, 30 min, 2 h, 12 h). Répondez `2xx` en moins de 10 s.
servers:
  - url: https://app.belive360.com/functions/v1/partner-api
    description: Production
  - url: https://preprod.belive360.com/functions/v1/partner-api
    description: Test (preprod)
security:
  - bearerKey: []
tags:
  - name: Venue
  - name: Availability
  - name: Reservations
  - name: Webhooks
paths:
  /v1/venue:
    get:
      tags: [Venue]
      summary: Établissement, tables, créneaux, politiques
      responses:
        "200":
          description: OK
          content:
            application/json:
              example:
                venue: { id: "31f705c0-…", name: "La Cale Rabat", slug: "la-cale-rabat", status: "active" }
                reservationMode: prepaid
                autoConfirm: false
                requestExpiryHours: 24
                tables: [ { id: "27ced4e4-…", name: "Table 9", number: "T9", capacityMin: 1, capacityMax: 6, minimumSpend: 1800, type: "standard", bookable: true } ]
                slots: [ { id: "…", name: "Dîner", start: "19:00", end: "21:00", daysOfWeek: [1,2,3,4,5,6,0] } ]
                policies: null
                currency: MAD
        "401": { $ref: "#/components/responses/Unauthorized" }
  /v1/availability:
    get:
      tags: [Availability]
      summary: Disponibilité des tables pour une date (et un créneau)
      parameters:
        - { name: date, in: query, required: true, schema: { type: string, format: date }, example: "2026-10-16" }
        - { name: slot, in: query, required: false, schema: { type: string, pattern: "^\\d{2}:\\d{2}$" }, example: "21:30", description: Sans créneau, toute la date. Une réservation sans créneau bloque tous les créneaux. }
      responses:
        "200":
          description: OK
          content:
            application/json:
              example:
                date: "2026-10-16"
                slot: "21:30"
                tables: [ { id: "27ced4e4-…", name: "Table 9", available: false, blockedBy: partner } ]
        "400": { $ref: "#/components/responses/BadRequest" }
  /v1/reservations:
    get:
      tags: [Reservations]
      summary: Réservations de l'établissement (toutes sources)
      description: Email/téléphone du client ne sont inclus que si l'établissement est en mode « sans paiement » (`reservation_mode = free`). `externalRef` n'est renvoyé que pour vos propres réservations.
      parameters:
        - { name: from, in: query, schema: { type: string, format: date } }
        - { name: to, in: query, schema: { type: string, format: date } }
        - { name: status, in: query, schema: { type: string }, example: "confirmed,completed", description: liste séparée par des virgules }
        - { name: page, in: query, schema: { type: integer, minimum: 1, default: 1 } }
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 200, default: 50 } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  items: { type: array, items: { $ref: "#/components/schemas/Reservation" } }
                  page: { type: integer }
                  limit: { type: integer }
                  total: { type: integer }
                  contactDetailsIncluded: { type: boolean }
    post:
      tags: [Reservations]
      summary: Bloquer une table (réservation prise chez vous)
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateReservation" }
            example:
              tableId: "27ced4e4-c115-41b5-af27-93a7bd57cbcc"
              date: "2026-10-16"
              slot: "21:30"
              guests: 4
              customer: { name: "Yasmine Alaoui", email: "yasmine@example.com", phone: "+212600000001" }
              notes: "Anniversaire"
              externalRef: "LACALE-4421"
              idempotencyKey: "lacale-4421-v1"
              notifyCustomer: true
      responses:
        "201":
          description: Réservation créée (confirmée, sans paiement en ligne, jamais commissionnée)
          content:
            application/json:
              example:
                beliveReservationId: "1e72349e-…"
                accessCode: "BLV-T2VS"
                status: confirmed
                guestAccessUrl: "https://app.belive360.com/access/<jeton>"
                claimUrl: "https://app.belive360.com/claim-guest/<jeton>"
                accessLinkExpiresAt: "2026-10-18T06:00:00.000Z"
                notifications: { email: sent, sms: skipped }
        "200": { description: Rejeu d'une `idempotencyKey` déjà traitée (même corps, en-tête `Idempotent-Replayed: true`) }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { description: "`TABLE_NOT_FOUND`" }
        "409": { description: "`TABLE_TAKEN` (table déjà réservée pour ce créneau), `VENUE_INACTIVE`" }
  /v1/reservations/{id}:
    delete:
      tags: [Reservations]
      summary: Annuler une réservation créée avec cette clé (libère la table)
      parameters: [ { name: id, in: path, required: true, schema: { type: string, format: uuid } } ]
      responses:
        "200": { description: "`{ beliveReservationId, status: cancelled, cancelledAt }` — idempotent (`alreadyCancelled: true`)" }
        "404": { description: "`NOT_FOUND` — pas une réservation de cette clé" }
  /v1/webhooks:
    get:
      tags: [Webhooks]
      summary: Endpoints webhook de l'établissement
      responses: { "200": { description: "`{ items: [ { id, url, events, active, createdAt } ], events: [...] }`" } }
    post:
      tags: [Webhooks]
      summary: Créer un endpoint (secret affiché une seule fois)
      requestBody:
        content:
          application/json:
            example: { url: "https://votre-systeme.ma/belive/webhook", events: [reservation.created, reservation.cancelled, reservation.checked_in] }
      responses:
        "201": { description: "`{ id, url, events, active, createdAt, secret: \"whsec_…\" }`" }
        "409": { description: "`TOO_MANY_ENDPOINTS` (5 actifs max)" }
  /v1/webhooks/{id}:
    delete:
      tags: [Webhooks]
      summary: Désactiver un endpoint
      parameters: [ { name: id, in: path, required: true, schema: { type: string, format: uuid } } ]
      responses: { "200": { description: "`{ id, active: false }`" }, "404": { description: NOT_FOUND } }
  /v1/webhooks/test:
    post:
      tags: [Webhooks]
      summary: Envoyer un événement `webhook.test` (même signature)
      requestBody: { content: { application/json: { example: { endpointId: "optionnel" } } } }
      responses: { "202": { description: "`{ queued, deliveryIds }`" } }
components:
  securitySchemes:
    bearerKey: { type: http, scheme: bearer, bearerFormat: "blv_live_<48 hex>" }
  responses:
    Unauthorized: { description: "`UNAUTHORIZED` (clé absente/invalide) ou `KEY_REVOKED`" }
    BadRequest: { description: "`INVALID_JSON`, `INVALID_DATE`, `INVALID_SLOT`, `INVALID_TABLE`, `INVALID_GUESTS`, `GUESTS_OUT_OF_RANGE`, `CUSTOMER_NAME_REQUIRED`, `CUSTOMER_CONTACT_REQUIRED`, `INVALID_EMAIL`, `IDEMPOTENCY_KEY_REQUIRED`" }
  schemas:
    CreateReservation:
      type: object
      required: [tableId, date, guests, customer, idempotencyKey]
      properties:
        tableId: { type: string, format: uuid }
        date: { type: string, format: date }
        slot: { type: string, pattern: "^\\d{2}:\\d{2}$", description: optionnel ; sans créneau la table est bloquée toute la soirée }
        guests: { type: integer, minimum: 1 }
        customer:
          type: object
          required: [name]
          properties:
            name: { type: string }
            email: { type: string, format: email, description: email ou phone requis (accès invité) }
            phone: { type: string }
        notes: { type: string, maxLength: 500 }
        externalRef: { type: string, maxLength: 120, description: votre identifiant de réservation }
        idempotencyKey: { type: string, maxLength: 120 }
        notifyCustomer: { type: boolean, default: true, description: "false = Belive n'envoie pas l'email ; intégrez guestAccessUrl vous-même" }
    Reservation:
      type: object
      properties:
        id: { type: string, format: uuid }
        accessCode: { type: [string, "null"], example: "BLV-T2VS" }
        status: { type: string, enum: [requested, confirmed, completed, pending, partial, cancelled, declined] }
        date: { type: string, format: date }
        slot: { type: [string, "null"] }
        table: { type: object, properties: { id: { type: string }, name: { type: string } } }
        guests: { type: integer }
        customerFirstName: { type: [string, "null"] }
        source: { type: string, enum: [belive, manual, partner] }
        checkedInAt: { type: [string, "null"], format: date-time }
        createdAt: { type: string, format: date-time }
        externalRef: { type: string, description: uniquement vos réservations }
        customerEmail: { type: [string, "null"], description: uniquement si l'établissement est en mode sans paiement }
        customerPhone: { type: [string, "null"], description: idem }
    WebhookEvent:
      type: object
      description: Corps livré aux endpoints (signé)
      properties:
        id: { type: string, format: uuid, description: identifiant de livraison (X-Belive-Delivery) }
        type: { type: string, enum: [reservation.created, reservation.confirmed, reservation.cancelled, reservation.declined, reservation.checked_in, webhook.test] }
        occurredAt: { type: string, format: date-time }
        data:
          type: object
          properties:
            reservation: { $ref: "#/components/schemas/Reservation" }
      example:
        id: "6e1a…"
        type: reservation.checked_in
        occurredAt: "2026-10-08T21:45:12.000Z"
        data:
          reservation: { beliveReservationId: "e753ec50-…", accessCode: "BLV-S8Z9", status: confirmed, date: "2026-10-08", slot: "21:30", table: { id: "27ced4e4-…", name: "Table 9" }, guests: 3, customerFirstName: "Othmane", source: partner, externalRef: "LACALE-5001", checkedInAt: "2026-10-08T21:45:11.000Z", venue: { id: "31f705c0-…", name: "La Cale Rabat" } }
