openapi: 3.1.0
info:
  title: Restaurateur.ch API
  version: '1.0'
  description: |
    API REST publique pour la gestion des réservations Restaurateur.ch.

    - Authentification : header `X-API-Key` (`= admin_token` du restaurant)
    - Format : JSON UTF-8
    - CORS : ouvert (`*`)
    - Rate-limit : 600 req/min/clé
  contact:
    name: Support intégration
    email: api@restaurateur.ch
    url: https://docs.restaurateur.ch
  license:
    name: Propriétaire
    url: https://restaurateur.ch
servers:
  - url: https://api.restaurateur.ch/api/v1
    description: Production

security:
  - ApiKeyAuth: []
  - BearerAuth: []
  - AdminTokenAuth: []

tags:
  - name: Restaurant
    description: Infos du restaurant lié à la clé
  - name: Bookings
    description: Consultation et gestion des réservations
  - name: Calendar
    description: Configuration calendrier, créneaux et dates bloquées
  - name: Services
    description: Services (lunch/dinner/brunch) avec capacité cumulée
  - name: Notifications
    description: Configuration des emails en CC
  - name: Stats
    description: Statistiques temps réel et comptages

paths:
  /restaurant:
    get:
      tags: [Restaurant]
      summary: Infos du restaurant lié à la clé
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  restaurant: { $ref: '#/components/schemas/Restaurant' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /bookings:
    get:
      tags: [Bookings]
      summary: Lister les réservations
      parameters:
        - in: query
          name: status
          schema: { type: string, enum: [pending, confirmed, cancelled, no_show] }
        - in: query
          name: date
          schema: { type: string, format: date }
        - in: query
          name: date_from
          schema: { type: string, format: date }
        - in: query
          name: date_to
          schema: { type: string, format: date }
        - in: query
          name: search
          schema: { type: string }
          description: Recherche dans le nom et l'email
        - in: query
          name: page
          schema: { type: integer, default: 1, minimum: 1 }
        - in: query
          name: limit
          schema: { type: integer, default: 50, maximum: 200 }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  bookings:
                    type: array
                    items: { $ref: '#/components/schemas/Booking' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /bookings/{token}:
    parameters:
      - $ref: '#/components/parameters/BookingToken'
    get:
      tags: [Bookings]
      summary: Détail d'une réservation
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  booking: { $ref: '#/components/schemas/Booking' }
        '404': { $ref: '#/components/responses/NotFound' }
    patch:
      tags: [Bookings]
      summary: Modifier une réservation (sans changer le statut)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                booking_date: { type: string, format: date }
                booking_time: { type: string, pattern: '^\d{2}:\d{2}$' }
                party_size: { type: integer, minimum: 1 }
                notes: { type: string, nullable: true }
                guest_phone: { type: string, nullable: true }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  booking: { $ref: '#/components/schemas/Booking' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }

  /bookings/{token}/accept:
    parameters:
      - $ref: '#/components/parameters/BookingToken'
    post:
      tags: [Bookings]
      summary: Accepter (confirmer) — envoie l'email de confirmation au client
      responses:
        '200': { $ref: '#/components/responses/BookingActionOk' }
        '404': { $ref: '#/components/responses/NotFound' }

  /bookings/{token}/reject:
    parameters:
      - $ref: '#/components/parameters/BookingToken'
    post:
      tags: [Bookings]
      summary: Refuser (annuler) — envoie l'email + refund acompte automatique
      responses:
        '200': { $ref: '#/components/responses/BookingActionOk' }
        '404': { $ref: '#/components/responses/NotFound' }

  /bookings/{token}/no-show:
    parameters:
      - $ref: '#/components/parameters/BookingToken'
    post:
      tags: [Bookings]
      summary: Marquer en no-show
      responses:
        '200': { $ref: '#/components/responses/BookingActionOk' }
        '400':
          description: Réservation déjà facturée
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '404': { $ref: '#/components/responses/NotFound' }

  /stats:
    get:
      tags: [Stats]
      summary: Statistiques temps réel
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  stats:
                    type: object
                    properties:
                      total: { type: integer }
                      pending: { type: integer }
                      confirmed: { type: integer }
                      cancelled: { type: integer }
                      no_show: { type: integer }
                      today: { type: integer }
                      upcoming: { type: integer }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /notifications:
    get:
      tags: [Notifications]
      summary: Lire la config CC des notifications
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  notifications: { $ref: '#/components/schemas/NotificationsConfig' }
    put:
      tags: [Notifications]
      summary: Définir / effacer le(s) email(s) en CC
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [cc_email]
              properties:
                cc_email:
                  type: string
                  nullable: true
                  description: Un email, ou plusieurs séparés par `,` ou `;`. `null`/`""` efface.
                  example: "ops@resto.com, manager@resto.com"
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  notifications: { $ref: '#/components/schemas/NotificationsConfig' }
        '400':
          description: Email(s) invalide(s)
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Error'
                  - type: object
                    properties:
                      invalid_emails:
                        type: array
                        items: { type: string }

  /calendar:
    get:
      tags: [Calendar]
      summary: Config hebdomadaire + overrides à venir
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  week:
                    type: array
                    items: { $ref: '#/components/schemas/WeekDayConfig' }
                  overrides:
                    type: array
                    items: { $ref: '#/components/schemas/DateOverride' }

  /calendar/slots:
    get:
      tags: [Calendar]
      summary: Créneaux disponibles d'une date
      parameters:
        - in: query
          name: date
          required: true
          schema: { type: string, format: date }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SlotsResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }

  /calendar/week/{dayOfWeek}:
    parameters:
      - in: path
        name: dayOfWeek
        required: true
        schema: { type: integer, minimum: 0, maximum: 6 }
        description: 0 = dimanche, 6 = samedi
    put:
      tags: [Calendar]
      summary: Modifier un jour de la semaine
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                is_open: { type: boolean }
                open_time: { type: string, pattern: '^\d{2}:\d{2}$' }
                close_time: { type: string, pattern: '^\d{2}:\d{2}$' }
                slot_duration_min: { type: integer, enum: [15, 30, 60, 90, 120] }
                max_bookings_per_slot: { type: integer, minimum: 1 }
                discount_percent: { type: integer, minimum: 0, maximum: 100 }
      responses:
        '200':
          description: OK

  /calendar/dates/{date}:
    parameters:
      - in: path
        name: date
        required: true
        schema: { type: string, format: date }
    put:
      tags: [Calendar]
      summary: Bloquer ou modifier une date spéciale (override)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                is_open: { type: boolean, description: false = bloque la date }
                label: { type: string, description: "Ex: 'Privatisation', 'Jour férié'" }
                open_time: { type: string, pattern: '^\d{2}:\d{2}$' }
                close_time: { type: string, pattern: '^\d{2}:\d{2}$' }
                slot_duration_min: { type: integer }
                max_bookings_per_slot: { type: integer }
                discount_percent: { type: integer, minimum: 0, maximum: 100 }
      responses:
        '200':
          description: OK
    delete:
      tags: [Calendar]
      summary: Retirer l'override (retour config hebdo normale)
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  deleted: { type: boolean }

  /services:
    get:
      tags: [Services]
      summary: Lister les services (+ usage optionnel pour une date)
      parameters:
        - in: query
          name: date
          schema: { type: string, format: date }
          description: Si fourni, ajoute `usage_on_date` sur chaque service
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  services:
                    type: array
                    items: { $ref: '#/components/schemas/Service' }
    post:
      tags: [Services]
      summary: Créer un service
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ServiceCreate' }
      responses:
        '201':
          description: Créé
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  service: { $ref: '#/components/schemas/Service' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '409':
          description: Code de service déjà utilisé
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /services/{id}:
    parameters:
      - in: path
        name: id
        required: true
        schema: { type: integer }
    put:
      tags: [Services]
      summary: Modifier un service
      description: |
        Tous les champs sont optionnels. Pour passer en illimité,
        envoyer explicitement `"max_total_guests": null`.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ServiceUpdate' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  service: { $ref: '#/components/schemas/Service' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Services]
      summary: Supprimer un service
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  deleted: { type: boolean }

  /stats/counts:
    get:
      tags: [Stats]
      summary: Nb réservations + nb couverts (par date / période / service)
      parameters:
        - in: query
          name: date
          schema: { type: string, format: date }
        - in: query
          name: date_from
          schema: { type: string, format: date }
        - in: query
          name: date_to
          schema: { type: string, format: date }
        - in: query
          name: service
          schema: { type: string }
          description: Code du service (ex. `lunch`)
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CountsResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404':
          description: Service inconnu
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /calendar/pause-day:
    post:
      tags: [Calendar]
      summary: Raccourci pour bloquer un jour
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [date]
              properties:
                date: { type: string, format: date }
                reason: { type: string }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  date: { type: string }
                  blocked: { type: boolean }
                  reason: { type: string, nullable: true }

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
    BearerAuth:
      type: http
      scheme: bearer
    AdminTokenAuth:
      type: apiKey
      in: header
      name: X-Admin-Token

  parameters:
    BookingToken:
      in: path
      name: token
      required: true
      schema: { type: string }
      description: Token public de la réservation

  responses:
    Unauthorized:
      description: Clé API manquante ou invalide
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    NotFound:
      description: Ressource introuvable
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    BadRequest:
      description: Body / paramètres invalides
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    BookingActionOk:
      description: Action effectuée
      content:
        application/json:
          schema:
            type: object
            properties:
              success: { type: boolean }
              booking: { $ref: '#/components/schemas/Booking' }
              unchanged: { type: boolean, description: true si déjà au bon statut }

  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error: { type: string }

    Pagination:
      type: object
      properties:
        page: { type: integer }
        limit: { type: integer }
        total: { type: integer }
        total_pages: { type: integer }

    Booking:
      type: object
      properties:
        id: { type: integer }
        token: { type: string }
        status:
          type: string
          enum: [pending, confirmed, cancelled, no_show]
        guest_name: { type: string }
        guest_email: { type: string, format: email }
        guest_phone: { type: string, nullable: true }
        party_size: { type: integer }
        booking_date: { type: string, format: date }
        booking_time: { type: string }
        discount_percent: { type: number }
        notes: { type: string, nullable: true }
        deposit_status:
          type: string
          enum: [none, pending, paid, refunded, forfeited]
          nullable: true
        deposit_amount_chf: { type: number, nullable: true }
        no_show: { type: boolean }
        billed_at: { type: string, format: date-time, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    Restaurant:
      type: object
      properties:
        id: { type: integer }
        slug: { type: string }
        name: { type: string }
        email: { type: string, format: email, nullable: true }
        phone: { type: string, nullable: true }
        address: { type: string, nullable: true }
        website: { type: string, format: uri, nullable: true }
        primary_color: { type: string }
        timezone: { type: string, example: Europe/Zurich }
        locale: { type: string, example: fr-CH }
        booking_notice_hours: { type: integer }
        max_advance_days: { type: integer }
        default_party_size: { type: integer }
        group_discount_percent: { type: integer }
        group_discount_min_guests: { type: integer }
        notification_cc_email: { type: string, nullable: true }
        is_active: { type: boolean }
        created_at: { type: string, format: date-time }

    NotificationsConfig:
      type: object
      properties:
        cc_email:
          type: string
          nullable: true
          description: Valeur brute stockée (CSV) — null si non configuré
        cc_emails:
          type: array
          items: { type: string, format: email }
          description: Tableau parsé pour confort
        fallback_email:
          type: string
          nullable: true
          description: Email principal du resto, utilisé si `cc_email` est null
        effective_cc:
          type: array
          items: { type: string, format: email }
          description: Liste réellement utilisée en CC

    WeekDayConfig:
      type: object
      properties:
        day_of_week: { type: integer, minimum: 0, maximum: 6 }
        is_open: { type: boolean }
        open_time: { type: string }
        close_time: { type: string }
        slot_duration_min: { type: integer }
        max_bookings_per_slot: { type: integer }
        discount_percent: { type: integer }

    DateOverride:
      type: object
      properties:
        date: { type: string, format: date }
        is_open: { type: boolean }
        open_time: { type: string, nullable: true }
        close_time: { type: string, nullable: true }
        slot_duration_min: { type: integer, nullable: true }
        max_bookings_per_slot: { type: integer, nullable: true }
        discount_percent: { type: integer, nullable: true }
        label: { type: string, nullable: true }

    SlotsResponse:
      type: object
      properties:
        date: { type: string, format: date }
        is_open: { type: boolean }
        discount_percent: { type: integer }
        open_time: { type: string }
        close_time: { type: string }
        slot_duration_min: { type: integer }
        max_bookings_per_slot: { type: integer }
        label: { type: string, nullable: true }
        blocked_reason: { type: string, nullable: true }
        notice_cutoff:
          type: string
          nullable: true
          description: "Heure minimale (`HH:MM`) à laquelle on peut encore réserver aujourd'hui (= now + booking_notice_hours). null si ce n'est pas aujourd'hui."
        services:
          type: array
          items: { $ref: '#/components/schemas/ServiceUsage' }
          description: Usage cumulé par service actif pour cette date
        slots:
          type: array
          items:
            type: object
            properties:
              time: { type: string }
              available: { type: boolean }
              remaining: { type: integer }
              booked: { type: integer, description: Nb de réservations sur ce créneau }
              guests: { type: integer, description: Nb de couverts sur ce créneau }
              max_bookings_per_slot: { type: integer }
              service: { type: string, nullable: true, description: Code du service auquel appartient le créneau }
              service_full: { type: boolean, description: true si le service est plein }
              unavailable_reason:
                type: string
                enum: [slot_full, service_full]
                description: "Présent uniquement si `available: false`"

    Service:
      type: object
      properties:
        id: { type: integer }
        code: { type: string, example: lunch }
        label: { type: string, nullable: true, example: Déjeuner }
        start_time: { type: string, example: '12:00' }
        end_time: { type: string, example: '14:00' }
        slot_duration_min: { type: integer, example: 15 }
        max_bookings_per_slot: { type: integer, example: 8 }
        max_total_guests:
          type: integer
          nullable: true
          description: null = illimité ; sinon capacité cumulée sur le service / jour
        unlimited: { type: boolean }
        is_active: { type: boolean }
        sort_order: { type: integer }
        usage_on_date:
          $ref: '#/components/schemas/ServiceUsageOnDate'
          description: Présent uniquement si `?date=` est passé

    ServiceCreate:
      type: object
      required: [code, start_time, end_time]
      properties:
        code: { type: string, pattern: '^[a-z0-9_-]+$', example: lunch }
        label: { type: string }
        start_time: { type: string, pattern: '^\d{2}:\d{2}$' }
        end_time: { type: string, pattern: '^\d{2}:\d{2}$' }
        slot_duration_min: { type: integer, default: 30 }
        max_bookings_per_slot: { type: integer, default: 5 }
        max_total_guests: { type: integer, nullable: true }
        is_active: { type: boolean, default: true }
        sort_order: { type: integer, default: 0 }

    ServiceUpdate:
      type: object
      properties:
        label: { type: string }
        start_time: { type: string, pattern: '^\d{2}:\d{2}$' }
        end_time: { type: string, pattern: '^\d{2}:\d{2}$' }
        slot_duration_min: { type: integer }
        max_bookings_per_slot: { type: integer }
        max_total_guests:
          type: integer
          nullable: true
          description: Passer `null` explicitement pour repasser en illimité
        is_active: { type: boolean }
        sort_order: { type: integer }

    ServiceUsage:
      type: object
      properties:
        id: { type: integer }
        code: { type: string }
        label: { type: string, nullable: true }
        start: { type: string }
        end: { type: string }
        max_total_guests: { type: integer, nullable: true }
        guests: { type: integer }
        remaining: { type: integer, nullable: true, description: null si illimité }
        full: { type: boolean }

    ServiceUsageOnDate:
      type: object
      properties:
        date: { type: string, format: date }
        bookings: { type: integer }
        guests: { type: integer }
        max_total_guests: { type: integer, nullable: true }
        remaining: { type: integer, nullable: true }
        full: { type: boolean }

    CountsResponse:
      type: object
      properties:
        range:
          type: object
          properties:
            from: { type: string, format: date }
            to: { type: string, format: date }
        service:
          oneOf:
            - { $ref: '#/components/schemas/Service' }
            - { type: 'null' }
        total:
          type: object
          properties:
            bookings: { type: integer }
            guests: { type: integer }
        days:
          type: array
          items:
            type: object
            properties:
              date: { type: string, format: date }
              bookings: { type: integer }
              guests: { type: integer }
              max_total_guests: { type: integer, nullable: true }
              remaining: { type: integer, nullable: true }
              full: { type: boolean }
