docs: document settings framework and telecart-pulse feature

Каркас системы настроек (docs/features/settings) и фича TeleCart Pulse
с heartbeat (docs/features/telecart-pulse); спека синхронизирована с opt-in.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 15:08:05 +03:00
parent d126dc3ef0
commit adcca7cbc4
4 changed files with 192 additions and 19 deletions

View File

@@ -36,8 +36,10 @@ docs/features/
## Реестр фич
| Фича | Путь | Краткое описание |
|------|----------------------------------|-------------------------------------------------------------|
| cart | [cart/CLAUDE.md](cart/CLAUDE.md) | Корзина: наполнение, просмотр, передача в оформление заказа |
| Фича | Путь | Краткое описание |
|----------------|--------------------------------------------------------|--------------------------------------------------------------------------|
| cart | [cart/CLAUDE.md](cart/CLAUDE.md) | Корзина: наполнение, просмотр, передача в оформление заказа |
| settings | [settings/CLAUDE.md](settings/CLAUDE.md) | Каркас настроек: форма в админке → хранение → чтение на backend |
| telecart-pulse | [telecart-pulse/CLAUDE.md](telecart-pulse/CLAUDE.md) | Связка с SaaS: события кампаний и heartbeat (телеметрия окружения) |
<!-- Новые фичи добавляй строкой в таблицу выше, по алфавиту. -->

View File

@@ -0,0 +1,88 @@
# Настройки модуля
## Назначение
Сквозная подсистема настроек модуля: форма в админ-панели, хранение значений и чтение их
на backend. Это **каркас**, в который встраиваются остальные фичи — когда фиче нужен свой
флаг/параметр, она добавляет поле в этот пайплайн, а не строит собственный механизм.
## Точки входа
| action | Метод | Где объявлен |
|--------------------|------------------------------------|---------------------|
| `getSettingsForm` | `SettingsHandler::getSettingsForm` | `bastion/routes.php` |
| `saveSettingsForm` | `SettingsHandler::saveSettingsForm`| `bastion/routes.php` |
| `resetCache` | `SettingsHandler::resetCache` | `bastion/routes.php` |
| `getSystemInfo` | `SettingsHandler::getSystemInfo` | `bastion/routes.php` |
## Ключевые файлы по слоям
- **Дефолты:** `module/oc_telegram_shop/upload/oc_telegram_shop/configs/app.php` — структура
всех настроек с дефолтными значениями (секции `app`, `telegram`, `metrics`, `store`,
`orders`, `texts`, `sliders`, `mainpage_blocks`, `pulse`, `cron`).
- **Handler (админка):** `module/.../bastion/Handlers/SettingsHandler.php`
`getSettingsForm()` отдаёт срез конфига (`Arr::getWithKeys` по списку секций),
`saveSettingsForm()` сохраняет тот же срез.
- **Сохранение:** `module/.../bastion/Services/SettingsService.php``update()` пишет в
OpenCart-таблицу `oc_setting` через `model_setting_setting->editSetting('module_telecart',
['module_telecart_settings' => $data])`, синхронизирует статус модуля `module_tgshop` и
очищает кэш.
- **Чтение (backend):** `Openguru\OpenCartFramework\Config\Settings``get('a.b.c')` /
`getAll()`. Расширение для витрины — `App\Services\SettingsService` (наследует `Settings`)
и DTO в `src/DTO/Settings/` с типизированными геттерами (`is*()`, `get*()`).
- **Сборка конфига:** `module/.../bastion/ApplicationFactory.php` (и точка входа
`admin/controller/extension/module/tgshop.php`) — дефолты из `configs/app.php` мержатся с
сохранёнными `module_telecart_settings` через `Arr::mergeArraysRecursively`, результат
отдаётся в `Application`.
- **Фронтенд (стор):** `frontend/admin/src/stores/settings.js` — Pinia setup-store: дефолтный
state, `fetchSettings()` (`getSettingsForm`), `saveSettings()` (`saveSettingsForm`).
- **Фронтенд (поля):** `frontend/admin/src/components/Settings/``ItemBool` (toggle),
`ItemInput`, `ItemSelect`, `ItemImage`, `ItemToggleButton`. `ItemBool`
`Switcher``ToggleSwitch` (PrimeVue). Используются во `views/*View.vue`.
## Поток данных
Чтение формы:
`getSettingsForm``Settings::getAll()` (уже смерженный конфиг) → срез по секциям →
JSON → стор `fetchSettings()` → поля во `*View.vue`.
Сохранение:
поле (`v-model`) → стор `saveSettings()``saveSettingsForm``SettingsService::update()`
`oc_setting` (`module_telecart_settings`) + сброс кэша.
Чтение на backend:
дефолты `configs/app.php` ⊕ сохранённые значения → `Application``Config\Settings` / DTO
→ сервисы (через DI-провайдеры, которые инжектят `getConfigValue('...')` в конструкторы).
## Как добавить новый toggle (чек-лист)
1. **Дефолт:** добавить ключ в нужную секцию `configs/app.php` (для булева — `true`/`false`).
2. **Backend-потребитель:** прочитать значение там, где оно нужно. Идиома модуля — читать в
ServiceProvider (`$app->getConfigValue('секция.ключ')`) и **инжектить** в конструктор
сервиса (не дёргать конфиг из глубины класса). При наличии DTO — добавить геттер.
3. **Стор:** добавить ключ с дефолтом в соответствующую секцию state в
`frontend/admin/src/stores/settings.js`.
4. **View:** добавить компонент поля (для тумблера — `<ItemBool label="…"
v-model="settings.items.<секция>.<ключ>">…текст-подсказка…</ItemBool>`) в нужный
`*View.vue`.
5. Убедиться, что секция уже перечислена в списках `Arr::getWithKeys` в `getSettingsForm`/
`saveSettingsForm` (если добавляешь поле в новую секцию — добавить и саму секцию в оба
списка).
## Связанные фичи
- [telecart-pulse](../telecart-pulse/CLAUDE.md) — heartbeat использует этот каркас:
флаг `pulse.heartbeat_enabled` проходит весь путь от формы до backend-гейта.
## Нюансы и подводные камни
- **PHP 7.4** в backend-коде (без `match`, промоушена свойств, union-типов и т.п.).
- `getSettingsForm`/`saveSettingsForm` работают строго по **белому списку секций**
(`Arr::getWithKeys`). Поле вне перечисленных секций не отдастся и не сохранится.
- Секция `cron` содержит read-only поля (`cli_path`, `last_run`), которые добавляются при
выдаче и вырезаются перед сохранением — не сохраняй вычисляемые значения.
- `saveSettingsForm` сейчас вызывает пустой `validate()` — серверной валидации значений
настроек по сути нет; не полагайся на неё.
- После сохранения кэш сбрасывается целиком (`SettingsService::update` → `cache->clear()`).
- Существующие установки не имеют сохранённого значения нового ключа — оно берётся из
дефолта `configs/app.php` (мерж). Выбирай дефолт так, чтобы поведение «как было».

View File

@@ -0,0 +1,78 @@
# TeleCart Pulse
## Назначение
Связка модуля с внешним SaaS `pulse.telecart.pro`. Включает две концептуально разные части:
- **События кампаний** (`WEBAPP_OPEN`, `ORDER_CREATED`, …) — аналитика маркетинговых
кампаний и Telegram-рассылок. Завязана на действия продавца: событие сохраняется и
отправляется только если в `start_param` пришли `campaign_id` и `tracking_id`, и только
при заданном `pulse.api_key`.
- **Heartbeat** — раз в час отправляет технические сведения об установке (домен, имя бота,
версии PHP/модуля/OpenCart). Используется для статистики установок и техподдержки.
Управляется **отдельным** переключателем `pulse.heartbeat_enabled` (по умолчанию
**выключен**, opt-in), независимым от `pulse.api_key`.
В коде обе части живут в namespace `Openguru\OpenCartFramework\TeleCartPulse`.
## Точки входа
| action | Метод | Где объявлен |
|-------------|------------------------------|------------------|
| `ingest` | `TelemetryHandler::ingest` | `src/routes.php` |
| `heartbeat` | `TelemetryHandler::heartbeat`| `src/routes.php` |
Триггерятся из SPA (`frontend/spa/src/stores/Pulse.js`, вызов при инициализации WebApp в
`frontend/spa/src/main.js`). Heartbeat кэшируется на 1 час (`telecart_pulse_heartbeat`).
## Ключевые файлы по слоям
- **Handler:** `module/.../src/Handlers/TelemetryHandler.php``ingest()` (события) и
`heartbeat()` (телеметрия окружения); оба делегируют в `TeleCartPulseService`.
- **Сервис:** `module/.../framework/TeleCartPulse/TeleCartPulseService.php`
`handleIngest()` (валидация campaign/tracking, сохранение события в БД) и
`handleHeartbeat()` (сбор окружения, подпись, POST). В начале `handleHeartbeat()`
гейт `if (! $this->heartbeatEnabled) return;`.
- **Отправка событий:** `module/.../framework/TeleCartPulse/TeleCartPulseEventsSender.php`
POST накопленных событий на `/events` с `Authorization: Bearer <api_key>`.
- **Регистрация в DI:** `module/.../framework/TeleCartPulse/TeleCartPulseServiceProvider.php`
— читает `pulse.api_key`, секрет подписи (`pulse.heartbeat_secret` / env
`PULSE_HEARTBEAT_SECRET`) и `pulse.heartbeat_enabled`, инжектит в сервисы.
- **Модель/данные:** `module/.../framework/TeleCartPulse/TeleCartEvent.php`, таблица
`telecart_events` (миграция `database/migrations/20260101000010_create_telecart_events_table.php`).
- **Расписание:** `configs/schedule.php``TeleCartPulseSendEventsTask` каждые 10 минут;
CLI-аналог — `console/Commands/PulseSendEventsCommand.php` (`pulse:send`).
- **Конфиг/дефолты:** `configs/app.php`, секция `pulse` (`api_key`, `heartbeat_enabled`,
`batch_size`, `max_attempts`).
- **Фронтенд (админка):** `frontend/admin/src/views/TeleCartPulseView.vue` — поля настроек,
включая переключатель heartbeat (`ItemBool`) с раскрытием состава отправляемых данных;
дефолты в `frontend/admin/src/stores/settings.js` (секция `pulse`).
## Поток данных
**События:** SPA `ingest``handleIngest()` → проверка `api_key` + наличия
`campaign_id`/`tracking_id` → запись в `telecart_events` (status `pending`) → каждые 10 мин
`TeleCartPulseSendEventsTask``TeleCartPulseEventsSender::sendEvent()` → POST `/events`.
**Heartbeat:** SPA `heartbeat``handleHeartbeat()` → гейт `heartbeat_enabled` → кэш-чек
(1 ч) → `getMe()` → payload окружения → подпись (если есть signer) → POST `/heartbeat`.
## Связанные фичи
- [settings](../settings/CLAUDE.md) — переключатель heartbeat и `api_key` проходят через
общий каркас настроек.
## Нюансы и подводные камни
- **PHP 7.4** в backend-коде.
- **Heartbeat и Pulse — независимы.** `pulse.heartbeat_enabled` НЕ завязан на `api_key`.
Heartbeat реально уходит только при `heartbeat_enabled = true` **и** наличии signer
(секрет настроен). Выключенный тумблер → ранний `return`, даже кэш не ставится.
- **События гейтятся `api_key`:** при пустом `pulse.api_key` `handleIngest()` выходит сразу,
ничего не сохраняется и не отправляется.
- **Прозрачность heartbeat — обязательна.** Состав отправляемых данных раскрыт покупателю в
`TeleCartPulseView.vue`; при изменении полей payload в `handleHeartbeat()` синхронизируй
текст раскрытия.
- Дефолт `heartbeat_enabled = false` (opt-in): без явного включения покупателем heartbeat не
отправляется; существующие установки без сохранённого значения тоже ничего не шлют.
- Тест гейта: `tests/Unit/Framework/TeleCartPulse/TeleCartPulseServiceHeartbeatTest.php`.