# Itecho ERP API — правила для агента Документ — **единственный источник правды** для LLM/агента, который пишет интеграцию с API Itecho ERP / E-Commerce (`https://api.gigma.ru/api`). Содержит auth-flow, форматы, справочники, энумы, типичные ошибки, ловушки. Прочитай один раз — дальше работай по этим правилам, не дёргая `WebFetch` на каждый шаг. Связанные онлайн-источники (если есть доступ к интернету): - Индекс API: https://artypoul-docs-gigma-7b80.twc1.net/llms.txt - Полный корпус: https://artypoul-docs-gigma-7b80.twc1.net/llms-full.txt - Соглашения (HTML): https://artypoul-docs-gigma-7b80.twc1.net/conventions/ - Энумы (HTML): https://artypoul-docs-gigma-7b80.twc1.net/enums/ - **OpenAPI 3.1 спека** (235 операций, 29 тегов, ~700 KB): https://artypoul-docs-gigma-7b80.twc1.net/openapi.json - **Swagger UI** (интерактивная навигация + Try It): https://artypoul-docs-gigma-7b80.twc1.net/api-docs/ Если работаете локально (LAN dev-сервер) — те же пути доступны через `http://:5174/`. **Для агента:** OpenAPI спека — это самый компактный способ получить контракт. Один HTTP GET — и видишь все 235 endpoint'ов с типами, обязательностью полей, примерами. Не нужно WebFetch'ить llms-full.txt и парсить через summarising-модель. ================================================================ ## 1. Базовый URL и заголовки ================================================================ Базовый URL: ``` https://api.gigma.ru/api ``` Все endpoint-пути в документации указаны относительно этого корня (например, `POST /api/login` → `https://api.gigma.ru/api/login`). Минимальные заголовки для запроса с телом: ``` Accept: application/json Content-Type: application/json ``` Для авторизованных запросов добавляется: ``` Authorization: Bearer ``` Для загрузки файлов (`POST /api/files`, `POST /api/orders/{id}/files`, …) `Content-Type` меняется на `multipart/form-data`. ================================================================ ## 2. Авторизация ================================================================ ### Flow 1. **Получить токен**: - ERP (сотрудник): `POST /api/login` с телом `{ "login": "", "password": "" }` - E-Commerce (клиент): `POST /api/counterparty/login` с телом `{ "phone": "", "password": "", "device": "" }` — ⚠ поле `phone` **принимает и email тоже** (валидатор детектит формат), плюс есть опциональный `device` (min:3, max:50) для разделения сессий по устройствам. 2. **Сохранить** `access_token.value` из ответа (обычно в localStorage / переменной). 3. **Слать в каждом последующем запросе**: ``` Authorization: Bearer ``` 4. **Срок действия** — поле `access_token.expires_at` (ISO 8601). По истечении или после `401 Unauthenticated` нужно повторить шаг 1. ### Сброс пароля для ERP `POST /api/send_password` с телом `{ "login": "" }` — отправляет на email временный пароль. Затем `POST /api/login` с этим паролем. ### Сброс пароля для E-Commerce `POST /api/counterparty/send_password` с телом `{ "phone": "" }` — инициирует звонок-сброс. Пароль — последние 4 цифры номера, с которого был совершён звонок. ### Miniapp auth для E-Commerce Канонический вход miniapp-клиента живёт только в counterparty-контуре: ``` POST /api/counterparty/miniapps/{provider}/contact_auth Token: ``` - Сейчас рабочий provider: `max`. - Ответ успешного входа — обычный `counterparty` с `access_token.value`, как у `POST /api/counterparty/login`. - Тело MAX flow: `{ "phone", "auth_date", "hash", "init_data", "device?" }`. - Backend проверяет signed `init_data`, свежесть `auth_date`, подпись contact payload и только потом выдаёт counterparty token. - Подпись проверяется по исходному `phone`; нормализованный номер используется для поиска/создания counterparty. - Для Telegram нельзя выдавать counterparty token по произвольному `phone`, пока нет signed proof владения телефоном: ожидаем `422 miniapp_contact_auth_not_supported`. - **Не добавлять alias routes** вида `/api/miniapps/.../auth/...` без явного решения владельца. Фронт должен ходить в canonical `/api/counterparty/miniapps/...`. ### Пароль в примерах В примерах этого документа для ERP стоит условный пароль `"1111"`. **На реальном сервере пароль строго из письма** (после `POST /api/send_password`). ### Формат Bearer-токена Бэкенд — Laravel Sanctum. Каждый токен имеет вид: ``` | ``` Например: `28|dCWBGIqC9algoNXr6NVVg9D2fKWBaq7BJkRFyxq009cd040b` - `` — целое число переменной длины (1, 28, 1024, …). Это первичный ключ записи токена в БД. - `|` — **обязательный** разделитель. Без него токен невалиден. - `` — секретная часть, alphanumeric. ⚠ При хранении токена в CSV / INI / shell-переменных следите за тем, чтобы `|` не потерялся. Корректное хранение: целиком в quoted-строке или в env-переменной с экранированием. ### Текущий пользователь / контрагент - ERP: `GET /api/user` — возвращает текущего сотрудника со всем сразу: `id`, `role` (с описанием), `branch`, `department`, `login`, `phone`, ФИО, `avatar`, `creator` (кто создал), `is_banned`, `is_sick`, `active_time`, `last_activity_at`, и **полный массив `permissions[]`** со `{id, screen, name, description}`. - E-Commerce: `GET /api/counterparty` — возвращает текущего клиента (профиль). ⚠ **Не вызывай `GET /api/permissions` после `GET /api/user`** — права уже в ответе. Отдельный `/api/permissions` нужен только для админских сценариев (список всех прав в системе, не только моих). ### Выход - ERP: `POST /api/user/logout` с телом `{ "from_all_devices": }`. - E-Commerce: `POST /api/counterparty/logout` с телом `{ "from_all_devices": }`. ================================================================ ## 3. Справочники: концепт → endpoint (ловушки имён) ================================================================ Полный список справочных endpoint'ов с типами ответов — в **openapi.json** (`jq '.paths | keys[]'` или ищи по `summary`/тегу «Справочники»). Здесь только то, что спека не выражает: неочевидные имена, confusing aliases, поисковые helper-endpoint'ы. | Концепт | Endpoint | ⚠ Ловушка / заметка | |---|---|---| | Бизнесы / филиалы | `GET /api/branches` | **НЕ** `/api/businesses` | | Виды номенклатуры | `GET /api/nomenclature_kinds` | **НЕ** `/api/kinds`, **НЕ** `/api/nomenclatures/kinds` | | Единицы измерения | `GET /api/storage_units` | **НЕ** `/api/units` | | Ставки НДС | `GET /api/vats` | **НЕ** `/api/vat` (singular) | | Способы доставки | `GET /api/delivery_types` | + подтипы: `GET /api/delivery_types/{id}/subtypes` | | Способы оплаты | `GET /api/counterparty/payment_types` | ⚠ ERP-токен НЕ работает: `/api/payment_types` → 404. Доступно только через counterparty-токен. | | Типы файлов | `GET /api/file_types` | значения захардкожены — см. §12.2 | | Меню текущего юзера | `GET /api/menus`, `GET /api/menus/default/items` | для отрисовки sidebar | | Поиск адреса (DaData) | `POST /api/search_address` | тело: `{ query }` | | Поиск банка | `POST /api/search_bank` | по БИК → авто-заполнение реквизитов (см. §18.3) | | Поиск компании | `POST /api/search_company` | по ИНН/ОГРН/имени → авто-заполнение (см. §18.2) | | Поиск пользователей | `GET /api/users?query=` | возвращает полный объект; `GET /api/managers` — упрощённый для фильтров | **Правило для агента:** в начале сессии один раз снять «карту справочников» (kinds, vats, units, categories, statuses) и держать в контексте. Не вызывать справочник перед каждым POST'ом — это пустая трата запросов. ### ⚠ Wrapper-ключи в ответах **непоследовательны** (СВЕРЕНО) Имя ключа корневого массива в ответе **не всегда соответствует пути**: | Путь | Wrapper-ключ ответа | |---|---| | `GET /api/vats` | `vats` ✓ | | `GET /api/brands`, `/branches`, `/categories`, `/roles`, `/screens` | snake_plural ✓ | | `GET /api/storage_units` | **`units`** (НЕ `storage_units`!) | | `GET /api/nomenclature_kinds` | **`nomenclatureKinds`** (camelCase) | | `GET /api/counterparty_types` | **`counterpartyTypes`** (camelCase) | | `GET /api/file_types` | **`fileTypes`** (camelCase) | | `GET /api/delivery_types` | **`deliveryTypes`** (camelCase) | Рядом всегда лежит `Count` или `_count` поле с длиной массива: `counterpartyTypesCount`, `unitsCount`, `brandsCount` и т.д. **Для агента:** не угадывай ключ из имени endpoint'а. Бери из openapi.json (`jq '.paths["/api/storage_units"].get.responses["200"].content."application/json".schema.properties'`) или из примера ответа в маркдауне. ### ⚠ Реальные значения `counterparty_types` (СВЕРЕНО) | id | name | |---|---| | 1 | Клиент | | 2 | Поставщик | | 3 | Свой | **НЕ путать с `is_company`** — это отдельное булево поле для разделения «физлицо/юрлицо», независимо от `counterparty_type_id`. ### ⚠ Схемы записей справочников — **не унифицированы** Запись в каждом справочнике имеет свой набор полей. Не надейся, что у всех есть `avatar` или `created_at`: | Справочник | Уникальные/недостающие поля | |---|---| | `storage_units` | **только** `{id, name, abbreviation}` — нет `avatar`/`created_at` (`abbreviation` = «шт»/«л»/«кг»/…) | | `nomenclature_kinds`, `counterparty_types`, `file_types` | `{id, name, avatar, created_at}` — полный набор | | `vats` | `{id, name, avatar, created_at}` — `name` это строка («Без НДС»/«10%»/«20%»), не число | | `categories` | `{id, code (число!), name, description (HTML), parent, photo, ...}` — `code` приходит **числом**, не строкой | | `branches` | большой объект с `avatar`, `bank_requisites[]`, `legal_address`, и т.д. | **Для агента:** перед использованием поля — проверить его наличие в первом элементе массива. Не предполагать, что `entry.avatar` есть везде. ================================================================ ## 4. Форматы данных ================================================================ Точные типы и форматы — в **openapi.json** (`format: decimal`, `format: date`, `format: date-time`, `format: uri`). Ниже только то, что регулярно становится источником ошибок: - **Денежные суммы — строка** с фиксированной точностью (`"2100.00"`), не число. В openapi.json — `type: string, format: decimal`. При генерации TS-типов не маппить в `number` — потеряешь точность. - **Количества — число** (`42`). Не путать с суммами при генерации типов. - **Телефоны — E.164 без `+`** (`"79991234567"`), 11 цифр, ведущая 7. - **Boolean** — обычно `true`/`false`, но в legacy-полях встречается `1`/`0`. - **Идентификаторы** (`id`, `*_id`) — `int`, auto-increment, минимум 1. - **Локаль** `ru-RU`, кодировка **UTF-8** везде. ================================================================ ## 5. Пагинация (Laravel) ================================================================ Endpoint'ы вида `GET /api/tables/...` и `GET /api/.../history` возвращают пагинированный ответ. Query-параметры: - `page` (int, опционально) — номер страницы, с 1 - `per_page` (int, опционально) — размер страницы, по умолчанию 15 Структура ответа: ```json { "items": [ /* массив записей */ ], "pagination": { "total": 42, "per_page": 15, "current_page": 1, "last_page": 3, "from": 1, "to": 15 } } ``` Для history-endpoint'ов пагинатор приходит в развёрнутом виде (Laravel paginator) с `links[]`, `first_page_url`, `next_page_url`, `prev_page_url`, `path`. ### ⚠ `GET /api/tables/*` — особый envelope (СВЕРЕНО) В отличие от обычной пагинации, `/tables/*` возвращает **простой** envelope с тремя секциями: ```json { "columns": [ { "id": 65, "table_id": 9, "order": 0, "key": "code", "has_icon": 0, "text": "Код" }, { "id": 66, "table_id": 9, "order": 1, "key": "name", "has_icon": 1, "text": "Название" } ], "nomenclatures": [ // ← ключ = имя ресурса, НЕ "data" или "items" { "id": { "icon": null, "value": 33733, "url": "https://cloud.gigma.ru/.../33733" }, "code": 4, "name": { "icon": "https://...default.svg", "value": "Аренда...", "link": "..." }, "price":{ "icon": null, "value": "1200.00" } } ], "pagination": { "total": 463, "per_page": 10, "current_page": 1, "last_page": 47, "from": 1, "to": 10 } } ``` **Ключевые отличия от обычной пагинации:** - Ключ массива строк = **имя ресурса** (`nomenclatures`, `inventories`, `orders`, …), а не `data` или `items`. - Каждое поле строки — **не плоское значение**, а объект `{icon, value, url?, link?}`. Чтобы достать сырое значение: `row..value`. Простые поля (без иконок/ссылок) могут приходить плоско — проверяй тип. - `columns[]` — описание столбцов для рендера UI-таблицы (id, table_id, order, key, has_icon, text). Агент может игнорировать, если не строит UI. - В openapi.json эта форма описана через `components.schemas.TablesEnvelope` + специфичный для пути `` ключ. ================================================================ ## 6. Фильтры ================================================================ Передаются как query-параметры: | Тип фильтра | Формат | Пример | |---|---|---| | Простое поле | `?field=value` | `?is_company=true` | | Массив | `?field[]=v1&field[]=v2` | `?counterparty_type_id[]=2&counterparty_type_id[]=3` | | Диапазон дат | `?date_from=YYYY-MM-DD&date_to=YYYY-MM-DD` | `?date_from=2024-01-01&date_to=2024-12-31` | | Сравнение | `?credit_gt=100&credit_lt=1000` | `_gt` = greater, `_lt` = less | | Поиск | `?query=строка` | `?query=иванов` | Кириллица в `query` должна быть URL-encoded. ================================================================ ## 7. Коды ответа ================================================================ | Код | Когда | Тело | |---|---|---| | **200 OK** | Успешное чтение / обновление | endpoint-specific | | **201 Created** | Успешное создание объекта (POST на коллекцию) | обычно сам объект | | **204 No Content** | Успешное удаление без тела ответа | пустое | | **401 Unauthenticated** | Нет / невалидный / протухший токен | `{ "message": "Unauthenticated." }` — перелогиниться | | **403 Forbidden** | Токен валидный, нет прав | `{ "message": "..." }` — проверить `permissions` | | **422 Unprocessable Entity** | Валидация полей не прошла | `{ "errors": { "field_name": ["сообщение"] }, "message": "..." }` | | **429 Too Many Requests** | Превышение rate limit | `{ "message": "Too Many Attempts." }` | | **500 Internal Server Error** | Внутренняя ошибка | `{ "message": "Server error" }` | ⚠ **Не делай жёсткой проверки `status === 200` на write-запросах.** POST-на-коллекцию обычно возвращает **201**, DELETE — **204**. Используй диапазон `2xx` (200-299). ### Структура 422 ```json { "errors": { "password": ["The password field is required."], "phone_1": ["The phone 1 must be at least 11 characters."], "products.0.id": ["The selected products.0.id is invalid."] }, "message": "The given data was invalid." } ``` Ключи могут быть **вложенными через точку** (`products.0.id`) — для массивов в теле. Это стандартный Laravel array-validation формат. ### Обработка на стороне клиента (рекомендуется) - **401** — очистить локальный токен, редирект на login-форму. - **403** — показать «нет прав» и не повторять. - **422** — показать пользователю по полям из `errors`. - **429** — backoff (см. ниже). - **5xx** — toast с `{ message }`, при идемпотентном запросе можно повторить через паузу. ================================================================ ## 8. Rate limits ================================================================ Бэкенд использует стандартный Laravel-throttle (`throttle:api` middleware). Дефолт фреймворка — **60 запросов в минуту** на пользователя / IP. Точное значение в проде не подтверждено; для надёжности: - При **429** ждать минимум 60 секунд (или значение из `Retry-After`, если придёт). - Для массовых импортов — **не более 1 запроса в секунду** на токен. - Параллельных запросов — не более 5 одновременно от одного клиента. ### Endpoint-specific лимиты (сверено по middleware в routes/api.php) - `POST /api/counterparty/orders/precalculate` — **30 запросов / минуту** (`throttle:30,1`). Жёстче глобального — потому что часто вызывается на UI при изменении состава корзины. Кешировать результаты на стороне клиента. ================================================================ ## 9. Идемпотентность ================================================================ Стандартной поддержки `Idempotency-Key` **нет**. POST-запросы создания объектов не идемпотентны: повторный вызов **создаст дубль**. Безопасно повторять: - `GET` — всегда - `DELETE` — да (второй вызов вернёт 404, но состояние корректно) - `PUT` — да, заменяет объект целиком Требует осторожности: - `POST` на коллекцию — НЕ повторять без проверки, что прошлый запрос провалился до создания объекта. **Правило для агента:** перед любой write-операцией показать пользователю полный body запроса и подтвердить **одним вопросом**, а не задавать 3-4 уточняющих. ================================================================ ## 10. Webhooks ================================================================ **Не поддерживаются.** Бэкенд не отправляет push-уведомления о смене состояния заказов, контрагентов, номенклатуры. Для отслеживания изменений — polling через GET-endpoint раз в N секунд. Адекватный интервал — **30–60 секунд** (учитывая rate limit). ================================================================ ## 11. Версионирование URL ================================================================ URL **не версионируется**. Префикса `/api/v1/…` или `/api/v2/…` нет — все endpoint'ы под единым корнем `https://api.gigma.ru/api`. Изменения накатываются на этот корень; следить за breaking changes можно через docs.gigma.ru или эмпирически. ================================================================ ## 12. Энумы (справочник значений) ================================================================ Большинство ID — это FK в БД, значения которых живут в справочниках. **Перед write-операцией с `*_id`-полем — fetch'и соответствующий list-endpoint** (см. раздел 3) и сохрани map `id → name` для сессии. Ниже — снимки справочников (могут отличаться между средами) и фиксированные значения (хардкод). ### 12.1 Статусы заказа (e-commerce / оплата) **⚠ ID этих статусов ЗАХАРДКОЖЕНЫ в бэкенде** (константы модели `OrderStatus`) — платёжный поток YooKassa переводит заказ именно по ID, не по названию. Для логики оплаты/отмены ориентируйся на ID: | id | Константа в коде | Типичное название | Смысл | |---|---|---|---| | 1 | `IS_CREATED` | Новый | заказ создан | | 2 | `IS_PAYMENT_WAITING` | Ожидание оплаты | ждёт оплаты | | 3 | `IS_PROCESSING` | В сборке | в обработке | | 4 | `IS_READY` | Можно забирать | готов к выдаче | | 5 | `IS_ISSUED` | Выдан | выдан клиенту | | 6 | `IS_CANCELED` | Отменён | отменён | | 22 | `IS_PAID` | Оплачен | оплата прошла | **Для агента (отслеживание заказа):** оплачен = `order_status_id == 22`, отменён = `== 6`, ждёт оплаты = `== 2`. Это стабильно (хардкод в коде), ID непоследовательны (7-21 не используются) — это нормально, не «дыра». **⚠ Названия — НЕ универсальны.** На проектно-производственных установках справочник статусов вообще другой (`Расчёт`, `Замер`, `Производство`, `Монтаж`, `Приёмка` — см. страницу /enums). Поэтому: для **платёжной логики** — ID-константы выше; для **отображения имени** — `GET /api/order_statuses` (live). Тип каждой записи: `{id, name, avatar (https://.../default.svg), created_at}`. **Tone** — внутренний маркер для UI: - `neutral` — нейтрально - `info` — в процессе - `pos` — успех / завершено - `neg` — отменено / ошибка Эвристика для нестандартных статусов: - содержит «отмен» → `neg` - содержит «готов» / «выполн» / «оплач» → `pos` - содержит «работе» / «процессе» / «ожидан» → `info` - иначе → `neutral` ### 12.2 Типы файлов (3) — хардкод Стабильные ID (есть как TS-константы в `event_ai/src/constants/file-types.ts`): | ID | Название | Константа | Когда использовать | |---|---|---|---| | 1 | Трудовой договор | `EMPLOYMENT_CONTRACT_ID` | Документы сотрудника | | 2 | Аватар | `AVATAR` | Аватар пользователя/контрагента | | 3 | Файл заказа | `ORDER_FILE` | Вложения к заказу (`POST /api/orders/{id}/files`) | Используется в поле `file_type_id` при `POST /api/files`. Полный список — `GET /api/file_types`. ### 12.3 Виды номенклатуры Из реальной практики (`GET /api/nomenclature_kinds`): | ID | Название | |---|---| | 1 | Услуга | | 2 | Товар | ### 12.4 Типы контрагентов Из дефолтного seed'а: | ID | Название | Назначение | |---|---|---| | 1 | Юр. лицо | `is_company: true` | | 2 | Розница | `is_company: false` | ⚠ ID могут различаться между установками. Перед созданием контрагента — `GET /api/counterparty_types`. ### 12.5 Роли сотрудников (из примеров) Полный список — `GET /api/roles`. | ID | name | description | |---|---|---| | 1 | `owner` | Собственник | | 3 | `manager` | Руководитель отдела | | 4 | `accountant` | Бухгалтер | (ID 2 не встречался в примерах — вероятно `admin` или схожая роль.) ### 12.6 Права доступа — модель Spatie (СВЕРЕНО по коду) Полный список — `GET /api/permissions`. **Уже встроен в `/api/user.permissions[]`** — не дёргать дважды (см. §2). **Как проверяются:** не через route middleware (там только `auth:user` + `banned`), а через **Laravel Policy** в каждом контроллере. `authorizeResource(Model::class, 'model')` в конструкторе биндит 5 методов: `viewAny`, `view`, `create`, `update`, `delete`. Каждый вызывает `$user->hasPermissionTo('-')`. **Permission naming:** kebab-case `-` (plural). | Verb | Что разрешает | Маппинг на HTTP | |---|---|---| | `view-` | index + show (read-only) | GET | | `create-` | create | POST | | `edit-` | create **+** update **+** delete | POST/PUT/PATCH/DELETE | ⚠ **`edit-` — superset:** включает `create-` и неявно «delete-». Отдельного `delete-` permission **не существует**. Соответственно когда видишь `edit-counterparties` в `/api/user.permissions[]` — это и создание, и обновление, и удаление. **Multi-tenant guard:** `view` policy дополнительно проверяет `$user->project_id == $resource->project_id`. Доступ к чужим записям → **403**, даже если permission есть. **Примеры маппинга:** - `edit-branches`, `view-branches`, `create-branches` (BranchPolicy) - `edit-counterparties`, `view-counterparties`, `create-counterparties` - `edit-orders`, `view-orders` - `edit-nomenclatures`, `view-nomenclatures` - `edit-warehouses`, `edit-shops`, `edit-categories`, `edit-applications`, `edit-tasks` **Для агента:** 1. Перед вызовом write-endpoint'а проверить наличие соответствующего `edit-` в `user.permissions[].name` — иначе будет `403`. 2. Не повторять запрос на `403` — это не временная ошибка, прав не появится. 3. Сообщить пользователю «не хватает прав ``» — он может попросить роль с этим правом. ### 12.7 Экраны (screens) Из `permission.screen` в JSON-примерах. Полный список — `GET /api/screens`. | ID | Название | |---|---| | 1 | Контрагенты | | 2 | Заказы | | 3 | Задачи | ### 12.8 Тоны истории (color) — 8 значений Используется в `histories[].color` / `counterparties.data[].color`: | Значение | Семантика | |---|---| | `primary` | Основное действие | | `secondary` | Вторичное действие | | `info` | Информация | | `success` | Успех | | `warning` | Предупреждение | | `error` | Ошибка | | `dark` | Тёмный нейтральный | | `light` | Светлый нейтральный | ================================================================ ## 13. Cheat-sheet для агента ================================================================ ### В начале сессии 1. Получи токен (раздел 2). Запомни `|` целиком, не теряй `|`. 2. **Скачай OpenAPI один раз** (см. раздел 21): `curl https://artypoul-docs-gigma-7b80.twc1.net/openapi.json -o ~/.gigma/openapi.json`. Дальше — `jq` / `grep` по локальному файлу, без сетевых запросов. 3. Один раз сними **«карту справочников»** (раздел 3): статусы, виды номенклатуры, типы контрагентов, единицы измерения, НДС, способы доставки/оплаты — всё, что планируешь использовать. **Сохрани её локально** (например `~/.gigma/dictionary.json` или `MEMORY.md` в проекте), не перепроверяй в каждой новой сессии. Карта меняется редко — обновлять раз в неделю. 4. Прочитай этот документ один раз. Не делай WebFetch на `llms-full.txt` в каждом запросе — он 400+ KB и проходит через summarising-модель, которая режет факты. ### Бюджет начальных запросов При работе с этим API в новой сессии расходуй максимум: - **1** на OpenAPI скачивание (раз в N дней — из локального кэша) - **6-8** на справочники, если ещё не закэшированы (`order_statuses`, `nomenclature_kinds`, `storage_units`, `vats`, `delivery_types`, `payment_types`, `counterparty_types`, `roles`) - **0** на «попробую и посмотрю что вернётся» — body schemas есть в OpenAPI или в разделе 18. ### Перед write-операцией 1. **Собери все недостающие поля одним блоком уточнений**, не задавай по одному. Если для создания заказа нужны 5 полей — спроси в одном `AskUserQuestion` все сразу, не растягивай на 5 раундов. 2. **Покажи пользователю полный body** и спроси одним вопросом «отправлять?». 3. Не повторяй POST вслепую — он не идемпотентен. 4. Используй `Content-Type: application/json` (для файлов — `multipart/form-data`). 5. Ожидай **201** (create) или **204** (delete), не **200**. Проверяй диапазон `2xx`, не равенство `=== 200`. 6. На 5xx — **первая гипотеза «неполное тело»**, а не «упал сервер» (бэкенд-баг, см. 19.1). Свериться со схемой в OpenAPI или разделе 18. ### При ошибке | Код | Действие | |---|---| | 401 | Перелогинься (повтори раздел 2, шаг 1) | | 403 | Не повторяй. Проверь `permissions` пользователя. | | 422 | Покажи пользователю по полям из `errors`. Ключи могут быть вложенными через точку (`products.0.id`). | | 429 | Backoff минимум 60 сек или `Retry-After`. | | 5xx | Можно повторить идемпотентный запрос через паузу. | ### Чего НЕ делать - ❌ Не угадывать имена endpoint'ов (`/api/units`, `/api/businesses` не существуют). - ❌ Не отправлять пустой POST «чтобы узнать, какие поля обязательные» из 422. Перечитай документ. - ❌ Не парсить body на 204 — он пустой. - ❌ Не путать `int` (`42`) и `decimal-string` (`"42.00"`) для цен. - ❌ Не считать enum'ы стабильными между средами без подтверждения через справочник. - ❌ Не терять `|` в Bearer-токене. ================================================================ ## 14. Известные ловушки ================================================================ | Ловушка | Что делать | |---|---| | `storage_units`, не `units` | Использовать корректное имя из раздела 3 | | `vats`, не `vat` | То же | | `branches`, не `businesses` | То же | | `nomenclature_kinds`, не `kinds` | То же | | Цена — строка, не число | Парсить как `parseFloat(price)`, выводить как `${price}` | | `is_active` бывает `1` / `0` или `true` / `false` | Привести к bool: `Boolean(Number(x))` | | Bearer-токен теряет `\|` в CSV/INI | Хранить в quoted-строке или env с экранированием | | Заказ создан → возвращает 201, не 200 | Проверять диапазон `2xx` | | Delete → возвращает 204 без body | Не парсить body | | 422 errors могут быть `products.0.id` | Парсить как dotted path | | **`/api/reservations` → 404** при POST/PUT/DELETE | Резерв = строка заказа. Создаётся через `POST /api/orders/{order}/nomenclatures` — модель `Reservation` под капотом, route-binding `{nomenclature}` указывает на запись Reservation, не на Nomenclature. GET листинг доступен только через `/api/tables/reservations`. | | **Counterparty login принимает email** | Поле `phone` в `POST /api/counterparty/login` валидируется условно: если значение похоже на email → проверяется `Rule::exists('counterparties', 'email')`, иначе на `phone_1`. Не обязательно слать именно phone. | | **`/api/counterparty/payment_types` существует** (через `token` middleware) | См. routes/api.php — endpoint жив, просто на ERP-токене даёт 401. ERP сторона `/api/payment_types` действительно отсутствует. | | **Order's `{nomenclature}` в URL — это id Reservation** | `PUT /api/orders/{order}/nomenclatures/{nomenclature}` и `DELETE …/{nomenclature}` принимают id записи Reservation, **не** nomenclature_id. Берётся из `GET /api/orders/{id}` → `reservations[].id`. | | **`is_company` НЕ возвращается в response** контрагента | Ни в `GET /api/counterparties`, ни в `GET /api/counterparties/{id}`. Инферь по полям: компания имеет `name`/`inn`/`kpp`; физлицо — `first_name`/`last_name`/`birthday`. Но `is_company` обязательно передавать в `POST`/`PUT`. | | **`/api/counterparties/{id}/history` НЕ пагинирован** | Простой envelope `{histories[], historiesCount}` без `pagination`/`links`. Поле `dateTime` — camelCase. `?page=` может игнорироваться. | | **`?counterparty_id` и `?client_id` на `/tables/orders` — РАЗНЫЕ фильтры** | Не алиасы. counterparty_id фильтрует по владельцу заказа, client_id — по клиенту. Результаты не пересекаются полностью. | | **`DELETE /api/reservations/{id}` описан в доке, но 404 на бэке** | `ReservationController` имеет только `tableIndex`. Удалять резерв надо через `DELETE /api/orders/{order}/nomenclatures/{nomenclatureId}` (см. §18.7). Запись в `.svx` помечена как «не работает». | | **`POST /api/nomenclatures/export` отдаёт **бинарный .xlsx**, не JSON** | Content-Type `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`. Принимать как Blob/binary, не парсить как JSON. Без параметров — экспортирует ВСЁ. | | **`POST /api/nomenclatures/import` требует multipart с файлом, не JSON** | `POST {}` → 500 `{error: "No ReaderType or WriterType..."}` (maatwebsite/excel reader error). Реально нужен `multipart/form-data` с `file` (xls/xlsx/csv). Не задокументировано. | | **`/api/promotions` существует, но `POST {}` → 500** (не 422!) | Бэк-баг: валидатор отсутствует, любое битое тело крашит. Перед запросом обязательно собирать полное тело по реальному примеру. | | **`/api/discounts` ≠ `/api/promotions`** | Это **другая фича** — discount-коды с `magic_link`/`share_path`/`audience`. Структура ответа: `{data: {discounts[], discountsCount, pagination}, links, meta}` — **третий** вариант Laravel-пагинации! | | **POST с пустым телом меньше валидируется, чем кажется** | Реальный required список — обычно 1-3 поля, см. §18.0. Не блокировать создание из-за «недостающих» полей, которые не в required. | | **`order_statuses`: статусы оплаты — фиксированные ID** (2=ожидание, 22=оплачен, 6=отменён, хардкод в коде) | Платёжную логику вязать на эти ID (§12.1); имена для отображения — fetch live | ================================================================ ## 15. Структура endpoint'а в полной документации ================================================================ В `https://artypoul-docs-gigma-7b80.twc1.net//<Раздел>/` каждый endpoint описан в формате: ``` ### <Название endpoint'а> Метод: POST URL: https://api.gigma.ru/api/<путь> Авторизация: Bearer token | Не требуется Headers: Accept: application/json; Content-Type: application/json #### Параметры запроса - — описание #### Пример запроса ```json { ... } ``` #### Ответ При успешном действии возвращается HTTP код 200 (или 201 для create / 204 для delete). ```json { ... } ``` ##### Описание полей ответа - — описание ``` Используется компонент `` с метаданными в атрибутах. В `/llms-full.txt` это плоский markdown. ================================================================ ## 16. Что закрыто в документации (статус на сегодня) ================================================================ Сверены с реальным кодом frontend'а `event_ai` (admin): - ERP/Авторизация, ERP/Контрагенты, ERP/Бизнесы (PR #1) - ERP/Файлы, ERP/Резервирование, ERP/Меню, ERP/Стратегии продаж, ERP/Пользователи, ERP/Вспомогательные запросы (PR #5) Не сверены с реальным кодом (могут быть отклонения от docs.gigma.ru): - ERP: Заказы, Склады, Остатки, Номенклатура, Приложения, Блоки, Магазины, Промоакции, Страницы, Справочники - E-Commerce: все 10 разделов (нужен исходник storefront) Для несверенных разделов агенту следует читать docs.gigma.ru / llms-full.txt с особой осторожностью — base URL может быть без `/api`-префикса (старая дока), headers могут содержать выдуманный `Token: {токен приложения из Gigma}` (игнорировать — реальные headers см. раздел 1). ================================================================ ## 17. Use-case сценарии (end-to-end) ================================================================ Каждый сценарий — пошаговая инструкция: что вызвать, в каком порядке, какое поле ответа подставить в следующий запрос. Везде где сценарий завязан на **несверенный раздел**, ставлю пометку «⚠ unverified» — поведение нужно подтверждать на бэке. В каждом сценарии перед write-операцией предполагается: - В заголовках: `Accept: application/json`, `Content-Type: application/json`, `Authorization: Bearer `. - Перед использованием `*_id` — fetch соответствующего справочника (раздел 3). - Реакция на ошибки — по таблице из раздела 7. ---------------------------------------------------------------- ### 17.1 «Купить от лица клиента» (E-Commerce, end-to-end) ⚠ Все endpoint'ы E-Commerce в текущей доке указаны без префикса `/api`. На реальном бэке, скорее всего, корректно `https://api.gigma.ru/api/counterparty/...` (как и для ERP). Подтвердить эмпирически или дождаться сверки с исходником storefront. **Цель:** клиент логинится, находит товар, оформляет заказ, отслеживает статус. 1. **Запрос пароля.** `POST /api/counterparty/send_password` с телом `{ "phone": "79991234567" }`. Ответ `200`: `{ "message": "Password successfully send" }`. В примере — условный пароль `"1111"`; на боевом сервере он приходит звонком. 2. **Логин.** `POST /api/counterparty/login` с `{ "phone": "79991234567", "password": "1111" }`. Из ответа сохранить `counterparty.access_token.value` — это Bearer на все последующие запросы. 3. **Карта справочников** (один раз за сессию): `GET /api/counterparty/categories`, `GET /api/counterparty/brands`, `GET /api/counterparty/countries`, `GET /api/counterparty/delivery_types`, `GET /api/counterparty/payment_types`. Сохранить map'ы `id → name`. 4. **Каталог.** `GET /api/counterparty/products?category_id[]=…&brand_id[]=…&query=…&sale=true`. Возвращает список с `id`, `name`, `price` (decimal-string), `sale` (bool). 5. **Карточка товара** (опционально): `GET /api/counterparty/products/{id}`. 6. **Избранное** (опционально): `POST /api/counterparty/products/favourites` — `{ "product_id": }`; список — `GET /api/counterparty/products/favourites`; убрать — `DELETE /api/counterparty/products/favourites/{id}`. 7. **Предварительный расчёт стоимости.** `POST /api/counterparty/orders/precalculate` с телом: ```json { "products": [{ "id": , "quantity": 2 }], "delivery_type_id": , "address": "..." } ``` Возвращает итоговые суммы (товары, доставка, скидка, итого). **Не создаёт заказ.** 8. **Создание заказа.** `POST /api/counterparty/orders`. Точный контракт (сверено по коду) — в §18.8. Кратко: всегда `delivery_type_id` + `products[]` (с `id`, `quantity`); условно — `shop_id` (если delivery_type=1, самовывоз), `delivery_subtype_id` (если delivery_type=2), `address` (если delivery_subtype=2); опционально `payment_type_id`, `promo_code`. ⚠ `phone` и `comment` НЕ принимаются (телефон берётся из профиля). ```json { "delivery_type_id": 2, "delivery_subtype_id": 2, "address": "...", "payment_type_id": 2, "products": [{ "id": , "quantity": 2 }] } ``` Ответ `200`: объект заказа (`order`) с `id` и `status`. **POST не идемпотентен** — не повторять. 9. **Отслеживание.** `GET /api/counterparty/orders` — список своих заказов; `GET /api/counterparty/orders/{id}` — детали с текущим статусом. 10. **Уведомления.** ⚠ unverified: `GET /api/counterparty/notifications`, `POST /api/counterparty/notifications/{id}/read`. **Реакция на ошибки:** - 401 → повторить шаг 1-2. - 422 → ключи могут быть `products.0.id`, `address`, `phone` — показать пользователю. - 429 → backoff 60 сек. ---------------------------------------------------------------- ### 17.2 «CRM-операции: найти и обновить контрагента» (ERP) ✅ Сверено с реальным кодом (event_ai) — высокая уверенность. **Цель:** оператор находит клиента по телефону, обновляет адрес и смотрит его заказы. 1. **Логин оператора.** `POST /api/login` с `{ "login": "user@itecho.ru", "password": "..." }`. В примерах пароль — условный `"1111"`; на боевом сервере из письма. Сохранить `user.access_token.value`. 2. **Поиск контрагента по телефону.** `GET /api/counterparties?query=79991234567`. Возвращает массив `counterparties[]`. Если несколько — показать пользователю выбор. 3. **Получить карточку.** `GET /api/counterparties/{id}`. Возвращает `counterparty` с полным набором полей (физлицо или компания, по `is_company`). 4. **Обновление.** `PUT /api/counterparties/{id}` с полями тела (тот же набор, что в POST из раздела «ERP/Контрагенты»). Для физлица: `is_company: false, first_name, last_name, middle_name, birthday, phone_1, phone_2, email, address, counterparty_type_id`. Ответ `200/201`: обновлённый объект. 5. **История изменений.** `GET /api/counterparties/{id}/history?page=1&per_page=20`. Возвращает Laravel-пагинатор `counterparties.data[]` с событиями (`icon`, `color`, `title`, `description`, `dateTime`). 6. **Заказы клиента.** `GET /api/tables/orders?counterparty_id={id}` — ⚠ unverified фильтр; возможно поле называется иначе (`client_id`). **Реакция на ошибки:** - 403 → у юзера нет `edit-counterparties` (см. раздел 12.6). - 422 на PUT → ключи: `phone_1`, `email`, `inn` (для компании) — стандартные правила. ---------------------------------------------------------------- ### 17.3 «Заполнить витрину»: бизнес → банк → магазин → склад → товар (ERP admin) ✅ Шаги 1-3 верифицированы. Шаги 4-6 ⚠ unverified (Магазины / Склады / Номенклатура). **Цель:** с нуля развернуть всё для приёма заказов. 1. **Логин.** `POST /api/login` → токен. 2. **Бизнес.** `POST /api/branches` с телом: ```json { "title": "Продажа косметики", "name": "ООО \"АЙТЕКО\"", "code": "1349", "inn": "5403057658", "kpp": "540301001", "head": "Снегирёв А.И.", "phone_1": "...", "email": "...", "address": "...", "legal_address": "...", "responsible_user_id": } ``` Ответ `201`: `{ "branch": { "id": , ... } }`. Запомнить `branch.id`. 3. **Банковский реквизит.** `POST /api/branches/{branch.id}/bank_requisites` с `{ name, bik, kpp, payment_account, address }`. Можно автозаполнить через `POST /api/search_bank` с `{ query: "БИК или название банка" }` — вернётся шаблон. 4. **Магазин.** ⚠ unverified. Ожидается `POST /api/shops` с привязкой к `branch_id`. После сверки ERP/Магазины — конкретное body. 5. **Склад.** ⚠ unverified. Ожидается `POST /api/warehouses` с `{ name, address, storage_unit_id, city_id, ... }`. 6. **Номенклатура (товар или услуга).** ⚠ unverified. Ожидается `POST /api/nomenclatures` с телом вида: ```json { "name": "...", "kind_id": 2, // 2=Товар (см. 12.3) "type_id": , // из GET /api/nomenclature_types "unit_id": , // из GET /api/storage_units "vat_id": , // из GET /api/vats "price": "1000.00", "category_id": , "brand_id": } ``` Перед запросом подтянуть `nomenclature_kinds`, `storage_units`, `vats`, `categories`, `brands` — все 5 справочников в раздел 3. 7. **Фото товара.** `POST /api/files` (multipart, `file_type_id=2` = Аватар) → получить `file.id` → передать `avatar_id` в `PUT /api/nomenclatures/{id}`. **Ключевая ловушка:** все `*_id` в шаге 6 — обязательно через справочники (раздел 3). `unit_id` — это `/api/storage_units`, не `/api/units` (раздел 14). ---------------------------------------------------------------- ### 17.4 «Обработать заказ оператором» (ERP) ⚠ unverified (раздел Заказы ещё не сверен — endpoint'ы из docs.gigma.ru). **Цель:** оператор фильтрует заказы, открывает, резервирует товар, меняет статус. 1. **Логин оператора.** 2. **Карта статусов.** `GET /api/order_statuses` → map `id → name`. 3. **Фильтр.** `GET /api/tables/orders?order_status_id[]=&date_from=2026-05-01&date_to=2026-05-15&page=1&per_page=15`. Возвращает таблицу с пагинацией. 4. **Открыть заказ.** `GET /api/orders/{id}`. Возвращает заказ с массивом `nomenclatures[]`, контактами клиента, текущим статусом. 5. **Зарезервировать позицию.** ⚠ unverified, ожидается `POST /api/orders/{id}/reservations` или `POST /api/reservations` с `{ order_id, nomenclature_id, warehouse_id, quantity }`. Из event_ai видно, что есть `GET /api/tables/reservations` и `DELETE /api/reservations/{id}` — полный CRUD пока не вскрыт. 6. **Сменить статус.** ⚠ unverified, ожидается `PUT /api/orders/{id}` с `{ order_status_id: }` или специальный endpoint `POST /api/orders/{id}/status`. 7. **Уведомление клиенту.** Автоматически бэкенд должен — у клиента в E-Commerce должен появиться элемент в `GET /api/counterparty/notifications`. ⚠ требует подтверждения. **Ловушка по статусам:** статусы оплаты захардкожены по ID (оплачен=22, отменён=6, ожидание=2 — §12.1); прочие имена для отображения брать через `GET /api/order_statuses`. ---------------------------------------------------------------- ### 17.5 «Запустить промоакцию» (ERP) ⚠ unverified (Промоакции / Стратегии продаж — есть endpoint'ы в доке, но не сверены). **Цель:** маркетолог создаёт промо, привязывает стратегию, проверяет видимость на витрине. 1. **Логин.** 2. **Подготовка справочников.** `GET /api/sales_strategies` (есть в доке, ✅ верифицирован) — выбрать `sales_strategy_id`. `GET /api/categories`, `GET /api/brands` — на что распространяется акция. 3. **Создать промоакцию.** ⚠ unverified, ожидается `POST /api/promotions` или `POST /api/discounts` с телом вида: ```json { "name": "Скидка 20% на бренд", "sales_strategy_id": , "discount_percent": 20, "starts_at": "2026-05-15T00:00:00", "expires_at": "2026-06-15T23:59:59", "brand_id": } ``` В event_ai есть `discounts.api.ts` — структура с `expires_at` подтверждена, но точный path неизвестен без сверки. 4. **Проверка через E-Commerce.** `GET /api/counterparty/products?brand_id[]=` — товары соответствующего бренда. Поле `sale: true` и `price` со скидкой должны быть видны клиенту. **Ключевые поля:** `expires_at` — иногда возвращается как `null` (бессрочная), смотреть `nullable`. ---------------------------------------------------------------- ### 17.6 «Загрузить файл и привязать» (ERP) ✅ Сверено для базового upload. Привязка к конкретной сущности зависит от целевого endpoint'а. **Цель:** загрузить аватар контрагенту / договор сотруднику / документ к заказу. 1. **Загрузка файла.** `POST /api/files` с `Content-Type: multipart/form-data` и формой: - `file` — бинарник - `file_type_id` — `1` (договор), `2` (аватар), `3` (файл заказа) — см. 12.2 - `link` (опционально) — произвольный URL Ответ `201`: `{ "file": { "id": , "path": "https://...", "type": {...} } }`. Запомнить `file.id`. 2. **Привязка через `*_id` поле родительской сущности.** Например: - Аватар контрагента: `PUT /api/counterparties/{id}` с `{ avatar_id: , ... остальные поля }`. - Аватар бизнеса: `PUT /api/branches/{id}` с `{ avatar_id, ... }`. 3. **Прямая привязка к заказу.** ⚠ unverified: `POST /api/orders/{id}/files` (тот же multipart, без `file_type_id`, бэк ставит `ORDER_FILE` сам). В event_ai есть функция `NewOrderFile` именно с таким endpoint. **Ловушка:** при `PUT` нужно слать **весь объект целиком**, не только `avatar_id`. Иначе остальные поля могут затереться. Сначала `GET`, потом `PUT` с обновлённым полем. ---------------------------------------------------------------- ### 17.7 «Импорт товаров из Excel на склад» (ETL) ⚠ Создание номенклатуры (`POST /api/nomenclatures`) и привязка к складу (`POST /api/inventories` или эквивалент) — **unverified**. Точные body schemas нужно подтверждать на бэке или через `/openapi.json`. **Цель:** агент получает .xlsx/.csv со списком товаров и заливает их в ERP, привязывая к указанному складу. Обработать дубли, картинки, фокус на rate-limit. **Высокоуровневый flow:** ``` Excel → Parse → Validate → Resolve refs → Upload images → Create nomenclatures → Add to warehouse → Report ``` #### Этап 1: Pre-flight (один раз перед началом) 1. Логин (раздел 2), получить токен. 2. Загрузить **все нужные справочники одним батчем** (раздел 3) и построить map'ы `name → id`: - `GET /api/brands`, `/api/categories`, `/api/countries` - `GET /api/nomenclature_kinds`, `/api/nomenclature_types` - `GET /api/storage_units`, `/api/vats` - `GET /api/warehouses` ⚠ unverified — список складов, выбрать target 3. Сохранить map'ы в локальном кеше (`~/.gigma/dictionary.json`) — не перезапрашивать в той же сессии. #### Этап 2: Парсинг Excel - **JS:** `xlsx` (SheetJS) или `exceljs`. - **Python:** `openpyxl` или `pandas.read_excel`. - **Bash quick-n-dirty:** `xlsx2csv` → `csv` parser. Прочитать первую строку как **headers**, остальное — data rows. #### Этап 3: Mapping столбцов → ERP полей (показать пользователю до запуска) Типовая таблица соответствий (агент строит её по headers Excel; если не находит — спрашивает): | Excel column (примеры) | ERP field | Resolution | |---|---|---| | «Название», «Наименование», «Name» | `name` | as-is, trim | | «Артикул», «SKU», «Код» | `code` | as-is, **для дедупликации** | | «Цена», «Розничная цена», «Price» | `price` | parse → `"1000.00"` (decimal-string!) | | «Себестоимость», «Закупка», «Cost» | `cost` | то же | | «Бренд», «Производитель» | `brand_id` | lookup в `brands` map | | «Категория» | `category_id` | lookup | | «Вид» | `kind_id` | "Товар" → 2, "Услуга" → 1 (см. 12.3 / live map) | | «Тип» | `type_id` | lookup в `nomenclature_types` | | «Единица», «Ед.», «Unit» | `unit_id` | lookup в `storage_units`; «шт» / «штук» → ищем «Штука» | | «НДС», «VAT» | `vat_id` | "20%" → vat, чей `name` содержит "20" | | «Страна» | `country_id` | lookup | | «Описание» | `description` | as-is | | «Фото», «Изображение», «URL фото» | `avatar_id` | загрузить через POST /api/files (этап 5) | | «Количество», «Остаток» | (для inventory) | отдельный POST, не часть номенклатуры | Перед стартом — показать таблицу маппинга пользователю единым блоком, дать поправить. #### Этап 4: Резолвинг enum'ов с fuzzy matching Не падай на первой непрямой строке. Цепочка резолва: ``` 1. Прямое: brandMap.get(value.trim()) 2. Lowercase: brandMap.get(value.trim().toLowerCase()) 3. Нормализация: убрать "ООО ", "ПАО ", «"», лишние пробелы 4. Подстрочно: для НДС "20%" находит "НДС 20%"; для брендов — содержит подстроку 5. Левенштейн ≤ 2: для опечаток 6. Нет — пометить error("BRAND_NOT_FOUND: "), решать в этапе 8 ``` #### Этап 5: Загрузка изображений (если есть колонка с URL/path) Для каждой data-row, у которой есть путь к фото: 1. Скачать/прочитать бинарник. 2. `POST /api/files` multipart (см. 18.3): - `file` = binary - `file_type_id` = `2` (Аватар, см. 12.2) 3. Сохранить `response.file.id` → `row.avatarId`. 4. **Соблюсти rate-limit:** ≤ 1 запрос/сек, параллельно ≤ 5. Если на этапе картинки 429 — backoff 60 сек и продолжить. #### Этап 6: Дедупликация по `code` Перед `POST /api/nomenclatures` — проверка существующего: ``` GET /api/nomenclatures?code= ``` Варианты, выбираемые пользователем заранее в чек-листе: - **skip** — не создавать, в отчёте пометить «duplicate, skipped» - **update** — `PUT /api/nomenclatures/{id}` с новыми полями (помнить: PUT заменяет объект целиком, нужно сначала GET + merge) - **duplicate** — добавить суффикс к `code` (например `-2`) и создать #### Этап 7: Создание номенклатуры `POST /api/nomenclatures` с телом (⚠ unverified, ориентир из docs.gigma.ru и event_ai типов): ```json { "name": "...", "code": "SKU-001", "kind_id": 2, "type_id": , "unit_id": , "vat_id": , "category_id": , "brand_id": , "country_id": , "price": "1000.00", "cost": "700.00", "description": "...", "avatar_id": } ``` Ожидать **201**. Сохранить `response.nomenclature.id` в локальный map `sku → nomenclatureId`. #### Этап 8: Привязка к складу `POST /api/inventories` ⚠ unverified, точная схема — в `/openapi.json` после Scribe-разметки бэкенда: ```json { "warehouse_id": , "nomenclature_id": , "quantity": 50, "price": "1000.00", "vat_id": } ``` Если endpoint вернёт 404 / 500 — гипотеза: путь другой (`/api/warehouses/{id}/inventories`, `/api/stock`, `/api/balances`). Проверь через `/openapi.json` или спроси бэк. #### Этап 9: Батчинг, прогресс, ошибки - **Не запускать параллельно больше 5** запросов на токен. - **Между батчами** — пауза ≥1 сек (rate limit 60/min, раздел 8). - **Прогресс** — отчёт пользователю каждые 10 строк: `[20/100] OK=15, skip=3, error=2`. - **Сохранять промежуточный результат** в локальный JSON (`import-state.json`) — на случай прерывания. Перезапуск читает state, начинает с последней необработанной строки. - **На 5xx** при POST nomenclature — первая гипотеза «неполное тело» (19.1). Сверь со схемой, **не повторяй вслепую**. - **На 422** — лог `errors.field`, пропустить строку, продолжить остальные. В конце — список error-rows для ручного разбора. #### Этап 10: Финальный отчёт ``` Import complete. Processed: 100 rows Created: 87 Updated: 3 Skipped: 8 (duplicates) Errors: 2 (see error-rows.json) Errors by category: - BRAND_NOT_FOUND: 1 - VALIDATION_422: 1 Failed rows: Row 42 (SKU="ABC-12"): brand "Неизвестный" not found in /api/brands Row 73 (SKU="XYZ-99"): 422 errors.name = ["The name field is required."] ``` #### Чек-лист перед запуском (ОДИН AskUserQuestion с несколькими полями) - Excel-файл прочитан: **N строк**, **M колонок** - Маппинг колонок: показать таблицу из этапа 3 - Target warehouse: «<название>» (id=) - Стратегия дубликатов: **skip / update / duplicate** - Стратегия пропущенных FK (несуществующий бренд / категория): **create / fail row / fail import** - Стратегия картинок: **upload / skip** После подтверждения → запуск. **Один блок вопросов, не серия.** #### Известные подводные камни импорта | Ловушка | Что делать | |---|---| | Цена в Excel — `"1 000,00 ₽"` | Удалить пробелы, запятые, валюту → `parseFloat` → `.toFixed(2)` | | Картинка встроена в ячейку, не URL | Извлечь из `xl/media/` внутри .xlsx (это zip); связать по `xl/drawings/` | | Бренд «ООО Сбер» vs «ПАО Сбербанк» в БД | Нормализация + fuzzy (этап 4) | | Excel сохранён с BOM | xlsx-парсеры обычно справляются; для CSV — `--encoding=utf-8-sig` | | Артикул как число `100500` в Excel, в БД `string` | Привести к string перед сравнением: `String(value).trim()` | | Описание с переносами строк | Сохранять `\n` как есть, JSON их экранирует | | 100+ строк × 1 rps = 100+ секунд | Показать прогресс, разрешить пользователю Ctrl+C → resume | ---------------------------------------------------------------- ### Общий шаблон шага «вызов endpoint'а» При выполнении любого шага сценария: 1. **Заголовки** (раздел 1) — добавить во все запросы. 2. **Авторизация** (раздел 2) — токен из логина в `Authorization`. 3. **Тело** — по схеме конкретного endpoint'а (см. полную доку на docs.gigma.ru / llms-full.txt). 4. **Ожидаемый код**: 200 / 201 / 204 (раздел 7) — проверять диапазон `2xx`. 5. **Ошибка** — по таблице раздела 7. 6. **Сохранить** ID и ключевые поля ответа для следующего шага. ================================================================ ## 18. Body schemas для горячих write-endpoint'ов ================================================================ Точные тела запросов с типами, обязательностью полей и enum-значениями — в **openapi.json**: ```bash jq '.paths["/api/counterparties"].post.requestBody.content."application/json".schema' openapi.json ``` Здесь только то, что openapi.json **не выражает**: ветвящиеся тела, multipart-пробелы, авто-заполнение через helper-endpoint'ы, и эндпоинты с пометкой «не сверено». ---------------------------------------------------------------- ### 18.1 Multipart — `POST /api/files` (gap в openapi.json) ⚠ В текущей openapi.json `requestBody` для `/api/files` пуст: генератор не делает fallback на multipart, если в `.svx` нет JSON-примера тела. Реальный контракт: `Content-Type: multipart/form-data` с формой: - `file` (binary, **обязательно**) - `file_type_id` (int, **обязательно**) — `1`=Трудовой договор, `2`=Аватар, `3`=Файл заказа (см. §12.2) - `link` (string, опционально) **Response 201:** `{ file: { id, name, type, path, link, ... } }`. Сохрани `file.id`, чтобы привязать к родителю отдельным запросом: - `PUT /api/counterparties/{id}` с `{ avatar_id: , ...остальные поля }` - `PUT /api/branches/{id}` с `{ avatar_id, ... }` Особый case: `POST /api/orders/{id}/files` принимает сам файл **без** `file_type_id` — бэк сам проставит `ORDER_FILE` (3). ---------------------------------------------------------------- ### 18.2 Ветвящееся тело — `POST /api/counterparties` Структура тела зависит от `is_company`. openapi.json показывает **плоский** список required без branching — это упрощение, использовать с оглядкой: - `is_company: false` (физлицо) → требуются `first_name`, `last_name`, `phone_1`. Опционально: `middle_name`, `birthday`, `phone_2`, `email`, `address`. **Не слать** `name`/`inn`/`kpp`/`head` — поля компании. - `is_company: true` (компания) → требуются `name`, `inn` (10 или 12 цифр), `kpp` (9 цифр), `head` (ФИО директора). Опционально: `registered_at`, `legal_address`. **Не слать** `first_name`/`last_name`/`birthday`. Общие для обеих веток: `counterparty_type_id` (см. enum в openapi.json), `avatar_id` (от `POST /api/files`). **Авто-заполнение по ИНН:** `POST /api/search_company` с `{ field: "inn", query: "5403057658" }` → готовый шаблон полей для компании. ---------------------------------------------------------------- ### 18.3 Авто-заполнение реквизитов — `POST /api/branches/{id}/bank_requisites` По БИК: `POST /api/search_bank` с `{ query: "045004641" }` → шаблон (банк, корреспондентский счёт, адрес банка). Дальше POST реквизита с подставленными значениями. ---------------------------------------------------------------- ### 18.4 ⚠ Неверифицированные тела (требуют проверки на storefront) Эндпоинты, для которых openapi.json показывает схему по примеру из доки, но реальный storefront-код **не сверялся** — поля могут отличаться: - `POST /api/counterparty/orders/precalculate` — расчёт суммы без создания заказа. Тело (сверено по коду): только `products[]` (минимум 1: `{id, quantity}`) + опционально `promo_code`. `delivery_type_id`/`address` для расчёта НЕ требуются. **Не создаёт** заказ. - `POST /api/counterparty/orders` — оформление заказа. Точный контракт (сверено по коду) — в §18.8. ⚠ `phone` и `comment` НЕ принимаются. **POST не идемпотентен** — не повторять без проверки результата. Перед использованием в продакшене — сверить с актуальным storefront-кодом или дёрнуть на тестовом контрагенте. ---------------------------------------------------------------- ### 18.0 ⚠ Required-поля (СВЕРЕНО против FormRequest кода бэка) Источник: `itecho-erp-backend/app/Http/Requests//StoreRequest.php`. **Authoritative**, не угадано — взято из `rules()` метода Laravel-валидаторов. | Endpoint | Required (всегда) | Required условно | Полезные опциональные | |---|---|---|---| | `POST /api/nomenclatures` | `name`, `kind_id` | — | code (numeric unique), type_id, storage_unit_id, vat_id, price (regex `^\d+(\.\d{1,2})?$`), category_id, brand_id, country_id, branch_id, cost_price, quantity, discount (0-100), markup, photos[] {photo_id,order}, tags[], is_import, pieces_per_pack | | `POST /api/categories` | `name` (min:3, max:1024) | — | code (numeric unique), description (max 65535), parent_id, avatar_id, photo_id, tag_id[], branch_id | | `POST /api/inventories` | `nomenclature_id`, `warehouse_id` | — | code (numeric unique), counterparty_id (**только если counterparty_type_id=2=Поставщик**), price (decimal), vat_id, quantity (≥0), discount (0-100), markup (≥0) | | `POST /api/warehouses` | `name` (max:255), `address` (max:255), `storage_unit_id` | — | code (numeric unique), avatar_id, owned_by_us (bool), storage_capacity (≥1), counterparty_id, is_price_from_inventories (bool) | | `POST /api/shops` | `name` (max:255), `address`, `phone` | — | code (numeric unique), avatar_id, is_shop (bool), branches[] (FK), warehouses[] (FK) | | `POST /api/branches` | `title`, `name`, `inn` (unique), `phone_1`, `email`, `address`, `legal_address` | — | code (numeric unique), avatar_id, kpp, phone_2, head, responsible_user_id | | `POST /api/counterparties` | `is_company` (bool), `counterparty_type_id` | **if `is_company=true`:** name, inn (numeric unique), kpp (numeric unique), head, registered_at, legal_address. **if `is_company=false`:** first_name, last_name, middle_name, birthday, address | code (numeric unique), avatar_id, phone_1 (numeric unique), phone_2, email | ### Ключевые открытия из FormRequest'ов 1. **`code` — числовое поле**, не строка. Везде: nomenclatures, categories, branches, warehouses, shops, counterparties. `Rule::unique` scoped по `project_id` (мультитенантность). 2. **Counterparty — две взаимоисключающие ветки** через `required_if:is_company`. Для юрлица — name/inn/kpp/head/registered_at/legal_address. Для физлица — first_name/last_name/middle_name/birthday/address. **Не слать поля чужой ветки.** 3. **Цены в decimal-string** проверяются regex `^\d+(\.\d{1,2})?$` — `"1000"` и `"1000.00"` оба валидны. 4. **`counterparty_id` в inventory** должен указывать на контрагента типа `PROVIDER` (id=2). Other types — `422`. 5. **`inn`/`kpp` в counterparties** — `numeric` (без `0` ведущих, как число), но в branches `inn` — `string`. Бэк-неконсистентность, ловушка. **Для агента:** строго требовать только всегда-required. Условно-required включать когда соответствует `is_company`. Остальное опционально — не блокировать создание. ---------------------------------------------------------------- ### 18.5 Складские write-операции — `POST /api/nomenclatures`, `POST /api/categories`, `POST /api/inventories` Самые частые write-операции при работе со складом. Полные тела с типами — в **openapi.json** (`jq '.paths["/api/nomenclatures"].post.requestBody'`). Здесь — карта обязательных FK-полей и сценарных подсказок. **`POST /api/categories`** (категория номенклатуры) | Поле | Обязательно | Откуда брать | |---|---|---| | `code`, `name` | ✓ | от пользователя | | `description` (HTML) | — | от пользователя | | `parent_id` | — | `GET /api/categories` (для вложенности) | | `avatar_id`, `photo_id` | — | предварительный `POST /api/files` | | `tag_id[]` | — | `GET /api/tags` | Ответ **201** с `category.id` — запомнить для привязки номенклатуры. **`POST /api/nomenclatures`** (товар или услуга) Обязательные FK — **6 справочников**, подтянуть один раз pre-flight (см. §17.3): | Поле | Откуда | |---|---| | `kind_id` | `GET /api/nomenclature_kinds` (1=Услуга, 2=Товар) | | `type_id` | `GET /api/nomenclature_types` | | `storage_unit_id` | `GET /api/storage_units` (**НЕ** `/api/units`) | | `vat_id` | `GET /api/vats` (**НЕ** `/api/vat`) | | `category_id` *(опц.)* | `POST /api/categories` или `GET /api/categories` | | `brand_id` *(опц.)* | `GET /api/brands` | Обязательные простые: `code` (уникальный SKU), `name`, `price` (decimal-string, для услуг обязательно). Опциональные: `country_id`, `branch_id`, `description`/`specification` (HTML), `cost_price`, `markup`, `discount`, `avatar_id`/`preview_id` (от `POST /api/files` c `file_type_id=2`), `photos[{photo_id, order}]`, `tags[]`. ⚠ openapi.json пометит required по факту наличия поля в примере — реальные FormRequest правила могут быть строже. Стандартный паттерн на 422: `errors.{field}: ["The X field is required."]`. **`POST /api/inventories`** (запись остатка на складе) Все 6 полей обязательны: `code` (string), `warehouse_id` (int), `nomenclature_id` (int), `vat_id` (int), `quantity` (int ≥ 0), `price` (decimal-string). Опционально: `discount` (int %), `markup` (int %). Связка с предыдущим шагом: `nomenclature_id` — это `id` из ответа `POST /api/nomenclatures`. `warehouse_id` — из `GET /api/warehouses` (⚠ список endpoint неверифицирован, см. §3). ---------------------------------------------------------------- ### 18.7 Заказы и резервации — `POST /api/orders` + `POST /api/orders/{order}/nomenclatures` Источник: `Order/StoreRequest`, `OrderNomenclature/StoreRequest`, `OrderNomenclatureController` (использует модель `Reservation`). **Шаг 1 — создать заголовок заказа.** `POST /api/orders` с минимальным телом: | Поле | Required | |---|---| | `counterparty_id` | ✅ (FK → counterparties) | | `manager_id` | ✅ (FK → users) | Опциональные поля: `delivery_type_id`, `shop_id`, `address` (min:3), `branch_id`, `object_id`, `application_id`, `sales_channel_id`, `promotion_id`, `contract_number`, `contract_id` (FK → files), `contract_start_date`/`contract_end_date`, `invoice_number`, `invoice_start_date`/`invoice_end_date`, `avatar_id`. Ответ **201** с `order.id` — сохрани для шага 2. **Шаг 2 — добавить позиции = создать резервации.** ⚠ **Главный инсайт:** `POST /api/orders/{order}/nomenclatures` фактически создаёт записи в таблице **`reservations`** (модель `Reservation`, controller использует её для route-binding `{nomenclature}` в `update`/`destroy`). Резервация = строка заказа. Поэтому `/api/reservations` как отдельного ресурса **не существует** (только `GET /api/tables/reservations` для листинга). Создание/обновление/удаление — через позиции заказа. | Поле | Required | |---|---| | `quantity` | ✅ (min:1) | Опциональные: `nomenclature_id` (можно создать строку без привязки к позиции номенклатуры!), `storage_unit_id`, `price`, `vat_id`. `PUT /api/orders/{order}/nomenclatures/{nomenclature}` и `DELETE /api/orders/{order}/nomenclatures/{nomenclature}` — обновление/удаление, **`{nomenclature}` в URL — это id записи Reservation**, не nomenclature_id. **Шаг 3 (опц.) — расчёт цены без сохранения.** `POST /api/orders/calculator` (controller `InventoryController::calculatePrice`) — даёт цену по составу до создания заказа. **Альтернатива на E-Commerce:** `POST /api/counterparty/orders/precalculate` (rate-limit 30/min!) — расчёт для клиентского flow. Не создаёт заказ. ---------------------------------------------------------------- ### 18.8 E-Commerce заказ — `POST /api/counterparty/orders` (СВЕРЕНО по коду) Источник: `Counterparties\Order\StoreRequest`, `Counterparties\OrderController::store()`. **Always required:** | Поле | Правило | |---|---| | `delivery_type_id` | FK exists:delivery_types | | `products` | array, ≥ 1 элемент | | `products.*.id` | FK exists:nomenclatures | | `products.*.quantity` | int, min:1 | **Conditional required (через `required_if`):** | Условие | Дополнительно required | |---|---| | `delivery_type_id == 1` (самовывоз) | `shop_id` | | `delivery_type_id == 2` (доставка) | `delivery_subtype_id` | | `delivery_subtype_id == 1` | `delivery_subtype_param_id` | | `delivery_subtype_id == 2` (по адресу) | `address` (min:1) | **Optional:** - `payment_type_id` (FK; `1`=cash, `2`=card) - `promo_code` (regex `^[A-Za-z0-9\-]+$`, max:64) ⚠ **`phone` и `comment` НЕ принимаются.** В предыдущих версиях доки они упоминались — это была неверная информация. Контактный телефон берётся из профиля counterparty, отдельное поле не нужно. ### Side-effects при `POST /api/counterparty/orders` Бэк автоматически проставляет: - `order_status_id = 2` (IS_PAYMENT_WAITING) - `started_at = now()` - `application_id`, `branch_id` — из контекста (token middleware инжектит application) - `counterparty_id` = auth()->id() - pricing-поля: `price`, `original_price`, `product_discount_amount`, `promo_discount_amount`, `total_discount_amount`, `final_price` — считаются `PricingService::calculate()` - **Резервации создаются под капотом** (`reservePricingItems()`) — отдельным API дёргать не надо - При наличии промокода → создаётся запись `DiscountUsage` (status=`reserved` для карты, `confirmed` для нала); `discounts.total_used_count` инкрементится; запись `discount` блокируется `lockForUpdate()` - При `payment_type_id == 2` (card) → создаётся YooKassa-платёж через `yooKassaService->storeOrderPayment()` → `payment_link` сохраняется в order ### ⚠ Нестандартные форматы ошибок E-Commerce order endpoints возвращают **разные форматы 422** в зависимости от источника: | Сценарий | Формат | |---|---| | Стандартная Laravel-валидация (поля required, FK invalid) | `{message, errors: {field: [...]}}` | | `OrderPricingException` в preCalculate | `{error: "сообщение"}` — **без `errors`** | | `OrderPricingException` в store | `{message: "сообщение"}` — **без `errors`** | | «Часть товаров закончилась» (checkPricingAvailability) | `{message: "Часть товаров закончилась. ..."}` | | Промокод не найден | `{message, code}` где `code` — числовой `DiscountValidationException::CODE_*` | | `502 Bad Gateway` от YooKassa | `{message: "Не удалось создать платёж. Заказ отменён..."}` — заказ откатан в статус `IS_CANCELED` (6), DiscountUsage откатан | **Для агента:** не предполагать что 422 всегда содержит `errors{}`. Сначала проверить наличие `errors`, потом `code`, потом `error` (single), потом `message`. ### `POST /api/counterparty/contact_form` — отдельный flow Не для авторизованных клиентов. Required: `first_name`, `last_name`, `phone` (numeric, 10-12 цифр), `products[]` с `products.*.id` (⚠ **FK на `warehouse_nomenclatures`**, не на `nomenclatures` — это inventory id!), `products.*.quantity`. Опц: `middle_name`, `address`. Use-case: гость кликает «оставить заявку» с корзиной — без логина. ---------------------------------------------------------------- ### 18.6 Массовый импорт остатков — `POST /api/inventories/upload` Источник: `ImportInventoryRequest` в бэке. **ДВА режима**, выбираются по `Content-Type`: **Always required:** `warehouse_id` (int, FK на warehouses). **Режим А — файл (multipart/form-data):** - `file` — binary. MIME-типы: **xmr, xml, csv, txt, xls, xlsx** (не только `.xmr` как ранее писали). **Режим Б — JSON с items + накладной (application/json):** - `items[]` (array, min:1) с полями на каждую строку: `row_number` (numeric), `item_name` (string), `quantity` (numeric), `price` (numeric), `sum_without_vat` (numeric), `sum_with_vat` (numeric), `vat_rate` (string!), `vat_amount` (nullable), `sku` (nullable), `barcode` (nullable). - Плюс метаданные накладной: `invoice_number` (string), `invoice_date` (date), `supplier_inn` (string), `supplier_name` (string), `currency_code` (string, size:3). **Response 200:** `{ "message": "Import successful" }`. **Response 422:** ошибки валидации `errors.items.0.field` для конкретных строк. Use-case: альтернатива поэтапному `POST /api/inventories`. Режим Б полезен для интеграций (1С, ЕРП-поставщика), которые отдают накладные напрямую как JSON, без промежуточного файла. ---------------------------------------------------------------- ### 18.9 YooKassa webhook — `POST /api/yookassa` (СВЕРЕНО по коду) Endpoint **публичный** (нет auth), вне `auth:user`/`auth:counterparty` групп. Принимает webhook от YooKassa. **Body:** JSON `{event: "payment.succeeded"|..., object: {id, metadata: {order_id}, ...}}`. **Обрабатываемые `event`:** - `payment.succeeded` → `order.order_status_id = 22` (IS_PAID), создаётся/обновляется `Payment` record с `amount, card_type, card_first_6, card_last_4`. - `payment.waiting_for_capture` → двухстадийный hold, статус остаётся IS_PAYMENT_WAITING пока не придёт succeeded. - `payment.canceled` → отмена платежа. - **Unknown events** → `Log::warning` + 200 OK (молчаливо игнорируется). **Ответ:** всегда `{status: "ok"}` с **200 OK**. Webhook идемпотентный. **Когда возвращается 5xx:** только при transient errors (DB lock, YooKassa API недоступен, failed capture). Laravel пропускает throw → 5xx → YooKassa делает retry. Это **штатный механизм** восстановления — глушить через try/catch нельзя, иначе потеряем платёж. ### Хранение платежей в Order | Поле | Описание | |---|---| | `payment_link` | URL для редиректа клиента на платёжку (заполняется при создании ордера с `payment_type_id=2`) | | `payment_id` | FK на внутреннюю таблицу `payments` | | `yookassa_payment_id` | ID платежа в YooKassa (для webhooks) | Полная история платежей — в `Payment` model (отдельная таблица), не в самом Order. ### Multi-shop credentials через `warehouse_integrations` YooKassa creds (`shop_id`, `secret_key`) хранятся per-warehouse через `warehouse_integrations` таблицу. `YooKassaService::warehouseYooKassaCredentials($order)` находит склад заказа и берёт соответствующие credentials. Разные склады могут использовать разные YK-кабинеты → подходит для маркетплейсов / мульти-юрлица. ### Анти-гонка `DB::transaction + Order::lockForUpdate()` в `paymentSucceeded()` — потому что есть параллельная console-команда `yookassa:reconcile-pending`, которая может обрабатывать тот же заказ. Без локов был бы double-status-transition. ### Для агента (E-Commerce 17.1) 1. После `POST /api/counterparty/orders` с `payment_type_id=2` (card) — взять `order.payment_link` и редиректить пользователя. 2. Не ожидать synchronous подтверждения — статус заказа меняется через webhook. 3. Polling: `GET /api/counterparty/orders/{id}` пока `order_status_id != 22 (IS_PAID)` и не `6 (IS_CANCELED)`. Интервал ≥ 5 сек. 4. На тестовом стенде `payment_link` может не работать — это normal для тестового окружения. ================================================================ ## 19. Известное поведение бэкенда (workarounds) ================================================================ ### 19.1 500 вместо 422 на write с неполным телом **Симптом:** `POST /api/orders` или другой write с пустым/неполным body возвращает `500 Server Error` вместо ожидаемого `422 Unprocessable Entity`. Это бэкенд-баг (Laravel-валидатор не отрабатывает в каком-то edge-case). **Workaround для агента:** - На любой **5xx-код на POST/PUT** считать «неполное тело» **первой гипотезой**, а не «упал сервер». - Не повторять запрос вслепую. Сначала свериться с body schema (раздел 18 или OpenAPI). - Если уверены, что тело полное — тогда уже бэкенд упал, можно репортить. ### 19.2 Нет bulk-операций **Симптом:** чтобы удалить 9 товаров, нужно 9 отдельных `DELETE /api/nomenclatures/{id}`. Чтобы залить 50 — 50 POST'ов. **Workaround:** - Закладывать **rate limit 60/min** (раздел 8) — для массовых операций не более 1 запроса в секунду. - Параллелить максимум 5 одновременно. - Между батчами — пауза 1-2 секунды чтобы не словить 429. - Перед запуском массовой операции — показать пользователю прогноз времени (`N запросов * ~1с = ~Nс`). ### 19.3 Pagination metadata тяжёлая **Симптом:** Laravel-пагинатор тащит `first_page_url`, `last_page_url`, `links[]` (HTML-стиль навигации) — для агента это шум, который грузит токены. **Workaround:** - Парсить только нужное: `data[]`, `total`, `current_page`, `per_page`, `last_page`. Всё остальное игнорировать. - На клиенте генерировать `next_page = current_page + 1` если `current_page < last_page`, без чтения `next_page_url`. ### 19.4 Пустой стенд **Симптом:** на свежем тестовом стенде 0 контрагентов, 0 заказов, 0 номенклатуры. Агент пытается оформить заказ → 422 «counterparty_id required», создаёт контрагента → 422 «counterparty_type_id required», читает справочник → пустой массив (типы не засидены). **Workaround:** - В начале сессии проверять `count` каждого ключевого справочника. Если 0 — сообщить пользователю «стенд пустой, нужны seed-данные». - Не пытаться создать «правильную цепочку с нуля» — это вопрос к админу системы, не задача агента. ### 19.5 `is_active` как `1`/`0` vs `true`/`false` **Симптом:** некоторые legacy-поля возвращают `1`/`0` (как int), новые — `true`/`false` (как bool). Один и тот же `is_active` может быть в разных форматах в разных endpoint'ах. **Workaround:** - Парсить через `Boolean(Number(value))` — обработает оба варианта. - При записи — посмотреть, что отдаёт `GET` на этот же объект, и повторять тот же формат в `PUT`. ### 19.6 URL заказов не симметричны **Симптом:** в E-Commerce заказы под `/api/counterparty/orders` (клиентская сторона), в ERP — под `/api/orders` (админская). Это разные ресурсы с разными правами, не путать. **Workaround:** - Для клиентских операций (мои заказы, оформить) — `/api/counterparty/orders`. - Для админских (все заказы, фильтр по контрагенту, смена статуса) — `/api/orders` (или `/api/tables/orders` для таблицы). ### 19.7 Список buggy write-endpoint'ов (стабильно 500) Эти эндпоинты на тестовом стенде отдают `500 Server Error` даже на корректных входных данных: | Endpoint | Симптом | Что делать | |---|---|---| | `POST /api/orders/{id}/nomenclatures` | 500 на корректном `{quantity, warehouse_nomenclature_id}` | Bypass через E-Commerce `POST /api/counterparty/orders` (резерв создаётся внутри транзакции). Или ждать фикса. | | `DELETE /api/orders/{id}` | 500 на любой id | Менять `order_status_id` на `6` (IS_CANCELED) через `PUT /api/orders/{id}` — заказы помечаются отменёнными, не удаляются. | | `POST /api/calls` | 500 | Не задокументирован в `.svx`. Если нужен — связь с бэк-командой. | | `POST /api/tags` | 500 | Тэги создаются только через включение в `tag_id[]` родительских ресурсов (например `nomenclatures.tags[]`). | | `POST /api/promotions` | 500 на любом теле, включая `POST {}` (валидатор не настроен — fail-fast невозможен) | Использовать `/api/discounts` — отдельная фича со своим CRUD, статусами и stats. См. use-case 17.5. | | `POST /api/sales_strategies` | 500 | Эндпоинт админский, через UI Itecho — не через API. | ### 19.8 Удаление возвращает разное **Симптом:** `DELETE /api/files/{id}` возвращает `{ "message": "File successfully deleted" }` с кодом 200. `DELETE /api/branches/{id}` возвращает `{ "message": "..." }` с кодом 200. `DELETE /api/reservations/{id}` может вернуть 204 без body. **Workaround:** - Не полагаться на конкретный код / формат тела. - Считать успехом любой `2xx` (200, 201, 204). - Body парсить только если `content-length > 0` и `content-type: application/json`. ================================================================ ## 20. Wishlist для бэкенд-команды ================================================================ Список улучшений на стороне `itecho-erp-backend`, отсортированный по эффекту на агентов / клиентов API. Каждый пункт снимает класс проблем, а не отдельный кейс. | Приоритет | Что | Затраты | Эффект | |---|---|---|---| | **1** | **OpenAPI / Swagger через Laravel Scribe** | 1-2 ч установка + аннотации | Снимает ~70% разведочных запросов. Каждый клиент получает машиночитаемый контракт. См. ниже инструкцию. | | **2** | **422 вместо 500 на неполный body** | 1-2 ч фикс валидаторов | Агент перестаёт ходить вслепую. Текущий 500 ≡ «неполное тело» (раздел 19.1) — это маскировка валидации под крэш. | | **3** | **Inline enum-таблицы в Scribe-аннотациях** | 1 ч на ключевые поля | Указывать `@queryParam kind_id integer required Enum: 1=Услуга, 2=Товар`. Scribe пробросит в OpenAPI. | | **4** | **Bulk endpoint'ы** | 2-3 ч на каждый ресурс | `POST /api/nomenclatures/bulk` с `{ items: [...] }`, `DELETE /api/nomenclatures/bulk` с `{ ids: [...] }`. Экономит 10x запросов на массовых операциях. | | **5** | **`?light=1` для list-endpoint'ов** | 1-2 ч | Возвращать только `data + { total, page, per_page, last_page }`, без `links[]` / `first_page_url` / `last_page_url`. Снимет 30-40% веса каждого листинга. | | **6** | **Stable seed для стендов** | 1 ч | Скрипт `php artisan db:seed --class=DemoSeeder` — 10 товаров, 5 клиентов, 3 заказа. Новый разработчик/агент сразу видит работающий стенд. | | **7** | **`is_default` на типы единиц / статусы / счета** | 30 мин | «Штука» для товара, «Смена» для услуги — отметить как `is_default: true`. Агент проставит по умолчанию без выбора. | | **8** | **`meta.hints` на пустые ответы** | 1-2 ч | На `counterpartiesCount: 0` отдавать `meta.hints: ["create a counterparty via POST /api/counterparties"]`. Агент понимает что делать дальше. | | **9** | **`Retry-After` header на 429** | 30 мин | Laravel-throttle уже умеет; нужно включить. Агент будет точно знать сколько ждать. | | **10** | **Webhook'и** | 1-2 недели | Опционально. Уведомления о смене статуса заказа / новом контрагенте. Снимает класс polling-нагрузки. | | **11** | **Версионирование URL `/api/v1/`** | 1 день миграция | Защитит клиентов от breaking changes. Можно ввести при следующем серьёзном изменении схемы. | ### Установка Scribe (приоритет 1) ```bash # В itecho-erp-backend composer require --dev knuckleswtf/scribe php artisan vendor:publish --tag=scribe-config # Минимальная разметка в FormRequest (пример для CounterpartyRequest): # /** # * @bodyParam phone string required Phone in E.164 without "+". Example: 79991234567 # * @bodyParam password string required Password from SMS / call. Example: 1111 # */ php artisan scribe:generate # → public/docs/openapi.yaml ← готовая спека # → public/docs/ ← Swagger UI с интерактивным "попробовать запрос" ``` Дальше можно отдавать `public/docs/openapi.yaml` с фронта и через CDN. ================================================================ ## 21. Auto-generated OpenAPI (на нашей стороне) ================================================================ Параллельно с возможным бэкенд-Scribe, на стороне docs-сайта (`docs.gigma.ru`) есть **авто-генератор** OpenAPI из метаданных компонента `` + JSON-примеров. **URL:** - `https://artypoul-docs-gigma-7b80.twc1.net/openapi.json` — машиночитаемая спека - `https://artypoul-docs-gigma-7b80.twc1.net/api-docs/` — Swagger UI с «Try It» **Что внутри:** - 235 операций под 29 тегами (по разделам sidebar'а) - Метод / URL / auth / headers — из атрибутов `` (100% точность) - Request / response schemas — выведены из JSON-примеров через type inference (~80% точность; `string`, `int`, `decimal-string` (`/^-?\d+\.\d{2,}$/`), `date`, `date-time`, `uri`) - Path parameters (`{id}`, `{slug}`) — извлечены из URL-шаблонов **Чем отличается от бэкенд-Scribe:** - ✅ Не зависит от доступа к бэк-репо - ✅ Обновляется при каждом редактировании доки - ❌ Required/optional флаги выведены приближённо (по присутствию в примере). Точная валидация — только из FormRequest. - ❌ Не покрывает endpoint'ы, ещё не задокументированные ``-компонентом. **Использование агентом:** ```bash # Скачать один раз curl https://artypoul-docs-gigma-7b80.twc1.net/openapi.json -o ~/.gigma/openapi.json # Найти схему конкретного endpoint'а через jq: jq '.paths["/api/login"].post' ~/.gigma/openapi.json # Сгенерировать TS-клиент: npx openapi-typescript-codegen --input ~/.gigma/openapi.json --output ./src/api # Импортировать в Postman: Import → File → openapi.json ``` ================================================================ END OF DOCUMENT ================================================================