# La Fleur & Cafe — backend и управление

## Текущее локальное окружение

- PHP 8.1: `C:\OSPanel\modules\php\PHP_8.1\php.exe` (PDO MySQL, mbstring, fileinfo, curl).
- Используется существующая MariaDB OSPanel на `127.0.0.1:3306`, отдельная БД `flowers_lafleur`. Настройки подключения — в закрытом `config/local.php`.
- Веб-сервер для просмотра: `http://127.0.0.1:8097/`; админка: `http://127.0.0.1:8097/admin.php`.
- Логин и случайный пароль админа записаны в **`config/local-admin.txt`**. Этот файл нельзя публиковать. После смены пароля в админке старый пароль в файле больше не действует.
- Работает демонстрационный каталог из 8 товаров. Тестовые заказы помечены `TEST`. Интеграция WEBPAY подготовлена, но закрыта без реквизитов и публичного HTTPS-домена; транзакции не проверялись с кабинетом продавца.
- Адрес: г. Солигорск, ул. Л. Комсомола 30. УНП `000000000` — заполнитель. Доставка выключена до определения условий. Самовывоз доступен для проверки сценария.

Первоначальный временный экземпляр MariaDB на 3319 **остановлен**, данные перенесены на 3306. `storage/mysql` оставлен как локальный технический архив. Скрипты `start-db.ps1` / `stop-db.ps1` относятся только к временному экземпляру и для обычной работы не нужны. Не запускайте второй экземпляр для этой конфигурации.

## Установка с нуля

Настройте MySQL/MariaDB и PHP с расширениями `pdo_mysql`, `mbstring`, `fileinfo`. Создайте `config/local.php` с массивом `db_host`, `db_port`, `db_name`, `db_user`, `db_password` для отдельной базы. Пользователь БД должен иметь права только на эту базу. Не используйте локальную учётную запись root на публичном хостинге.

CLI-установщик:

```powershell
& 'C:\OSPanel\modules\php\PHP_8.1\php.exe' .\tools\install.php
```

Если `local.php` отсутствует, установщик подключается по параметрам `config/app.php` / переменным `LAFLEUR_DB_HOST`, `LAFLEUR_DB_PORT`, `LAFLEUR_DB_NAME`, а учётные данные установщика берёт из `LAFLEUR_INSTALL_DB_USER` (по умолчанию root) и `LAFLEUR_INSTALL_DB_PASSWORD`. Он создаёт отдельную БД и учётную запись `lafleur`. Не используйте этот путь, если одноимённая учётная запись нужна другому проекту; в таком случае подготовьте `local.php` вручную.

Имя админа можно задать через `LAFLEUR_ADMIN_USER`, пароль — `LAFLEUR_ADMIN_PASSWORD` (от 12 символов). Без переменной генерируется случайный 24-символьный пароль. Повторная установка не перезаписывает существующего админа, настройки и seed-товары.

Папки `app`, `config`, `database`, `storage`, `tests`, `tools` должны быть закрыты от HTTP. Настройка корневого веб-маршрутизатора и `.htaccess` обязательна. Пароли не входят в публичный API. Для публичного размещения нужны HTTPS и отдельная настройка резервных копий, доступа к персональным данным и регламент их удаления.

## Админка

- **Обзор:** счётчики, незаполненные реквизиты, журнал изменений.
- **Каталог:** добавление и изменение товаров, состав, уход, цена в BYN, остаток, категория, сортировка, локальное изображение либо загрузка JPEG/PNG/WebP до 4 МБ. Загруженное фото проверяется, ограничивается 12 мегапикселями и пересохраняется в WebP; админка показывает предпросмотр. Убирайте товар в архив вместо удаления истории продаж. Если остаток изменился после открытия формы, сохранение остановится с просьбой обновить страницу.
- **Категории:** добавление, название и порядок. Постоянный код связывает категорию с товарами.
- **Заказы:** поиск по номеру, имени и телефону, фильтр, детали, снимок условий на дату заказа, история статусов. Название, описание, состав и цена сохраняются отдельно от каталога; последующее редактирование товара не изменяет принятый заказ.
- **Настройки:** контакты, адрес, доставка/самовывоз, реквизиты, условия, политика обработки данных, режим торговли.
- **Доступ:** смена пароля с проверкой текущего.

Допустимые переходы: `new → confirmed → preparing → ready → completed`. Отмена возможна из любого незавершённого статуса и возвращает остаток ровно один раз. Отменённый или выполненный заказ не переоткрывается.

## Защита от преждевременной торговли

`demo_mode=true` допускает только явно подтверждённые тестовые заказы (`demo_acknowledged=true`). Рабочий режим требует одновременно: `demo_mode=false`, `checkout_enabled=true`, заполненных обязательных реквизитов, УНП из 9 цифр не из нулей и `legal_reviewed=true`. Значение `legal_ready` вычисляется на сервере. Нельзя сделать магазин юридически готовым, просто передав `legal_ready=true` в запросе.

Демонстрационные товары (`is_demo=true`) недоступны в рабочем режиме, а реальные товары (`is_demo=false`) недоступны в тестовом заказе: демонстрация не расходует реальный остаток. Перед открытием нужно заменить демонстрационные сведения и фото реальными, проверить цены/состав/остатки и снять отметку демонстрационного товара. Проверка заполнения полей не является юридическим заключением: требуется проверка законодательства и документов под конкретного продавца.

Обработка данных для исполнения заказа не подменяется обязательным согласием на рекламу. В форме есть ознакомление с условиями; подписка и отправка маркетинговых сообщений отсутствуют.

## WEBPAY

Подключена стандартная интеграция через размещаемую сервером HTML-форму `*scart` и защищённую страницу WEBPAY. В форму входят сумма и позиции из снимка заказа, BYN, номер заказа, серверная подпись SHA1 и адреса возврата/уведомления. Возврат покупателя сам по себе не подтверждает оплату. Подписанное уведомление сверяется с ответом `get_transaction` WEBPAY; проверяются номер, сумма, валюта и успешный тип транзакции. Только после проверки `payment_status` меняется на `paid`, а админка позволяет подтвердить заказ.

В `Настройки магазина → WEBPAY` владелец может сохранить режим Sandbox/рабочий, идентификатор магазина, ключ подписи, логин и пароль кабинета, публичный HTTPS-домен. Реквизиты хранятся в игнорируемом Git файле `config/webpay.local.php`, закрытом от HTTP; секреты не попадают в bootstrap API и журнал аудита. Пустые поля пароля и ключа сохраняют действующее значение; отдельный флажок очищает их. Сейчас файл не создан и интеграция выключена. Для Sandbox нужны тестовые реквизиты и адрес сайта, доступный извне по HTTPS; рабочая среда дополнительно требует открытого реального оформления и подтверждённой юридической готовности.

Публичный API возвращает только `settings.webpay.available`, `enabled`, `mode` и `configured`. Для оплаты в запрос заказа можно добавить `payment_method: "webpay"`; без поля остаётся обычный способ оплаты. Sandbox доступен только для тестовых заказов в демонстрационном режиме. Ответ содержит короткую ссылку `/pay.php?t=...`; токен хранится в БД только в виде SHA-256. Уведомление принимается по `POST /webpay-notify.php`; страницы возврата показывают только серверный статус. Заказ резервирует товар; отмена неоплаченного заказа блокируется на 20 минут после запуска платёжной сессии.

Перед запуском WEBPAY нужно завершить тестовые оплаты и уведомления на публичном HTTPS-стенде, затем отдельно согласовать рабочее подключение. Официальные материалы: [интеграция для разработчиков WEBPAY](https://webpay.by/dev/), [карточная форма и поля](https://docs.webpay.by/paymentIntegration/cardIntegration/paymentFormFields/), [подпись заказа](https://docs.webpay.by/paymentIntegration/cardIntegration/orderSignature/), [уведомления](https://docs.webpay.by/paymentIntegration/cardIntegration/paymentNotification/), [проверка платежа](https://docs.webpay.by/paymentIntegration/cardIntegration/paymentVerification/).

## SEO и параметры доставки по городам

В `/admin.php?page=seo` задаются публичный HTTPS-домен, метаданные главной и список городов. Для каждого города сохраняются `active`, `delivery_enabled`, `published`, `delivery_fee_minor`, `free_delivery_threshold_minor`, `minimum_order_minor`, `payment_methods`, а также отдельные SEO-заголовок, описание, вступление и основной текст. В админской форме денежные поля вводятся в BYN с копейками; в настройках и заказах хранятся целыми числами копеек. `delivery_enabled` в общих настройках — общий выключатель, городская настройка дополнительно включает/выключает доставку в конкретном городе.

Оформление передаёт `delivery_city` (slug). Сервер заново проверяет, что город активен и принимает доставку, выбранный способ оплаты разрешён в его `payment_methods`, сумма товаров не меньше `minimum_order_minor`, затем применяет `delivery_fee_minor` или нулевую стоимость при достижении положительного `free_delivery_threshold_minor`. Сумма доставки не входит в минимальную сумму товаров и порог бесплатной доставки. `expected_total_minor` остаётся лишь проверкой согласия с рассчитанным сервером итогом. Город назначения и применённая доставка сохраняются в заказе.

Для самовывоза применяются `pickup_payment_methods`. Поддерживаемые значения: `cash`, `card_on_delivery`, `webpay`; WEBPAY скрывается и отклоняется сервером, пока платёжный кабинет не настроен и выбранный режим недоступен. `manual` сохранён только для старых тестовых клиентов и не принимается в рабочем режиме.

Маршруты: `/robots.txt`, `/sitemap.xml`, `/product/<slug>`, `/occasion/<slug>`, `/delivery/<city-slug>`. Пока общий критерий готовности не пройден, страницы содержат `noindex,nofollow`, а карта сайта не перечисляет страницы. Для индексации города нужны также активная доставка в этом городе, `published=true`, индивидуальные title/description и минимум 250 знаков основного текста; страницы с в точности одинаковым основным текстом не допускаются в индекс. Минимальная длина не гарантирует SEO-качество — текст должен быть полезен и правдив о фактической услуге.

После публичного развёртывания настройте домен и HTTPS, проверьте canonical/robots/sitemap с обоими вебмастерами и отправьте sitemap в Google Search Console и Яндекс Вебмастер. Это ручной шаг после получения домена и доступов.

## API

`GET /api.php?action=bootstrap` возвращает:

```json
{"products":[],"categories":[],"settings":{"webpay":{"available":false,"enabled":false,"mode":"sandbox","configured":false}},"csrf_token":"...","demo_mode":true}
```

Товар содержит `id`, `name`, `slug`, `category`, `description`, `composition`, `care`, `story_title`, `story_text`, `seo_title`, `seo_description`, `occasions`, `price_minor`, `image`, `images`, `stock`, `badge`, `is_demo`; `image` — главное фото, `images` — галерея объектов с `id`, `image_path`, `sort_order` и `is_primary`, `occasions` — массив кодов подборок. Каталог содержит только активные товары. Полная серверная карточка доступна по `/product/<slug>`, страницы подборок — по `/occasion/<slug>`; модальное окно остаётся на главной. В админке можно загрузить до 12 снимков на товар, выбрать главное фото, задать историю и SEO-тексты, отметить поводы и удалить ненужные снимки; загрузки проверяются и пересохраняются в WebP. Цена — целое число копеек BYN. Настройки включают контакты, реквизиты, условия, разрешённые способы получения, `checkout_enabled`, вычисленное `legal_ready` и `legal_missing`. Индексация включается при валидном публичном HTTPS-домене, выключенном деморежиме, хотя бы одном активном реальном товаре и заполненных/проверенных сведениях продавца. Страница доставки дополнительно требует включённой доставки в городе и явной публикации страницы.

`POST /api.php?action=order`, JSON с CSRF-токеном из bootstrap (в `csrf_token` или заголовке `X-CSRF-Token`) и той же cookie сессии:

```json
{
  "csrf_token":"...",
  "items":[{"product_id":1,"quantity":1}],
  "expected_total_minor":14500,
  "payment_method":"manual",
  "customer_name":"Имя покупателя",
  "phone":"+375 29 000-00-00",
  "email":"",
  "recipient_name":"",
  "recipient_phone":"",
  "fulfillment":"pickup",
  "address":"",
  "date":"2026-10-13",
  "slot":"По согласованию",
  "card_message":"",
  "comment":"",
  "terms_accepted":true,
  "demo_acknowledged":true,
  "idempotency_key":"new-random-unique-key"
}
```

`product_id` и `quantity` — JSON integers; количество от 1 до 50 на товар, до 50 строк. Обязательное целое `expected_total_minor` — итог, который видел покупатель, вместе с доставкой. Сервер независимо пересчитывает сумму и при несовпадении возвращает 409 без заказа и списания остатка: клиент должен загрузить актуальную корзину и получить новое подтверждение. Дата — от текущей даты Минска до +60 дней. Интервалы: `10:00–13:00`, `13:00–16:00`, `16:00–19:00`, `По согласованию`. Дата в примере должна заменяться актуальной. Ключ идемпотентности: 16–100 латинских букв/цифр/`_`/`-`, один ключ на одну попытку заказа, при сетевом повторе используйте тот же ключ и тело.

Ответ 201 для обычного заказа: `{"order":{"number":"TEST-...","total_minor":14500,"delivery_minor":0,"is_demo":true,"payment_method":"manual","payment_status":"not_required"}}`. При настроенном WEBPAY и выборе `payment_method:"webpay"` ответ содержит `payment_status:"pending"` и `payment_url`; ключ и параметры кабинета браузеру не передаются.

Ошибка: `{"error":"Сообщение","fields":{"phone":"Проверьте номер"}}`, статусы 403 (сессия/CSRF), 422 (поля/недоступный способ получения), 409 (остаток или конфликт повторного запроса), 429 (лимит), 503 (торговля закрыта/недоступная БД). Другие методы — 405, превышение 32 КБ — 413.

Цена, остаток и доступность проверяются на сервере внутри транзакции с блокировкой строк. Идемпотентность привязана к сессии и нормализованному телу запроса, дополнительно защищена уникальным индексом и блокировкой `GET_LOCK`. Клиентские цены игнорируются. Сессии HttpOnly/SameSite=Lax; Secure при HTTPS; ID обновляется при входе. Все операции админа и заказ защищены CSRF. Вход: до 10 попыток на IP за 15 минут, заказы: до 30 попыток на IP за час. Сессия админа истекает после двух часов бездействия.

## Проверки

```powershell
& 'C:\OSPanel\modules\php\PHP_8.1\php.exe' .\tests\backend.php
& 'C:\OSPanel\modules\php\PHP_8.1\php.exe' .\tests\backend-http.php
```

`backend.php` проверяет реальную MariaDB: расчёт, остатки, повтор заказа, конфликт тела/сессии, отмену, юридический шлюз, недоступную доставку, валидацию и CSRF. Тест временно меняет настройки, восстанавливает их и удаляет свои товары/заказы. Запускайте только на локальной тестовой базе без параллельного оформления заказов.

Последний прогон на MariaDB 3306: **26 backend-проверок + 23 HTTP-проверки успешно**. HTTP-проверки включают загрузку нескольких фотографий товара, выбор главного снимка, удаление неиспользуемого файла, сохранение истории и поводов товара, выдачу SEO-полей в API, страницы товара и подборки, а также закрытие демокаталога от индексации. Дополнительно проверяются защита реальных остатков от тестовых заказов, итоговая цена и неизменность снимка состава/описания. Обновление существующей схемы выполняется идемпотентно командой `php tools/migrate-schema.php`; установщик также вызывает миграцию автоматически.

`backend-http.php` требует работающий локальный веб-сервер и первоначальный действующий пароль в `config/local-admin.txt`. Проверяет вход, создание/изменение/архив товара, API-заказ, повтор и отмену через админку; удаляет свои товар и заказ. Изменения теста остаются в журнале аудита. После смены пароля первоначальный HTTP-тест входа нужно адаптировать к выделенной тестовой учётной записи.

## Граница готовности

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