Compare commits
3 Commits
docs/featu
...
feat/hones
| Author | SHA1 | Date | |
|---|---|---|---|
| adcca7cbc4 | |||
| d126dc3ef0 | |||
| 0198c8fcf2 |
@@ -37,7 +37,9 @@ 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 (телеметрия окружения) |
|
||||||
|
|
||||||
<!-- Новые фичи добавляй строкой в таблицу выше, по алфавиту. -->
|
<!-- Новые фичи добавляй строкой в таблицу выше, по алфавиту. -->
|
||||||
|
|||||||
88
docs/features/settings/CLAUDE.md
Normal file
88
docs/features/settings/CLAUDE.md
Normal 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` (мерж). Выбирай дефолт так, чтобы поведение «как было».
|
||||||
78
docs/features/telecart-pulse/CLAUDE.md
Normal file
78
docs/features/telecart-pulse/CLAUDE.md
Normal 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`.
|
||||||
@@ -16,9 +16,11 @@
|
|||||||
|
|
||||||
- **Прозрачность:** в админ-панели покупатель видит, что именно собирается, куда
|
- **Прозрачность:** в админ-панели покупатель видит, что именно собирается, куда
|
||||||
отправляется и зачем.
|
отправляется и зачем.
|
||||||
- **Контроль:** отдельный переключатель, которым покупатель может выключить heartbeat.
|
- **Контроль:** отдельный переключатель, которым покупатель управляет heartbeat.
|
||||||
- **По умолчанию включено** (текущее поведение сохраняется, но становится видимым и
|
- **По умолчанию выключено (opt-in).** Без явного включения покупателем на сервер ничего не
|
||||||
управляемым).
|
уходит — в том числе у существующих установок. (Изначально планировался opt-out с дефолтом
|
||||||
|
«включено»; решение пересмотрено в пользу opt-in из-за юридических рисков сбора
|
||||||
|
идентифицирующих данных — домена и имени бота — без согласия.)
|
||||||
|
|
||||||
## Границы (scope)
|
## Границы (scope)
|
||||||
|
|
||||||
@@ -62,12 +64,12 @@
|
|||||||
### 1. Backend
|
### 1. Backend
|
||||||
|
|
||||||
**`module/.../configs/app.php`** — секция `pulse` (строки 86–90): добавить флаг с дефолтом
|
**`module/.../configs/app.php`** — секция `pulse` (строки 86–90): добавить флаг с дефолтом
|
||||||
`true`.
|
`false` (opt-in).
|
||||||
|
|
||||||
```php
|
```php
|
||||||
'pulse' => [
|
'pulse' => [
|
||||||
'api_key' => '',
|
'api_key' => '',
|
||||||
'heartbeat_enabled' => true, // новый флаг
|
'heartbeat_enabled' => false, // новый флаг, opt-in
|
||||||
'batch_size' => 50,
|
'batch_size' => 50,
|
||||||
'max_attempts' => 3,
|
'max_attempts' => 3,
|
||||||
],
|
],
|
||||||
@@ -106,25 +108,29 @@ public function handleHeartbeat(): void
|
|||||||
|
|
||||||
- перечислить, что отправляется: домен, имя бота, версия PHP, версия модуля, версии
|
- перечислить, что отправляется: домен, имя бота, версия PHP, версия модуля, версии
|
||||||
OpenCart;
|
OpenCart;
|
||||||
- указать адрес назначения: `pulse.telecart.pro`;
|
- получателя назвать обобщённо — «разработчику» (без адреса сервера и периодичности: они не
|
||||||
|
важны покупателю);
|
||||||
|
- явно указать, что персональные данные покупателей и заказы **не** передаются;
|
||||||
- описать цель мягко: для улучшения продукта и техподдержки (без акцента на проверке
|
- описать цель мягко: для улучшения продукта и техподдержки (без акцента на проверке
|
||||||
лицензии).
|
лицензии);
|
||||||
|
- отметить, что по умолчанию отправка отключена и её можно включить/отключить в любой момент.
|
||||||
|
|
||||||
Точную формулировку владелец отредактирует; черновик в нейтральном тоне готовит реализация.
|
Итоговая формулировка согласована с владельцем (нейтральный тон, вариант с оговоркой про
|
||||||
|
отсутствие персональных данных).
|
||||||
|
|
||||||
### 3. Поведение для существующих установок
|
### 3. Поведение для существующих установок
|
||||||
|
|
||||||
Дефолт `true` в конфиге мержится с сохранёнными настройками
|
Дефолт `false` в конфиге мержится с сохранёнными настройками
|
||||||
(`ApplicationFactory`: `Arr::mergeArraysRecursively`). У установок без сохранённого значения
|
(`ApplicationFactory`: `Arr::mergeArraysRecursively`). У установок без сохранённого значения
|
||||||
heartbeat остаётся включённым — поведение не меняется, но теперь переключатель виден.
|
heartbeat **выключен** — на сервер ничего не уходит, пока покупатель явно не включит тумблер.
|
||||||
Когда покупатель выключает тумблер и сохраняет, значение пишется в `oc_setting`
|
Когда покупатель сохраняет настройку, значение пишется в `oc_setting`
|
||||||
(`module_telecart_settings`) и читается на backend.
|
(`module_telecart_settings`) и читается на backend.
|
||||||
|
|
||||||
### 4. Гейтинг — итоговая логика
|
### 4. Гейтинг — итоговая логика
|
||||||
|
|
||||||
heartbeat реально отправляется только если **одновременно**: `heartbeat_enabled = true`
|
heartbeat реально отправляется только если **одновременно**: `heartbeat_enabled = true`
|
||||||
**и** есть signer (секрет настроен). Переключатель — пользовательский контроль поверх
|
(покупатель включил) **и** есть signer (секрет настроен). Переключатель — пользовательский
|
||||||
существующего серверного гейта; они независимы.
|
контроль поверх существующего серверного гейта; они независимы.
|
||||||
|
|
||||||
## Тестирование
|
## Тестирование
|
||||||
|
|
||||||
@@ -161,8 +167,7 @@ Unit-тест на `TeleCartPulseService::handleHeartbeat()`:
|
|||||||
|
|
||||||
## Явно вне scope (YAGNI)
|
## Явно вне scope (YAGNI)
|
||||||
|
|
||||||
- Не делаем opt-in (по умолчанию остаётся включено).
|
- Делаем opt-in (по умолчанию выключено); отдельный consent-баннер на первой установке не
|
||||||
- Не делаем отдельный consent-баннер на первой установке (достаточно видимого
|
нужен — достаточно выключенного по умолчанию переключателя с раскрытием.
|
||||||
переключателя с раскрытием).
|
|
||||||
- Не трогаем события кампаний и рассылки Pulse.
|
- Не трогаем события кампаний и рассылки Pulse.
|
||||||
- Не выносим heartbeat в отдельный namespace/фичу из `TeleCartPulse`.
|
- Не выносим heartbeat в отдельный namespace/фичу из `TeleCartPulse`.
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ export const useSettingsStore = defineStore('settings', {
|
|||||||
|
|
||||||
pulse: {
|
pulse: {
|
||||||
api_key: '',
|
api_key: '',
|
||||||
|
heartbeat_enabled: false,
|
||||||
batch_size: 50,
|
batch_size: 50,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -96,6 +96,16 @@
|
|||||||
При малом значении нагрузка ниже, но обработка занимает больше времени.
|
При малом значении нагрузка ниже, но обработка занимает больше времени.
|
||||||
Рекомендуемое значение: 50.
|
Рекомендуемое значение: 50.
|
||||||
</ItemInput>
|
</ItemInput>
|
||||||
|
|
||||||
|
<ItemBool label="Отправка технических данных разработчику"
|
||||||
|
v-model="settings.items.pulse.heartbeat_enabled"
|
||||||
|
>
|
||||||
|
Чтобы улучшать продукт и быстрее помогать в поддержке, модуль отправляет разработчику
|
||||||
|
минимальный набор технических данных: домен магазина, имя бота и версии PHP, модуля и
|
||||||
|
OpenCart. <strong>Персональные данные покупателей и информация о заказах не
|
||||||
|
передаются.</strong> По умолчанию отправка отключена; включить или отключить её можно
|
||||||
|
в любой момент — на работу магазина это не влияет.
|
||||||
|
</ItemBool>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -103,6 +113,7 @@
|
|||||||
import {onMounted, ref} from "vue";
|
import {onMounted, ref} from "vue";
|
||||||
import {useSettingsStore} from "@/stores/settings.js";
|
import {useSettingsStore} from "@/stores/settings.js";
|
||||||
import ItemInput from "@/components/Settings/ItemInput.vue";
|
import ItemInput from "@/components/Settings/ItemInput.vue";
|
||||||
|
import ItemBool from "@/components/Settings/ItemBool.vue";
|
||||||
import {apiGet} from "@/utils/http.js";
|
import {apiGet} from "@/utils/http.js";
|
||||||
import SettingsItem from "@/components/SettingsItem.vue";
|
import SettingsItem from "@/components/SettingsItem.vue";
|
||||||
|
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ HTML,
|
|||||||
|
|
||||||
'pulse' => [
|
'pulse' => [
|
||||||
'api_key' => '',
|
'api_key' => '',
|
||||||
|
'heartbeat_enabled' => false,
|
||||||
'batch_size' => 50,
|
'batch_size' => 50,
|
||||||
'max_attempts' => 3,
|
'max_attempts' => 3,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class TeleCartPulseService
|
|||||||
private TeleCartEvent $eventModel;
|
private TeleCartEvent $eventModel;
|
||||||
private ?string $apiKey;
|
private ?string $apiKey;
|
||||||
private ?PayloadSigner $heartbeatPayloadSigner;
|
private ?PayloadSigner $heartbeatPayloadSigner;
|
||||||
|
private bool $heartbeatEnabled;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
TelegramInitDataDecoder $initDataDecoder,
|
TelegramInitDataDecoder $initDataDecoder,
|
||||||
@@ -30,7 +31,8 @@ class TeleCartPulseService
|
|||||||
LoggerInterface $logger,
|
LoggerInterface $logger,
|
||||||
TeleCartEvent $eventModel,
|
TeleCartEvent $eventModel,
|
||||||
?string $apiKey = null,
|
?string $apiKey = null,
|
||||||
?PayloadSigner $heartbeatPayloadSigner = null
|
?PayloadSigner $heartbeatPayloadSigner = null,
|
||||||
|
bool $heartbeatEnabled = false
|
||||||
) {
|
) {
|
||||||
$this->initDataDecoder = $initDataDecoder;
|
$this->initDataDecoder = $initDataDecoder;
|
||||||
$this->telegramService = $telegramService;
|
$this->telegramService = $telegramService;
|
||||||
@@ -39,6 +41,7 @@ class TeleCartPulseService
|
|||||||
$this->eventModel = $eventModel;
|
$this->eventModel = $eventModel;
|
||||||
$this->apiKey = $apiKey;
|
$this->apiKey = $apiKey;
|
||||||
$this->heartbeatPayloadSigner = $heartbeatPayloadSigner;
|
$this->heartbeatPayloadSigner = $heartbeatPayloadSigner;
|
||||||
|
$this->heartbeatEnabled = $heartbeatEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -132,6 +135,10 @@ class TeleCartPulseService
|
|||||||
*/
|
*/
|
||||||
public function handleHeartbeat(): void
|
public function handleHeartbeat(): void
|
||||||
{
|
{
|
||||||
|
if (! $this->heartbeatEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if ($this->cache->get('telecart_pulse_heartbeat')) {
|
if ($this->cache->get('telecart_pulse_heartbeat')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -193,6 +200,10 @@ class TeleCartPulseService
|
|||||||
*/
|
*/
|
||||||
private function pushHeartbeat(array $json): void
|
private function pushHeartbeat(array $json): void
|
||||||
{
|
{
|
||||||
|
$this->logger->debug('Pushing heartbeat', [
|
||||||
|
'data' => $json,
|
||||||
|
]);
|
||||||
|
|
||||||
$baseUri = rtrim(env('PULSE_API_HOST', 'http://localhost'), '/') . '/';
|
$baseUri = rtrim(env('PULSE_API_HOST', 'http://localhost'), '/') . '/';
|
||||||
|
|
||||||
$client = new Client([
|
$client = new Client([
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class TeleCartPulseServiceProvider extends ServiceProvider
|
|||||||
$this->container->singleton(TeleCartPulseService::class, function (Container $app) {
|
$this->container->singleton(TeleCartPulseService::class, function (Container $app) {
|
||||||
$heartbeatSecret = $app->getConfigValue('pulse.heartbeat_secret') ?? env('PULSE_HEARTBEAT_SECRET');
|
$heartbeatSecret = $app->getConfigValue('pulse.heartbeat_secret') ?? env('PULSE_HEARTBEAT_SECRET');
|
||||||
$heartbeatSigner = $heartbeatSecret ? new PayloadSigner($heartbeatSecret) : null;
|
$heartbeatSigner = $heartbeatSecret ? new PayloadSigner($heartbeatSecret) : null;
|
||||||
|
$heartbeatEnabled = (bool) $app->getConfigValue('pulse.heartbeat_enabled');
|
||||||
|
|
||||||
return new TeleCartPulseService(
|
return new TeleCartPulseService(
|
||||||
$app->get(TelegramInitDataDecoder::class),
|
$app->get(TelegramInitDataDecoder::class),
|
||||||
@@ -31,6 +32,7 @@ class TeleCartPulseServiceProvider extends ServiceProvider
|
|||||||
$app->get(TeleCartEvent::class),
|
$app->get(TeleCartEvent::class),
|
||||||
$app->getConfigValue('pulse.api_key'),
|
$app->getConfigValue('pulse.api_key'),
|
||||||
$heartbeatSigner,
|
$heartbeatSigner,
|
||||||
|
$heartbeatEnabled,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit\Framework\TeleCartPulse;
|
||||||
|
|
||||||
|
use Mockery as m;
|
||||||
|
use Openguru\OpenCartFramework\Cache\CacheInterface;
|
||||||
|
use Openguru\OpenCartFramework\TeleCartPulse\PayloadSigner;
|
||||||
|
use Openguru\OpenCartFramework\TeleCartPulse\TeleCartEvent;
|
||||||
|
use Openguru\OpenCartFramework\TeleCartPulse\TeleCartPulseService;
|
||||||
|
use Openguru\OpenCartFramework\Telegram\TelegramInitDataDecoder;
|
||||||
|
use Openguru\OpenCartFramework\Telegram\TelegramService;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
|
class TeleCartPulseServiceHeartbeatTest extends TestCase
|
||||||
|
{
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
m::close();
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDisabledHeartbeatSendsNothingAndTouchesNoCache(): void
|
||||||
|
{
|
||||||
|
$cache = m::mock(CacheInterface::class);
|
||||||
|
$cache->shouldNotReceive('get');
|
||||||
|
$cache->shouldNotReceive('set');
|
||||||
|
|
||||||
|
$telegramService = m::mock(TelegramService::class);
|
||||||
|
$telegramService->shouldNotReceive('getMe');
|
||||||
|
|
||||||
|
$service = new TeleCartPulseService(
|
||||||
|
m::mock(TelegramInitDataDecoder::class),
|
||||||
|
$telegramService,
|
||||||
|
$cache,
|
||||||
|
m::mock(LoggerInterface::class),
|
||||||
|
m::mock(TeleCartEvent::class),
|
||||||
|
'some-api-key',
|
||||||
|
m::mock(PayloadSigner::class),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
$service->handleHeartbeat();
|
||||||
|
|
||||||
|
// shouldNotReceive() expectations are verified on Mockery::close() in tearDown.
|
||||||
|
$this->addToAssertionCount(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEnabledHeartbeatPassesGateAndConsultsCache(): void
|
||||||
|
{
|
||||||
|
// Cache hit → метод выходит сразу после проверки кэша, не доходя до getMe()/HTTP.
|
||||||
|
$cache = m::mock(CacheInterface::class);
|
||||||
|
$cache->shouldReceive('get')->once()->with('telecart_pulse_heartbeat')->andReturn(time());
|
||||||
|
$cache->shouldNotReceive('set');
|
||||||
|
|
||||||
|
$telegramService = m::mock(TelegramService::class);
|
||||||
|
$telegramService->shouldNotReceive('getMe');
|
||||||
|
|
||||||
|
$service = new TeleCartPulseService(
|
||||||
|
m::mock(TelegramInitDataDecoder::class),
|
||||||
|
$telegramService,
|
||||||
|
$cache,
|
||||||
|
m::mock(LoggerInterface::class),
|
||||||
|
m::mock(TeleCartEvent::class),
|
||||||
|
'some-api-key',
|
||||||
|
m::mock(PayloadSigner::class),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
$service->handleHeartbeat();
|
||||||
|
|
||||||
|
$this->addToAssertionCount(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user