# Стартовый OpenAPI-скелет (smoke A3-интеграции, 2026-05-17). # Покрывает только группу /api/deals*. Полная спека REST API — отдельная задача вне scope A3. openapi: 3.1.0 info: title: Лидерра CRM — Deals API version: 0.1.0 description: > Частичная спека REST API Лидерры. Скелет покрывает только группу /api/deals*. Все эндпоинты требуют аутентификации (Laravel Sanctum) и разрешают доступ только к сделкам текущего тенанта (RLS + middleware tenant). servers: - url: https://app.liderra.ru description: Production security: - sanctumCookie: [] tags: - name: deals description: Управление сделками (CRUD + bulk-операции + экспорт) paths: /api/deals: get: operationId: deals.index tags: [deals] summary: Список сделок тенанта description: > Возвращает сделки тенанта с пагинацией. Поддерживает два режима пагинации: keyset (cursor) — O(1) глубины; offset-based — backward-совместимость. При count_only=true возвращает только {"total": N} без строк. parameters: - name: status_in in: query description: > Фильтр по статусам (можно несколько). На проводе сериализуется Laravel array-binding: status_in[]=NEW&status_in[]=WON. Имя параметра в спецификации — без скобок: ключи свойств MCP-инструмента обязаны матчить ^[a-zA-Z0-9_.-]{1,64}$ (скобки запрещены, иначе Anthropic tools-схема падает с 400). required: false schema: type: array items: type: string style: form explode: true - name: project_id in: query required: false schema: type: integer - name: manager_id in: query required: false schema: type: integer - name: search in: query description: ILIKE-поиск по phone / contact_name required: false schema: type: string - name: limit in: query required: false schema: type: integer minimum: 1 maximum: 500 default: 100 - name: offset in: query description: Используется только без cursor (OFFSET-режим) required: false schema: type: integer minimum: 0 default: 0 - name: cursor in: query description: base64-encoded keyset cursor от предыдущей страницы required: false schema: type: string - name: only_deleted in: query description: Показывать только soft-deleted (корзина) required: false schema: type: boolean default: false - name: count_only in: query description: 'Вернуть только {"total": N}, без строк (для бейджа сайдбара)' required: false schema: type: boolean default: false responses: '200': description: Успех content: application/json: schema: oneOf: - $ref: '#/components/schemas/DealsListResponse' - $ref: '#/components/schemas/DealsCountResponse' examples: list: summary: Обычный список value: deals: - id: 42 tenant_id: 1 project_id: 5 project_name: "B1-Москва" phone: "+79001234567" contact_name: "Иван Иванов" status: "new" manager_id: 3 manager_name: "Мария К." manager_initials: "МК" received_at: "2026-05-17T10:00:00+03:00" total: 1 offset: 0 limit: 100 next_cursor: null count_only: summary: count_only=true value: total: 157 '401': $ref: '#/components/responses/Unauthorized' post: operationId: deals.store tags: [deals] summary: Создать сделку вручную description: > Ручное создание сделки из UI (не webhook). source_crm_id = NULL, баланс не списывается. Project резолвится или создаётся по project_name. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/DealStoreRequest' example: project_name: "B1-Москва" phone: "+79001234567" contact_name: "Иван Иванов" status: "new" manager_id: 3 comment: "Заинтересован, перезвонить в 15:00" responses: '201': description: Сделка создана content: application/json: schema: $ref: '#/components/schemas/DealStoreResponse' '422': $ref: '#/components/responses/ValidationError' '401': $ref: '#/components/responses/Unauthorized' delete: operationId: deals.bulkDestroy tags: [deals] summary: Bulk soft-delete сделок description: Мягкое удаление нескольких сделок. Идемпотентно. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/BulkIdsRequest' example: ids: [42, 43, 44] responses: '200': description: Результат удаления content: application/json: schema: $ref: '#/components/schemas/BulkDestroyResponse' example: deleted: 3 requested: 3 '422': $ref: '#/components/responses/ValidationError' '401': $ref: '#/components/responses/Unauthorized' /api/deals/{id}: get: operationId: deals.show tags: [deals] summary: Детали сделки + лог активности description: > Возвращает сделку с relations и до 50 последних событий activity_log. Используется в DealDetailDrawer. parameters: - $ref: '#/components/parameters/DealId' responses: '200': description: Успех content: application/json: schema: $ref: '#/components/schemas/DealShowResponse' '404': $ref: '#/components/responses/NotFound' '401': $ref: '#/components/responses/Unauthorized' patch: operationId: deals.update tags: [deals] summary: Редактировать сделку (частичное обновление) description: > Частичное обновление: comment / manager_id / status. Каждое изменение пишется в activity_log. NO-OP (значение не изменилось) — лог не пишется. parameters: - $ref: '#/components/parameters/DealId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/DealUpdateRequest' example: status: "in_progress" manager_id: 5 comment: "Уточнить условия" responses: '200': description: Сделка обновлена content: application/json: schema: $ref: '#/components/schemas/DealUpdateResponse' '404': $ref: '#/components/responses/NotFound' '422': $ref: '#/components/responses/ValidationError' '401': $ref: '#/components/responses/Unauthorized' /api/deals/export: post: operationId: deals.export tags: [deals] summary: Экспорт сделок в CSV или XLSX description: > Streaming-экспорт через OpenSpout (O(1) memory). Формат по умолчанию — csv. CSV: UTF-8 + BOM, разделитель ;. XLSX: bold-заголовок, sheet «Сделки». requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/DealExportRequest' example: ids: [42, 43, 44] format: csv responses: '200': description: Файл экспорта (streamed) content: text/csv: schema: type: string format: binary application/vnd.openxmlformats-officedocument.spreadsheetml.sheet: schema: type: string format: binary '422': $ref: '#/components/responses/ValidationError' '401': $ref: '#/components/responses/Unauthorized' /api/deals/transition: post: operationId: deals.bulkTransition tags: [deals] summary: Bulk смена статуса сделок description: > Массовая смена статуса. Bulk-UPDATE + bulk-INSERT в activity_log (2 запроса вместо N). NO-OP (status уже совпадает) не считается. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/DealTransitionRequest' example: ids: [42, 43] status: "in_progress" responses: '200': description: Результат перехода content: application/json: schema: $ref: '#/components/schemas/BulkTransitionResponse' example: updated: 2 requested: 2 status: "in_progress" '422': $ref: '#/components/responses/ValidationError' '401': $ref: '#/components/responses/Unauthorized' /api/deals/restore: post: operationId: deals.bulkRestore tags: [deals] summary: Bulk восстановление soft-deleted сделок description: Восстановление из корзины. Идемпотентно (уже живые — NO-OP). requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/BulkIdsRequest' example: ids: [42, 43] responses: '200': description: Результат восстановления content: application/json: schema: $ref: '#/components/schemas/BulkRestoreResponse' example: restored: 2 requested: 2 '422': $ref: '#/components/responses/ValidationError' '401': $ref: '#/components/responses/Unauthorized' components: securitySchemes: sanctumCookie: type: apiKey in: cookie name: liderra_session description: Laravel Sanctum session cookie (SPA auth) parameters: DealId: name: id in: path required: true description: ID сделки (только цифры, regex [0-9]+) schema: type: integer minimum: 1 schemas: DealSummary: type: object description: Краткое представление сделки (для списка) properties: id: type: integer tenant_id: type: integer project_id: type: integer project_name: type: [string, "null"] phone: type: string contact_name: type: [string, "null"] status: type: string description: Slug из таблицы lead_statuses manager_id: type: [integer, "null"] manager_name: type: [string, "null"] manager_initials: type: [string, "null"] received_at: type: [string, "null"] format: date-time DealDetail: type: object description: Полное представление сделки (для DealDetailDrawer) properties: id: type: integer tenant_id: type: integer project_id: type: integer project_name: type: [string, "null"] phone: type: string contact_name: type: [string, "null"] comment: type: [string, "null"] status: type: string manager_id: type: [integer, "null"] manager_name: type: [string, "null"] manager_initials: type: [string, "null"] received_at: type: [string, "null"] format: date-time assigned_at: type: [string, "null"] format: date-time ActivityEvent: type: object properties: id: type: integer event: type: string description: > Тип события: deal.created, deal.status_changed, deal.assigned, deal.commented, deal.deleted, deal.restored context: type: object description: Произвольный JSON-контекст (from/to/source и т.п.) additionalProperties: true created_at: type: [string, "null"] format: date-time actor: type: [object, "null"] properties: id: type: integer name: type: string initials: type: string DealsListResponse: type: object properties: deals: type: array items: $ref: '#/components/schemas/DealSummary' total: type: [integer, "null"] description: Только в OFFSET-режиме (без cursor) offset: type: [integer, "null"] description: Только в OFFSET-режиме limit: type: integer next_cursor: type: [string, "null"] description: base64-encoded cursor для следующей страницы DealsCountResponse: type: object description: Ответ при count_only=true properties: total: type: integer DealShowResponse: type: object properties: deal: $ref: '#/components/schemas/DealDetail' events: type: array items: $ref: '#/components/schemas/ActivityEvent' DealStoreRequest: type: object required: [project_name, phone] properties: project_name: type: string maxLength: 255 phone: type: string maxLength: 20 contact_name: type: [string, "null"] maxLength: 255 status: type: [string, "null"] maxLength: 50 description: Slug из lead_statuses; по умолчанию "new" manager_id: type: [integer, "null"] minimum: 1 comment: type: [string, "null"] maxLength: 5000 DealStoreResponse: type: object properties: deal: type: object properties: id: type: integer tenant_id: type: integer project_id: type: integer phone: type: string status: type: string contact_name: type: [string, "null"] manager_id: type: [integer, "null"] received_at: type: string format: date-time message: type: string example: "Сделка создана." DealUpdateRequest: type: object description: Все поля опциональны; хотя бы одно должно присутствовать properties: comment: type: [string, "null"] maxLength: 5000 manager_id: type: [integer, "null"] minimum: 1 status: type: [string, "null"] maxLength: 50 DealUpdateResponse: type: object properties: deal: type: object properties: id: type: integer tenant_id: type: integer project_id: type: integer phone: type: string contact_name: type: [string, "null"] comment: type: [string, "null"] status: type: string manager_id: type: [integer, "null"] received_at: type: [string, "null"] format: date-time assigned_at: type: [string, "null"] format: date-time DealExportRequest: type: object required: [ids] properties: ids: type: array items: type: integer minimum: 1 minItems: 1 maxItems: 10000 format: type: string enum: [csv, xlsx] default: csv DealTransitionRequest: type: object required: [ids, status] properties: ids: type: array items: type: integer minimum: 1 minItems: 1 maxItems: 1000 status: type: string maxLength: 50 description: Slug из lead_statuses BulkIdsRequest: type: object required: [ids] properties: ids: type: array items: type: integer minimum: 1 minItems: 1 maxItems: 1000 BulkTransitionResponse: type: object properties: updated: type: integer description: Реально изменённых (без NO-OP) requested: type: integer status: type: string BulkDestroyResponse: type: object properties: deleted: type: integer requested: type: integer BulkRestoreResponse: type: object properties: restored: type: integer requested: type: integer ErrorMessage: type: object properties: message: type: string ValidationErrorResponse: type: object properties: message: type: string errors: type: object additionalProperties: type: array items: type: string responses: Unauthorized: description: Не аутентифицирован content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' example: message: "Unauthenticated." NotFound: description: Сделка не найдена content: application/json: schema: $ref: '#/components/schemas/ErrorMessage' example: message: "Сделка не найдена." ValidationError: description: Ошибка валидации content: application/json: schema: $ref: '#/components/schemas/ValidationErrorResponse' example: message: "The given data was invalid." errors: status: - "Slug не найден в lead_statuses."