# Импорт каталога из подготовленного manifest

`tools/import-catalog.php` заменяет каталог проверенными данными и фотографиями. Excel и ZIP сначала преобразуются в JSON и локальные WebP; сам импортёр не выполняет инструкции из исходных документов.

Проверка без изменений:

```powershell
& 'C:\OSPanel\modules\php\PHP_8.1\php.exe' tools/import-catalog.php 'tmp/catalog-import-YYYYMMDD/manifest.json'
```

Замена каталога после успешной проверки:

```powershell
& 'C:\OSPanel\modules\php\PHP_8.1\php.exe' tools/import-catalog.php 'tmp/catalog-import-YYYYMMDD/manifest.json' --apply
```

Без `--apply` скрипт только читает файлы и БД. Импорт не включает оформление заказов, индексацию или оплату и не изменяет настройки магазина.

## Подготовка данных

`tools/prepare-catalog-import.py` читает один лист с колонками `Имя файла`, `Название букета`, `Описание букета`, `Что таит в себе этот букет`, `Состав`, `Уход за букетом`, `Цена`. Имена фотографий в ячейке разделяются `|`. Исходники открываются только для чтения, формулы и неоднозначные соответствия отклоняются.

Для запуска нужны Python с `openpyxl`, `Pillow` и `pillow-heif`. В этой рабочей среде первые две библиотеки входят в комплект; HEIC-декодер установлен отдельно в `.runtime/catalog-import-python`. Не изменяйте общий каталог зависимостей.

```powershell
$pythonExe = 'C:\Users\arsru\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe'
$env:PYTHONPATH = (Resolve-Path .runtime\catalog-import-python).Path
$env:PYTHONIOENCODING = 'utf-8'
& $pythonExe tools/prepare-catalog-import.py --xlsx 'путь-к-каталогу.xlsx' --photos 'путь-к-фотографиям.zip' --editorial 'путь-к-меткам.json' --output 'tmp/catalog-import-новая-папка'
```

Папка результата должна быть новой. Скрипт сохраняет `manifest.json`, фотографии в `images/` и контрольный лист изображений `contact-sheet.jpg`. Фотографии переводятся в sRGB, корректируется ориентация по EXIF, максимальная сторона ограничивается 2400 пикселями. WebP сохраняется без EXIF и координат GPS.

Файл `--editorial` необязателен. Его поля — `categories` и `products`; ключи `products` соответствуют номерам строк Excel. Для строки можно задать `slug`, `category`, `story_title`, `occasions`, `seo_title`, `seo_description`, `primary_photo`. Главное фото по умолчанию первое; `primary_photo` позволяет выбрать другое имя из той же галереи. Тексты описания, истории, состава и ухода берутся из Excel без редакторских изменений. При отсутствии остатков импортируется 0. При отсутствии отдельных SEO-полей заголовок создаётся из названия букета, описание — из исходного описания.

В импорте от 18 октября 2026 года подготовлены 10 букетов и 19 фотографий. Цена 999,99 BYN во всех строках подтверждена владельцем. У «Розового признания» главным выбран крупный план `IMG_9136.HEIC`; широкий кадр также сохранён в галерее.

Manifest хранится вне каталога сайта либо в закрытом `storage/` или `tmp/`. Рядом должна находиться папка `images/`. Каждый файл фотографии называется `32-символьный-hex.webp` (только строчные `a-f`, цифры).

Корневые поля JSON:

- `schema_version`: целое `1`.
- `source.workbook`: `path`, `sha256`, `sheet`.
- `source.photos`: `path`, `sha256` — исходный ZIP.
- `categories`: непустой массив `{id,name,sort_order}`.
- `products`: непустой массив товаров.

Все исходные файлы должны существовать и соответствовать SHA256. `path` исходников рекомендуется задавать абсолютным; относительные исходники разрешаются от текущей рабочей папки CLI. Пути фотографий разрешаются от папки manifest.

Каждый товар содержит все поля:

```text
source_row, name, slug, category, description, composition, care,
price_minor, stock, active, is_demo, badge, sort_order,
story_title, story_text, seo_title, seo_description, occasions, images
```

Числа — целые JSON-числа. `price_minor` — цена в копейках, от 1 до 10000000. `stock` — фактический остаток от 0 до 100000; при отсутствии сведений указывается 0. `active` — boolean; `is_demo` для настоящего каталога должен быть `false`. Неиспользуемые текстовые поля передаются пустыми строками. `slug` уникален, состоит из строчных латинских букв и цифр с дефисами между словами. Категория должна существовать в массиве `categories`. `source_row` — уникальный номер строки исходной таблицы.

`occasions` — массив уникальных кодов:

```text
mom, anniversary, jubilee, birthday, wedding,
love, gratitude, colleague, no_reason
```

У товара от 1 до 12 изображений; ровно одно имеет `is_primary: true`. Формат записи:

```json
{
  "source_file": "исходное имя файла в ZIP.jpg",
  "path": "images/0123456789abcdef0123456789abcdef.webp",
  "sha256": "64 строчных шестнадцатеричных символа",
  "width": 1200,
  "height": 1600,
  "is_primary": true
}
```

Размер фотографии — до 4 МБ, не более 12 мегапикселей, каждая сторона до 7000 пикселей. Скрипт проверяет SHA256, WebP, размеры и полное декодирование средствами GD. Относительные пути с выходом из папки, неожиданные поля и ошибочные типы отклоняются до удаления данных.

## Что происходит при применении

1. Скрипт получает блокировку от параллельного импорта, начинает транзакцию InnoDB и блокирует текущие строки категорий, товаров и фотографий.
2. В `storage/backups/catalog-<дата>-<случайный-id>/` сохраняются `catalog-before.json`, исходный manifest и копии всех изображений прежнего каталога с проверенными SHA256. Резервная копия содержит только каталог, схему трёх таблиц и числа зависимостей; данные покупателей и секреты туда не записываются.
3. Подготовленные WebP копируются в `public/assets/uploads/`. Существующие файлы разрешены только при совпадающем SHA256.
4. Старые товары, на которые ссылаются позиции заказов, переводятся в архив (`active=0`). Остальные старые товары удаляются; связанные фотографии удаляются внешним ключом. Заказы и их позиции сохраняются. ID не сбрасываются и не используются повторно.
5. Категории обновляются, новый каталог вставляется с галереями. Нужные архивным товарам категории сохраняются. Если новый slug совпадает с архивным товаром из заказа, импорт останавливается — следует выбрать другой новый slug.
6. Перед COMMIT проверяются количество товаров, архив, галереи, главные фотографии и неизменность истории заказов. Приватный `import-report.json` содержит новые ID, исходники, контрольные суммы и путь к резервной копии.

При ошибке до COMMIT транзакция откатывается; удаляются только файлы, созданные этим запуском. Старые изображения после успешного импорта остаются на месте. Если ответ на COMMIT потерян, скрипт сохраняет новые фотографии и сообщает о необходимости сверить БД с подготовленным отчётом.

Повторный запуск с теми же SHA256 Excel/ZIP и неизменившимся каталогом возвращает `already_imported` и ничего не заменяет. Если каталог после предыдущего импорта был изменён, повторное применение выполняет новую замену с новой резервной копией.

## Проверка после импорта

После импорта выполните проверки данных, страниц, галерей и подборок через оба локальных сервера:

```powershell
& 'C:\OSPanel\modules\php\PHP_8.1\php.exe' tests/catalog-import.php 'tmp/catalog-import-20261018-final/manifest.json' http://flowers.local
& 'C:\OSPanel\modules\php\PHP_8.1\php.exe' tests/catalog-import.php 'tmp/catalog-import-20261018-final/manifest.json' http://127.0.0.1:8097
```

Эти проверки выполняют только GET-запросы, сверяют поля API и HTML с manifest и проверяют контрольную сумму каждой фотографии. Они не создают заказы и не меняют каталог.

## Ручное восстановление

`catalog-before.json` содержит полные строки `categories`, `products`, `product_images`, исходную схему и список соответствий файлов с SHA256. `import-report.json` перечисляет добавленные товары и созданные файлы. До восстановления приостановите редактирование каталога и оформление заказов и проверьте, не ссылаются ли новые заказы на импортированные товары.

Восстанавливайте в транзакции с действующими внешними ключами: сначала необходимые категории, затем исходные товары с сохранением ID, затем их галереи. Удаление новых товаров допустимо лишь при отсутствии связанных позиций заказов; связанные товары следует архивировать. Копии изображений лежат в `assets/public/assets/` с исходной структурой путей. Не очищайте заказы, не отключайте внешние ключи и не сбрасывайте счётчики ID.
