{
  "openapi": "3.0.3",
  "info": {
    "title": "Restora 360 — Public API",
    "version": "1.0.0",
    "description": "Public-API endpoints for ordering, reservations, menu, payments, and gift cards on Restora 360-hosted restaurants.\n\nSee https://restora360.com/docs/api/getting-started for auth, rate limits, error envelope, and idempotency conventions.",
    "contact": {
      "name": "Restora 360",
      "url": "https://restora360.com/docs"
    },
    "license": {
      "name": "Proprietary"
    }
  },
  "servers": [
    {
      "url": "https://restora360.com",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Bazar auth",
      "description": "Bazar marketplace customer accounts — cookie-session register/login/me/logout."
    },
    {
      "name": "Orders",
      "description": "Submit, retrieve, cancel customer orders."
    },
    {
      "name": "Reservations",
      "description": "Public reservation booking and availability."
    },
    {
      "name": "Menu",
      "description": "Read-only menu filter taxonomy."
    },
    {
      "name": "Payments",
      "description": "Payment intents and coupon validation."
    },
    {
      "name": "Gift cards",
      "description": "Public gift-card purchase and lookup."
    }
  ],
  "components": {
    "parameters": {
      "TenantId": {
        "name": "tenantId",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string"
        },
        "description": "Tenant identifier (slug or short ID)."
      },
      "OrderId": {
        "name": "orderId",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string"
        }
      },
      "ReservationId": {
        "name": "reservationId",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string"
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Machine-readable error code (e.g. `OUTSIDE_RADIUS`)."
          },
          "message": {
            "type": "string",
            "description": "Human-readable detail."
          }
        }
      },
      "OrderItem": {
        "type": "object",
        "required": [
          "productId",
          "quantity",
          "price"
        ],
        "properties": {
          "productId": {
            "type": "string"
          },
          "productName": {
            "type": "string"
          },
          "quantity": {
            "type": "integer",
            "minimum": 1
          },
          "price": {
            "type": "number",
            "description": "Re-validated server-side against the menu; trust the response, not this field."
          },
          "selectedOptions": {
            "type": "object",
            "additionalProperties": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "description": "Dict shape: `{ \"Size\": [\"Large\"], \"Toppings\": [\"Pepperoni\"] }`."
          }
        }
      },
      "OrderRequest": {
        "type": "object",
        "required": [
          "type",
          "customerName",
          "customerEmail",
          "customerPhone",
          "paymentMethod",
          "items"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "pickup",
              "delivery"
            ]
          },
          "customerName": {
            "type": "string",
            "maxLength": 100
          },
          "customerEmail": {
            "type": "string",
            "format": "email"
          },
          "customerPhone": {
            "type": "string"
          },
          "deliveryAddress": {
            "type": "string",
            "maxLength": 300
          },
          "postcode": {
            "type": "string"
          },
          "notes": {
            "type": "string"
          },
          "paymentMethod": {
            "type": "string",
            "enum": [
              "cash",
              "stripe",
              "paypal",
              "swish"
            ]
          },
          "couponCode": {
            "type": "string"
          },
          "giftCardCode": {
            "type": "string"
          },
          "customerCoords": {
            "type": "object",
            "description": "Required for the delivery-radius gate to fire (Phase 55). Absent → fail-open.",
            "properties": {
              "lat": {
                "type": "number"
              },
              "lng": {
                "type": "number"
              }
            }
          },
          "items": {
            "type": "array",
            "minItems": 1,
            "items": {
              "$ref": "#/components/schemas/OrderItem"
            }
          }
        }
      },
      "BazarRegisterRequest": {
        "type": "object",
        "required": [
          "email",
          "password",
          "name",
          "gdprConsent"
        ],
        "properties": {
          "email": {
            "type": "string",
            "format": "email"
          },
          "password": {
            "type": "string",
            "minLength": 8
          },
          "name": {
            "type": "string"
          },
          "gdprConsent": {
            "type": "boolean",
            "enum": [
              true
            ],
            "description": "Must be `true`. The server treats `false` as a 400."
          }
        }
      },
      "BazarLoginRequest": {
        "type": "object",
        "required": [
          "email",
          "password"
        ],
        "properties": {
          "email": {
            "type": "string",
            "format": "email"
          },
          "password": {
            "type": "string"
          }
        }
      },
      "ReservationRequest": {
        "type": "object",
        "required": [
          "customerName",
          "customerEmail",
          "customerPhone",
          "guestCount",
          "date",
          "timeSlot"
        ],
        "properties": {
          "customerName": {
            "type": "string"
          },
          "customerEmail": {
            "type": "string",
            "format": "email"
          },
          "customerPhone": {
            "type": "string"
          },
          "guestCount": {
            "type": "integer",
            "minimum": 1,
            "maximum": 20
          },
          "date": {
            "type": "string",
            "format": "date",
            "description": "YYYY-MM-DD"
          },
          "timeSlot": {
            "type": "string",
            "description": "24h HH:mm"
          },
          "specialRequests": {
            "type": "string",
            "maxLength": 500
          }
        }
      }
    },
    "responses": {
      "RateLimited": {
        "description": "Too many requests. `Retry-After` header set to the wait in seconds.",
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "integer"
            },
            "description": "Seconds."
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "ValidationError": {
        "description": "Request body failed validation.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  },
  "paths": {
    "/api/bazar/auth/register": {
      "post": {
        "tags": [
          "Bazar auth"
        ],
        "summary": "Register a bazar marketplace account",
        "description": "Creates a BazarUser and issues the `bazar_user_session` HttpOnly cookie. 409 when email already taken.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BazarRegisterRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Account created; session cookie set."
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "409": {
            "description": "Email already taken."
          }
        }
      }
    },
    "/api/bazar/auth/login": {
      "post": {
        "tags": [
          "Bazar auth"
        ],
        "summary": "Log in to a bazar account",
        "description": "Verifies credentials and issues a fresh session cookie. Rate limit: 8 / 15 min / IP + 300 / 15 min global (Phase 67 stacked cap).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BazarLoginRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Authenticated; session cookie set."
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "description": "Invalid credentials."
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/bazar/auth/me": {
      "get": {
        "tags": [
          "Bazar auth"
        ],
        "summary": "Current bazar user",
        "description": "Returns the signed-in user's public profile.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated."
          }
        }
      }
    },
    "/api/bazar/auth/logout": {
      "post": {
        "tags": [
          "Bazar auth"
        ],
        "summary": "Log out",
        "description": "Clears the session cookie. Idempotent; returns 200 even without an active session.",
        "responses": {
          "200": {
            "description": "Logged out."
          }
        }
      }
    },
    "/api/public/{tenantId}/orders": {
      "post": {
        "tags": [
          "Orders"
        ],
        "summary": "Submit an order",
        "description": "Server re-validates prices against the menu — do not trust client `price`. Returns 409 `OUTSIDE_RADIUS` when delivery destination is beyond the tenant's `deliveryRadiusKm`. Rate limit: 10 / 10 min / IP / tenant.",
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OrderRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Order created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "409": {
            "description": "OUTSIDE_RADIUS or product out-of-stock.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/public/{tenantId}/orders/{orderId}": {
      "get": {
        "tags": [
          "Orders"
        ],
        "summary": "Retrieve an order",
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantId"
          },
          {
            "$ref": "#/components/parameters/OrderId"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "Order not found"
          }
        }
      }
    },
    "/api/public/{tenantId}/orders/{orderId}/cancel": {
      "post": {
        "tags": [
          "Orders"
        ],
        "summary": "Cancel an order",
        "description": "Customer-initiated cancellation. Refund eligibility depends on the order status at time of request.",
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantId"
          },
          {
            "$ref": "#/components/parameters/OrderId"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Cancelled"
          },
          "409": {
            "description": "Status no longer cancellable"
          }
        }
      }
    },
    "/api/public/{tenantId}/reservation/config": {
      "get": {
        "tags": [
          "Reservations"
        ],
        "summary": "Reservation config",
        "description": "Tenant policy: party-size range, blocked dates, default slot length. Use to populate booking forms.",
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantId"
          }
        ],
        "responses": {
          "200": {
            "description": "Config"
          },
          "404": {
            "description": "Tenant not found"
          }
        }
      }
    },
    "/api/public/{tenantId}/reservation/availability": {
      "get": {
        "tags": [
          "Reservations"
        ],
        "summary": "Check availability",
        "description": "Returns `{ slots: [\"12:00\", ...] }`. Empty array means no availability.",
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantId"
          },
          {
            "name": "date",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "guestCount",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/api/public/{tenantId}/reservation": {
      "post": {
        "tags": [
          "Reservations"
        ],
        "summary": "Create reservation",
        "description": "Returns a cancellation token by email so the customer can self-cancel later. Rate limit: 5 / 10 min / IP / tenant.",
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReservationRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Booked"
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "409": {
            "description": "Slot no longer available"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/public/{tenantId}/reservation/{reservationId}/cancel": {
      "post": {
        "tags": [
          "Reservations"
        ],
        "summary": "Cancel reservation (token-gated)",
        "description": "Token from booking confirmation email.",
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantId"
          },
          {
            "$ref": "#/components/parameters/ReservationId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "token"
                ],
                "properties": {
                  "token": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Cancelled"
          },
          "403": {
            "description": "Token invalid"
          }
        }
      }
    },
    "/api/public/{tenantId}/menu/filters": {
      "get": {
        "tags": [
          "Menu"
        ],
        "summary": "Menu filter taxonomy",
        "description": "Allergens + dietary tags the operator has exposed on the public menu page.",
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantId"
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/api/public/{tenantId}/payment-intent": {
      "post": {
        "tags": [
          "Payments"
        ],
        "summary": "Create Stripe PaymentIntent",
        "description": "Returns a client_secret. Submit the order with the resulting paymentIntentId after Stripe Elements confirms.",
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OrderRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/api/public/{tenantId}/validate-coupon": {
      "post": {
        "tags": [
          "Payments"
        ],
        "summary": "Validate a coupon",
        "description": "Pre-flight check. Returns the resolved discount + reason if invalid.",
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "code",
                  "subtotal"
                ],
                "properties": {
                  "code": {
                    "type": "string"
                  },
                  "subtotal": {
                    "type": "number"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/api/public/{tenantId}/gift-cards/purchase": {
      "post": {
        "tags": [
          "Gift cards"
        ],
        "summary": "Initiate gift-card purchase",
        "description": "Returns a Stripe client_secret if Stripe is enabled; otherwise a pendingId for manual confirmation.",
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/api/public/{tenantId}/gift-cards/lookup": {
      "post": {
        "tags": [
          "Gift cards"
        ],
        "summary": "Look up gift card balance",
        "description": "Anyone with the code can read remaining balance + expiry — by design.",
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "code"
                ],
                "properties": {
                  "code": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    }
  }
}