Интеграция каталога с 1С (REST JSON API)
Общее описание
Этот API позволяет 1С управлять каталогом наличия на сайте:
- категории;
- товары (карточки товаров, атрибуты, фото);
- остатки (через
product_supplies).
Источник данных — 1С, сайт только принимает изменения и показывает их пользователям.
Все методы расположены по префиксу: /api/1c/* (Laravel автоматически добавляет /api).
Авторизация и безопасность
Включение интеграции и настройки
Настройки находятся в админке:
- Страница:
Настройка сайта→ таб1С интеграция. - Хранение настроек — класс
App\Settings\GeneralSettings.
Доступные опции:
- Включить интеграцию с 1С (
onec_enabled). - Склад для остатков 1С (
onec_storage_id) —storages.id. - Точка выдачи для остатков 1С (
onec_pick_up_point_id) —pick_up_points.id. - Разрешённые IP для запросов 1С (
onec_allowed_ips) — список IP; если пусто, IP не ограничиваются. - API токен 1С (
onec_token) — случайная строка, используемая в Authorization-заголовке.
Токен можно:
- Сгенерировать/перевыпустить кнопкой «Сгенерировать / перевыпустить токен».
- При перевыпуске старый токен становится недействительным.
Как работает проверка на бэкенде
Middleware: App\Http\Middleware\VerifyOnecToken, алиас onec.token.
Проверки:
Интеграция включена
- Если
GeneralSettings->onec_enabled === false:- Ответ:
503 Service Unavailable - Тело:
{ "message": "1C integration is disabled." }
- Ответ:
- Если
Наличие и валидность токена
Берётся значение
GeneralSettings->onec_token.1С должна отправлять заголовок:
httpAuthorization: Bearer {onec_token}Ошибки:
- Нет заголовка или неверный формат:
401 Unauthorized,{ "message": "Missing or invalid Authorization header." }
- Токен не совпадает:
401 Unauthorized,{ "message": "Invalid 1C API token." }
- Нет заголовка или неверный формат:
Проверка IP (опционально)
- Берётся массив
GeneralSettings->onec_allowed_ips. - Если массив не пустой и IP запроса не содержится в списке:
403 Forbidden,{ "message": "Access from this IP address is not allowed for 1C API." }
- Берётся массив
Базовый health-check
GET /api/1c/ping
Проверяет:
- доступность API;
- корректность токена и (опционально) IP.
Пример запроса:
GET /api/1c/ping HTTP/1.1
Host: example.com
Authorization: Bearer {TOKEN_IZ_NASTROEK}Пример ответа:
{
"status": "ok"
}Категории (ProductCategory)
Структура модели
- Таблица:
product_categories - Важные поля:
id— внутренний ID сайта;onec_id— внешний ID категории в 1С;parent_id— связь с родителем;name,description,slug,is_active,sort.- медиа — коллекция
product_categories(Spatie Media Library).
1. Batch upsert категорий
Метод: POST /api/1c/categories/batch-upsert
Назначение: создать/обновить дерево категорий по onec_id.
Поддерживаются два формата тела:
[
{
"id_1c": "cat-1",
"parent_id_1c": null,
"name": "Тормозная система",
"description": "Описание категории",
"slug": "tormoznaya-sistema",
"sort_order": 10,
"is_active": true,
"image_url": "https://files.example.com/cat-1.jpg"
}
]или
{
"categories": [
{
"id_1c": "cat-1",
"parent_id_1c": null,
"name": "Тормозная система"
}
]
}Валидация:
categories—required|array(если тело — объект).categories.*.id_1c—required|string|max:191categories.*.parent_id_1c—nullable|string|max:191categories.*.name—required|string|max:191categories.*.description—nullable|stringcategories.*.slug—nullable|string|max:191categories.*.sort_order—nullable|integercategories.*.is_active—nullable|booleancategories.*.image_url—nullable|string
Поведение:
- По
id_1cищется/создаётся запись вproduct_categories(полеonec_id). - Обновляются:
name,description,is_active(по умолчаниюtrue),sort.slug— если передан, иначе генерируется уникальный slug изname.
- После создания/обновления всех категорий во втором проходе:
- выставляются
parent_idпоparent_id_1c, если родитель найден.
- выставляются
- Если задан
image_url:- старая медиа-коллекция
product_categoriesочищается; - с URL скачивается и прикрепляется новое изображение.
- старая медиа-коллекция
Пример успешного ответа:
{
"processed": 3,
"created": 2,
"updated": 1,
"errors": []
}Пример частичной ошибки:
{
"processed": 3,
"created": 2,
"updated": 1,
"errors": [
{
"id_1c": "cat-3",
"message": "Parent category with id_1c=cat-999 not found."
}
]
}2. Деактивация категорий
Метод: POST /api/1c/categories/delete
Тело:
{
"ids_1c": ["cat-1", "cat-2"]
}Поведение:
- Для всех найденных категорий по
onec_id:is_active = false;- без физического удаления.
Ответ:
{
"requested": 2,
"deactivated": 2,
"not_found": []
}Товары (Product)
Структура модели
- Таблица:
products - Важные поля:
id— внутренний ID;onec_id— внешний ID товара в 1С;name,brand,article,description,slug,is_active,sort;- связи:
categories(pivotproducts_categories);supplies(ProductSupply) — остатки/цены;attributeValues(ProductAttributeValue) — атрибуты.
- медиа — коллекция
products.
1. Batch upsert товаров
Метод: POST /api/1c/products/batch-upsert
Назначение: создать/обновить товары, привязать к категориям, атрибутам и фото.
Тело (формат 1 — массив):
[
{
"id_1c": "prod-1",
"name": "Тормозные колодки",
"brand": "BOSCH",
"article": "ABC-123",
"category_id_1c": "cat-1",
"description_short": "Краткое описание",
"description_full": "Полное описание...",
"slug": "tormoznye-kolodki",
"sort_order": 10,
"is_active": true,
"attributes": [
{ "name": "Цвет", "value": "Черный", "type": "text" },
{ "name": "Страна", "value": "Германия" }
],
"images": [
{ "url": "https://files.example.com/prod-1-1.jpg", "sort_order": 1, "is_main": true },
{ "url": "https://files.example.com/prod-1-2.jpg", "sort_order": 2, "is_main": false }
]
}
]или формат 2 — объект:
{
"products": [
{
"id_1c": "prod-1",
"name": "Тормозные колодки"
}
]
}Валидация:
products—required|array(если тело — объект).products.*.id_1c—required|string|max:191products.*.name—required|string|max:191products.*.brand—nullable|string|max:191products.*.article—nullable|string|max:191products.*.category_id_1c—nullable|string|max:191products.*.description_short—nullable|stringproducts.*.description_full—nullable|stringproducts.*.slug—nullable|string|max:191products.*.sort_order—nullable|integerproducts.*.is_active—nullable|booleanproducts.*.attributes—nullable|arrayattributes.*.name—required_with:attributes|string|max:191attributes.*.value—nullable|stringattributes.*.type—nullable|string|max:50
products.*.images—nullable|arrayimages.*.url—required_with:images|stringimages.*.sort_order—nullable|integerimages.*.is_main—nullable|boolean
Поведение:
- По
id_1cищется/создаётсяProduct(полеonec_id). - Обновляются:
name,brand,article;description:- если есть
description_full— берётся оно; - иначе, если описания нет и есть
description_short— берётся оно;
- если есть
is_active(по умолчаниюtrue);sort— изsort_orderили0.
slug:- если передан — используем;
- иначе — генерируем уникальный slug из
name + article.
- Категория:
- если есть
category_id_1c, ищемProductCategoryпоonec_id; - при успехе —
syncWithoutDetachingсвязи вproducts_categories.
- если есть
Атрибуты
Простой вариант (inline):
1С присылает:
json"attributes": [ { "name": "Цвет", "value": "Черный", "type": "text" }, { "name": "Страна", "value": "Германия" } ]Бэкенд:
- в таблице
attributes(модельAttribute) делаетfirstOrCreateпоname; - в таблице
product_attribute_values(ProductAttributeValue) делаетupdateOrCreateпоproduct_id + attribute_id + type, полеvalue— строкой.
- в таблице
Таким образом, можно использовать уже существующую систему атрибутов и фильтрации.
Фото
- Для каждого товара используется коллекция медиа
products. - При каждом запросе:
- старая коллекция очищается:
clearMediaCollection('products'); - для каждого изображения вызывается
addMediaFromUrl(url)с custom properties:sort_orderis_main
- старая коллекция очищается:
Пример успешного ответа:
{
"processed": 5,
"created": 3,
"updated": 2,
"errors": []
}Пример частичной ошибки (нет категории):
{
"processed": 1,
"created": 0,
"updated": 1,
"errors": [
{
"id_1c": "prod-1",
"message": "Category with id_1c=cat-999 not found."
}
]
}2. Деактивация товаров
Метод: POST /api/1c/products/delete
Тело:
{
"ids_1c": ["prod-1", "prod-2"]
}Поведение:
- Для всех найденных товаров:
is_active = false;- вызывается
$product->delete()— мягкое удаление (SoftDeletes).
Ответ:
{
"requested": 2,
"deactivated": 2,
"not_found": []
}Остатки (ProductSupply)
Структура
- Таблица:
product_supplies - Важные поля:
product_id,pick_up_point_id,storage_id;purchase_price,selling_price,old_price;quantity,reserved,shipped;supply_date,delivery_days.
Batch update остатков
Метод: POST /api/1c/stocks/batch-update
Назначение: выставление актуальных количеств по товарам от 1С.
- Привязка к складу/точке выдачи:
- Используются настройки
onec_storage_idиonec_pick_up_point_idизGeneralSettings.
- Используются настройки
Тело (массив):
[
{
"product_id_1c": "prod-1",
"quantity": 15,
"supply_date": "2026-02-10",
"delivery_days": 2
},
{
"product_id_1c": "prod-2",
"quantity": 0
}
]или объект:
{
"stocks": [
{
"product_id_1c": "prod-1",
"quantity": 15
}
]
}Валидация:
stocks—required|array(если тело — объект).stocks.*.product_id_1c—required|string|max:191stocks.*.quantity—required|integer|min:0stocks.*.supply_date—nullable|datestocks.*.delivery_days—nullable|integer|min:0
Предварительные проверки:
Если
GeneralSettings->onec_enabled === false:json{ "message": "1C integration is disabled." }с кодом
503.Если не настроены
onec_storage_idилиonec_pick_up_point_id:json{ "message": "Default storage and pick-up point for 1C stocks must be configured." }с кодом
500.
Поведение:
- Для каждой записи:
- ищется
Productпоonec_id = product_id_1c; - если не найден — в
errorsдобавляется сообщение и запись пропускается; - по комбинации
product_id + pick_up_point_id + storage_id:- делается
firstOrNewвproduct_supplies; - устанавливается
quantityв указанное значение; supply_dateберётся из запроса или, если пусто, из существующего значения илиnow();delivery_days— из запроса или из старого значения или0;- поля
reservedиshippedне изменяются.
- делается
- ищется
Пример ответа:
{
"processed": 2,
"updated": 2,
"errors": []
}Пример ответа с ошибкой:
{
"processed": 2,
"updated": 1,
"errors": [
{
"product_id_1c": "prod-999",
"message": "Product not found for given 1C ID."
}
]
}Типичный сценарий обмена
Настроить интеграцию в админке
- Включить
1С интеграция. - Выбрать склад и точку выдачи.
- Сгенерировать токен.
- (Опционально) задать список разрешённых IP.
- Включить
Проверить доступность
- 1С вызывает
GET /api/1c/pingс заголовкомAuthorization: Bearer {token}.
- 1С вызывает
Загрузить/обновить категории
POST /api/1c/categories/batch-upsert.
Загрузить/обновить товары
POST /api/1c/products/batch-upsert.
Обновлять остатки
- Периодически вызывать
POST /api/1c/stocks/batch-update.
- Периодически вызывать
Удалять/скрывать неактуальные данные
POST /api/1c/categories/deletePOST /api/1c/products/delete
Быстрые примеры запросов (curl)
Пример: ping
curl -X GET "https://example.com/api/1c/ping" \
-H "Authorization: Bearer YOUR_TOKEN_HERE"Пример: загрузка одной категории
curl -X POST "https://example.com/api/1c/categories/batch-upsert" \
-H "Authorization: Bearer YOUR_TOKEN_HERE" \
-H "Content-Type: application/json" \
-d '[
{
"id_1c": "cat-1",
"name": "Тормозная система",
"parent_id_1c": null,
"is_active": true
}
]'Пример: загрузка одного товара с фото и атрибутами
curl -X POST "https://example.com/api/1c/products/batch-upsert" \
-H "Authorization: Bearer YOUR_TOKEN_HERE" \
-H "Content-Type: application/json" \
-d '[
{
"id_1c": "prod-1",
"name": "Тормозные колодки",
"brand": "BOSCH",
"article": "ABC-123",
"category_id_1c": "cat-1",
"is_active": true,
"attributes": [
{ "name": "Цвет", "value": "Черный" },
{ "name": "Страна", "value": "Германия" }
],
"images": [
{ "url": "https://files.example.com/prod-1-1.jpg", "sort_order": 1, "is_main": true }
]
}
]'Пример: обновление остатков
curl -X POST "https://example.com/api/1c/stocks/batch-update" \
-H "Authorization: Bearer YOUR_TOKEN_HERE" \
-H "Content-Type: application/json" \
-d '[
{ "product_id_1c": "prod-1", "quantity": 10 },
{ "product_id_1c": "prod-2", "quantity": 0 }
]'