{
  "openapi": "3.1.0",
  "info": {
    "title": "MIB Clean API",
    "description": "API for MIB Clean — professional dry-cleaning and laundry service with door-to-door pickup and delivery in 100+ French cities.\n\n**Base URL:** https://mibcleanos.polsia.app\n\n**Authentication:** JWT Bearer token (obtain via POST /api/auth/login). Most read endpoints are public.",
    "version": "1.0.0",
    "contact": {
      "name": "MIB Clean Support",
      "url": "https://mibcleanos.polsia.app",
      "email": "agence@mib-clean.com"
    },
    "license": {
      "name": "Proprietary"
    }
  },
  "servers": [
    {
      "url": "https://mibcleanos.polsia.app",
      "description": "Production server"
    }
  ],
  "security": [],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "JWT token obtained from POST /api/auth/login"
      }
    },
    "schemas": {
      "City": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "example": 1
          },
          "name": {
            "type": "string",
            "example": "Paris"
          },
          "postal_codes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "example": [
              "75001",
              "75002"
            ]
          },
          "active": {
            "type": "boolean",
            "example": true
          }
        }
      },
      "Service": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "example": 1
          },
          "name": {
            "type": "string",
            "example": "Pressing (nettoyage à sec)"
          },
          "description": {
            "type": "string",
            "example": "Nettoyage à sec professionnel de vêtements"
          },
          "unit": {
            "type": "string",
            "example": "pièce",
            "enum": [
              "pièce",
              "kg",
              "lot"
            ]
          },
          "base_price": {
            "type": "number",
            "example": 12.9
          },
          "vat_rate": {
            "type": "number",
            "example": 20
          },
          "active": {
            "type": "boolean",
            "example": true
          }
        }
      },
      "Article": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "prestation_id": {
            "type": "integer"
          },
          "name": {
            "type": "string",
            "example": "Veste"
          },
          "price_ttc": {
            "type": "number",
            "example": 14.9
          },
          "photo_url": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "TimeSlot": {
        "type": "object",
        "properties": {
          "slot": {
            "type": "string",
            "example": "08:00-10:00"
          },
          "label": {
            "type": "string",
            "example": "8h - 10h"
          },
          "date": {
            "type": "string",
            "format": "date",
            "example": "2026-03-25"
          },
          "available": {
            "type": "boolean",
            "example": true
          },
          "remaining_capacity": {
            "type": "integer",
            "example": 3
          }
        }
      },
      "Order": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "reference": {
            "type": "string",
            "example": "MIB-2026-0042"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "pending_pickup",
              "picked_up",
              "processing",
              "ready",
              "delivered",
              "cancelled"
            ],
            "example": "processing"
          },
          "pickup_date": {
            "type": "string",
            "format": "date-time"
          },
          "delivery_date": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "total_ttc": {
            "type": "number",
            "example": 45.8
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OrderItem"
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "OrderItem": {
        "type": "object",
        "properties": {
          "article_id": {
            "type": "integer"
          },
          "article_name": {
            "type": "string",
            "example": "Veste"
          },
          "quantity": {
            "type": "integer",
            "example": 2
          },
          "unit_price_ttc": {
            "type": "number",
            "example": 14.9
          },
          "total_ttc": {
            "type": "number",
            "example": 29.8
          }
        }
      },
      "QuoteRequest": {
        "type": "object",
        "required": [
          "city_id",
          "items"
        ],
        "properties": {
          "city_id": {
            "type": "integer",
            "example": 1,
            "description": "City ID from /api/catalog/cities"
          },
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "article_id": {
                  "type": "integer",
                  "example": 5
                },
                "quantity": {
                  "type": "integer",
                  "example": 3
                }
              }
            }
          },
          "promo_code": {
            "type": "string",
            "nullable": true,
            "example": "BIENVENUE10"
          }
        }
      },
      "QuoteResponse": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OrderItem"
            }
          },
          "subtotal_ht": {
            "type": "number",
            "example": 38.17
          },
          "vat_amount": {
            "type": "number",
            "example": 7.63
          },
          "total_ttc": {
            "type": "number",
            "example": 45.8
          },
          "discount_amount": {
            "type": "number",
            "example": 0
          },
          "currency": {
            "type": "string",
            "example": "EUR"
          }
        }
      },
      "OrderRequest": {
        "type": "object",
        "required": [
          "city_id",
          "pickup_slot",
          "items",
          "address"
        ],
        "properties": {
          "city_id": {
            "type": "integer",
            "example": 1
          },
          "pickup_slot": {
            "type": "object",
            "properties": {
              "date": {
                "type": "string",
                "format": "date",
                "example": "2026-03-25"
              },
              "slot": {
                "type": "string",
                "example": "08:00-10:00"
              }
            }
          },
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "article_id": {
                  "type": "integer"
                },
                "quantity": {
                  "type": "integer"
                }
              }
            }
          },
          "address": {
            "type": "object",
            "properties": {
              "street": {
                "type": "string",
                "example": "12 rue de la Paix"
              },
              "postal_code": {
                "type": "string",
                "example": "75001"
              },
              "city": {
                "type": "string",
                "example": "Paris"
              }
            }
          },
          "notes": {
            "type": "string",
            "nullable": true,
            "example": "Code porte: 1234"
          },
          "promo_code": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "AuthRequest": {
        "type": "object",
        "required": [
          "email",
          "password"
        ],
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "example": "user@example.com"
          },
          "password": {
            "type": "string",
            "format": "password"
          }
        }
      },
      "AuthResponse": {
        "type": "object",
        "properties": {
          "token": {
            "type": "string",
            "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
          },
          "user": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "email": {
                "type": "string"
              },
              "name": {
                "type": "string"
              }
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "example": "Resource not found"
          },
          "message": {
            "type": "string",
            "example": "The requested resource does not exist"
          }
        }
      }
    }
  },
  "paths": {
    "/health": {
      "get": {
        "summary": "API health check",
        "description": "Returns the health status of the API and database connection.",
        "operationId": "healthCheck",
        "tags": [
          "System"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "API is healthy",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "ok"
                    },
                    "timestamp": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/catalog/cities": {
      "get": {
        "summary": "List available cities",
        "description": "Returns all cities where MIB Clean service is available. Use the city ID in other API calls.",
        "operationId": "getCities",
        "tags": [
          "Catalog"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "List of cities",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/City"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/catalog/b2c": {
      "get": {
        "summary": "Get B2C service catalog",
        "description": "Returns the full public catalog of services and articles with prices for B2C customers.",
        "operationId": "getB2CCatalog",
        "tags": [
          "Catalog"
        ],
        "security": [],
        "parameters": [
          {
            "name": "city_id",
            "in": "query",
            "description": "Filter catalog by city ID to get city-specific pricing",
            "required": false,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Service catalog with prices",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "prestations": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Service"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/prestations": {
      "get": {
        "summary": "List service types",
        "description": "Returns all available service types (pressing, ironing, laundry, etc.) with their configuration.",
        "operationId": "getServices",
        "tags": [
          "Catalog"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "List of service types",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Service"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/prestations/{id}/articles": {
      "get": {
        "summary": "Get articles for a service",
        "description": "Returns all priceable items (articles) for a specific service type.",
        "operationId": "getServiceArticles",
        "tags": [
          "Catalog"
        ],
        "security": [],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Service (prestation) ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of articles with prices",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Article"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/slots/available-pickup": {
      "get": {
        "summary": "Get available pickup slots",
        "description": "Returns ALL configured time slots for pickup on a given date, including evening slots (19h-21h etc). Pass city_id to load real slots from database (recommended). Without city_id, returns default hardcoded slots only. If prestation_id is also provided, automatically selects the best available partner and checks their availability.",
        "operationId": "getAvailablePickupSlots",
        "tags": [
          "Slots"
        ],
        "security": [],
        "parameters": [
          {
            "name": "date",
            "in": "query",
            "description": "Date to check availability (YYYY-MM-DD). Defaults to next available day.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2026-03-25"
            }
          },
          {
            "name": "city_id",
            "in": "query",
            "description": "City ID — REQUIRED to get all configured slots including evening slots from database. Without this param, only 4 default hardcoded slots are returned.",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "prestation_id",
            "in": "query",
            "description": "Service type ID to check partner availability for specific service",
            "required": false,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Available pickup slots",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "slots": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/TimeSlot"
                      }
                    },
                    "service_unavailable": {
                      "type": "boolean",
                      "description": "True if no partner is available for the requested service in this city"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/slots/available-delivery": {
      "get": {
        "summary": "Get available delivery slots",
        "description": "Returns available time slots for order delivery. Usually 2–5 business days after pickup.",
        "operationId": "getAvailableDeliverySlots",
        "tags": [
          "Slots"
        ],
        "security": [],
        "parameters": [
          {
            "name": "pickup_date",
            "in": "query",
            "description": "Pickup date to calculate delivery window from (YYYY-MM-DD)",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "city_id",
            "in": "query",
            "description": "City ID",
            "required": false,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Available delivery slots",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "slots": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/TimeSlot"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/auth/register": {
      "post": {
        "summary": "Create a new account",
        "description": "Register a new customer account.",
        "operationId": "register",
        "tags": [
          "Authentication"
        ],
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email",
                  "password",
                  "first_name",
                  "last_name"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "password": {
                    "type": "string",
                    "minLength": 8
                  },
                  "first_name": {
                    "type": "string",
                    "example": "Marie"
                  },
                  "last_name": {
                    "type": "string",
                    "example": "Dupont"
                  },
                  "phone": {
                    "type": "string",
                    "example": "+33612345678"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Account created successfully",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid input",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Email already registered"
          }
        }
      }
    },
    "/api/auth/login": {
      "post": {
        "summary": "Authenticate and get JWT token",
        "description": "Authenticate with email and password. Returns a JWT token valid for 7 days.",
        "operationId": "login",
        "tags": [
          "Authentication"
        ],
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AuthRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Authentication successful",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthResponse"
                }
              }
            }
          },
          "401": {
            "description": "Invalid credentials"
          }
        }
      }
    },
    "/api/subscriptions": {
      "post": {
        "summary": "Place a new order",
        "description": "Create a new laundry/dry-cleaning order. Requires authentication.",
        "operationId": "placeOrder",
        "tags": [
          "Orders"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OrderRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Order created successfully",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Order"
                }
              }
            }
          },
          "400": {
            "description": "Invalid order data"
          },
          "401": {
            "description": "Authentication required"
          }
        }
      },
      "get": {
        "summary": "List user orders",
        "description": "Returns all orders for the authenticated user.",
        "operationId": "listOrders",
        "tags": [
          "Orders"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "description": "Filter by order status",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "pending_pickup",
                "picked_up",
                "processing",
                "ready",
                "delivered",
                "cancelled"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 20,
              "maximum": 100
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of orders",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "orders": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Order"
                      }
                    },
                    "total": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/subscriptions/{id}": {
      "get": {
        "summary": "Get order details and tracking",
        "description": "Returns full details and current status of a specific order.",
        "operationId": "trackOrder",
        "tags": [
          "Orders"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Order ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Order details",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Order"
                }
              }
            }
          },
          "404": {
            "description": "Order not found"
          }
        }
      }
    }
  },
  "tags": [
    {
      "name": "System",
      "description": "Health and system endpoints"
    },
    {
      "name": "Catalog",
      "description": "Browse services, articles, and available cities — all public, no auth required"
    },
    {
      "name": "Slots",
      "description": "Check pickup and delivery time slot availability"
    },
    {
      "name": "Authentication",
      "description": "Register and authenticate to obtain a JWT token for protected endpoints"
    },
    {
      "name": "Orders",
      "description": "Place orders and track their status — requires authentication"
    }
  ]
}