---
name: 1c-exchange
description: Обмен с 1С — приём каталога POST /api/catalog/1c-import под неизменную обработку заказчика, туннель Cloudflare с прибора, проверка через Postman и из 1С, мок рабочих данных на время теста. Использовать при правке import_1c.py, схем Import1C*, туннеля и при вопросах «1С не видит весы».
---

# Обмен с 1С

## Контракт диктует обработка

`integrations/1c/original-Libra.bsl` — внешняя обработка 1С 8.2, уже работает у
заказчика и **не правится без явной просьбы владельца**. Наш приёмник
`backend/app/api/import_1c.py` подстроен под неё, а не наоборот:

| Обработка шлёт | Приёмник |
| --- | --- |
| заголовок `X-API-Key` | основной способ; `Authorization: Bearer` оставлен для curl. Пробелы по краям токена срезаются — его копируют руками |
| `article` (числовой артикул) | это и есть `plu`: в схеме `validation_alias=AliasChoices("plu","article")`, поле необязательное |
| позиции без артикула / длиннее 5 цифр | не 422 на весь пакет, а пропуск с записью в `errors[]` (`plu: 0`, `name`) — 1С печатает их в своём логе |
| `GET /health` («Проверить связь») | явный маршрут в `main.py` вместе с `/healthz`; без него SPA-заглушка отвечала 200 при любом состоянии |
| `in_stock` всегда `true` | отбор «только в наличии» делает 1С; чего нет в пакете — гасится (`active=0`) |
| `replace_images` | снимок существующему товару кладётся только с ним; новым — всегда |

На время записи киоск закрыт экраном «Оновлення»: бэкенд шлёт `live.notify`
«catalog_busy» до записи и «catalog» после — в `finally`, поэтому сорвавшаяся
выгрузка не оставит прибор за экраном ожидания (плюс минутная страховка в киоске).
Без этого покупатель успевал нажать карточку посреди выгрузки и получить этикетку
со старой ценой. После снятия экрана киоск возвращается к началу каталога, но не
тогда, когда взвешенное ещё не забрали.

Полная синхронизация опасна при сбое выгрузки: пустой пакет и пакет без единого
пригодного артикула отклоняются (400) до того, как что-то погаснет. После приёма —
`live.notify("catalog")`. Поля настройки `SMK_HTTP_Настройки` и формат тела —
`integrations/1c/README.md`; менять контракт можно только с обеих сторон разом.

## Как 1С достаёт до прибора

Сервер 1С доступен только по RDP, приборы — за NAT провайдера без белого IP
(проверено: WAN роутера — приватный `192.168.12.x`, проброс снаружи не отвечает).
Поэтому прибор сам держит исходящий туннель Cloudflare (сервис `tunnel` в
`deploy/docker/compose.yml`, скилл `deployment`), а 1С ходит на имя
`vesy-N.<домен>`, `SSL = Истина`. Tailscale отпал: на сервер 1С клиента ставить
нельзя. Если 8.2 не договорится по TLS — `SSL = Ложь` через порт 80 работает
(Cloudflare принимает HTTP), но у домена должно быть выключено «Always Use HTTPS».

## Постоянный адрес

Домен `smk-retail.com` в аккаунте Cloudflare владельца. Туннель `vesy-dev`
(ID `ef35cc5d-…`) с маршрутом `vesy-dev.smk-retail.com` → `http://127.0.0.1:8000` —
это машина разработчика: `cloudflared tunnel run --token <из backend/.env,
CLOUDFLARE_TUNNEL_TOKEN>`. Токен надстройка Claude в Chrome не отдаёт (и правильно) —
его копирует владелец руками. Снаружи проверено: `/health` 200 из Кипра и Германии,
`/admin` 401 без пароля и 200 с ним, приём 1С отвечает своим 401/400. HTTP без TLS
через туннель тоже проходит — запасной путь для 1С 8.2 без OpenSSL.

## Проверка на машине разработчика

1. Бэкенд слушает `0.0.0.0` (`tools/dev.py` так и запускает); в `backend/.env`
   задан `S2L_IMPORT_TOKEN`.
2. Быстрый туннель без домена: `cloudflared tunnel --no-autoupdate --url http://127.0.0.1:8000`
   печатает имя `*.trycloudflare.com`; живёт, пока идёт команда, меняется при
   каждом старте. Убедиться снаружи: `curl https://<имя>/health` и, если есть
   сомнения в сети, check-host.net (TCP/DNS с чужих узлов).
3. Postman: вкладка **Authorization → No Auth**, заголовок `X-API-Key` во вкладке
   Headers. Первая ловушка теста: в Authorization стоял Bearer с другим значением —
   401, хотя curl с тем же токеном проходил.
4. Пакет — ровно как у обработки (`article`, не `plu`), плюс позиция без артикула
   и с шестизначным — проверить `errors[]`. Отрицательные: без ключа 401, пустой
   пакет 400.
5. Из 1С: сначала «Проверить связь» (то же поле `Сервер`, без тела), потом
   «Заполнить» → «ВыгрузитьВВесы». `Couldn't resolve host name` — в поле `Сервер`
   лишний `https://`, путь или пробел, либо DNS на RDP-сервере не отдаёт
   `trycloudflare.com` (`nslookup` там же). Именованный туннель со своим доменом
   такие фильтры не трогают.

Результат первого прогона 2026-09-15: 415 позиций, 290 со снимками, 4 группы —
киоск перерисовался сам.

## Мок рабочих данных

Выгрузка гасит всё, чего нет в пакете, поэтому демо-каталог на время теста
уезжает в `backend/data/mock/` (гитигнор):

    cd backend && .venv/Scripts/python tools/mock_data.py stash     # база в мок
    cd backend && .venv/Scripts/python tools/mock_data.py restore   # вернуть

Снимки мок не трогает: присланные из 1С лежат в `backend/data/photos/` и
перекрывают демо-набор из сборки по коду товара (скилл `product-photos`).

Оба — при остановленном бэкенде: SQLite занят, на Windows файл не переименовать.
`S2L_SEED_DEMO=0` в `backend/.env`, иначе пустая база на старте снова засеется
демо. Ловушка Windows: после убийства родителя `uvicorn --reload` дочерние
воркеры (`--multiprocessing-fork` в командной строке) живут дальше и держат базу —
добивать их отдельно.

Проверять приём одной позицией через curl нельзя: это полная синхронизация, и
такой запрос погасил 415 боевых товаров — восстанавливать пришлось повторной
отправкой всего каталога из базы. Тестовую позицию слать только внутри полного
пакета или на пустой базе.
