{
    "openapi": "3.1.0",
    "info": {
        "title": "LoyalSparta API",
        "description": "API для интеграции с программой лояльности LoyalSparta.\n\n**Аутентификация:** Bearer-токен в заголовке `Authorization`. Ключи управляются в разделе **Интеграции → API-ключи**.\n\n**Идемпотентность:** для `POST /orders/process` обязателен заголовок `X-Idempotency-Key` (UUID). При повторном запросе с тем же ключом возвращается кешированный ответ без повторной обработки бонусов.",
        "version": "1.0.0"
    },
    "servers": [
        {
            "url": "https://loyal.rc9.ru/api/v1",
            "description": "API v1"
        }
    ],
    "security": [
        {
            "BearerAuth": []
        }
    ],
    "tags": [
        {
            "name": "Клиенты",
            "description": "Поиск клиентов, баланс бонусов и история транзакций"
        },
        {
            "name": "Заказы",
            "description": "Расчёт и обработка заказов с начислением / списанием бонусов"
        },
        {
            "name": "События",
            "description": "Начисления вне заказа: регистрация, отзывы"
        }
    ],
    "components": {
        "securitySchemes": {
            "BearerAuth": {
                "type": "http",
                "scheme": "bearer",
                "bearerFormat": "API Key",
                "description": "API-ключ из раздела «Интеграции → API-ключи»"
            }
        },
        "schemas": {
            "ClientResponse": {
                "type": "object",
                "properties": {
                    "phone": {
                        "type": "string",
                        "example": "+79001234567",
                        "description": "Номер телефона клиента"
                    },
                    "anket_id": {
                        "type": "integer",
                        "example": 42,
                        "description": "ID анкеты клиента в организации"
                    },
                    "balance": {
                        "type": "integer",
                        "example": 150,
                        "description": "Баланс бонусов (целое число)"
                    }
                }
            },
            "Error": {
                "type": "object",
                "properties": {
                    "error": {
                        "type": "string",
                        "description": "Описание ошибки"
                    }
                }
            },
            "OrderItem": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "integer",
                        "nullable": true,
                        "description": "ID товара (опционально, для /calculate)"
                    },
                    "name": {
                        "type": "string",
                        "nullable": true,
                        "description": "Название товара (опционально, для /process)"
                    },
                    "price": {
                        "type": "number",
                        "nullable": true,
                        "example": 500
                    },
                    "qty": {
                        "type": "integer",
                        "nullable": true,
                        "example": 2
                    }
                }
            },
            "CalculateResponse": {
                "type": "object",
                "properties": {
                    "can_accrue": {
                        "type": "integer",
                        "example": 50,
                        "description": "Бонусов будет начислено"
                    },
                    "can_redeem_max": {
                        "type": "integer",
                        "example": 100,
                        "description": "Максимум бонусов для списания (0 если redeem не передан)"
                    },
                    "client_balance": {
                        "type": "integer",
                        "example": 150,
                        "description": "Текущий баланс клиента"
                    }
                }
            },
            "ProcessResponse": {
                "type": "object",
                "properties": {
                    "order_id": {
                        "type": "integer",
                        "example": 15
                    },
                    "order_number": {
                        "type": "string",
                        "example": "ORD-2026-001"
                    },
                    "accrued": {
                        "type": "integer",
                        "example": 50,
                        "description": "Начислено бонусов"
                    },
                    "redeemed": {
                        "type": "integer",
                        "example": 50,
                        "description": "Списано бонусов"
                    },
                    "balance_after": {
                        "type": "integer",
                        "example": 150,
                        "description": "Баланс клиента после операции"
                    }
                }
            },
            "TransactionList": {
                "type": "object",
                "properties": {
                    "phone": {
                        "type": "string",
                        "example": "+79001234567"
                    },
                    "anket_id": {
                        "type": "integer",
                        "example": 42
                    },
                    "data": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "id": {
                                    "type": "integer"
                                },
                                "type": {
                                    "type": "string",
                                    "enum": [
                                        "deposit",
                                        "withdraw"
                                    ],
                                    "description": "deposit — начисление, withdraw — списание"
                                },
                                "amount": {
                                    "type": "integer",
                                    "description": "Сумма в бонусах"
                                },
                                "description": {
                                    "type": "string",
                                    "nullable": true
                                },
                                "created_at": {
                                    "type": "string",
                                    "format": "date-time"
                                },
                                "event": {
                                    "type": "string",
                                    "description": "Бизнес-тип операции",
                                    "enum": [
                                        "accrued",
                                        "redeemed",
                                        "redemption_refunded",
                                        "accrual_reverted",
                                        "expired",
                                        "event_awarded",
                                        "adjustment",
                                        "manual_deposit",
                                        "manual_withdraw"
                                    ]
                                },
                                "order_number": {
                                    "type": "string",
                                    "nullable": true
                                },
                                "order_id": {
                                    "type": "integer",
                                    "nullable": true
                                },
                                "status": {
                                    "type": "string",
                                    "enum": [
                                        "completed",
                                        "pending"
                                    ],
                                    "description": "pending — начисление ещё не активировано и в баланс не входит"
                                },
                                "expires_at": {
                                    "type": "string",
                                    "format": "date-time",
                                    "nullable": true
                                }
                            }
                        }
                    },
                    "meta": {
                        "type": "object",
                        "properties": {
                            "current_page": {
                                "type": "integer"
                            },
                            "last_page": {
                                "type": "integer"
                            },
                            "total": {
                                "type": "integer"
                            }
                        }
                    }
                }
            },
            "BonusBucket": {
                "type": "object",
                "nullable": true,
                "properties": {
                    "amount": {
                        "type": "integer",
                        "example": 1200
                    },
                    "activates_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                    },
                    "expires_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                    }
                }
            },
            "BalanceResponse": {
                "type": "object",
                "description": "Баланс с разбивкой. Ключ balance сохранён для совместимости и равен доступному остатку.",
                "properties": {
                    "phone": {
                        "type": "string"
                    },
                    "anket_id": {
                        "type": "integer"
                    },
                    "balance": {
                        "type": "integer",
                        "description": "Доступно к трате"
                    },
                    "available": {
                        "type": "integer"
                    },
                    "pending": {
                        "type": "integer",
                        "description": "Начислено, но ещё не активировано"
                    },
                    "pending_next": {
                        "$ref": "#/components/schemas/BonusBucket"
                    },
                    "expiring_next": {
                        "$ref": "#/components/schemas/BonusBucket"
                    },
                    "expiring_within_30d": {
                        "type": "integer"
                    }
                }
            },
            "TierRef": {
                "type": "object",
                "nullable": true,
                "properties": {
                    "code": {
                        "type": "string",
                        "example": "gold"
                    },
                    "name": {
                        "type": "string",
                        "example": "MILLER'S 10%"
                    },
                    "threshold": {
                        "type": "number",
                        "example": 150000
                    }
                }
            },
            "LoyaltyResponse": {
                "type": "object",
                "properties": {
                    "phone": {
                        "type": "string"
                    },
                    "anket_id": {
                        "type": "integer"
                    },
                    "tier": {
                        "$ref": "#/components/schemas/TierRef"
                    },
                    "next_tier": {
                        "$ref": "#/components/schemas/TierRef"
                    },
                    "lifetime_total": {
                        "type": "number",
                        "example": 162350
                    },
                    "amount_to_next": {
                        "type": "number",
                        "example": 12350
                    }
                }
            },
            "ClientUpdateRequest": {
                "type": "object",
                "properties": {
                    "name": {
                        "type": "string"
                    },
                    "last_name": {
                        "type": "string"
                    },
                    "middle_name": {
                        "type": "string"
                    },
                    "birth_date": {
                        "type": "string",
                        "format": "date",
                        "description": "Правка перезапускает отсчёт права на бонус ко дню рождения"
                    },
                    "email": {
                        "type": "string",
                        "format": "email"
                    },
                    "telegram_chat_id": {
                        "type": "string"
                    }
                }
            },
            "ClientUpdateResponse": {
                "type": "object",
                "properties": {
                    "phone": {
                        "type": "string"
                    },
                    "anket_id": {
                        "type": "integer"
                    },
                    "name": {
                        "type": "string",
                        "nullable": true
                    },
                    "last_name": {
                        "type": "string",
                        "nullable": true
                    },
                    "birth_date": {
                        "type": "string",
                        "format": "date",
                        "nullable": true
                    },
                    "has_email": {
                        "type": "boolean"
                    },
                    "has_telegram": {
                        "type": "boolean"
                    }
                }
            },
            "EventRequest": {
                "type": "object",
                "required": [
                    "phone",
                    "event_type"
                ],
                "properties": {
                    "phone": {
                        "type": "string"
                    },
                    "event_type": {
                        "type": "string",
                        "example": "review_photo"
                    },
                    "subject_type": {
                        "type": "string",
                        "example": "product"
                    },
                    "subject_id": {
                        "type": "string",
                        "example": "1024"
                    },
                    "external_key": {
                        "type": "string"
                    },
                    "meta": {
                        "type": "object",
                        "additionalProperties": true
                    }
                }
            },
            "EventResponse": {
                "type": "object",
                "properties": {
                    "event_type": {
                        "type": "string"
                    },
                    "awarded": {
                        "type": "integer"
                    },
                    "already_awarded": {
                        "type": "boolean"
                    },
                    "award_id": {
                        "type": "integer",
                        "nullable": true
                    },
                    "expires_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                    },
                    "balance": {
                        "type": "integer"
                    },
                    "reason": {
                        "type": "string",
                        "nullable": true,
                        "enum": [
                            "already_awarded",
                            "rule_inactive",
                            "outside_period",
                            "limit_reached",
                            null
                        ]
                    }
                }
            },
            "EventTypeList": {
                "type": "object",
                "properties": {
                    "data": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "code": {
                                    "type": "string"
                                },
                                "name": {
                                    "type": "string"
                                },
                                "amount": {
                                    "type": "integer"
                                },
                                "requires_subject": {
                                    "type": "boolean"
                                },
                                "limit_scope": {
                                    "type": "string",
                                    "enum": [
                                        "once_per_client",
                                        "once_per_subject",
                                        "unlimited"
                                    ]
                                },
                                "min_order_amount": {
                                    "type": "number",
                                    "nullable": true
                                }
                            }
                        }
                    }
                }
            },
            "ReturnRequest": {
                "type": "object",
                "required": [
                    "order_number",
                    "return_number"
                ],
                "properties": {
                    "order_number": {
                        "type": "string"
                    },
                    "return_number": {
                        "type": "string",
                        "description": "Уникален внутри организации"
                    },
                    "total": {
                        "type": "number",
                        "description": "Сумма возврата; либо передайте full"
                    },
                    "full": {
                        "type": "boolean",
                        "description": "Вернуть весь остаток заказа"
                    },
                    "items": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "additionalProperties": true
                        }
                    }
                }
            },
            "ReturnResponse": {
                "type": "object",
                "properties": {
                    "return_id": {
                        "type": "integer"
                    },
                    "order_id": {
                        "type": "integer"
                    },
                    "order_number": {
                        "type": "string"
                    },
                    "return_number": {
                        "type": "string"
                    },
                    "full": {
                        "type": "boolean"
                    },
                    "ratio": {
                        "type": "number"
                    },
                    "accrual_revoked": {
                        "type": "integer"
                    },
                    "redemption_refunded": {
                        "type": "integer"
                    },
                    "shortfall": {
                        "type": "integer",
                        "description": "Не удалось снять: клиент уже потратил начисленное"
                    },
                    "balance_after": {
                        "type": "integer"
                    }
                }
            }
        }
    },
    "paths": {
        "/clients": {
            "post": {
                "tags": [
                    "Клиенты"
                ],
                "summary": "Создать или найти клиента",
                "description": "Создаёт нового клиента или возвращает существующего. Если клиент уже привязан к организации — возвращает его данные. Поле `name` устанавливается только если имя ещё не задано.",
                "operationId": "createClient",
                "security": [
                    {
                        "BearerAuth": []
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "required": [
                                    "phone"
                                ],
                                "properties": {
                                    "phone": {
                                        "type": "string",
                                        "maxLength": 20,
                                        "example": "+79001234567",
                                        "description": "Номер телефона — уникальный идентификатор клиента"
                                    },
                                    "name": {
                                        "type": "string",
                                        "nullable": true,
                                        "maxLength": 255,
                                        "example": "Иван",
                                        "description": "Имя (задаётся только при первом создании)"
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "201": {
                        "description": "Клиент создан или найден",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ClientResponse"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/clients/{phone}/balance": {
            "get": {
                "tags": [
                    "Клиенты"
                ],
                "summary": "Баланс бонусов клиента",
                "description": "Возвращает текущий баланс бонусных баллов клиента в данной организации. Требует разрешение `read:balance`.",
                "operationId": "getClientBalance",
                "security": [
                    {
                        "BearerAuth": []
                    }
                ],
                "parameters": [
                    {
                        "name": "phone",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "example": "+79001234567",
                        "description": "Номер телефона клиента (URL-encoded, напр. %2B79001234567)"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/BalanceResponse"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "Клиент не найден или не привязан к организации",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/clients/{phone}/transactions": {
            "get": {
                "tags": [
                    "Клиенты"
                ],
                "summary": "История транзакций",
                "description": "Пагинированный список бонусных транзакций клиента (20 на страницу, сортировка по убыванию даты). Требует разрешение `read:transactions`.",
                "operationId": "getClientTransactions",
                "security": [
                    {
                        "BearerAuth": []
                    }
                ],
                "parameters": [
                    {
                        "name": "phone",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "example": "+79001234567",
                        "description": "Номер телефона клиента (URL-encoded)"
                    },
                    {
                        "name": "page",
                        "in": "query",
                        "required": false,
                        "schema": {
                            "type": "integer",
                            "default": 1,
                            "minimum": 1
                        },
                        "description": "Номер страницы"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/TransactionList"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "Клиент не найден",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/clients/{phone}/pass": {
            "get": {
                "tags": [
                    "Клиенты"
                ],
                "summary": "Скачать карту Apple Wallet",
                "description": "Возвращает файл .pkpass с картой лояльности клиента.\n\nТребует настроенного шаблона карты и сертификатов в разделе **Бонусы → Карта Wallet**. Разрешение: `read:balance`.",
                "operationId": "downloadClientPass",
                "parameters": [
                    {
                        "name": "phone",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "example": "+79001234567",
                        "description": "Номер телефона клиента (URL-encoded, напр. %2B79001234567)"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Файл карты",
                        "content": {
                            "application/vnd.apple.pkpass": {
                                "schema": {
                                    "type": "string",
                                    "format": "binary"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Недействительный или истёкший API-ключ"
                    },
                    "403": {
                        "description": "У ключа нет разрешения read:balance"
                    },
                    "404": {
                        "description": "Клиент не найден или не привязан к организации"
                    },
                    "500": {
                        "description": "Не удалось собрать карту (нет сертификатов или шаблона)"
                    }
                }
            }
        },
        "/orders/calculate": {
            "post": {
                "tags": [
                    "Заказы"
                ],
                "summary": "Расчёт бонусов без записи",
                "description": "Рассчитывает возможное начисление и максимальное списание бонусов **без создания заказа** в базе данных. Передайте любое значение в `redeem`, чтобы получить `can_redeem_max`. Требует разрешение `calculate:bonuses`.\n\n**Позиции.** Переданные `items` участвуют в расчёте, поэтому правила «за каждую позицию» и «от суммы товаров» здесь дают то же число, что и `POST /orders/process` на том же теле запроса.",
                "operationId": "calculateOrder",
                "security": [
                    {
                        "BearerAuth": []
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "required": [
                                    "phone",
                                    "total"
                                ],
                                "properties": {
                                    "phone": {
                                        "type": "string",
                                        "example": "+79001234567"
                                    },
                                    "total": {
                                        "type": "number",
                                        "minimum": 0,
                                        "example": 1000,
                                        "description": "Сумма заказа"
                                    },
                                    "items": {
                                        "type": "array",
                                        "nullable": true,
                                        "items": {
                                            "$ref": "#/components/schemas/OrderItem"
                                        }
                                    },
                                    "redeem": {
                                        "type": "integer",
                                        "nullable": true,
                                        "minimum": 0,
                                        "example": 1,
                                        "description": "Передайте любое значение, чтобы рассчитать can_redeem_max"
                                    },
                                    "catalog_id": {
                                        "type": "integer",
                                        "nullable": true,
                                        "example": 7,
                                        "description": "ID каталога товаров организации. Без него заказ не привязан ни к одному каталогу, и правила бонусов по каталогу, категории и свойствам товара не сработают. Если передан, `items[].id` сверяются с товарами этого каталога."
                                    },
                                    "catalog_code": {
                                        "type": "string",
                                        "nullable": true,
                                        "example": "tpl_65f0a1b2c3d4e",
                                        "description": "Альтернатива `catalog_id` — код каталога. Указывать оба можно, но они должны ссылаться на один каталог."
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "Результат расчёта",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/CalculateResponse"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "Клиент не найден в организации",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "Каталог не найден в организации, `catalog_id` и `catalog_code` противоречат друг другу, либо `items[].id` отсутствуют в каталоге (тогда в ответе есть `unknown_items`)",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/orders/process": {
            "post": {
                "tags": [
                    "Заказы"
                ],
                "summary": "Обработка заказа + бонусы",
                "description": "Создаёт заказ и выполняет начисление / списание бонусов. Операция **идемпотентна**: при повторном запросе с тем же `X-Idempotency-Key` возвращается кешированный ответ без повторной обработки. Требует разрешение `write:transactions`.\n\n**Отказ в списании.** Если запрошенная сумма не проходит по правилам организации, заказ **не проводится**: возвращается `422` с полями `redeem_requested` и `max_redeemable`, в базе не остаётся ни заказа, ни начисления. Повтор с тем же `X-Idempotency-Key` отдаёт тот же отказ. Чтобы заранее узнать доступный лимит, используйте `POST /orders/calculate`.",
                "operationId": "processOrder",
                "security": [
                    {
                        "BearerAuth": []
                    }
                ],
                "parameters": [
                    {
                        "name": "X-Idempotency-Key",
                        "in": "header",
                        "required": true,
                        "schema": {
                            "type": "string",
                            "format": "uuid"
                        },
                        "example": "550e8400-e29b-41d4-a716-446655440000",
                        "description": "UUID для идемпотентности. При повторной отправке с тем же ключом возвращается предыдущий ответ без повторной обработки."
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "required": [
                                    "phone",
                                    "order_number",
                                    "total"
                                ],
                                "properties": {
                                    "phone": {
                                        "type": "string",
                                        "example": "+79001234567"
                                    },
                                    "order_number": {
                                        "type": "string",
                                        "maxLength": 255,
                                        "example": "ORD-2026-001",
                                        "description": "Номер заказа из вашей системы"
                                    },
                                    "total": {
                                        "type": "number",
                                        "minimum": 0,
                                        "example": 1000,
                                        "description": "Итоговая сумма заказа"
                                    },
                                    "items": {
                                        "type": "array",
                                        "nullable": true,
                                        "items": {
                                            "$ref": "#/components/schemas/OrderItem"
                                        }
                                    },
                                    "redeem": {
                                        "type": "integer",
                                        "nullable": true,
                                        "minimum": 0,
                                        "example": 50,
                                        "description": "Количество бонусов для списания (0 — не списывать)"
                                    },
                                    "catalog_id": {
                                        "type": "integer",
                                        "nullable": true,
                                        "example": 7,
                                        "description": "ID каталога товаров организации. Без него заказ не привязан ни к одному каталогу, и правила бонусов по каталогу, категории и свойствам товара не сработают. Если передан, `items[].id` сверяются с товарами этого каталога."
                                    },
                                    "catalog_code": {
                                        "type": "string",
                                        "nullable": true,
                                        "example": "tpl_65f0a1b2c3d4e",
                                        "description": "Альтернатива `catalog_id` — код каталога. Указывать оба можно, но они должны ссылаться на один каталог."
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "Заказ успешно обработан",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ProcessResponse"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "Клиент не найден в организации",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "Списание бонусов невозможно по правилам организации — заказ не проведён. Тот же код возвращается, если каталог не найден в организации или `items[].id` отсутствуют в каталоге — в последнем случае ответ содержит `unknown_items`",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "error": {
                                            "type": "string",
                                            "example": "Списание бонусов невозможно по правилам организации"
                                        },
                                        "redeem_requested": {
                                            "type": "integer",
                                            "description": "Запрошенная сумма списания",
                                            "example": 5000
                                        },
                                        "max_redeemable": {
                                            "type": "integer",
                                            "description": "Сколько можно списать по правилам организации",
                                            "example": 1000
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "Ошибка обработки заказа",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/clients/{phone}": {
            "patch": {
                "tags": [
                    "Клиенты"
                ],
                "summary": "Дописать анкету клиента",
                "description": "Заполняет имя, дату рождения и контакты. Правка даты рождения перезапускает отсчёт права на бонус ко дню рождения — иначе дату вписывали бы за три дня до праздника. Контакты наружу не отдаются, только признаки их наличия. Требует разрешение `write:clients`.",
                "operationId": "updateClient",
                "security": [
                    {
                        "BearerAuth": []
                    }
                ],
                "parameters": [
                    {
                        "name": "phone",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "example": "+79001234567",
                        "description": "Номер телефона клиента (URL-encoded)"
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/ClientUpdateRequest"
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ClientUpdateResponse"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "Клиент не найден",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "Ошибка валидации",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/clients/{phone}/loyalty": {
            "get": {
                "tags": [
                    "Клиенты"
                ],
                "summary": "Уровень клиента и прогресс",
                "description": "Текущий уровень, следующий и сколько осталось купить до него. `tier` равен null, если уровни у организации не настроены; `next_tier` — если клиент на вершине. Требует разрешение `read:balance`.",
                "operationId": "getClientLoyalty",
                "security": [
                    {
                        "BearerAuth": []
                    }
                ],
                "parameters": [
                    {
                        "name": "phone",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "example": "+79001234567",
                        "description": "Номер телефона клиента (URL-encoded)"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/LoyaltyResponse"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "Клиент не найден",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/events": {
            "post": {
                "tags": [
                    "События"
                ],
                "summary": "Начислить бонус за событие",
                "description": "Сумма берётся из настроек организации, в теле запроса она не принимается. Повтор и исчерпанный лимит возвращают 200 с `awarded: 0` и причиной: эндпоинт зовут с ретраями, и 4xx попал бы в мониторинг интегратора как авария, хотя система отработала по настройкам. Уникальность держит бизнес-ключ (организация, клиент, событие, объект), а не заголовок идемпотентности — сайт, переустановивший интеграцию, сгенерирует новый ключ, а бонус за регистрацию всё равно должен остаться один. Требует разрешение `write:events`.",
                "operationId": "awardEventBonus",
                "security": [
                    {
                        "BearerAuth": []
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/EventRequest"
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "Начислено, повтор или отказ по правилам",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/EventResponse"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "Клиент не найден",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "Событие не описано в настройках или не передан обязательный объект",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/events/types": {
            "get": {
                "tags": [
                    "События"
                ],
                "summary": "Какие события организация оплачивает",
                "description": "Чтобы витрина писала «получите 500 баллов за отзыв с фото» без хардкода. Требует разрешение `write:events`.",
                "operationId": "listEventTypes",
                "security": [
                    {
                        "BearerAuth": []
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/EventTypeList"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/orders/return": {
            "post": {
                "tags": [
                    "Заказы"
                ],
                "summary": "Возврат по заказу",
                "description": "Полный или частичный. Начисленное снимается с округлением вверх, списанное возвращается с округлением вниз — в обе стороны в пользу магазина, иначе серией частичных возвратов можно доить баланс. Списанное возвращается в исходные партии с их сроками. Требует заголовок `X-Idempotency-Key` и разрешение `write:returns`.",
                "operationId": "returnOrder",
                "security": [
                    {
                        "BearerAuth": []
                    }
                ],
                "parameters": [
                    {
                        "name": "X-Idempotency-Key",
                        "in": "header",
                        "required": true,
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/ReturnRequest"
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ReturnResponse"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "Заказ или клиент не найдены",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "409": {
                        "description": "Возврат с таким номером уже проведён",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "Сумма возврата больше остатка по заказу",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}