{
  "openapi": "3.1.1",
  "info": {
    "title": "API-мастерская: заявки сервисной компании",
    "version": "1.0.0",
    "description": "Восемь практических ситуаций на настоящем PHP. Все данные вымышлены; POST/PATCH ничего не сохраняют. HTTP 429 фиксирован. Публичные токены demo-reader и demo-editor предназначены только для обучения. Не передавайте реальные персональные данные или секреты."
  },
  "servers": [
    {
      "url": "./api",
      "description": "PHP-маршруты рядом с openapi.json"
    }
  ],
  "tags": [
    {
      "name": "Заявки",
      "description": "Каталог, карточка, расчёт и проверка версии."
    },
    {
      "name": "Доступ",
      "description": "Учебные 401/403/200 без настоящих учётных записей."
    },
    {
      "name": "Файлы",
      "description": "Обработка text/plain в памяти."
    },
    {
      "name": "Контракт",
      "description": "Ограничение частоты и устаревшая операция."
    }
  ],
  "paths": {
    "/catalog.php": {
      "get": {
        "operationId": "listTickets",
        "tags": [
          "Заявки"
        ],
        "summary": "Каталог заявок с фильтром и пагинацией",
        "description": "Всего 18 фиксированных заявок. Пустая страница — это 200 и пустой data. Значения page и limit ограничены сервером.",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Фильтр состояния",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "new",
                "in_progress",
                "done"
              ],
              "default": "all"
            },
            "example": "in_progress"
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Номер страницы",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 1
            },
            "example": 1
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Размер страницы",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 6,
              "default": 4
            },
            "example": 4
          }
        ],
        "responses": {
          "200": {
            "description": "Страница каталога",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "demo": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Ticket"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "page": {
                          "type": "integer",
                          "minimum": 1,
                          "maximum": 100
                        },
                        "limit": {
                          "type": "integer",
                          "minimum": 1,
                          "maximum": 6
                        },
                        "total": {
                          "type": "integer",
                          "minimum": 0,
                          "maximum": 18
                        },
                        "pages": {
                          "type": "integer",
                          "minimum": 0,
                          "maximum": 18
                        }
                      },
                      "required": [
                        "page",
                        "limit",
                        "total",
                        "pages"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "demo",
                    "data",
                    "meta"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "422": {
            "description": "Проверка данных не прошла",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "fields": {
                    "value": {
                      "demo": true,
                      "error": {
                        "code": "VALIDATION_FAILED",
                        "message": "Исправьте отмеченные поля",
                        "fields": {
                          "customer.email": "Нужен корректный email"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/detail.php": {
      "get": {
        "operationId": "getTicket",
        "tags": [
          "Заявки"
        ],
        "summary": "Карточка по идентификатору",
        "description": "id здесь query-параметр, чтобы пример работал на обычном PHP-хостинге без rewrite. Для REST-router аналогом будет GET /tickets/{id}.",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": false,
            "description": "Идентификатор; 1–18 существуют",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 999,
              "default": 1
            },
            "example": 3
          }
        ],
        "responses": {
          "200": {
            "description": "Карточка",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "demo": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/Ticket"
                    }
                  },
                  "required": [
                    "demo",
                    "data"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "404": {
            "description": "Карточка отсутствует",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "missing": {
                    "value": {
                      "demo": true,
                      "error": {
                        "code": "TICKET_NOT_FOUND",
                        "message": "Учебная заявка не найдена",
                        "fields": {}
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Проверка данных не прошла",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "fields": {
                    "value": {
                      "demo": true,
                      "error": {
                        "code": "VALIDATION_FAILED",
                        "message": "Исправьте отмеченные поля",
                        "fields": {
                          "customer.email": "Нужен корректный email"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/quote.php": {
      "post": {
        "operationId": "previewQuote",
        "tags": [
          "Заявки"
        ],
        "summary": "Проверить JSON и рассчитать заявку",
        "description": "Сложение ведётся в целых копейках. 201 показывает учебный результат создания расчёта, но persisted всегда false. Лимит тела 12 КиБ. Не создаёт заказ, платёж или запись в базе.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/QuoteInput"
              },
              "examples": {
                "valid": {
                  "summary": "Корректная заявка",
                  "value": {
                    "customer": {
                      "name": "Марина",
                      "email": "marina@example.test"
                    },
                    "items": [
                      {
                        "title": "Диагностика",
                        "quantity": 2,
                        "priceMinor": 125000
                      },
                      {
                        "title": "Материалы",
                        "quantity": 1,
                        "priceMinor": 40000
                      }
                    ]
                  }
                },
                "invalid": {
                  "summary": "Несколько ошибок",
                  "value": {
                    "customer": {
                      "name": "М",
                      "email": "wrong"
                    },
                    "items": [
                      {
                        "title": "Работа",
                        "quantity": 0,
                        "priceMinor": "100"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Предпросмотр расчёта готов",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuoteResult"
                },
                "examples": {
                  "quote": {
                    "value": {
                      "demo": true,
                      "data": {
                        "id": "DEMO-Q1001",
                        "totalMinor": 290000,
                        "currency": "RUB",
                        "items": 2,
                        "persisted": false
                      },
                      "message": "Предпросмотр расчёта готов. Заявка не сохранялась."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "JSON повреждён или вместо объекта пришёл массив",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Тело превысило допустимый размер",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "Неподдерживаемый Content-Type",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Проверка данных не прошла",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "fields": {
                    "value": {
                      "demo": true,
                      "error": {
                        "code": "VALIDATION_FAILED",
                        "message": "Исправьте отмеченные поля",
                        "fields": {
                          "customer.email": "Нужен корректный email"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/account.php": {
      "get": {
        "operationId": "checkDemoRole",
        "tags": [
          "Доступ"
        ],
        "summary": "Сравнить 401, 403 и разрешённое действие",
        "description": "Для read подходит demo-reader или demo-editor; для edit — только demo-editor. Это публичные демонстрационные строки, не секреты и не production-аутентификация.",
        "security": [
          {
            "demoBearer": []
          }
        ],
        "parameters": [
          {
            "name": "action",
            "in": "query",
            "required": false,
            "description": "Демонстрационное действие",
            "schema": {
              "type": "string",
              "enum": [
                "read",
                "edit"
              ],
              "default": "read"
            },
            "example": "edit"
          }
        ],
        "responses": {
          "200": {
            "description": "Учебная роль допускает действие",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GenericResult"
                }
              }
            }
          },
          "401": {
            "description": "Токен отсутствует или неизвестен",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Токен известен, но права не подходят",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Проверка данных не прошла",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "fields": {
                    "value": {
                      "demo": true,
                      "error": {
                        "code": "VALIDATION_FAILED",
                        "message": "Исправьте отмеченные поля",
                        "fields": {
                          "customer.email": "Нужен корректный email"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/revision.php": {
      "patch": {
        "operationId": "previewRevision",
        "tags": [
          "Заявки"
        ],
        "summary": "Проверить ожидаемую версию изменения",
        "description": "Фикстура всегда имеет version=3. Успешный ответ показывает version=4, но общая карточка не меняется. Для реального API нужен атомарный UPDATE с условием версии.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Revision"
              },
              "examples": {
                "current": {
                  "value": {
                    "expectedVersion": 3,
                    "title": "Проверить освещение"
                  }
                },
                "stale": {
                  "value": {
                    "expectedVersion": 2,
                    "title": "Проверить освещение"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Учебный результат изменения без сохранения",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GenericResult"
                }
              }
            }
          },
          "400": {
            "description": "JSON повреждён или вместо объекта пришёл массив",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Конфликт версии",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "conflict": {
                    "value": {
                      "demo": true,
                      "error": {
                        "code": "VERSION_CONFLICT",
                        "message": "Данные изменились: перечитайте карточку",
                        "fields": {}
                      },
                      "currentVersion": 3
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "Тело превысило допустимый размер",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "Неподдерживаемый Content-Type",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Проверка данных не прошла",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "fields": {
                    "value": {
                      "demo": true,
                      "error": {
                        "code": "VALIDATION_FAILED",
                        "message": "Исправьте отмеченные поля",
                        "fields": {
                          "customer.email": "Нужен корректный email"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/upload.php": {
      "post": {
        "operationId": "inspectText",
        "tags": [
          "Файлы"
        ],
        "summary": "Прочитать маленький UTF-8 текст без сохранения",
        "description": "Тело передаётся как text/plain; это не multipart/form-data. Максимум 4096 байт, не символов. Возвращаются число байт, строк и SHA-256. Сервер не сохраняет файл.",
        "requestBody": {
          "required": true,
          "content": {
            "text/plain": {
              "schema": {
                "type": "string",
                "description": "UTF-8, от 1 до 4096 байт"
              },
              "example": "Код;Задача\nA-01;Проверить щит\n"
            }
          }
        },
        "responses": {
          "200": {
            "description": "Текст разобран в памяти",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GenericResult"
                }
              }
            }
          },
          "413": {
            "description": "Тело превысило допустимый размер",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "Неподдерживаемый Content-Type",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Проверка данных не прошла",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "fields": {
                    "value": {
                      "demo": true,
                      "error": {
                        "code": "VALIDATION_FAILED",
                        "message": "Исправьте отмеченные поля",
                        "fields": {
                          "customer.email": "Нужен корректный email"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/limited.php": {
      "get": {
        "operationId": "showRateLimit",
        "tags": [
          "Контракт"
        ],
        "summary": "Получить учебный 429 с Retry-After",
        "description": "Фиксированный пример: ответ всегда 429, настоящего лимитера нет. Не запускайте бесконечные автоматические повторы.",
        "responses": {
          "429": {
            "description": "Учебный лимит",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Рекомендуемая задержка в секундах",
                "schema": {
                  "type": "integer",
                  "const": 3
                }
              }
            }
          }
        }
      }
    },
    "/legacy.php": {
      "get": {
        "operationId": "legacyTickets",
        "tags": [
          "Контракт"
        ],
        "summary": "Устаревшая операция, которая ещё отвечает",
        "deprecated": true,
        "description": "Используйте catalog.php. Пометка deprecated в документе не удаляет обработчик.",
        "responses": {
          "200": {
            "description": "Предупреждение о переходе на новый endpoint",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GenericResult"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "demoBearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "Только публичная строка demo-reader или demo-editor. Не вводите настоящий токен."
      }
    },
    "schemas": {
      "Ticket": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "minimum": 1,
            "maximum": 999
          },
          "title": {
            "type": "string",
            "description": "Краткое название работы"
          },
          "status": {
            "type": "string",
            "enum": [
              "new",
              "in_progress",
              "done"
            ]
          },
          "amountMinor": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100000000
          },
          "currency": {
            "const": "RUB",
            "type": "string"
          },
          "version": {
            "type": "integer",
            "minimum": 1,
            "maximum": 1000
          }
        },
        "required": [
          "id",
          "title",
          "status",
          "amountMinor",
          "currency",
          "version"
        ],
        "additionalProperties": false
      },
      "Error": {
        "type": "object",
        "properties": {
          "demo": {
            "const": true,
            "type": "boolean"
          },
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "description": "Машиночитаемый код"
              },
              "message": {
                "type": "string",
                "description": "Понятное описание"
              },
              "fields": {
                "type": "object",
                "additionalProperties": {
                  "type": "string"
                }
              }
            },
            "required": [
              "code",
              "message",
              "fields"
            ],
            "additionalProperties": false
          },
          "currentVersion": {
            "type": "integer",
            "minimum": 1,
            "maximum": 1000
          }
        },
        "required": [
          "demo",
          "error"
        ],
        "additionalProperties": false
      },
      "Customer": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Только вымышленное имя",
            "minLength": 2,
            "maxLength": 60
          },
          "email": {
            "type": "string",
            "description": "Только учебный адрес",
            "format": "email",
            "maxLength": 120
          }
        },
        "required": [
          "name",
          "email"
        ],
        "additionalProperties": false
      },
      "Item": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string",
            "description": "Услуга",
            "minLength": 1,
            "maxLength": 70
          },
          "quantity": {
            "type": "integer",
            "minimum": 1,
            "maximum": 20
          },
          "priceMinor": {
            "type": "integer",
            "minimum": 0,
            "maximum": 1000000
          }
        },
        "required": [
          "title",
          "quantity",
          "priceMinor"
        ],
        "additionalProperties": false
      },
      "QuoteInput": {
        "type": "object",
        "properties": {
          "customer": {
            "$ref": "#/components/schemas/Customer"
          },
          "items": {
            "type": "array",
            "minItems": 1,
            "maxItems": 5,
            "items": {
              "$ref": "#/components/schemas/Item"
            }
          }
        },
        "required": [
          "customer",
          "items"
        ],
        "additionalProperties": false
      },
      "QuoteResult": {
        "type": "object",
        "properties": {
          "demo": {
            "const": true,
            "type": "boolean"
          },
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "description": "Учебный идентификатор"
              },
              "totalMinor": {
                "type": "integer",
                "minimum": 0,
                "maximum": 100000000
              },
              "currency": {
                "type": "string",
                "const": "RUB"
              },
              "items": {
                "type": "integer",
                "minimum": 1,
                "maximum": 5
              },
              "persisted": {
                "type": "boolean",
                "const": false
              }
            },
            "required": [
              "id",
              "totalMinor",
              "currency",
              "items",
              "persisted"
            ],
            "additionalProperties": false
          },
          "message": {
            "type": "string",
            "description": "Результат без сохранения"
          }
        },
        "required": [
          "demo",
          "data",
          "message"
        ],
        "additionalProperties": false
      },
      "Revision": {
        "type": "object",
        "properties": {
          "expectedVersion": {
            "type": "integer",
            "minimum": 1,
            "maximum": 1000000
          },
          "title": {
            "type": "string",
            "description": "Новый заголовок",
            "minLength": 3,
            "maxLength": 70
          }
        },
        "required": [
          "expectedVersion",
          "title"
        ],
        "additionalProperties": false
      },
      "GenericResult": {
        "type": "object",
        "properties": {
          "demo": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "object",
            "additionalProperties": true
          },
          "message": {
            "type": "string"
          }
        },
        "required": [
          "demo",
          "data"
        ],
        "additionalProperties": false
      }
    }
  }
}
