ff3979d527
docs(a3): OpenAPI skeleton for /api/deals — A3 smoke artifact Стартовый OpenAPI 3.1 скелет для группы /api/deals* (8 эндпоинтов) как smoke-доказательство api-docs-тулинга. Redocly lint — valid (exit 0, 2 warning о неполноте, ожидаемо для скелета). Не полная спека API. Task 1 плана A3 integration-tooling. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> @
700 lines
20 KiB
YAML
700 lines
20 KiB
YAML
# Стартовый 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: Фильтр по статусам (можно несколько)
|
||
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."
|