19 Commits

Author SHA1 Message Date
24075dceb7 docs: add design spec for honest opt-out heartbeat
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 14:04:17 +03:00
d621baae45 docs: add docs/features scaffold for feature documentation
Introduce docs/features/ with an index (conventions + feature registry),
a per-feature template, and a filled cart example. Reference the structure
from the root CLAUDE.md so context can be restored per feature on demand.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 13:33:37 +03:00
ed9d2c1b1f build(deps): patch security vulnerabilities in composer and npm deps
PHP (composer.lock):
- guzzlehttp/guzzle 7.10.0 -> 7.13.1 (CVE-2026-55767, CVE-2026-55568)
- guzzlehttp/psr7 2.9.0 -> 2.12.3 (CVE-2026-55766, CVE-2026-49214, CVE-2026-48998)
- symfony/cache 5.4.46 -> 5.4.53 (CVE-2026-45073 SQL injection)

frontend/admin: npm audit fix (axios, shell-quote, form-data, js-yaml,
esbuild, vite) -> 0 vulnerabilities

frontend/spa: npm audit fix + vitest/@vitest/ui -> 4.1.9 (flatted) ->
0 vulnerabilities

Verified: 299 PHPUnit tests pass, 53 SPA vitest pass, both frontends build.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 13:18:15 +03:00
7f73343f3f docs: replace cursor rules with CLAUDE.md
Migrate project guidance from .cursor/ to a single CLAUDE.md covering
architecture, backend/frontend conventions, commands and PHP 7.4 constraint.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 13:13:20 +03:00
0d4ffa7411 chore(release): 2.3.0 2026-05-25 10:29:40 +03:00
c24e30f14c fix(spa): visual fixes for SPA (#106)
* fix(spa): prevent more-link button from wrapping in block header

Long block titles squeezed the "Смотреть всё" link in BaseBlock,
causing its text to wrap onto two lines. Allow the title column to
shrink and keep the link in a single line.

* fix(spa): enable typography styles for product description

Add @tailwindcss/typography plugin so the `prose` classes used in the
product description actually apply: paragraph spacing, headings, hr,
links and tables. Also make wide tables inside `.prose` horizontally
scrollable instead of overflowing the viewport.

* fix(spa): open description links via Telegram WebApp API

Links inside the v-html product description used to navigate the
Mini App's WebView away from the app on Telegram mobile. Intercept
clicks on anchors in the description and route them through
Telegram.WebApp.openTelegramLink for t.me/tg:// targets and
Telegram.WebApp.openLink for everything else, falling back to
window.open when running outside Telegram.

* fix(spa): refine categories list layout

Tighten category item typography (text-base, leading-snug) and slightly
enlarge the icon for a better balance with long titles. Drop the
duplicate "Назад к ..." link and replace the parent-category CTA with
a daisyUI soft block button "Показать все товары категории" so the
primary action is visually distinct from the list.
2026-05-25 10:13:42 +03:00
d43564481d fix(spa): handle empty category and products in carousel block
Render the products carousel block gracefully when category_id is
missing or when the selected category has no products, instead of
throwing Vue Router's "Missing required param 'category_id'" error.
2026-05-25 09:27:08 +03:00
dependabot[bot]
41b17925df build(deps): bump the npm_and_yarn group across 2 directories with 6 updates
Bumps the npm_and_yarn group with 4 updates in the /frontend/admin directory: [axios](https://github.com/axios/axios), [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite), [follow-redirects](https://github.com/follow-redirects/follow-redirects) and [postcss](https://github.com/postcss/postcss).
Bumps the npm_and_yarn group with 4 updates in the /frontend/spa directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite), [postcss](https://github.com/postcss/postcss), [js-cookie](https://github.com/js-cookie/js-cookie) and [ws](https://github.com/websockets/ws).


Updates `axios` from 1.13.6 to 1.15.2
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.13.6...v1.15.2)

Updates `vite` from 7.3.1 to 7.3.3
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v7.3.3/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.3.3/packages/vite)

Updates `follow-redirects` from 1.15.11 to 1.16.0
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.11...v1.16.0)

Updates `postcss` from 8.5.8 to 8.5.15
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.8...8.5.15)

Updates `vite` from 7.3.1 to 7.3.3
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v7.3.3/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.3.3/packages/vite)

Updates `postcss` from 8.5.8 to 8.5.15
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.8...8.5.15)

Updates `js-cookie` from 3.0.5 to 3.0.7
- [Release notes](https://github.com/js-cookie/js-cookie/releases)
- [Commits](https://github.com/js-cookie/js-cookie/compare/v3.0.5...v3.0.7)

Updates `ws` from 8.19.0 to 8.21.0
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.19.0...8.21.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.15.2
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: vite
  dependency-version: 7.3.3
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: follow-redirects
  dependency-version: 1.16.0
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: postcss
  dependency-version: 8.5.15
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: vite
  dependency-version: 7.3.3
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: postcss
  dependency-version: 8.5.15
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: js-cookie
  dependency-version: 3.0.7
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: ws
  dependency-version: 8.21.0
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-25 08:01:29 +03:00
dependabot[bot]
caf862cfed build(deps): bump the npm_and_yarn group across 2 directories with 1 update
Bumps the npm_and_yarn group with 1 update in the /frontend/admin directory: [brace-expansion](https://github.com/juliangruber/brace-expansion).
Bumps the npm_and_yarn group with 1 update in the /frontend/spa directory: [brace-expansion](https://github.com/juliangruber/brace-expansion).


Updates `brace-expansion` from 1.1.12 to 1.1.13
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v1.1.12...v1.1.13)

Updates `brace-expansion` from 2.0.2 to 2.0.3
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v1.1.12...v1.1.13)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.13
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: brace-expansion
  dependency-version: 2.0.3
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-25 07:57:50 +03:00
dependabot[bot]
eed36fe00e build(deps): bump the npm_and_yarn group across 2 directories with 2 updates
Bumps the npm_and_yarn group with 2 updates in the /frontend/admin directory: [flatted](https://github.com/WebReflection/flatted) and [picomatch](https://github.com/micromatch/picomatch).
Bumps the npm_and_yarn group with 1 update in the /frontend/spa directory: [picomatch](https://github.com/micromatch/picomatch).


Updates `flatted` from 3.4.1 to 3.4.2
- [Commits](https://github.com/WebReflection/flatted/compare/v3.4.1...v3.4.2)

Updates `picomatch` from 4.0.3 to 4.0.4
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/4.0.3...4.0.4)

Updates `picomatch` from 4.0.3 to 4.0.4
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/4.0.3...4.0.4)

---
updated-dependencies:
- dependency-name: flatted
  dependency-version: 3.4.2
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: picomatch
  dependency-version: 4.0.4
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: picomatch
  dependency-version: 4.0.4
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-28 01:57:04 +03:00
28c8ce3b68 feat(ocmod): add TeleCart admin sidebar menu and local dev setup (#57)
- Add telecart.ocmod.xml: inject TeleCart menu item (with Settings child) after Marketing
- Add scripts/apply_ocmod.sh to symlink ocmod file in Docker container
- Add make ocmod target
- In CI: set version in telecart.ocmod.xml from version_meta; use docs.telecart.pro link

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-03-15 21:58:11 +03:00
80c2467b4a feat(admin): telegram settings improvements (#63) 2026-03-15 21:28:36 +03:00
a3f54a87dc feat(spa): Increase max page limit to product lists (#88) 2026-03-15 21:25:47 +03:00
554822720b build: ci/cd ignore issues branches (#92) 2026-03-15 21:24:48 +03:00
67f2fe6af6 chore(release): 2.2.2 2026-03-15 14:56:03 +03:00
0b786c832f build: update telegram api and composer deps (#82)
* build: update composer dependencies (#82)
* feat(spa): upgrade telegram web app version (#82)
2026-03-15 14:42:39 +03:00
f894d33c46 fix: normalize order response to valid UTF-8 before JSON encoding (#83)
* fix: normalize cart json utf8 and add tests

Ensure cart and checkout responses are cleaned to valid UTF-8 before JSON encoding and cover Utf8Cleaner with unit tests.

Made-with: Cursor

* fix: normalize order response to valid UTF-8 before JSON encoding

- Add Utf8Cleaner::clean() for successful order creation response (data)
- Add Utf8Cleaner::clean() for validation error response (ErrorBag)
- Prevents Malformed UTF-8 characters error when creating order with
  non-UTF-8 data from OpenCart/DB (same approach as cart endpoints)

Made-with: Cursor
2026-03-14 15:36:09 +03:00
3e6fed813d build: update npm dependencies (#77) 2026-03-14 00:27:27 +03:00
dependabot[bot]
16c60e0e12 build(deps): bump the npm_and_yarn group across 2 directories with 2 updates
Bumps the npm_and_yarn group with 2 updates in the /frontend/admin directory: [minimatch](https://github.com/isaacs/minimatch) and [rollup](https://github.com/rollup/rollup).
Bumps the npm_and_yarn group with 1 update in the /frontend/spa directory: [rollup](https://github.com/rollup/rollup).


Updates `minimatch` from 3.1.3 to 3.1.5
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.1.3...v3.1.5)

Updates `rollup` from 4.53.2 to 4.59.0
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.53.2...v4.59.0)

Updates `rollup` from 4.53.2 to 4.59.0
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.53.2...v4.59.0)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-version: 3.1.5
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: rollup
  dependency-version: 4.59.0
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: rollup
  dependency-version: 4.59.0
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-14 00:22:59 +03:00
437 changed files with 4582 additions and 5615 deletions

BIN
.cache/v3.0.3.7.zip Normal file

Binary file not shown.

View File

@@ -1,64 +0,0 @@
# Cursor AI Agents Configuration
## Роли и правила поведения ИИ
### Основная роль: Senior Full-Stack Developer
Вы - опытный full-stack разработчик, специализирующийся на:
- OpenCart модульной разработке
- Кастомных фреймворках (OpenCart Framework)
- PHP 7.4+ с современными практиками
- Vue.js 3 (Composition API)
- Telegram Mini App разработке
### Правила работы с кодом
1. **Всегда используй существующие паттерны проекта**
2. **Не создавай дубликаты - используй существующие утилиты**
3. **Следуй соглашениям именования проекта**
4. **Тестируй изменения перед коммитом**
5. **Документируй публичные API**
6. **Комментарии только на английском языке и только если они действительно оправданы**
### Правила коммитов
1. **Следование Conventional Commits**
- Используй префиксы: `feat:`, `fix:`, `chore:`, `refactor:`, `style:`, `test:`, `docs:`
- Формат: `<type>: <subject>` (первая строка до 72 символов)
- После пустой строки - подробное описание изменений
2. **Язык коммитов**
- Все коммиты на **английском языке**
- Подробное описание изменений в теле коммита
- Перечисляй все измененные файлы и ключевые изменения
3. **Примеры правильных коммитов**
```
feat: add setting to control category products button visibility
- Add show_category_products_button field to StoreDTO
- Update SettingsSerializerService to support new field
- Add setting in admin panel on 'Store' tab with toggle
- Pass setting to SPA through SettingsHandler
- Button displays only for categories with child categories
- Add default value true to configuration
```
### Запрещено
- Хардкод значений (используй конфиги/настройки)
- Игнорирование обработки ошибок
- Создание циклических зависимостей
Для разработки FrontEnd используй:
- Vue.js 3 (Composition API)
- Старайся избегать функций watch там, где это возможно и где можно сделать более красиво.
- Для frontend/admin используй Tailwind 4 с префиксом `tw:`.
- Для frontend/spa используй Tailwind 4 без префикса.
- Для frontend/admin используй иконки от FontAwesome 4, потому что это уже встроено в OpenCart 3.
- Для frontend/admin используй компоненты VuePrime 4.
- Для frontend/spa используй Daisy UI.
- Чтобы получить название стандартной таблицы OpenCart, используй хелпер `db_table`, либо добавляй константу DB_PREFIX перед названием таблицы. Так ты получишь название таблицы с префиксом.
- Все таблицы моего модуля MegaPay начинаются с префикса `megapay_`. Примеры миграций лежат в `module/oc_telegram_shop/upload/oc_telegram_shop/database/migrations`

View File

@@ -1,44 +0,0 @@
{
"rules": {
"preferCompositionAPI": true,
"strictTypes": true,
"noHardcodedValues": true,
"useDependencyInjection": true
},
"paths": {
"megapay_module": "module/oc_telegram_shop/upload/oc_telegram_shop",
"frontendAdmin": "frontend/admin",
"telegramShopSpa": "frontend/spa",
"migrations": "module/oc_telegram_shop/upload/oc_telegram_shop/database/migrations",
"megapayHandlers": "module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers",
"adminHandlers": "module/oc_telegram_shop/upload/oc_telegram_shop/bastion/Handlers",
"models": "module/oc_telegram_shop/upload/oc_telegram_shop/src/Models",
"framework": "module/oc_telegram_shop/upload/oc_telegram_shop/framework"
},
"naming": {
"classes": "PascalCase",
"methods": "camelCase",
"variables": "camelCase",
"constants": "UPPER_SNAKE_CASE",
"files": "PascalCase for classes, kebab-case for others",
"tables": "snake_case with megapay_ prefix"
},
"php": {
"version": "7.4+",
"preferVersion": "7.4+",
"psr12": true
},
"javascript": {
"version": "ES2020+",
"framework": "Vue 3 Composition API",
"stateManagement": "Pinia",
"uiLibrary": "PrimeVue (admin), Tailwind (spa)"
},
"database": {
"queryBuilder": true,
"migrations": true,
"tablePrefix": "megapay_",
"noForeignKeys": true
}
}

View File

@@ -1,38 +0,0 @@
## MegaPay Pulse Heartbeat Telemetry
### Цель
Раз в час отправлять телеметрию (heartbeat) на MegaPay Pulse, чтобы фиксировать состояние магазина и версии окружения без участия пользователя.
### Backend (`module/oc_telegram_shop/upload/oc_telegram_shop`)
- `framework/MegaPayPulse/MegaPayPulseService.php`
- Новый метод `handleHeartbeat()` собирает данные: домен (через `Utils::getCurrentDomain()`), username бота (через `TelegramService::getMe()`), версии PHP, модуля (из `composer.json`), OpenCart (`VERSION` и `VERSION_CORE`), текущий UTC timestamp.
- Последний успешный пинг кешируется (ключ `megapay_pulse_heartbeat`, TTL 1 час) через существующий `CacheInterface`.
- Подпись heartbeat выполняется через отдельный `PayloadSigner`, который использует секрет `pulse.heartbeat_secret`/`PULSE_HEARTBEAT_SECRET`. Логируются предупреждения при ошибках кеша/бота/подписи.
- Отправка идет на эндпоинт `heartbeat` с таймаутом 2 секунды и заголовком `X-MEGAPAY-VERSION`, взятым из `composer.json`.
- `framework/MegaPayPulse/MegaPayPulseServiceProvider.php`
- Регистрирует основной `PayloadSigner` (по `pulse.api_key`) и отдельный heartbeat signer (по `pulse.heartbeat_secret` или `PULSE_HEARTBEAT_SECRET`), инжектит `LoggerInterface`.
- `src/Handlers/TelemetryHandler.php` + `src/routes.php`
- Добавлен маршрут `heartbeat`, который вызывает `handleHeartbeat()` и возвращает `{ status: "ok" }`. Логгер пишет warning при проблемах.
### Frontend (`frontend/spa`)
- `src/utils/ftch.js`: новая функция `heartbeat()` вызывает `api_action=heartbeat`.
- `src/stores/Pulse.js`: добавлен action `heartbeat`, использующий новую API-функцию и логирующий результат.
- `src/main.js`: после `pulse.ingest(...)` вызывается `pulse.heartbeat()` без блокировки цепочки.
### Конфигурация / ENV
- `PULSE_API_HOST` — базовый URL MegaPay Pulse (используется и для events, и для heartbeat).
- `PULSE_TIMEOUT` — общий таймаут HTTP (для heartbeat принудительно 2 секунды).
- `PULSE_HEARTBEAT_SECRET` (или `pulse.heartbeat_secret` в настройках) — общий секрет для подписания heartbeat. Обязателен, иначе heartbeat не будет отправляться.
- `pulse.api_key` — прежний API ключ, используется только для event-инджеста.
### Поведение
1. Frontend (SPA) вызывает `heartbeat` при инициализации приложения (fire-and-forget).
2. Backend проверяет кеш. Если часа еще не прошло, `handleHeartbeat()` возвращает без запросов.
3. При необходимости собираются данные, подписываются через heartbeat signer и отправляются POST-запросом на `/heartbeat`.
4. Любые сбои (bot info, подпись, HTTP) логируются как warning, чтобы не тревожить пользователей.
### TODO / Возможные улучшения
- При необходимости вынести heartbeat запуск в крон/CLI, чтобы не зависеть от фронтенда.
- Добавить метрики успешности heartbeat в админку.

View File

@@ -1,128 +0,0 @@
# Промпты для генерации API
## Создание нового API endpoint
```
Создай новый API endpoint [ENDPOINT_NAME] для [DESCRIPTION]:
1. Handler в [HANDLER_PATH]:
- Метод handle() принимает Request
- Валидация входных данных
- Использование Service для бизнес-логики
- Возврат JsonResponse с правильной структурой
- Обработка ошибок с логированием
2. Service в [SERVICE_PATH]:
- Бизнес-логика
- Работа с Model
- Валидация данных
- Обработка исключений
3. Model в [MODEL_PATH] (если нужен):
- Методы для работы с БД
- Использование Query Builder
- Типизация методов
4. Route в routes.php:
- Добавь маршрут с правильным именем
5. Миграция (если нужна новая таблица):
- Создай миграцию в database/migrations/
- Используй фиксированный префикс megapay_
- Добавь индексы где необходимо
Следуй архитектуре MVC-L проекта и используй существующие паттерны.
```
## Создание CRUD API
```
Создай полный CRUD API для сущности [ENTITY_NAME]:
1. Handler с методами:
- list() - список с пагинацией и фильтрацией
- get() - получение одной записи
- create() - создание
- update() - обновление
- delete() - удаление
2. Service с бизнес-логикой для всех операций
3. Model с методами:
- findAll() - список
- findById() - по ID
- create() - создание
- update() - обновление
- delete() - удаление
4. DTO для валидации данных
5. Миграция для таблицы [TABLE_NAME]
6. Routes для всех endpoints
Используй серверную пагинацию, фильтрацию и сортировку для list().
```
## Создание Admin API endpoint
```
Создай Admin API endpoint [ENDPOINT_NAME] в bastion/Handlers/:
1. Handler в bastion/Handlers/[HANDLER_NAME].php:
- Используй Request для получения параметров
- Валидация данных
- Работа через Service
- Возврат JsonResponse с структурой { data: { data: [...], totalRecords: ... } }
- Обработка ошибок
2. Service в bastion/Services/ (если нужен):
- Бизнес-логика для админки
- Работа с Models
3. Route в bastion/routes.php
4. Frontend компонент (если нужен UI):
- Vue компонент в frontend/admin/src/views/
- Используй PrimeVue компоненты
- Серверная пагинация/фильтрация
- Обработка ошибок с toast уведомлениями
Следуй существующим паттернам проекта.
```
## Создание Frontend API клиента
```
Создай функцию для работы с API endpoint [ENDPOINT_NAME]:
1. В frontend/[admin|spa]/src/utils/http.js:
- Функция api[Method] для вызова endpoint
- Правильная обработка ошибок
- Возврат структурированного ответа
2. Использование:
- В компонентах через import
- Обработка loading states
- Toast уведомления для ошибок
Следуй существующим паттернам в http.js.
```
## Создание миграции
```
Создай миграцию для таблицы [TABLE_NAME]:
1. Файл: database/migrations/[TIMESTAMP]_[DESCRIPTION].php
2. Используй фиксированный префикс megapay_ для таблицы
3. Добавь все необходимые поля с правильными типами
4. Добавь индексы для часто используемых полей
5. Используй utf8mb4_unicode_ci collation
6. Используй InnoDB engine
7. Добавь created_at и updated_at timestamps
8. Не создавай foreign keys (используй только индексы)
Следуй структуре существующих миграций.
```

View File

@@ -1,106 +0,0 @@
# Правила оформления Changelog для документации
## Общие требования
- **Формат**: Markdown
- **Структура**: Одна версия = одна страница
- **Стиль**: Профессиональный, лаконичный, без маркетинговых лозунгов
- **Язык**: Русский
- **Целевая аудитория**: Разработчики и владельцы магазинов
- **Содержимое**: Только ключевые изменения, без лишних технических деталей
## Структура страницы
### Вводный абзац
- Краткое описание релиза (1-2 предложения)
- Дополнить 1-2 предложениями о ключевых изменениях
- Упоминать основные фичи и улучшения
### Разделы (строго в этом порядке)
1. **🚀 Добавлено** - новые функции и возможности
2. **✨ Улучшено** - улучшения существующих функций
3. **🐞 Исправлено** - исправленные ошибки
4. **⚠️ Несовместимые изменения** - изменения без обратной совместимости
5. **🔧 Технические изменения** - технические изменения (отдельный раздел)
### Правила для разделов
- Раздел **НЕ** добавлять, если в нём нет пунктов
- Использовать маркдаун-списки, без нумерации
- Не добавлять ссылок, если они явно не указаны
- Не добавлять лишних пояснений, выводов и заключений
- Выводить только содержимое Markdown-файла, без комментариев
## Разделение изменений
### Бизнес-логика и процессы
Разделы "Добавлено", "Улучшено", "Исправлено", "Несовместимые изменения" содержат только изменения, связанные с:
- Бизнес-процессами магазина
- Пользовательским опытом
- Функциональностью для владельцев магазинов
- Продающими фичами
### Технические изменения
Все технические изменения выносятся в отдельный раздел **🔧 Технические изменения**:
- Без подразделов, всё в одном списке
- Только самые ключевые технические изменения
- Не расписывать детали, которые не интересны пользователю
## Стиль написания
### Терминология
- Английские термины писать по-английски (например: "navbar", а не "навбар")
- Избегать технических терминов в бизнес-разделах (например: "customer_id" → "автоматическое связывание покупателей")
### Описания
**Для ключевых продающих фич:**
- Давать подробное описание с деталями
- Указывать возможности и преимущества
- Подчеркивать ценность для пользователя
**Для технических изменений:**
- Кратко, только ключевое
- Без лишних деталей
**Для обычных функций:**
- Лаконично, но информативно
- Фокус на пользе для пользователя
## Порядок размещения
### В разделе "Добавлено"
Ключевые продающие фичи размещать **первыми**:
1. Система конфигурации главной страницы через блоки
2. Конструктор форм на базе FormKit
3. Интеграция с Яндекс.Метрикой
4. Политика конфиденциальности
5. Поддержка купонов и подарочных сертификатов
6. Остальные функции
## Примеры правильных формулировок
### ✅ Хорошо
- "Автоматическое связывание покупателей Telegram с покупателями OpenCart: система автоматически находит и связывает покупателей из Telegram-магазина с существующими покупателями в OpenCart по email или номеру телефона, создавая единую базу клиентов"
### ❌ Плохо
- "Сохранение customer_id вместе с заказом для связи с клиентами OpenCart: автоматическая связь заказов из Telegram с покупателями в OpenCart, единая база клиентов"
### ✅ Хорошо
- "Navbar с логотипом и названием приложения"
### ❌ Плохо
- "Навбар с логотипом и названием приложения"
## Что не включать
- Внутренние детали разработки (тесты, статический анализ, обфускация)
- Технические детали, не интересные пользователям
- Избыточные технические описания
- Маркетинговые лозунги и призывы

View File

@@ -1,62 +0,0 @@
# Промпты для документирования
## Документирование класса
```
Добавь PHPDoc документацию для класса [CLASS_NAME]:
1. Описание класса и его назначения
2. @package тег
3. @author тег
4. Документация для всех публичных методов
5. Документация для публичных свойств
6. Примеры использования где уместно
```
## Документирование метода
```
Добавь PHPDoc для метода [METHOD_NAME]:
1. Описание метода
2. @param для всех параметров с типами
3. @return с типом возвращаемого значения
4. @throws для всех исключений
5. Примеры использования если сложная логика
```
## Документирование API endpoint
```
Создай документацию для API endpoint [ENDPOINT_NAME]:
1. Описание назначения
2. HTTP метод и путь
3. Параметры запроса (query/body)
4. Формат ответа (JSON структура)
5. Коды ошибок
6. Примеры запросов/ответов
7. Требования к авторизации
```
## Документирование Vue компонента
```
Добавь документацию для Vue компонента [COMPONENT_NAME]:
1. Описание компонента
2. Props с типами и описаниями
3. Emits с описаниями
4. Slots если есть
5. Примеры использования
6. Зависимости от других компонентов
```
## Создание README
```
Создай README.md для [MODULE/COMPONENT]:
1. Описание назначения
2. Установка/настройка
3. Использование с примерами
4. API документация
5. Конфигурация
6. Troubleshooting
```

View File

@@ -1,88 +0,0 @@
# Промпты для рефакторинга
## Общий рефакторинг
```
Проанализируй код в файле [FILE_PATH] и выполни рефакторинг:
1. Убери дублирование кода
2. Улучши читаемость
3. Примени принципы SOLID
4. Добавь обработку ошибок где необходимо
5. Улучши типизацию
6. Добавь документацию для публичных методов
7. Убедись что код следует архитектуре MVC-L проекта
8. Используй существующие утилиты и сервисы проекта вместо создания новых
```
## Рефакторинг Handler
```
Рефакторинг Handler [HANDLER_NAME]:
1. Вынеси бизнес-логику в отдельный Service
2. Добавь валидацию входных данных
3. Улучши обработку ошибок с логированием
4. Используй DTO для передачи данных
5. Добавь PHPDoc комментарии
6. Убедись что используется Dependency Injection
7. Оптимизируй запросы к БД если необходимо
```
## Рефакторинг Model
```
Рефакторинг Model [MODEL_NAME]:
1. Убедись что все запросы используют Query Builder
2. Добавь методы для частых операций (findBy, findAll, create, update)
3. Добавь валидацию данных перед сохранением
4. Улучши типизацию методов
5. Добавь PHPDoc комментарии
6. Используй транзакции для сложных операций
```
## Рефакторинг Vue компонента
```
Рефакторинг Vue компонента [COMPONENT_NAME]:
1. Вынеси логику в composable функции
2. Улучши типизацию props и emits
3. Оптимизируй computed properties
4. Добавь обработку ошибок
5. Улучши структуру template
6. Добавь loading states
7. Используй существующие утилиты проекта
```
## Удаление дублирования
```
Найди и устрани дублирование кода в:
- [FILE_PATH_1]
- [FILE_PATH_2]
- [FILE_PATH_3]
Создай общие утилиты/сервисы где необходимо, следуя архитектуре проекта.
```
## Улучшение производительности
```
Проанализируй производительность кода в [FILE_PATH]:
1. Оптимизируй запросы к БД (используй индексы, избегай N+1)
2. Добавь кэширование где уместно
3. Оптимизируй алгоритмы
4. Уменьши количество запросов к API
5. Используй ленивую загрузку на фронтенде
```
## Улучшение безопасности
```
Улучши безопасность кода в [FILE_PATH]:
1. Добавь валидацию всех входных данных
2. Используй prepared statements (Query Builder)
3. Добавь CSRF защиту где необходимо
4. Валидируй права доступа
5. Санитизируй выходные данные
6. Добавь rate limiting где необходимо
```

View File

@@ -1,53 +0,0 @@
# Промпты для тестирования
## Создание unit теста
```
Создай unit тест для [CLASS_NAME] в tests/Unit/:
1. Используй PHPUnit
2. Покрой все публичные методы
3. Тестируй успешные сценарии
4. Тестируй обработку ошибок
5. Используй моки для зависимостей
6. Следуй структуре существующих тестов
7. Используй TestCase базовый класс проекта
```
## Создание integration теста
```
Создай integration тест для [FEATURE_NAME] в tests/Integration/:
1. Тестируй полный flow от запроса до ответа
2. Используй тестовую БД
3. Очищай данные после тестов
4. Тестируй реальные сценарии использования
5. Проверяй валидацию данных
6. Проверяй обработку ошибок
```
## Создание Vue компонент теста
```
Создай тест для Vue компонента [COMPONENT_NAME] в frontend/[admin|spa]/tests/:
1. Используй Vitest
2. Тестируй рендеринг компонента
3. Тестируй props
4. Тестируй события (emits)
5. Тестируй пользовательские взаимодействия
6. Используй моки для API вызовов
7. Следуй структуре существующих тестов
```
## Покрытие тестами
```
Проанализируй покрытие тестами для [FILE_PATH]:
1. Определи какие методы не покрыты тестами
2. Создай тесты для критичных методов
3. Убедись что тестируются граничные случаи
4. Добавь тесты для обработки ошибок
```

View File

@@ -1,201 +0,0 @@
# Архитектурные правила
## OpenCart Framework Architecture
### MVC-L Pattern
Проект использует модифицированный паттерн MVC-L (Model-View-Controller-Language):
- **Model**: Классы в `src/Models/` - работа с данными, доступ к БД
- **View**: Vue компоненты на фронтенде, JSON ответы на бэкенде
- **Controller**: Handlers в `src/Handlers/` и `bastion/Handlers/`
- **Language**: Переводчик в `framework/Translator/`
### Dependency Injection
Все зависимости внедряются через Container:
```php
// ✅ Правильно
public function __construct(
private Builder $builder,
private TelegramCustomer $telegramCustomerModel
) {}
// ❌ Неправильно
public function __construct() {
$this->builder = new Builder(...);
}
```
### Service Providers
Регистрация сервисов через Service Providers:
```php
class MyServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(MyService::class, function ($app) {
return new MyService($app->get(Dependency::class));
});
}
}
```
### Routes
Маршруты определяются в `routes.php`:
```php
return [
'actionName' => [HandlerClass::class, 'methodName'],
];
```
### Handlers (Controllers)
Handlers обрабатывают HTTP запросы:
```php
class MyHandler
{
public function handle(Request $request): JsonResponse
{
// Валидация
// Бизнес-логика через Services
// Возврат JsonResponse
}
}
```
### Models
Models работают с данными:
```php
class MyModel
{
public function __construct(
private ConnectionInterface $database,
private Builder $builder
) {}
public function findById(int $id): ?array
{
return $this->builder->newQuery()
->from($this->tableName)
->where('id', '=', $id)
->firstOrNull();
}
}
```
### Services
Services содержат бизнес-логику:
```php
class MyService
{
public function __construct(
private MyModel $model
) {}
public function doSomething(array $data): array
{
// Бизнес-логика
return $this->model->create($data);
}
}
```
### Migrations
Миграции в `database/migrations/`:
```php
return new class extends Migration {
public function up(): void
{
$this->database->statement('CREATE TABLE ...');
}
};
```
### Query Builder
Всегда используй Query Builder вместо прямых SQL:
```php
// ✅ Правильно
$query = $this->builder->newQuery()
->select(['id', 'name'])
->from('table_name')
->where('status', '=', 'active')
->get();
// ❌ Неправильно
$result = $this->database->query("SELECT * FROM table_name WHERE status = 'active'");
```
### Frontend Architecture
#### Admin Panel (Vue 3)
- Composition API
- Pinia для state management
- PrimeVue для UI компонентов
- Axios для HTTP запросов
- Vue Router для навигации
#### SPA (Telegram Mini App)
- Composition API
- Pinia stores
- Tailwind CSS для стилей
- Telegram WebApp API
- Vue Router
### Naming Conventions
- **Classes**: PascalCase (`TelegramCustomerService`)
- **Methods**: camelCase (`getCustomers`)
- **Variables**: camelCase (`$customerData`)
- **Constants**: UPPER_SNAKE_CASE (`MAX_RETRIES`)
- **Files**: PascalCase для классов, kebab-case для остального
- **Tables**: snake_case с префиксом `megapay_`
### Error Handling
Всегда обрабатывай ошибки:
```php
try {
$result = $this->service->doSomething();
} catch (SpecificException $e) {
$this->logger->error('Error message', ['exception' => $e]);
throw new UserFriendlyException('User message');
}
```
### Configuration
Используй конфигурационные файлы в `configs/`:
```php
$config = $this->app->getConfigValue('app.setting_name');
```
### Caching
Используй Cache Service для кэширования:
```php
$cache = $this->app->get(CacheInterface::class);
$value = $cache->get('key', function() {
return expensiveOperation();
});
```

View File

@@ -1,70 +0,0 @@
# FormBuilder System Context
## Architectural Overview
The FormBuilder ecosystem is a strictly typed Vue 3 application module designed to generate standard FormKit Schema JSON. It eschews internal DTOs in favor of direct schema manipulation.
### Core Components
1. **FormBuilderView (`views/FormBuilderView.vue`)**:
* **Role**: Smart container / Data fetcher.
* **Responsibility**: Fetches form data from API (`GET /api/admin/forms/{alias}`), handles loading states, and passes data to `FormBuilder`.
* **Contract**: Expects API response `{ data: { schema: Array, is_custom: Boolean, ... } }`.
2. **FormBuilder (`components/FormBuilder/FormBuilder.vue`)**:
* **Role**: Main Orchestrator / State Owner.
* **Responsibility**: Manages `v-model` (schema), mode switching (Visual/Code/Preview), and provides state to children.
* **State Management**: Uses `defineModel` for `formFields` (schema) and `isCustom` (mode flag). Uses `provide('formFields')` and `provide('selectedFieldId')` for deep dependency injection.
* **Modes**:
* **Visual**: Drag-and-drop interface using `vuedraggable`.
* **Code**: Direct JSON editing of the FormKit schema. Sets `isCustom = true`.
* **Preview**: Renders the current schema using `FormKit`.
3. **FormCanvas (`components/FormBuilder/FormCanvas.vue`)**:
* **Role**: Visual Editor Surface.
* **Responsibility**: Renders the draggable list of fields.
* **Implementation**: Uses `vuedraggable` bound to `formFields`.
* **UX**: Implements "Ghost" and "Drag" classes for visual feedback. Handles selection logic.
4. **FieldsPanel (`components/FormBuilder/FieldsPanel.vue`)**:
* **Role**: Component Palette.
* **Responsibility**: Source of truth for available field types.
* **Implementation**: Uses `vuedraggable` with `pull: 'clone', put: false` to spawn new fields.
5. **FieldSettings (`components/FormBuilder/FieldSettings.vue`)**:
* **Role**: Property Editor.
* **Responsibility**: Edits properties of the `selectedFieldId`.
* **Constraint**: Must use PrimeVue components for all inputs.
## Data Flow & Invariants
1. **Schema Authority**: The FormKit Schema JSON is the single source of truth. There is no "internal model" separate from the schema.
2. **Reactivity**:
* `formFields` is an Array of Objects.
* Mutations must preserve reactivity. When using `v-model` or `provide/inject`, ensure array methods (splice, push, filter) are used correctly or replace the entire array reference if needed to trigger watchers.
* **Immutability**: `useFormFields` composable uses immutable patterns (returning new array references) to ensure `defineModel` in parent detects changes.
3. **Mode Logic**:
* Switching to **Code** mode sets `isCustom = true`.
* Switching to **Visual** mode sets `isCustom = false`.
* **Safety**: Switching modes triggers JSON validation. Invalid JSON prevents mode switch.
4. **Drag and Drop**:
* Powered by `vuedraggable` (Sortable.js).
* **Clone Logic**: `FieldsPanel` clones from `availableFields`. `FormCanvas` receives the clone.
* **ID Generation**: Unique IDs are generated upon cloning/addition to ensure key stability.
## Naming & Conventions
* **Tailwind**: Use `tw:` prefix for all utility classes (e.g., `tw:flex`, `tw:p-4`).
* **Components**: PrimeVue components are the standard UI kit (Button, Panel, InputText, etc.).
* **Icons**: FontAwesome (`fa fa-*`).
* **Files**: PascalCase for components (`FormBuilder.vue`), camelCase for logic (`useFormFields.js`).
## Integration Rules
* **Backend**: The backend stores the JSON blob directly. `FormBuilder` does not transform data before save; it emits the raw schema.
* **API**: `useFormsStore` handles API communication.
## Pitfalls & Warnings
* **vuedraggable vs @formkit/drag-and-drop**: We strictly use `vuedraggable`. Do not re-introduce `@formkit/drag-and-drop`.
* **Watchers**: Avoid `watch` where `computed` or event handlers suffice, to prevent infinite loops in bidirectional data flow.
* **Tailwind Config**: Do not use `@apply` with `tw:` prefixed classes in `<style>` blocks; standard CSS properties should be used if custom classes are needed.
## Future Modifications
* **Adding Fields**: Update `constants/availableFields.js` and ensure `utils/fieldHelpers.js` supports the new type.
* **Validation**: FormKit validation rules string (e.g., "required|email") is edited as a raw string in `FieldSettings`. Complex validation builders would require a new UI component.

View File

@@ -1,332 +0,0 @@
# JavaScript/TypeScript Code Style Rules
## JavaScript Version
- ES2020+ features
- Modern async/await
- Optional chaining (`?.`)
- Nullish coalescing (`??`)
- Template literals
## Code Style
### Variable Declarations
```javascript
// ✅ Используй const по умолчанию
const customers = [];
const totalRecords = 0;
// ✅ let только когда нужно переназначение
let currentPage = 1;
currentPage = 2;
// ❌ Не используй var
var oldVariable = 'bad';
```
### Arrow Functions
```javascript
// ✅ Предпочтительно для коротких функций
const filtered = items.filter(item => item.isActive);
// ✅ Для методов объектов
const api = {
get: async (url) => {
return await fetch(url);
}
};
// ✅ Для сложной логики - обычные функции
function complexCalculation(data) {
// много строк кода
return result;
}
```
### Template Literals
```javascript
// ✅ Предпочтительно
const message = `User ${userId} not found`;
const url = `${baseUrl}/api/${endpoint}`;
// ❌ Не используй конкатенацию
const message = 'User ' + userId + ' not found';
```
### Optional Chaining
```javascript
// ✅ Используй optional chaining
const name = user?.profile?.name;
const count = data?.items?.length ?? 0;
// ❌ Избегай длинных проверок
const name = user && user.profile && user.profile.name;
```
### Nullish Coalescing
```javascript
// ✅ Используй ?? для значений по умолчанию
const page = params.page ?? 1;
const name = user.name ?? 'Unknown';
// ❌ Не используй || для чисел/булевых
const page = params.page || 1; // 0 будет заменено на 1
```
### Destructuring
```javascript
// ✅ Используй деструктуризацию
const { data, totalRecords } = response.data;
const [first, second] = items;
// ✅ В параметрах функций
function processUser({ id, name, email }) {
// ...
}
// ✅ С значениями по умолчанию
const { page = 1, limit = 20 } = params;
```
### Async/Await
```javascript
// ✅ Предпочтительно
async function loadCustomers() {
try {
const response = await apiGet('getCustomers', params);
return response.data;
} catch (error) {
console.error('Error:', error);
throw error;
}
}
// ❌ Избегай .then() цепочек
function loadCustomers() {
return apiGet('getCustomers', params)
.then(response => response.data)
.catch(error => console.error(error));
}
```
## Vue.js 3 Composition API
### Script Setup
```vue
<script setup>
// ✅ Используй <script setup>
import { ref, computed, onMounted } from 'vue';
import { apiGet } from '@/utils/http.js';
const customers = ref([]);
const loading = ref(false);
const totalRecords = computed(() => customers.value.length);
async function loadCustomers() {
loading.value = true;
try {
const result = await apiGet('getCustomers');
customers.value = result.data || [];
} finally {
loading.value = false;
}
}
onMounted(() => {
loadCustomers();
});
</script>
```
### Reactive State
```javascript
// ✅ Используй ref для примитивов
const count = ref(0);
const name = ref('');
// ✅ Используй reactive для объектов
import { reactive } from 'vue';
const state = reactive({
customers: [],
loading: false
});
// ✅ Или ref для объектов (предпочтительно)
const state = ref({
customers: [],
loading: false
});
```
### Computed Properties
```javascript
// ✅ Используй computed для производных значений
const filteredCustomers = computed(() => {
return customers.value.filter(c => c.isActive);
});
// ❌ Не используй методы для вычислений
function filteredCustomers() {
return customers.value.filter(c => c.isActive);
}
```
### Props
```vue
<script setup>
// ✅ Определяй props с типами
const props = defineProps({
customerId: {
type: Number,
required: true
},
showDetails: {
type: Boolean,
default: false
}
});
</script>
```
### Emits
```vue
<script setup>
// ✅ Определяй emits
const emit = defineEmits(['update', 'delete']);
function handleUpdate() {
emit('update', data);
}
</script>
```
## Pinia Stores
```javascript
// ✅ Используй setup stores
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useCustomersStore = defineStore('customers', () => {
const customers = ref([]);
const loading = ref(false);
const totalRecords = computed(() => customers.value.length);
async function loadCustomers() {
loading.value = true;
try {
const result = await apiGet('getCustomers');
customers.value = result.data || [];
} finally {
loading.value = false;
}
}
return {
customers,
loading,
totalRecords,
loadCustomers
};
});
```
## Error Handling
```javascript
// ✅ Всегда обрабатывай ошибки
async function loadData() {
try {
const result = await apiGet('endpoint');
if (result.success) {
return result.data;
} else {
throw new Error(result.error);
}
} catch (error) {
console.error('Failed to load data:', error);
toast.error('Не удалось загрузить данные');
throw error;
}
}
```
## Naming Conventions
### Variables and Functions
```javascript
// ✅ camelCase
const customerData = {};
const totalRecords = 0;
function loadCustomers() {}
// ✅ Константы UPPER_SNAKE_CASE
const MAX_RETRIES = 3;
const API_BASE_URL = '/api';
```
### Components
```vue
<!-- PascalCase для компонентов -->
<CustomerCard />
<ProductsList />
```
### Files
```javascript
// ✅ kebab-case для файлов
// customers-view.vue
// http-utils.js
// customer-service.js
```
## Imports
```javascript
// ✅ Группируй импорты
// 1. Vue core
import { ref, computed, onMounted } from 'vue';
// 2. Third-party
import { apiGet } from '@/utils/http.js';
import { useToast } from 'primevue';
// 3. Local components
import CustomerCard from '@/components/CustomerCard.vue';
// 4. Types (если TypeScript)
import type { Customer } from '@/types';
```
## TypeScript (где используется)
```typescript
// ✅ Используй типы
interface Customer {
id: number;
name: string;
email?: string;
}
function getCustomer(id: number): Promise<Customer> {
return apiGet(`customers/${id}`);
}
```

View File

@@ -1,243 +0,0 @@
# PHP Code Style Rules
## PHP Version
Проект поддерживает PHP 7.4+
## PSR Standards
- **PSR-1**: Basic Coding Standard
- **PSR-4**: Autoloading Standard
- **PSR-12**: Extended Coding Style
## Code Style
### Type Declarations
```php
// ✅ Правильно - строгая типизация
public function getCustomers(Request $request): JsonResponse
{
$id = (int) $request->get('id');
return new JsonResponse(['data' => $customers]);
}
// ❌ Неправильно - без типов
public function getCustomers($request)
{
return ['data' => $customers];
}
```
### Nullable Types
```php
// ✅ Правильно
public function findById(?int $id): ?array
{
if ($id === null) {
return null;
}
return $this->query->where('id', '=', $id)->firstOrNull();
}
```
### Strict Types
Всегда используй `declare(strict_types=1);`:
```php
<?php
declare(strict_types=1);
namespace App\Services;
```
### Array Syntax
```php
// ✅ Предпочтительно - короткий синтаксис
$array = ['key' => 'value'];
// ❌ Не использовать
$array = array('key' => 'value');
```
### String Interpolation
```php
// ✅ Предпочтительно
$message = "User {$userId} not found";
// ✅ Альтернатива
$message = sprintf('User %d not found', $userId);
```
### Arrow Functions (PHP 7.4+)
```php
// ✅ Для простых операций
$filtered = array_filter($items, fn($item) => $item->isActive());
// ❌ Для сложной логики - используй обычные функции
```
### Nullsafe Operator (PHP 8.0+)
```php
// ✅ Для PHP 7.4
$name = $user && $user->profile ? $user->profile->name : null;
```
## Naming Conventions
### Classes
```php
// ✅ PascalCase
class TelegramCustomerService {}
class UserRepository {}
```
### Methods
```php
// ✅ camelCase
public function getCustomers(): array {}
public function saveOrUpdate(array $data): array {}
```
### Variables
```php
// ✅ camelCase
$customerData = [];
$totalRecords = 0;
```
### Constants
```php
// ✅ UPPER_SNAKE_CASE
private const MAX_RETRIES = 3;
public const DEFAULT_PAGE_SIZE = 20;
```
### Private Properties
```php
// ✅ camelCase с модификатором доступа
private string $tableName;
private Builder $builder;
```
## Documentation
### PHPDoc
```php
/**
* @throws ValidationException Если параметры невалидны
*/
public function getCustomers(Request $request): JsonResponse
{
// ...
}
```
### Inline Comments
```php
// ✅ Полезные комментарии
// Применяем фильтры для подсчета общего количества записей
$countQuery = $this->buildCountQuery($filters);
// ❌ Очевидные комментарии
// Получаем данные
$data = $this->getData();
```
## Error Handling
### Exceptions
```php
// ✅ Специфичные исключения
if (!$userId) {
throw new InvalidArgumentException('User ID is required');
}
// ✅ Логирование
try {
$result = $this->service->process();
} catch (Exception $e) {
$this->logger->error('Processing failed', [
'exception' => $e,
'context' => $context,
]);
throw new ProcessingException('Failed to process', 0, $e);
}
```
## Query Builder Usage
### Always Use Query Builder
```php
// ✅ Правильно
$customers = $this->builder->newQuery()
->select(['id', 'name', 'email'])
->from('megapay_customers')
->where('status', '=', 'active')
->orderBy('created_at', 'DESC')
->get();
// В крайних случаях можно использовать прямые SQL
$result = $this->database->query("SELECT * FROM megapay_customers");
```
### Parameter Binding
```php
// ✅ Query Builder автоматически биндит параметры
$query->where('name', 'LIKE', "%{$search}%");
// ❌ Никогда не конкатенируй значения в SQL, избегай SQL Injection.
```
## Array Access
### Safe Array Access
```php
// ✅ Используй Arr::get()
use Openguru\OpenCartFramework\Support\Arr;
$value = Arr::get($data, 'key', 'default');
// ❌ Небезопасно
$value = $data['key']; // может вызвать ошибку
```
## Return Types
```php
// ✅ Всегда указывай return type
public function getData(): array {}
public function findById(int $id): ?array {}
public function process(): void {}
// ❌ Без типа
public function getData() {}
```
## Visibility Modifiers
```php
// ✅ Всегда указывай модификатор доступа
private string $tableName;
protected Builder $builder;
public function getData(): array {}
```

View File

@@ -1,370 +0,0 @@
# Vue.js 3 Rules
## Component Structure
### Template
```vue
<template>
<!-- Логическая структура -->
<div class="container">
<header>
<h2>{{ title }}</h2>
</header>
<main>
<DataTable :value="items" />
</main>
<footer>
<Button @click="handleSave">Save</Button>
</footer>
</div>
</template>
```
### Script Setup
```vue
<script setup>
// ✅ Всегда используй <script setup>
import { ref, computed, onMounted } from 'vue';
import DataTable from 'primevue/datatable';
import { apiGet } from '@/utils/http.js';
// Props
const props = defineProps({
title: {
type: String,
required: true
}
});
// Emits
const emit = defineEmits(['update', 'delete']);
// State
const items = ref([]);
const loading = ref(false);
// Computed
const totalItems = computed(() => items.value.length);
// Methods
async function loadItems() {
loading.value = true;
try {
const result = await apiGet('getItems');
items.value = result.data || [];
} finally {
loading.value = false;
}
}
// Lifecycle
onMounted(() => {
loadItems();
});
</script>
```
### Styles
```vue
<style scoped>
/* ✅ Используй scoped стили */
.container {
padding: 1rem;
}
/* ✅ Используй :deep() для доступа к дочерним компонентам */
:deep(.p-datatable) {
border: 1px solid #ccc;
}
</style>
```
## Component Naming
```vue
<!-- PascalCase для компонентов -->
<CustomerCard />
<ProductsList />
<OrderDetails />
<!-- kebab-case в шаблоне тоже работает -->
<customer-card />
```
## Props
```vue
<script setup>
// ✅ Всегда определяй типы и валидацию
const props = defineProps({
customerId: {
type: Number,
required: true,
validator: (value) => value > 0
},
showDetails: {
type: Boolean,
default: false
},
items: {
type: Array,
default: () => []
}
});
</script>
```
## Emits
```vue
<script setup>
// ✅ Определяй emits с типами
const emit = defineEmits<{
update: [id: number, data: object];
delete: [id: number];
cancel: [];
}>();
// ✅ Или с валидацией
const emit = defineEmits({
update: (id: number, data: object) => {
if (id > 0 && typeof data === 'object') {
return true;
}
console.warn('Invalid emit arguments');
return false;
}
});
</script>
```
## Reactive State
```vue
<script setup>
// ✅ ref для примитивов
const count = ref(0);
const name = ref('');
// ✅ ref для объектов (предпочтительно)
const customer = ref({
id: null,
name: '',
email: ''
});
// ✅ reactive только если нужно
import { reactive } from 'vue';
const state = reactive({
items: [],
loading: false
});
</script>
```
## Computed Properties
```vue
<script setup>
// ✅ Используй computed для производных значений
const filteredItems = computed(() => {
return items.value.filter(item => item.isActive);
});
// ✅ Computed с getter/setter
const fullName = computed({
get: () => `${firstName.value} ${lastName.value}`,
set: (value) => {
const parts = value.split(' ');
firstName.value = parts[0];
lastName.value = parts[1];
}
});
</script>
```
## Event Handlers
```vue
<template>
<!-- Используй kebab-case для событий -->
<Button @click="handleClick" />
<Input @input="handleInput" />
<Form @submit.prevent="handleSubmit" />
</template>
<script setup>
// ✅ Именуй обработчики с префиксом handle
function handleClick() {
// ...
}
function handleInput(event) {
// ...
}
function handleSubmit() {
// ...
}
</script>
```
## Conditional Rendering
```vue
<template>
<!-- Используй v-if для условного рендеринга -->
<div v-if="loading">
<LoadingSpinner />
</div>
<!-- v-show для частых переключений -->
<div v-show="hasItems">
<ItemsList :items="items" />
</div>
<!-- v-else для альтернатив -->
<div v-else>
<EmptyState />
</div>
</template>
```
## Lists
```vue
<template>
<!-- Всегда используй :key -->
<div v-for="item in items" :key="item.id">
{{ item.name }}
</div>
<!-- Для индексов -->
<div v-for="(item, index) in items" :key="`item-${index}`">
{{ item.name }}
</div>
</template>
```
## Form Handling
```vue
<template>
<form @submit.prevent="handleSubmit">
<!-- Используй v-model -->
<InputText v-model="form.name" />
<Textarea v-model="form.description" />
<!-- Для кастомных компонентов -->
<CustomInput v-model="form.email" />
</form>
</template>
<script setup>
const form = ref({
name: '',
description: '',
email: ''
});
function handleSubmit() {
// Валидация и отправка
}
</script>
```
## PrimeVue Components
```vue
<template>
<!-- Используй PrimeVue компоненты в админке -->
<DataTable
:value="customers"
:loading="loading"
paginator
:rows="20"
@page="onPage"
>
<Column field="name" header="Name" sortable />
</DataTable>
</template>
```
## Styling
```vue
<style scoped>
/* ✅ Используй scoped -->
.container {
padding: 1rem;
}
/* ✅ :deep() для дочерних компонентов */
:deep(.p-datatable) {
border: 1px solid #ccc;
}
/* ✅ :slotted() для слотов */
:slotted(.header) {
font-weight: bold;
}
</style>
```
## Composition Functions
```vue
<script setup>
// ✅ Выноси сложную логику в composables
import { useCustomers } from '@/composables/useCustomers.js';
const {
customers,
loading,
loadCustomers,
totalRecords
} = useCustomers();
onMounted(() => {
loadCustomers();
});
</script>
```
## Error Handling
```vue
<script setup>
import { useToast } from 'primevue';
const toast = useToast();
async function loadData() {
try {
const result = await apiGet('endpoint');
if (result.success) {
data.value = result.data;
} else {
toast.add({
severity: 'error',
summary: 'Ошибка',
detail: result.error
});
}
} catch (error) {
console.error('Error:', error);
toast.add({
severity: 'error',
summary: 'Ошибка',
detail: 'Не удалось загрузить данные'
});
}
}
</script>
```

View File

@@ -4,7 +4,6 @@ on:
push: push:
branches: branches:
- master - master
- 'issue/**'
- develop - develop
pull_request: pull_request:
types: types:
@@ -135,6 +134,11 @@ jobs:
MODULE_ROOT="module/oc_telegram_shop/upload/oc_telegram_shop" MODULE_ROOT="module/oc_telegram_shop/upload/oc_telegram_shop"
echo "${{ needs.version_meta.outputs.tag }}" > "${MODULE_ROOT}/version.txt" echo "${{ needs.version_meta.outputs.tag }}" > "${MODULE_ROOT}/version.txt"
- name: Set version in install.xml
run: |
TAG="${{ needs.version_meta.outputs.tag }}"
sed -i "s#<version>.*</version>#<version>${TAG}</version>#" module/oc_telegram_shop/install.xml
- name: Build module - name: Build module
run: | run: |
bash scripts/ci/build.sh "${GITHUB_WORKSPACE}" bash scripts/ci/build.sh "${GITHUB_WORKSPACE}"

View File

@@ -4,539 +4,576 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
<!--- END HEADER --> <!--- END HEADER -->
## [2.2.1](https://github.com/megapay-labs/megapay/compare/v2.2.0...v2.2.1) (2026-02-22) ## [2.3.0](https://github.com/telecart-labs/telecart/compare/v2.2.2...v2.3.0) (2026-05-25)
---
## [2.2.0](https://github.com/megapay-labs/megapay/compare/v2.1.0...v2.2.0) (2026-01-09)
### Features ### Features
* Add BETA label and UI improvements for MegaPay Pulse tab ([551c4a](https://github.com/megapay-labs/megapay/commit/551c4a3506ddedb1b11851e3d3cbcb4f3ed34e03))
* Add cache:clear CLI command for module cache clearing (#46) ([3d0a75](https://github.com/megapay-labs/megapay/commit/3d0a7536a64bc88dbb349a9640260757b46009c4))
* Add changelog ([bf99bf](https://github.com/megapay-labs/megapay/commit/bf99bfe8a442c8eaad64f348792b7ddcbfb4486c))
* Add config redis cache, categories cache (#44) ([0798f5](https://github.com/megapay-labs/megapay/commit/0798f5c3e98721efbb45e2988350364b606622cd))
* Add customer account page with profile information and actions ([ad94af](https://github.com/megapay-labs/megapay/commit/ad94afda6826dd1d120599353121bad000b675a7))
* Add customizable text for manager contact button ([0a7877](https://github.com/megapay-labs/megapay/commit/0a7877ddbe7908d6a17089e1005308a945d3d21f))
* Add haptic feedback toggle setting ([afade8](https://github.com/megapay-labs/megapay/commit/afade85d004872d10929119db3ac95ee3acd0251))
* Add product interaction mode selector with three scenarios ([ecf4df](https://github.com/megapay-labs/megapay/commit/ecf4df363d49bf0d8bdc1b9ca3a241f99f26cfb8))
* Add store_id conditions (#43) ([846418](https://github.com/megapay-labs/megapay/commit/84641868e98786264f517865d48619ac4bc1ef7a))
* Add system information drawer (#44) ([9da605](https://github.com/megapay-labs/megapay/commit/9da605b9eac82045b46c3edbb998d28a63c22188))
* Increase dock icons size and add click animation ([ce2ea9](https://github.com/megapay-labs/megapay/commit/ce2ea9dea1fcd24d70ea66345d761a739d25f9d1))
##### Admin ##### Admin
* Improve navigation UI and move logs to drawer ([6a635e](https://github.com/megapay-labs/megapay/commit/6a635e189614c50c8d4bdbeb2486eb5b32ba7da0)) * Telegram settings improvements (#63) ([80c246](https://github.com/telecart-labs/telecart/commit/80c2467b4a7c2cb1c4101c483fe3ac31a5ad0cf1))
##### Search ##### Ocmod
* Improvement search cache (#44) ([8a9bac](https://github.com/megapay-labs/megapay/commit/8a9bac8221146b5db4960cc10de3e29dcd75c9bf)) * Add TeleCart admin sidebar menu and local dev setup (#57) ([28c8ce](https://github.com/telecart-labs/telecart/commit/28c8ce3b6841ca1d4cc98c8e7bb275e1ad3adcf3))
##### Spa ##### Spa
* Add UTM markers for product view on OpenCart (#47) ([647e20](https://github.com/megapay-labs/megapay/commit/647e20c6b093f3de5e6aafcad474cf1a99189d2e)) * Increase max page limit to product lists (#88) ([a3f54a](https://github.com/telecart-labs/telecart/commit/a3f54a87dc8799b3355b294fc155c3cf9d22c3eb))
### Bug Fixes ### Bug Fixes
* Correct external .env loading ([089b68](https://github.com/megapay-labs/megapay/commit/089b68672262286f4568a6a40627a0c2e0c51b14))
* Correctly work with megapay customers without usernames ([0312b8](https://github.com/megapay-labs/megapay/commit/0312b882e1ad5596e823943924d1b284d5592b14))
* Missing store_id for carousel products ([3a1f8d](https://github.com/megapay-labs/megapay/commit/3a1f8dbf948c65c6f2d94392c277317f1ce5da75))
##### Admin
* Correct logs sorting by datetime with milliseconds ([115c13](https://github.com/megapay-labs/megapay/commit/115c13393f045a8f2eb7992be11ce20e94b23b96))
##### Spa ##### Spa
* Correct line breaks for long attribute names and values in Product.vue ([ff7263](https://github.com/megapay-labs/megapay/commit/ff7263649c208449f0b0b65df3e6088115ec78f6)) * Handle empty category and products in carousel block ([d43564](https://github.com/telecart-labs/telecart/commit/d43564481d1e35ea8bb92310d20bff2989ca6ea7))
* Correct privacy policy message margin ([79f234](https://github.com/megapay-labs/megapay/commit/79f23400d20ba3cb43160cf898ea589f30c7aa83)) * Visual fixes for SPA (#106) ([c24e30](https://github.com/telecart-labs/telecart/commit/c24e30f14cef8a4b6956aaa2ada5dc9b12fbb9cb))
--- ---
## [2.1.0](https://github.com/megapay-labs/megapay/compare/v2.0.0...v2.1.0) (2025-12-24) ## [2.2.2](https://github.com/telecart-labs/telecart/compare/v2.2.1...v2.2.2) (2026-03-15)
### Bug Fixes
* Normalize order response to valid UTF-8 before JSON encoding (#83) ([f894d3](https://github.com/telecart-labs/telecart/commit/f894d33c464e28a15cbdcd5c72dfd90bd5379e92))
---
## [2.2.1](https://github.com/telecart-labs/telecart/compare/v2.2.0...v2.2.1) (2026-02-22)
---
## [2.2.0](https://github.com/telecart-labs/telecart/compare/v2.1.0...v2.2.0) (2026-01-09)
### Features ### Features
* Add setting to control category products button visibility ([c3994b](https://github.com/megapay-labs/megapay/commit/c3994b2291790f21cd219d1c5e820c274cb6e085)) * Add BETA label and UI improvements for TeleCart Pulse tab ([551c4a](https://github.com/telecart-labs/telecart/commit/551c4a3506ddedb1b11851e3d3cbcb4f3ed34e03))
* Add cache:clear CLI command for module cache clearing (#46) ([3d0a75](https://github.com/telecart-labs/telecart/commit/3d0a7536a64bc88dbb349a9640260757b46009c4))
* Add changelog ([bf99bf](https://github.com/telecart-labs/telecart/commit/bf99bfe8a442c8eaad64f348792b7ddcbfb4486c))
* Add config redis cache, categories cache (#44) ([0798f5](https://github.com/telecart-labs/telecart/commit/0798f5c3e98721efbb45e2988350364b606622cd))
* Add customer account page with profile information and actions ([ad94af](https://github.com/telecart-labs/telecart/commit/ad94afda6826dd1d120599353121bad000b675a7))
* Add customizable text for manager contact button ([0a7877](https://github.com/telecart-labs/telecart/commit/0a7877ddbe7908d6a17089e1005308a945d3d21f))
* Add haptic feedback toggle setting ([afade8](https://github.com/telecart-labs/telecart/commit/afade85d004872d10929119db3ac95ee3acd0251))
* Add product interaction mode selector with three scenarios ([ecf4df](https://github.com/telecart-labs/telecart/commit/ecf4df363d49bf0d8bdc1b9ca3a241f99f26cfb8))
* Add store_id conditions (#43) ([846418](https://github.com/telecart-labs/telecart/commit/84641868e98786264f517865d48619ac4bc1ef7a))
* Add system information drawer (#44) ([9da605](https://github.com/telecart-labs/telecart/commit/9da605b9eac82045b46c3edbb998d28a63c22188))
* Increase dock icons size and add click animation ([ce2ea9](https://github.com/telecart-labs/telecart/commit/ce2ea9dea1fcd24d70ea66345d761a739d25f9d1))
##### Admin
* Improve navigation UI and move logs to drawer ([6a635e](https://github.com/telecart-labs/telecart/commit/6a635e189614c50c8d4bdbeb2486eb5b32ba7da0))
##### Search
* Improvement search cache (#44) ([8a9bac](https://github.com/telecart-labs/telecart/commit/8a9bac8221146b5db4960cc10de3e29dcd75c9bf))
##### Spa
* Add UTM markers for product view on OpenCart (#47) ([647e20](https://github.com/telecart-labs/telecart/commit/647e20c6b093f3de5e6aafcad474cf1a99189d2e))
### Bug Fixes
* Correct external .env loading ([089b68](https://github.com/telecart-labs/telecart/commit/089b68672262286f4568a6a40627a0c2e0c51b14))
* Correctly work with telecart customers without usernames ([0312b8](https://github.com/telecart-labs/telecart/commit/0312b882e1ad5596e823943924d1b284d5592b14))
* Missing store_id for carousel products ([3a1f8d](https://github.com/telecart-labs/telecart/commit/3a1f8dbf948c65c6f2d94392c277317f1ce5da75))
##### Admin
* Correct logs sorting by datetime with milliseconds ([115c13](https://github.com/telecart-labs/telecart/commit/115c13393f045a8f2eb7992be11ce20e94b23b96))
##### Spa
* Correct line breaks for long attribute names and values in Product.vue ([ff7263](https://github.com/telecart-labs/telecart/commit/ff7263649c208449f0b0b65df3e6088115ec78f6))
* Correct privacy policy message margin ([79f234](https://github.com/telecart-labs/telecart/commit/79f23400d20ba3cb43160cf898ea589f30c7aa83))
--- ---
## [2.0.0](https://github.com/megapay-labs/megapay/compare/v1.3.2...v2.0.0) (2025-12-23) ## [2.1.0](https://github.com/telecart-labs/telecart/compare/v2.0.0...v2.1.0) (2025-12-24)
### Features
* Add setting to control category products button visibility ([c3994b](https://github.com/telecart-labs/telecart/commit/c3994b2291790f21cd219d1c5e820c274cb6e085))
---
## [2.0.0](https://github.com/telecart-labs/telecart/compare/v1.3.2...v2.0.0) (2025-12-23)
### ⚠ BREAKING CHANGES ### ⚠ BREAKING CHANGES
* None ([9a93cc](https://github.com/megapay-labs/megapay/commit/9a93cc73421c9c85e3cfbe403cd2c8fb41ba3406)) * None ([9a93cc](https://github.com/telecart-labs/telecart/commit/9a93cc73421c9c85e3cfbe403cd2c8fb41ba3406))
### Features ### Features
* Add aspect ratio selector for products_carousel ([615e8c](https://github.com/megapay-labs/megapay/commit/615e8c54a60d076a65bc04e60d26c5cbb21c264f)) * Add aspect ratio selector for products_carousel ([615e8c](https://github.com/telecart-labs/telecart/commit/615e8c54a60d076a65bc04e60d26c5cbb21c264f))
* Add cron service to run megapay schedule tasks ([16a258](https://github.com/megapay-labs/megapay/commit/16a258ab682947f9856459797bc99b0adbf0d335)) * Add cron service to run telecart schedule tasks ([16a258](https://github.com/telecart-labs/telecart/commit/16a258ab682947f9856459797bc99b0adbf0d335))
* Add debug mode for developers. Logs improvements ([fbccd5](https://github.com/megapay-labs/megapay/commit/fbccd506752e8cdada461e92a85b7603335a8f23)) * Add debug mode for developers. Logs improvements ([fbccd5](https://github.com/telecart-labs/telecart/commit/fbccd506752e8cdada461e92a85b7603335a8f23))
* Add default configs ([2bc751](https://github.com/megapay-labs/megapay/commit/2bc751119cb5c55c7d29a90d28a24f015ba76692)) * Add default configs ([2bc751](https://github.com/telecart-labs/telecart/commit/2bc751119cb5c55c7d29a90d28a24f015ba76692))
* Added new products_carousel bock type ([f0837e](https://github.com/megapay-labs/megapay/commit/f0837e5c94ef3327f0d249e1994dc73a6da1c42b)) * Added new products_carousel bock type ([f0837e](https://github.com/telecart-labs/telecart/commit/f0837e5c94ef3327f0d249e1994dc73a6da1c42b))
* Add FormKit framework support and update dependencies ([6a59dc](https://github.com/megapay-labs/megapay/commit/6a59dcc0c9b4f8e6ee003c7e168b632d8199981e)) * Add FormKit framework support and update dependencies ([6a59dc](https://github.com/telecart-labs/telecart/commit/6a59dcc0c9b4f8e6ee003c7e168b632d8199981e))
* Add hide keyboard button on search page ([17ff88](https://github.com/megapay-labs/megapay/commit/17ff888c053983a7ae334ba695338ccd8b2db3ab)) * Add hide keyboard button on search page ([17ff88](https://github.com/telecart-labs/telecart/commit/17ff888c053983a7ae334ba695338ccd8b2db3ab))
* Add html editor for telegram messages ([97df5b](https://github.com/megapay-labs/megapay/commit/97df5b4c0aa1d5fbf19c2132436045af0846b5f1)) * Add html editor for telegram messages ([97df5b](https://github.com/telecart-labs/telecart/commit/97df5b4c0aa1d5fbf19c2132436045af0846b5f1))
* Add italy dump ([13f63e](https://github.com/megapay-labs/megapay/commit/13f63e09fcc3c33cb4de2e809a981a0bf532bb63)) * Add italy dump ([13f63e](https://github.com/telecart-labs/telecart/commit/13f63e09fcc3c33cb4de2e809a981a0bf532bb63))
* Add migrations, mantenance tasks, database cache, blocks cache ([c0a6cb](https://github.com/megapay-labs/megapay/commit/c0a6cb17b3fa5a75185ad2e42e8979b1c848c285)) * Add migrations, mantenance tasks, database cache, blocks cache ([c0a6cb](https://github.com/telecart-labs/telecart/commit/c0a6cb17b3fa5a75185ad2e42e8979b1c848c285))
* Add old browser checks ([76c32c](https://github.com/megapay-labs/megapay/commit/76c32c53200f33a5de8fee3587b6aa597ce6d04a)) * Add old browser checks ([76c32c](https://github.com/telecart-labs/telecart/commit/76c32c53200f33a5de8fee3587b6aa597ce6d04a))
* Add options to select aspect ratio and cron algo for product images ([e9c6ed](https://github.com/megapay-labs/megapay/commit/e9c6ed8ddf801d3cfbb91c08733ab118fec3de21)) * Add options to select aspect ratio and cron algo for product images ([e9c6ed](https://github.com/telecart-labs/telecart/commit/e9c6ed8ddf801d3cfbb91c08733ab118fec3de21))
* Add reactivity to formkit ([fdcfce](https://github.com/megapay-labs/megapay/commit/fdcfce0a79af94f5f7ff05e19b4edec0fad4d452)) * Add reactivity to formkit ([fdcfce](https://github.com/telecart-labs/telecart/commit/fdcfce0a79af94f5f7ff05e19b4edec0fad4d452))
* Add redis cache driver ([2b0f04](https://github.com/megapay-labs/megapay/commit/2b0f04eb9455e2f1abb5b9374f3348072ffd1d6a)) * Add redis cache driver ([2b0f04](https://github.com/telecart-labs/telecart/commit/2b0f04eb9455e2f1abb5b9374f3348072ffd1d6a))
* Add scheduler module ([65973d](https://github.com/megapay-labs/megapay/commit/65973d2d79a8c6bfbfc367b56a2b83e465fa2e32)) * Add scheduler module ([65973d](https://github.com/telecart-labs/telecart/commit/65973d2d79a8c6bfbfc367b56a2b83e465fa2e32))
* Add MegaPay Pulse heartbeat telemetry ([b60c77](https://github.com/megapay-labs/megapay/commit/b60c77e4539aab9d2cdb1e9916b7e60c9848d686)) * Add TeleCart Pulse heartbeat telemetry ([b60c77](https://github.com/telecart-labs/telecart/commit/b60c77e4539aab9d2cdb1e9916b7e60c9848d686))
* Add MegaPayPulse telemetry system and ETL endpoints ([e8d0f8](https://github.com/megapay-labs/megapay/commit/e8d0f8a8190c2877ac5aa1e0cc7a5a1663598fe5)) * Add TeleCartPulse telemetry system and ETL endpoints ([e8d0f8](https://github.com/telecart-labs/telecart/commit/e8d0f8a8190c2877ac5aa1e0cc7a5a1663598fe5))
* Add Telegram customers management system with admin panel ([9a93cc](https://github.com/megapay-labs/megapay/commit/9a93cc73421c9c85e3cfbe403cd2c8fb41ba3406)) * Add Telegram customers management system with admin panel ([9a93cc](https://github.com/telecart-labs/telecart/commit/9a93cc73421c9c85e3cfbe403cd2c8fb41ba3406))
* Add texts configuration ([34dfe9](https://github.com/megapay-labs/megapay/commit/34dfe9028693ad488d40f2015af482d789f012c6)) * Add texts configuration ([34dfe9](https://github.com/telecart-labs/telecart/commit/34dfe9028693ad488d40f2015af482d789f012c6))
* Add UI for CRON Scheduler ([7372b9](https://github.com/megapay-labs/megapay/commit/7372b9c330ba4ba83458ca8d722cc71f57316180)) * Add UI for CRON Scheduler ([7372b9](https://github.com/telecart-labs/telecart/commit/7372b9c330ba4ba83458ca8d722cc71f57316180))
* Add warmup images command ([ecd372](https://github.com/megapay-labs/megapay/commit/ecd372dad30e05c5913fa489e561475584b89079)) * Add warmup images command ([ecd372](https://github.com/telecart-labs/telecart/commit/ecd372dad30e05c5913fa489e561475584b89079))
* Better algorythm for image resize ([13e5bc](https://github.com/megapay-labs/megapay/commit/13e5bce8a548439da3dcd892b0c5600ffc995be6)) * Better algorythm for image resize ([13e5bc](https://github.com/telecart-labs/telecart/commit/13e5bce8a548439da3dcd892b0c5600ffc995be6))
* Button to show all products from category ([b2d29f](https://github.com/megapay-labs/megapay/commit/b2d29fd3e288991f77ba6c0bee4bc7c5092b6594)) * Button to show all products from category ([b2d29f](https://github.com/telecart-labs/telecart/commit/b2d29fd3e288991f77ba6c0bee4bc7c5092b6594))
* Change image crop algorythm for product view page ([262f52](https://github.com/megapay-labs/megapay/commit/262f52929063802404af6f0592741ca836c91bcd)) * Change image crop algorythm for product view page ([262f52](https://github.com/telecart-labs/telecart/commit/262f52929063802404af6f0592741ca836c91bcd))
* Clear cache after settings update ([6f9855](https://github.com/megapay-labs/megapay/commit/6f9855995dd3603b622a9e601162ac0b6da9a694)) * Clear cache after settings update ([6f9855](https://github.com/telecart-labs/telecart/commit/6f9855995dd3603b622a9e601162ac0b6da9a694))
* Correct stats for megapay dashboard ([05af49](https://github.com/megapay-labs/megapay/commit/05af4949bfcf2a42ece30f1d77816a3c3018eae2)) * Correct stats for telecart dashboard ([05af49](https://github.com/telecart-labs/telecart/commit/05af4949bfcf2a42ece30f1d77816a3c3018eae2))
* Design update, show avatar in navbar ([6ac6a4](https://github.com/megapay-labs/megapay/commit/6ac6a42e2105bb6f234c108e3a5d21096b87660f)) * Design update, show avatar in navbar ([6ac6a4](https://github.com/telecart-labs/telecart/commit/6ac6a42e2105bb6f234c108e3a5d21096b87660f))
* Disable source maps for frontend production builds ([770ec8](https://github.com/megapay-labs/megapay/commit/770ec81fdcd1456ad7787c9d1e31d92383849f8f)) * Disable source maps for frontend production builds ([770ec8](https://github.com/telecart-labs/telecart/commit/770ec81fdcd1456ad7787c9d1e31d92383849f8f))
* Dont migrate tg messages from v1 ([b87797](https://github.com/megapay-labs/megapay/commit/b87797ee6728523d8e17eeab13b85b014c157d95)) * Dont migrate tg messages from v1 ([b87797](https://github.com/telecart-labs/telecart/commit/b87797ee6728523d8e17eeab13b85b014c157d95))
* Expose module version ([f1a39e](https://github.com/megapay-labs/megapay/commit/f1a39eeb0023d9fdf99cfd95b21288d950730b23)) * Expose module version ([f1a39e](https://github.com/telecart-labs/telecart/commit/f1a39eeb0023d9fdf99cfd95b21288d950730b23))
* Fixed width and preloader for product view page ([5d775e](https://github.com/megapay-labs/megapay/commit/5d775e8eb6710cc1ca3501be1fdfc957582e8663)) * Fixed width and preloader for product view page ([5d775e](https://github.com/telecart-labs/telecart/commit/5d775e8eb6710cc1ca3501be1fdfc957582e8663))
* Fix opecart module status, remove .vite ([e72948](https://github.com/megapay-labs/megapay/commit/e729484fd7a698fcadacdfc99a30beb9d4acbb09)) * Fix opecart module status, remove .vite ([e72948](https://github.com/telecart-labs/telecart/commit/e729484fd7a698fcadacdfc99a30beb9d4acbb09))
* Hide greeting image from frontend ([2ec683](https://github.com/megapay-labs/megapay/commit/2ec683f0163804e7562de14d098b8a0c0f0f28da)) * Hide greeting image from frontend ([2ec683](https://github.com/telecart-labs/telecart/commit/2ec683f0163804e7562de14d098b8a0c0f0f28da))
* Image processing improve ([38668f](https://github.com/megapay-labs/megapay/commit/38668fb4a7f2a3f94a85e06e20ecdff98f5d160d)) * Image processing improve ([38668f](https://github.com/telecart-labs/telecart/commit/38668fb4a7f2a3f94a85e06e20ecdff98f5d160d))
* Images and products loading optimization ([bf6744](https://github.com/megapay-labs/megapay/commit/bf674473e97111aa7a2acad9c835fcea37c3b2ec)) * Images and products loading optimization ([bf6744](https://github.com/telecart-labs/telecart/commit/bf674473e97111aa7a2acad9c835fcea37c3b2ec))
* Improve mainpage ui/ux ([f5d9d4](https://github.com/megapay-labs/megapay/commit/f5d9d417b3b86c7b710da5751a1d50af10a42b6e)) * Improve mainpage ui/ux ([f5d9d4](https://github.com/telecart-labs/telecart/commit/f5d9d417b3b86c7b710da5751a1d50af10a42b6e))
* Increase default per_page products ([6ed2fd](https://github.com/megapay-labs/megapay/commit/6ed2fd2062295bc4296d9ef5c4852541e0e4138f)) * Increase default per_page products ([6ed2fd](https://github.com/telecart-labs/telecart/commit/6ed2fd2062295bc4296d9ef5c4852541e0e4138f))
* Integrate yandex metrika ecommerce ([2f74ab](https://github.com/megapay-labs/megapay/commit/2f74aba35f548d632beed65d81693483942289d5)) * Integrate yandex metrika ecommerce ([2f74ab](https://github.com/telecart-labs/telecart/commit/2f74aba35f548d632beed65d81693483942289d5))
* Maintenance tasks, logs ([ae9771](https://github.com/megapay-labs/megapay/commit/ae9771dec436bd3ff619b26c9c6ce811b1e876dd)) * Maintenance tasks, logs ([ae9771](https://github.com/telecart-labs/telecart/commit/ae9771dec436bd3ff619b26c9c6ce811b1e876dd))
* More fluent vuejs app error handler ([955747](https://github.com/megapay-labs/megapay/commit/955747334d7a7f4863e145f52bcc2864beb8818e)) * More fluent vuejs app error handler ([955747](https://github.com/telecart-labs/telecart/commit/955747334d7a7f4863e145f52bcc2864beb8818e))
* Move getImage response to admin ([f539bb](https://github.com/megapay-labs/megapay/commit/f539bbfbbf023995f88b684406ac5eb8f16fff66)) * Move getImage response to admin ([f539bb](https://github.com/telecart-labs/telecart/commit/f539bbfbbf023995f88b684406ac5eb8f16fff66))
* New settings and mainpage blocks ([6176c7](https://github.com/megapay-labs/megapay/commit/6176c720b1f4c0ce9f06a3cc4ff50b72a52ab0fb)) * New settings and mainpage blocks ([6176c7](https://github.com/telecart-labs/telecart/commit/6176c720b1f4c0ce9f06a3cc4ff50b72a52ab0fb))
* Provide current opencart timezone to App ([51f462](https://github.com/megapay-labs/megapay/commit/51f462922ec49c8cc5e1b0c7909a69180cbe8e72)) * Provide current opencart timezone to App ([51f462](https://github.com/telecart-labs/telecart/commit/51f462922ec49c8cc5e1b0c7909a69180cbe8e72))
* Remove unused js libs ([08f0e2](https://github.com/megapay-labs/megapay/commit/08f0e24859c4e201e85075f2186ed741e3180b38)) * Remove unused js libs ([08f0e2](https://github.com/telecart-labs/telecart/commit/08f0e24859c4e201e85075f2186ed741e3180b38))
* Send xdebug trigger from frontend ([2743b8](https://github.com/megapay-labs/megapay/commit/2743b83a2c624191d2b65a1b13f5b3645e69b71a)) * Send xdebug trigger from frontend ([2743b8](https://github.com/telecart-labs/telecart/commit/2743b83a2c624191d2b65a1b13f5b3645e69b71a))
* Separated coupon and voucher errors ([dd12cb](https://github.com/megapay-labs/megapay/commit/dd12cb8c3434cd3d6f3b8eed4e469db8cd02e3f5)) * Separated coupon and voucher errors ([dd12cb](https://github.com/telecart-labs/telecart/commit/dd12cb8c3434cd3d6f3b8eed4e469db8cd02e3f5))
* Set environment variables ([3716e8](https://github.com/megapay-labs/megapay/commit/3716e89811f2a4135d644cb5a6bae0bb57c367ee)) * Set environment variables ([3716e8](https://github.com/telecart-labs/telecart/commit/3716e89811f2a4135d644cb5a6bae0bb57c367ee))
* Show module version in admin ([116821](https://github.com/megapay-labs/megapay/commit/116821a20946bf3f341e1589af2b24ace1e904da)) * Show module version in admin ([116821](https://github.com/telecart-labs/telecart/commit/116821a20946bf3f341e1589af2b24ace1e904da))
* Store customer_id in with order ([8260d2](https://github.com/megapay-labs/megapay/commit/8260d2bc96bfb256e73673e13740b242756eede2)) * Store customer_id in with order ([8260d2](https://github.com/telecart-labs/telecart/commit/8260d2bc96bfb256e73673e13740b242756eede2))
* Tg bot start message customization ([152e6d](https://github.com/megapay-labs/megapay/commit/152e6d715bfff1cfd05bdab72c4d4b54f7878e4a)) * Tg bot start message customization ([152e6d](https://github.com/telecart-labs/telecart/commit/152e6d715bfff1cfd05bdab72c4d4b54f7878e4a))
* Track and push MegaPay Pulse events ([ef7856](https://github.com/megapay-labs/megapay/commit/ef785654b969e7abc955ed452d8367d6cf3aa55e)) * Track and push TeleCart Pulse events ([ef7856](https://github.com/telecart-labs/telecart/commit/ef785654b969e7abc955ed452d8367d6cf3aa55e))
* UI/UX, add reset cache to admin ([09f1e5](https://github.com/megapay-labs/megapay/commit/09f1e514a975fea5c4fcd3b8cc587f906ab30bd3)) * UI/UX, add reset cache to admin ([09f1e5](https://github.com/telecart-labs/telecart/commit/09f1e514a975fea5c4fcd3b8cc587f906ab30bd3))
* Update admin page ([cd818d](https://github.com/megapay-labs/megapay/commit/cd818d3356d5738a9fb534e056d2e1055b2016ce)) * Update admin page ([cd818d](https://github.com/telecart-labs/telecart/commit/cd818d3356d5738a9fb534e056d2e1055b2016ce))
* Update design for product and product cards ([8a777c](https://github.com/megapay-labs/megapay/commit/8a777cd4d280b7049b60fdd0d3fa0586561e0a65)) * Update design for product and product cards ([8a777c](https://github.com/telecart-labs/telecart/commit/8a777cd4d280b7049b60fdd0d3fa0586561e0a65))
* Update product page design ([c64170](https://github.com/megapay-labs/megapay/commit/c64170f2d8058d99ae60323d907579b59566c119)) * Update product page design ([c64170](https://github.com/telecart-labs/telecart/commit/c64170f2d8058d99ae60323d907579b59566c119))
* Update readme ([5fb450](https://github.com/megapay-labs/megapay/commit/5fb45000ac77de0019a256150089256d5c423d68), [540595](https://github.com/megapay-labs/megapay/commit/540595c9f0661a2ca4c16d8876be54f9258bc0a3), [1361fe](https://github.com/megapay-labs/megapay/commit/1361fea993bcc37b0495b8fff7c20a45ccbd8ca2)) * Update readme ([5fb450](https://github.com/telecart-labs/telecart/commit/5fb45000ac77de0019a256150089256d5c423d68), [540595](https://github.com/telecart-labs/telecart/commit/540595c9f0661a2ca4c16d8876be54f9258bc0a3), [1361fe](https://github.com/telecart-labs/telecart/commit/1361fea993bcc37b0495b8fff7c20a45ccbd8ca2))
* Update styles for swipe to back ([e6a9e6](https://github.com/megapay-labs/megapay/commit/e6a9e6797f518d27caba507ac79d07ac8c113b06)) * Update styles for swipe to back ([e6a9e6](https://github.com/telecart-labs/telecart/commit/e6a9e6797f518d27caba507ac79d07ac8c113b06))
* Use yaMetrika number in settings ([cedc49](https://github.com/megapay-labs/megapay/commit/cedc49f0d5c3107791c1e6ff87a2f024a8baf828)) * Use yaMetrika number in settings ([cedc49](https://github.com/telecart-labs/telecart/commit/cedc49f0d5c3107791c1e6ff87a2f024a8baf828))
* Visualize swipe back ([50bdb8](https://github.com/megapay-labs/megapay/commit/50bdb8601c04799a4ecdb1b854ee1151a02f00f1)) * Visualize swipe back ([50bdb8](https://github.com/telecart-labs/telecart/commit/50bdb8601c04799a4ecdb1b854ee1151a02f00f1))
* WIP add yandex metrika goals ([4e59c4](https://github.com/megapay-labs/megapay/commit/4e59c4e7888925a87ce63eb53587d5e21fec4561)) * WIP add yandex metrika goals ([4e59c4](https://github.com/telecart-labs/telecart/commit/4e59c4e7888925a87ce63eb53587d5e21fec4561))
* добавлена функциональность политики конфиденциальности и согласия на обработку ПД ([7a5eeb](https://github.com/megapay-labs/megapay/commit/7a5eebec91ee73a2d38509cfa4f9bbb87cb75225)) * добавлена функциональность политики конфиденциальности и согласия на обработку ПД ([7a5eeb](https://github.com/telecart-labs/telecart/commit/7a5eebec91ee73a2d38509cfa4f9bbb87cb75225))
* добавлен жест swipe back для навигации назад ([179729](https://github.com/megapay-labs/megapay/commit/17972993ca815072ad5ded2bbc7a29e97f1abc6f)) * добавлен жест swipe back для навигации назад ([179729](https://github.com/telecart-labs/telecart/commit/17972993ca815072ad5ded2bbc7a29e97f1abc6f))
##### Admin ##### Admin
* Add more details for admin errors ([17865d](https://github.com/megapay-labs/megapay/commit/17865d8af4ed4b7f1f02a5b065847281fa5ede5f)) * Add more details for admin errors ([17865d](https://github.com/telecart-labs/telecart/commit/17865d8af4ed4b7f1f02a5b065847281fa5ede5f))
* Refactor logs viewer with table display and detailed dialog ([b39a34](https://github.com/megapay-labs/megapay/commit/b39a344a7dac32225d6fe939ea81fcc67f4b5750)) * Refactor logs viewer with table display and detailed dialog ([b39a34](https://github.com/telecart-labs/telecart/commit/b39a344a7dac32225d6fe939ea81fcc67f4b5750))
* Remove legacy setting keys that not defined in defaults ([107741](https://github.com/megapay-labs/megapay/commit/1077417d717cbd601bdff82ab3dfbb61402c3640)) * Remove legacy setting keys that not defined in defaults ([107741](https://github.com/telecart-labs/telecart/commit/1077417d717cbd601bdff82ab3dfbb61402c3640))
##### Banner ##### Banner
* Add banner feature ([05e7ca](https://github.com/megapay-labs/megapay/commit/05e7cafd0f36b204e0dea51a0f46f1a2c795dceb)) * Add banner feature ([05e7ca](https://github.com/telecart-labs/telecart/commit/05e7cafd0f36b204e0dea51a0f46f1a2c795dceb))
##### Customers ##### Customers
* Track order meta and OC sync ([952d8e](https://github.com/megapay-labs/megapay/commit/952d8e58da2972ff834d7f6609749b6dbd15a938)) * Track order meta and OC sync ([952d8e](https://github.com/telecart-labs/telecart/commit/952d8e58da2972ff834d7f6609749b6dbd15a938))
##### Products-feed ##### Products-feed
* Replace fixed image dimensions with aspect ratio selection ([cd0606](https://github.com/megapay-labs/megapay/commit/cd060610fe991c7c6d0db81a24bfa2b062192d20)) * Replace fixed image dimensions with aspect ratio selection ([cd0606](https://github.com/telecart-labs/telecart/commit/cd060610fe991c7c6d0db81a24bfa2b062192d20))
##### Pulse ##### Pulse
* Implement reliable event tracking and delivery system ([4a3dcc](https://github.com/megapay-labs/megapay/commit/4a3dcc11d161420c58494d744909f48982bd2582)) * Implement reliable event tracking and delivery system ([4a3dcc](https://github.com/telecart-labs/telecart/commit/4a3dcc11d161420c58494d744909f48982bd2582))
##### Search ##### Search
* Add keyboard hide button and auto-hide Dock ([db8d13](https://github.com/megapay-labs/megapay/commit/db8d1360fc9d8702fa7f2607337ac447ea646c5d)) * Add keyboard hide button and auto-hide Dock ([db8d13](https://github.com/telecart-labs/telecart/commit/db8d1360fc9d8702fa7f2607337ac447ea646c5d))
* Improve search UI with sticky bar and keyboard handling ([64ead2](https://github.com/megapay-labs/megapay/commit/64ead29583086dc55ae59e5d2b775dae31f36944)) * Improve search UI with sticky bar and keyboard handling ([64ead2](https://github.com/telecart-labs/telecart/commit/64ead29583086dc55ae59e5d2b775dae31f36944))
##### Slider ##### Slider
* Add slider feature ([3049bd](https://github.com/megapay-labs/megapay/commit/3049bd3101a44259f2883b351244c6eb5564cf89)) * Add slider feature ([3049bd](https://github.com/telecart-labs/telecart/commit/3049bd3101a44259f2883b351244c6eb5564cf89))
##### Spa ##### Spa
* Add custom dock ([4936e6](https://github.com/megapay-labs/megapay/commit/4936e6f16c0cd44299d086911a347cd3626fa2af)) * Add custom dock ([4936e6](https://github.com/telecart-labs/telecart/commit/4936e6f16c0cd44299d086911a347cd3626fa2af))
* Add dock ([2e699e](https://github.com/megapay-labs/megapay/commit/2e699eb0d6aca08d3f87030ea822c1fc79d3d477)) * Add dock ([2e699e](https://github.com/telecart-labs/telecart/commit/2e699eb0d6aca08d3f87030ea822c1fc79d3d477))
* Correct radius for floating panel, small ui fixes ([72ab84](https://github.com/megapay-labs/megapay/commit/72ab842a95f090b886787f551bee274fc2f6932c)) * Correct radius for floating panel, small ui fixes ([72ab84](https://github.com/telecart-labs/telecart/commit/72ab842a95f090b886787f551bee274fc2f6932c))
* Show navbar with app logo and app name ([c3c0d6](https://github.com/megapay-labs/megapay/commit/c3c0d6d2c179c83a1700d773c496ff7a44cce99c)) * Show navbar with app logo and app name ([c3c0d6](https://github.com/telecart-labs/telecart/commit/c3c0d6d2c179c83a1700d773c496ff7a44cce99c))
* UI changes ([ed8592](https://github.com/megapay-labs/megapay/commit/ed8592c19dabf4f26d6ed45e55a3c6f7398d667e)) * UI changes ([ed8592](https://github.com/telecart-labs/telecart/commit/ed8592c19dabf4f26d6ed45e55a3c6f7398d667e))
##### Megapay ##### Telecart
* Add vouchers and coupons (#9) ([ac24f0](https://github.com/megapay-labs/megapay/commit/ac24f0376bee13cc14db49a2904867ef173dcf95)) * Add vouchers and coupons (#9) ([ac24f0](https://github.com/telecart-labs/telecart/commit/ac24f0376bee13cc14db49a2904867ef173dcf95))
##### Texts ##### Texts
* Add options to redefine text for zero product prices ([1fbbb7](https://github.com/megapay-labs/megapay/commit/1fbbb7b6db13a9dac745c32d11f2e71ed79e854e)) * Add options to redefine text for zero product prices ([1fbbb7](https://github.com/telecart-labs/telecart/commit/1fbbb7b6db13a9dac745c32d11f2e71ed79e854e))
##### Ya metrika ##### Ya metrika
* WIP yandex metrika ([d7666f](https://github.com/megapay-labs/megapay/commit/d7666f94ba22fc1a808299e9a91ead14e6b58b25)) * WIP yandex metrika ([d7666f](https://github.com/telecart-labs/telecart/commit/d7666f94ba22fc1a808299e9a91ead14e6b58b25))
### Bug Fixes ### Bug Fixes
* Admin mainpage builder drawer doesnot show ([ad54b1](https://github.com/megapay-labs/megapay/commit/ad54b14c6804fae8960a5e15dbceb0549d91c732)) * Admin mainpage builder drawer doesnot show ([ad54b1](https://github.com/telecart-labs/telecart/commit/ad54b14c6804fae8960a5e15dbceb0549d91c732))
* Base header color ([28d80d](https://github.com/megapay-labs/megapay/commit/28d80d0f19ee31fea5011c2b466edea1590ab71e)) * Base header color ([28d80d](https://github.com/telecart-labs/telecart/commit/28d80d0f19ee31fea5011c2b466edea1590ab71e))
* Browser check ([4cd49b](https://github.com/megapay-labs/megapay/commit/4cd49b17a6df65863dc9fd32efdd0bba4b4e44ff)) * Browser check ([4cd49b](https://github.com/telecart-labs/telecart/commit/4cd49b17a6df65863dc9fd32efdd0bba4b4e44ff))
* Center image on product view ([dc198c](https://github.com/megapay-labs/megapay/commit/dc198c63b7c4f66b92eeb55958983d8eaed0260f)) * Center image on product view ([dc198c](https://github.com/telecart-labs/telecart/commit/dc198c63b7c4f66b92eeb55958983d8eaed0260f))
* Correct cli.php path for phar ([57c840](https://github.com/megapay-labs/megapay/commit/57c8400904b74569c843cd898fe6c39552f91e6b)) * Correct cli.php path for phar ([57c840](https://github.com/telecart-labs/telecart/commit/57c8400904b74569c843cd898fe6c39552f91e6b))
* Correct counter id for yandex metrika test ([9870f2](https://github.com/megapay-labs/megapay/commit/9870f2f36364ec7d968b3aec14091aefae774199)) * Correct counter id for yandex metrika test ([9870f2](https://github.com/telecart-labs/telecart/commit/9870f2f36364ec7d968b3aec14091aefae774199))
* Correct crontab line ([613ce5](https://github.com/megapay-labs/megapay/commit/613ce520ee53be47ee06e101daf54c8f5136184b)) * Correct crontab line ([613ce5](https://github.com/telecart-labs/telecart/commit/613ce520ee53be47ee06e101daf54c8f5136184b))
* Correct path for cron ([185f30](https://github.com/megapay-labs/megapay/commit/185f3096e1e17507f4191104794991448d4d44bb)) * Correct path for cron ([185f30](https://github.com/telecart-labs/telecart/commit/185f3096e1e17507f4191104794991448d4d44bb))
* Correct url for hit ([515b82](https://github.com/megapay-labs/megapay/commit/515b82302ba603f61324ace576e23adbf82560fd)) * Correct url for hit ([515b82](https://github.com/telecart-labs/telecart/commit/515b82302ba603f61324ace576e23adbf82560fd))
* Disable fullscreen for desktop ([bf32d9](https://github.com/megapay-labs/megapay/commit/bf32d9081169206cef62d92f27338321d1cc1e69)) * Disable fullscreen for desktop ([bf32d9](https://github.com/telecart-labs/telecart/commit/bf32d9081169206cef62d92f27338321d1cc1e69))
* Fix dock layout ([bdbdfc](https://github.com/megapay-labs/megapay/commit/bdbdfc3650ff24e77f7f35059ac72e87cd02ddf2)) * Fix dock layout ([bdbdfc](https://github.com/telecart-labs/telecart/commit/bdbdfc3650ff24e77f7f35059ac72e87cd02ddf2))
* Fix errors and small improvements ([3b2e2c](https://github.com/megapay-labs/megapay/commit/3b2e2cb656bb8db6feebdbb23612202f96cdde3f)) * Fix errors and small improvements ([3b2e2c](https://github.com/telecart-labs/telecart/commit/3b2e2cb656bb8db6feebdbb23612202f96cdde3f))
* Fix search issues ([2f9a55](https://github.com/megapay-labs/megapay/commit/2f9a553ae356fe4fb7ee3a481010d14be7d94ad7)) * Fix search issues ([2f9a55](https://github.com/telecart-labs/telecart/commit/2f9a553ae356fe4fb7ee3a481010d14be7d94ad7))
* Fix type error ([836161](https://github.com/megapay-labs/megapay/commit/8361616dd647397777849fd87267134e0bc1fb9b)) * Fix type error ([836161](https://github.com/telecart-labs/telecart/commit/8361616dd647397777849fd87267134e0bc1fb9b))
* Glob not work with phar ([24db69](https://github.com/megapay-labs/megapay/commit/24db69fbbad6758d11dabdac54f075611cde9593)) * Glob not work with phar ([24db69](https://github.com/telecart-labs/telecart/commit/24db69fbbad6758d11dabdac54f075611cde9593))
* Grant +x permissions for cli.php ([0ee3b7](https://github.com/megapay-labs/megapay/commit/0ee3b7d091da970d19a755207c73c28689bfd2a4)) * Grant +x permissions for cli.php ([0ee3b7](https://github.com/telecart-labs/telecart/commit/0ee3b7d091da970d19a755207c73c28689bfd2a4))
* Handle missing tags in workflow ([bc50cf](https://github.com/megapay-labs/megapay/commit/bc50cf064854ad0597f1d7a39b0eb32d88d2598a)) * Handle missing tags in workflow ([bc50cf](https://github.com/telecart-labs/telecart/commit/bc50cf064854ad0597f1d7a39b0eb32d88d2598a))
* Image picker component name type ([30b010](https://github.com/megapay-labs/megapay/commit/30b0108fe78b2a594db0c749f563577921c189d0)) * Image picker component name type ([30b010](https://github.com/telecart-labs/telecart/commit/30b0108fe78b2a594db0c749f563577921c189d0))
* Many products in search ([a5e91d](https://github.com/megapay-labs/megapay/commit/a5e91dd488b1f13abf739797e75400ddf36ba7e1)) * Many products in search ([a5e91d](https://github.com/telecart-labs/telecart/commit/a5e91dd488b1f13abf739797e75400ddf36ba7e1))
* Order creation ([82ab81](https://github.com/megapay-labs/megapay/commit/82ab8134e19f2cc4066de5241e7ff29905d79b17)) * Order creation ([82ab81](https://github.com/telecart-labs/telecart/commit/82ab8134e19f2cc4066de5241e7ff29905d79b17))
* Pulse ingest ([95dd54](https://github.com/megapay-labs/megapay/commit/95dd545dc5718046cd421d70ad2d4ea137919852)) * Pulse ingest ([95dd54](https://github.com/telecart-labs/telecart/commit/95dd545dc5718046cd421d70ad2d4ea137919852))
* Scroll behaviour ([359395](https://github.com/megapay-labs/megapay/commit/359395b7e880d72dd34da504f6d9fe001d6f0aff)) * Scroll behaviour ([359395](https://github.com/telecart-labs/telecart/commit/359395b7e880d72dd34da504f6d9fe001d6f0aff))
* Search ([e5792a](https://github.com/megapay-labs/megapay/commit/e5792a059a0986b6d6c86df9dbcbda212bc0f548)) * Search ([e5792a](https://github.com/telecart-labs/telecart/commit/e5792a059a0986b6d6c86df9dbcbda212bc0f548))
* Settings numeric error ([44d2af](https://github.com/megapay-labs/megapay/commit/44d2af3b30a7133b550e56385c3096c0e8848df5)) * Settings numeric error ([44d2af](https://github.com/telecart-labs/telecart/commit/44d2af3b30a7133b550e56385c3096c0e8848df5))
* Store error ([ab5c2f](https://github.com/megapay-labs/megapay/commit/ab5c2f42b907d19f0c52c631cea02b981a199c39)) * Store error ([ab5c2f](https://github.com/telecart-labs/telecart/commit/ab5c2f42b907d19f0c52c631cea02b981a199c39))
* Switch between code and visual for custom forms ([0ab09a](https://github.com/megapay-labs/megapay/commit/0ab09aad10eb724cf0378bcc2f46001b5108fade)) * Switch between code and visual for custom forms ([0ab09a](https://github.com/telecart-labs/telecart/commit/0ab09aad10eb724cf0378bcc2f46001b5108fade))
* Test ([c4b192](https://github.com/megapay-labs/megapay/commit/c4b19286f36ad166a1092dda86e65d48e3390723)) * Test ([c4b192](https://github.com/telecart-labs/telecart/commit/c4b19286f36ad166a1092dda86e65d48e3390723))
* Use html for tg bot ([7e6502](https://github.com/megapay-labs/megapay/commit/7e6502b07e74e27e27326c9593f25c2c9c03418b)) * Use html for tg bot ([7e6502](https://github.com/telecart-labs/telecart/commit/7e6502b07e74e27e27326c9593f25c2c9c03418b))
##### Admin ##### Admin
* Fix error when chat_id is string ([8f6af0](https://github.com/megapay-labs/megapay/commit/8f6af04e732f853eb79504676dc5ae83ba151c93)) * Fix error when chat_id is string ([8f6af0](https://github.com/telecart-labs/telecart/commit/8f6af04e732f853eb79504676dc5ae83ba151c93))
##### Spa ##### Spa
* Remove html in price for some opencart custom themes ([3423dd](https://github.com/megapay-labs/megapay/commit/3423dd172748845ce5177ea6bf5894a6da977c37), [d6a436](https://github.com/megapay-labs/megapay/commit/d6a43605acaff1cf335dc044e2b297132d6eb2ce)) * Remove html in price for some opencart custom themes ([3423dd](https://github.com/telecart-labs/telecart/commit/3423dd172748845ce5177ea6bf5894a6da977c37), [d6a436](https://github.com/telecart-labs/telecart/commit/d6a43605acaff1cf335dc044e2b297132d6eb2ce))
##### Megapay ##### Telecart
* Fix products search ([98ee6d](https://github.com/megapay-labs/megapay/commit/98ee6d9ecac4349cad847bdba1b10cf8660c251f)) * Fix products search ([98ee6d](https://github.com/telecart-labs/telecart/commit/98ee6d9ecac4349cad847bdba1b10cf8660c251f))
--- ---
## [1.3.2](https://github.com/megapay-labs/megapay/compare/v1.3.1...v1.3.2) (2025-10-24) ## [1.3.2](https://github.com/telecart-labs/telecart/compare/v1.3.1...v1.3.2) (2025-10-24)
### Bug Fixes ### Bug Fixes
##### Products ##### Products
* Encode html for title on products page ([78ca4f](https://github.com/megapay-labs/megapay/commit/78ca4fd309e2254771a01ade75197d46e149c5f3)) * Encode html for title on products page ([78ca4f](https://github.com/telecart-labs/telecart/commit/78ca4fd309e2254771a01ade75197d46e149c5f3))
--- ---
## [1.3.1](https://github.com/megapay-labs/megapay/compare/v1.3.0...v1.3.1) (2025-10-19) ## [1.3.1](https://github.com/telecart-labs/telecart/compare/v1.3.0...v1.3.1) (2025-10-19)
### Bug Fixes ### Bug Fixes
##### App ##### App
* Fix unhandled exceptions ([aa4264](https://github.com/megapay-labs/megapay/commit/aa42643c34c1a7cb11aae2d3191ac63c0af3236a)) * Fix unhandled exceptions ([aa4264](https://github.com/telecart-labs/telecart/commit/aa42643c34c1a7cb11aae2d3191ac63c0af3236a))
--- ---
## [1.3.0](https://github.com/megapay-labs/megapay/compare/v1.2.0...v1.3.0) (2025-10-19) ## [1.3.0](https://github.com/telecart-labs/telecart/compare/v1.2.0...v1.3.0) (2025-10-19)
### Features ### Features
* Add filters to mainpage ([1e2a9b](https://github.com/megapay-labs/megapay/commit/1e2a9bc7051e14c65eb44b392dba11f766b95d33)) * Add filters to mainpage ([1e2a9b](https://github.com/telecart-labs/telecart/commit/1e2a9bc7051e14c65eb44b392dba11f766b95d33))
* Handle start command for megapay bot ([c936d7](https://github.com/megapay-labs/megapay/commit/c936d727b495b06f63d7f15949d540f2c9a2b9c0)) * Handle start command for telecart bot ([c936d7](https://github.com/telecart-labs/telecart/commit/c936d727b495b06f63d7f15949d540f2c9a2b9c0))
##### Admin ##### Admin
* Do not log assets cleanup message if nothing deleted ([00165b](https://github.com/megapay-labs/megapay/commit/00165b3b61841303a6eff0447ee134d860f4a8b9)) * Do not log assets cleanup message if nothing deleted ([00165b](https://github.com/telecart-labs/telecart/commit/00165b3b61841303a6eff0447ee134d860f4a8b9))
* Remove old assets ([01368b](https://github.com/megapay-labs/megapay/commit/01368bbfce831cd8949500ffdf5f5e4614316459)) * Remove old assets ([01368b](https://github.com/telecart-labs/telecart/commit/01368bbfce831cd8949500ffdf5f5e4614316459))
* Remove old maps ([31a990](https://github.com/megapay-labs/megapay/commit/31a9909cc37c953113c743d248c9dd4065f89acb)) * Remove old maps ([31a990](https://github.com/telecart-labs/telecart/commit/31a9909cc37c953113c743d248c9dd4065f89acb))
##### Bot ##### Bot
* Add bot commands ([023ace](https://github.com/megapay-labs/megapay/commit/023acee68fb8f247a5e84f62aade44c77cfc0ed5)) * Add bot commands ([023ace](https://github.com/telecart-labs/telecart/commit/023acee68fb8f247a5e84f62aade44c77cfc0ed5))
##### Filters ##### Filters
* Add filters for the main page ([e7e045](https://github.com/megapay-labs/megapay/commit/e7e045b695d227d2d895242b4c0f19883d07f69e)) * Add filters for the main page ([e7e045](https://github.com/telecart-labs/telecart/commit/e7e045b695d227d2d895242b4c0f19883d07f69e))
##### Spa ##### Spa
* Hide floating cart btn for filters page ([259154](https://github.com/megapay-labs/megapay/commit/259154e4f1ca2ac0e2e6a357e8a53be78a00441a)) * Hide floating cart btn for filters page ([259154](https://github.com/telecart-labs/telecart/commit/259154e4f1ca2ac0e2e6a357e8a53be78a00441a))
* Lock vertical orientation ([646721](https://github.com/megapay-labs/megapay/commit/6467216775c44a7f4cc924d4551cc88ca246b757)) * Lock vertical orientation ([646721](https://github.com/telecart-labs/telecart/commit/6467216775c44a7f4cc924d4551cc88ca246b757))
* Update Telegram Mini App to 59 version ([3ecb51](https://github.com/megapay-labs/megapay/commit/3ecb51b5cd1751f4e2ace73171225ee3a33e46c4)) * Update Telegram Mini App to 59 version ([3ecb51](https://github.com/telecart-labs/telecart/commit/3ecb51b5cd1751f4e2ace73171225ee3a33e46c4))
### Bug Fixes ### Bug Fixes
* Escape character for start message command ([a051ff](https://github.com/megapay-labs/megapay/commit/a051ff545e920760e3a0e6c34ef3cc94a0c1bfdb)) * Escape character for start message command ([a051ff](https://github.com/telecart-labs/telecart/commit/a051ff545e920760e3a0e6c34ef3cc94a0c1bfdb))
--- ---
## [1.2.0](https://github.com/megapay-labs/megapay/compare/v1.1.0...v1.2.0) (2025-09-27) ## [1.2.0](https://github.com/telecart-labs/telecart/compare/v1.1.0...v1.2.0) (2025-09-27)
### Features ### Features
##### Product ##### Product
* Add option to disable store feature ([d7dd05](https://github.com/megapay-labs/megapay/commit/d7dd055e245a5bb0772b382ca8542394e92fecd5)) * Add option to disable store feature ([d7dd05](https://github.com/telecart-labs/telecart/commit/d7dd055e245a5bb0772b382ca8542394e92fecd5))
### Bug Fixes ### Bug Fixes
* Correct update opencart config after defaults diff update ([e24e7c](https://github.com/megapay-labs/megapay/commit/e24e7c6d106597c627451abf8014723f42fdda34)) * Correct update opencart config after defaults diff update ([e24e7c](https://github.com/telecart-labs/telecart/commit/e24e7c6d106597c627451abf8014723f42fdda34))
--- ---
## [1.1.0](https://github.com/megapay-labs/megapay/compare/v1.0.7...v1.1.0) (2025-09-26) ## [1.1.0](https://github.com/telecart-labs/telecart/compare/v1.0.7...v1.1.0) (2025-09-26)
--- ---
## [1.0.7](https://github.com/megapay-labs/megapay/compare/v1.0.6...v1.0.7) (2025-09-26) ## [1.0.7](https://github.com/telecart-labs/telecart/compare/v1.0.6...v1.0.7) (2025-09-26)
### Features ### Features
##### Categories ##### Categories
* Added animations for categories list ([b7b255](https://github.com/megapay-labs/megapay/commit/b7b255887db2d04b8ba70a966382f44c92475df0)) * Added animations for categories list ([b7b255](https://github.com/telecart-labs/telecart/commit/b7b255887db2d04b8ba70a966382f44c92475df0))
* Add skeleton for categories loading ([294e0c](https://github.com/megapay-labs/megapay/commit/294e0cd17e2038f3088b504331ad9b009129e8ed)) * Add skeleton for categories loading ([294e0c](https://github.com/telecart-labs/telecart/commit/294e0cd17e2038f3088b504331ad9b009129e8ed))
* Hide button from categories ([f06606](https://github.com/megapay-labs/megapay/commit/f066069a1b6cf186046e272bc7af61ab46f79c0e)) * Hide button from categories ([f06606](https://github.com/telecart-labs/telecart/commit/f066069a1b6cf186046e272bc7af61ab46f79c0e))
##### Design ##### Design
* Add safe top padding for product page ([a3e5b8](https://github.com/megapay-labs/megapay/commit/a3e5b8b07a28813115662b566284f8622f0b3722)) * Add safe top padding for product page ([a3e5b8](https://github.com/telecart-labs/telecart/commit/a3e5b8b07a28813115662b566284f8622f0b3722))
* Product link in cart ([39a350](https://github.com/megapay-labs/megapay/commit/39a350d517d5d762720236f0e9b682299fd2b746)) * Product link in cart ([39a350](https://github.com/telecart-labs/telecart/commit/39a350d517d5d762720236f0e9b682299fd2b746))
##### Products ##### Products
* Show correct product prices ([35dd0d](https://github.com/megapay-labs/megapay/commit/35dd0de261a4497c01cd6eb54ed0d7032cea5f8b)) * Show correct product prices ([35dd0d](https://github.com/telecart-labs/telecart/commit/35dd0de261a4497c01cd6eb54ed0d7032cea5f8b))
### Bug Fixes ### Bug Fixes
##### Product ##### Product
* Decode html entities for product and category names ([acbfae](https://github.com/megapay-labs/megapay/commit/acbfaebcf415f42c6fb16c6a39d5e10f0776da90)) * Decode html entities for product and category names ([acbfae](https://github.com/telecart-labs/telecart/commit/acbfaebcf415f42c6fb16c6a39d5e10f0776da90))
* Fix error when image not found ([a381b3](https://github.com/megapay-labs/megapay/commit/a381b3a6ee6972775815db382269ec8ab3d31a4f)) * Fix error when image not found ([a381b3](https://github.com/telecart-labs/telecart/commit/a381b3a6ee6972775815db382269ec8ab3d31a4f))
* Fix select product option UI ([22a783](https://github.com/megapay-labs/megapay/commit/22a783f0ef833f5797e798222dce65493d71b34b)) * Fix select product option UI ([22a783](https://github.com/telecart-labs/telecart/commit/22a783f0ef833f5797e798222dce65493d71b34b))
--- ---
## [1.0.6](https://github.com/megapay-labs/megapay/compare/v1.0.5...v1.0.6) (2025-09-24) ## [1.0.6](https://github.com/telecart-labs/telecart/compare/v1.0.5...v1.0.6) (2025-09-24)
### Bug Fixes ### Bug Fixes
* Fix possible foreign error message on megapay page ([016eeb](https://github.com/megapay-labs/megapay/commit/016eeb445db7ce692825d323bed7c1dd815e30af)) * Fix possible foreign error message on telecart page ([016eeb](https://github.com/telecart-labs/telecart/commit/016eeb445db7ce692825d323bed7c1dd815e30af))
##### Categories ##### Categories
* Fix nested lvl > 2 categories rendering ([0f04cb](https://github.com/megapay-labs/megapay/commit/0f04cbf105252b88358095ae5be33fedca6f1e63)) * Fix nested lvl > 2 categories rendering ([0f04cb](https://github.com/telecart-labs/telecart/commit/0f04cbf105252b88358095ae5be33fedca6f1e63))
* Increase max categories count to display up to 100 ([9f6416](https://github.com/megapay-labs/megapay/commit/9f6416a1b7b7f065b558ecd3089c42ef397bd817)) * Increase max categories count to display up to 100 ([9f6416](https://github.com/telecart-labs/telecart/commit/9f6416a1b7b7f065b558ecd3089c42ef397bd817))
##### Database ##### Database
* Fix db connection error when not standard mysql port ([ec5cdf](https://github.com/megapay-labs/megapay/commit/ec5cdfcaa9321cb824c858df91ce4464d6158a2c)) * Fix db connection error when not standard mysql port ([ec5cdf](https://github.com/telecart-labs/telecart/commit/ec5cdfcaa9321cb824c858df91ce4464d6158a2c))
--- ---
## [1.0.5](https://github.com/megapay-labs/megapay/compare/v1.0.4...v1.0.5) (2025-09-24) ## [1.0.5](https://github.com/telecart-labs/telecart/compare/v1.0.4...v1.0.5) (2025-09-24)
### Features ### Features
##### Categories ##### Categories
* Add options to select what categories to show on front page ([9e4022](https://github.com/megapay-labs/megapay/commit/9e4022f64856082fffa7a0264949373319cdf9ff)) * Add options to select what categories to show on front page ([9e4022](https://github.com/telecart-labs/telecart/commit/9e4022f64856082fffa7a0264949373319cdf9ff))
--- ---
## [1.0.4](https://github.com/megapay-labs/megapay/compare/v1.0.3...v1.0.4) (2025-09-24) ## [1.0.4](https://github.com/telecart-labs/telecart/compare/v1.0.3...v1.0.4) (2025-09-24)
### Bug Fixes ### Bug Fixes
* Error when category doesnt have image ([490cbf](https://github.com/megapay-labs/megapay/commit/490cbfacf72095001dccaf374034292ea247e21b)) * Error when category doesnt have image ([490cbf](https://github.com/telecart-labs/telecart/commit/490cbfacf72095001dccaf374034292ea247e21b))
--- ---
## [1.0.3](https://github.com/megapay-labs/megapay/compare/v1.0.2...v1.0.3) (2025-09-24) ## [1.0.3](https://github.com/telecart-labs/telecart/compare/v1.0.2...v1.0.3) (2025-09-24)
### Bug Fixes ### Bug Fixes
* Init exception for some opencart versions ([0cf0c4](https://github.com/megapay-labs/megapay/commit/0cf0c438433f8c1895bef5f490bc0f9af86b0c04)) * Init exception for some opencart versions ([0cf0c4](https://github.com/telecart-labs/telecart/commit/0cf0c438433f8c1895bef5f490bc0f9af86b0c04))
--- ---
## [1.0.2](https://github.com/megapay-labs/megapay/compare/v1.0.1...v1.0.2) (2025-08-16) ## [1.0.2](https://github.com/telecart-labs/telecart/compare/v1.0.1...v1.0.2) (2025-08-16)
### Bug Fixes ### Bug Fixes
* UI fixes ([854dfd](https://github.com/megapay-labs/megapay/commit/854dfdf7f2dba7bc78b53c19f345c1909298c474)) * UI fixes ([854dfd](https://github.com/telecart-labs/telecart/commit/854dfdf7f2dba7bc78b53c19f345c1909298c474))
--- ---
## [1.0.1](https://github.com/megapay-labs/megapay/compare/v1.0.0...v1.0.1) (2025-08-16) ## [1.0.1](https://github.com/telecart-labs/telecart/compare/v1.0.0...v1.0.1) (2025-08-16)
### Bug Fixes ### Bug Fixes
* Check code phrase when configure chat_id ([a0abc1](https://github.com/megapay-labs/megapay/commit/a0abc14c6db91fb6cec14f8aa64297d671e88a7e)) * Check code phrase when configure chat_id ([a0abc1](https://github.com/telecart-labs/telecart/commit/a0abc14c6db91fb6cec14f8aa64297d671e88a7e))
--- ---
## [1.0.0](https://github.com/megapay-labs/megapay/compare/v0.0.2...v1.0.0) (2025-08-16) ## [1.0.0](https://github.com/telecart-labs/telecart/compare/v0.0.2...v1.0.0) (2025-08-16)
### Features ### Features
* Add bot_token validation ([d7df5a](https://github.com/megapay-labs/megapay/commit/d7df5a4b5c8abdf5117c07a9bb7fc7744c23eb1d)) * Add bot_token validation ([d7df5a](https://github.com/telecart-labs/telecart/commit/d7df5a4b5c8abdf5117c07a9bb7fc7744c23eb1d))
* Add carousel for images ([a40089](https://github.com/megapay-labs/megapay/commit/a40089ef553eaf30d813a9e2b2495fe3aa7dd0d4)) * Add carousel for images ([a40089](https://github.com/telecart-labs/telecart/commit/a40089ef553eaf30d813a9e2b2495fe3aa7dd0d4))
* Add Categories ([6a8ea0](https://github.com/megapay-labs/megapay/commit/6a8ea048ea52e6bd3c146b4ec311e9633fce269a)) * Add Categories ([6a8ea0](https://github.com/telecart-labs/telecart/commit/6a8ea048ea52e6bd3c146b4ec311e9633fce269a))
* Add custom BottomButton instead of TG ([b0cc02](https://github.com/megapay-labs/megapay/commit/b0cc0237af12ea5560835092bb808e4bc742c380)) * Add custom BottomButton instead of TG ([b0cc02](https://github.com/telecart-labs/telecart/commit/b0cc0237af12ea5560835092bb808e4bc742c380))
* Add fullscreen viewer ([4ae8d5](https://github.com/megapay-labs/megapay/commit/4ae8d593280774527fbeda3e52d924bd23a12813)) * Add fullscreen viewer ([4ae8d5](https://github.com/telecart-labs/telecart/commit/4ae8d593280774527fbeda3e52d924bd23a12813))
* Add fulscreen mode, dark mode ([252854](https://github.com/megapay-labs/megapay/commit/252854e67ea93716c271e2e20d25b0d73e24e380)) * Add fulscreen mode, dark mode ([252854](https://github.com/telecart-labs/telecart/commit/252854e67ea93716c271e2e20d25b0d73e24e380))
* Add haptictouch to bottom buttons ([51ce6e](https://github.com/megapay-labs/megapay/commit/51ce6ed959e9b673a0cfd9fac614f743b24d582f)) * Add haptictouch to bottom buttons ([51ce6e](https://github.com/telecart-labs/telecart/commit/51ce6ed959e9b673a0cfd9fac614f743b24d582f))
* Add hero block ([3c819e](https://github.com/megapay-labs/megapay/commit/3c819e6c6cf9d25088c2a8024da13e8e0180bde7)) * Add hero block ([3c819e](https://github.com/telecart-labs/telecart/commit/3c819e6c6cf9d25088c2a8024da13e8e0180bde7))
* Add manufacturer to product view ([b25f6d](https://github.com/megapay-labs/megapay/commit/b25f6d3c7335c42487702aa7fff2c5003fd63046)) * Add manufacturer to product view ([b25f6d](https://github.com/telecart-labs/telecart/commit/b25f6d3c7335c42487702aa7fff2c5003fd63046))
* Add new mainpage products options, hide attributes ([d9fd26](https://github.com/megapay-labs/megapay/commit/d9fd26d3541e02d4656d32af547a3e338bbbc4ff)) * Add new mainpage products options, hide attributes ([d9fd26](https://github.com/telecart-labs/telecart/commit/d9fd26d3541e02d4656d32af547a3e338bbbc4ff))
* Add preloader for product page ([b66a02](https://github.com/megapay-labs/megapay/commit/b66a02fd57a2f0233b37bb76b30a360e71333256)) * Add preloader for product page ([b66a02](https://github.com/telecart-labs/telecart/commit/b66a02fd57a2f0233b37bb76b30a360e71333256))
* Add product view page ([f13e12](https://github.com/megapay-labs/megapay/commit/f13e128d03831598ecd058217a0e8874f0831f75)) * Add product view page ([f13e12](https://github.com/telecart-labs/telecart/commit/f13e128d03831598ecd058217a0e8874f0831f75))
* Add telegram api ([b958fe](https://github.com/megapay-labs/megapay/commit/b958feaec751b2e3a4134f925a74c75d5d2d1b42)) * Add telegram api ([b958fe](https://github.com/telecart-labs/telecart/commit/b958feaec751b2e3a4134f925a74c75d5d2d1b42))
* Add telegram safe content area ([1715c0](https://github.com/megapay-labs/megapay/commit/1715c01b1d1b99d4e99a8fe6f40107a384250326)) * Add telegram safe content area ([1715c0](https://github.com/telecart-labs/telecart/commit/1715c01b1d1b99d4e99a8fe6f40107a384250326))
* Add validation and use opencart logger ([9f35ac](https://github.com/megapay-labs/megapay/commit/9f35acf39935416bfbb35735c3749baf0af20995)) * Add validation and use opencart logger ([9f35ac](https://github.com/telecart-labs/telecart/commit/9f35acf39935416bfbb35735c3749baf0af20995))
* Allow only vertical orientation ([fe4188](https://github.com/megapay-labs/megapay/commit/fe4188eb8b3d58cb5fa25c267e0e0ba46effbbac)) * Allow only vertical orientation ([fe4188](https://github.com/telecart-labs/telecart/commit/fe4188eb8b3d58cb5fa25c267e0e0ba46effbbac))
* Cache frontpage products and categories ([5f785e](https://github.com/megapay-labs/megapay/commit/5f785e82e6689283526dd5a218d76908078e7942)) * Cache frontpage products and categories ([5f785e](https://github.com/telecart-labs/telecart/commit/5f785e82e6689283526dd5a218d76908078e7942))
* Create new order ([c057f4](https://github.com/megapay-labs/megapay/commit/c057f4be76544466af62556237f7031c874f5f51)) * Create new order ([c057f4](https://github.com/telecart-labs/telecart/commit/c057f4be76544466af62556237f7031c874f5f51))
* Deny direct access to the spa ([41e74b](https://github.com/megapay-labs/megapay/commit/41e74bad121d76b9a4be2a2f02822d8323e739cc)) * Deny direct access to the spa ([41e74b](https://github.com/telecart-labs/telecart/commit/41e74bad121d76b9a4be2a2f02822d8323e739cc))
* Diplicate webhook info request ([6249b2](https://github.com/megapay-labs/megapay/commit/6249b218a137e105e64fbfb0b6c8829e2ca01349)) * Diplicate webhook info request ([6249b2](https://github.com/telecart-labs/telecart/commit/6249b218a137e105e64fbfb0b6c8829e2ca01349))
* Display product options ([f47bb4](https://github.com/megapay-labs/megapay/commit/f47bb46751fea79e43a96e2b63afde4cb7ef801b)) * Display product options ([f47bb4](https://github.com/telecart-labs/telecart/commit/f47bb46751fea79e43a96e2b63afde4cb7ef801b))
* Do not check signature if bot token not set ([1d892f](https://github.com/megapay-labs/megapay/commit/1d892f7d090a1ff91f724871e688b18a40df768e)) * Do not check signature if bot token not set ([1d892f](https://github.com/telecart-labs/telecart/commit/1d892f7d090a1ff91f724871e688b18a40df768e))
* Encode images to webp for telegram mini app ([c282b6](https://github.com/megapay-labs/megapay/commit/c282b6ea3b5c04ae92708eb1984ad14d2ea46cfa)) * Encode images to webp for telegram mini app ([c282b6](https://github.com/telecart-labs/telecart/commit/c282b6ea3b5c04ae92708eb1984ad14d2ea46cfa))
* Expand mini app on mounted ([1e454b](https://github.com/megapay-labs/megapay/commit/1e454b8f2387d9a4e2e4316253d7f8bddadccc1c)) * Expand mini app on mounted ([1e454b](https://github.com/telecart-labs/telecart/commit/1e454b8f2387d9a4e2e4316253d7f8bddadccc1c))
* Fix module name in admin ([9770a0](https://github.com/megapay-labs/megapay/commit/9770a09fc0abe57d7b97137c9fef4bfaf5687278)) * Fix module name in admin ([9770a0](https://github.com/telecart-labs/telecart/commit/9770a09fc0abe57d7b97137c9fef4bfaf5687278))
* Infinity scroll, load more, resore scroll ([bb2ee3](https://github.com/megapay-labs/megapay/commit/bb2ee38118e8626f8d85070047e256ad8305c1e5)) * Infinity scroll, load more, resore scroll ([bb2ee3](https://github.com/telecart-labs/telecart/commit/bb2ee38118e8626f8d85070047e256ad8305c1e5))
* Make two columns grid for product list ([34bd64](https://github.com/megapay-labs/megapay/commit/34bd64e9025fbd61cd3c64c1e9a9bebb4bf98e5d)) * Make two columns grid for product list ([34bd64](https://github.com/telecart-labs/telecart/commit/34bd64e9025fbd61cd3c64c1e9a9bebb4bf98e5d))
* Product options, speedup home page, themes ([e3cc0d](https://github.com/megapay-labs/megapay/commit/e3cc0d4b10edf3a7c655a8e6d9a39ca587d6ecbc)) * Product options, speedup home page, themes ([e3cc0d](https://github.com/telecart-labs/telecart/commit/e3cc0d4b10edf3a7c655a8e6d9a39ca587d6ecbc))
* Remove cache, refactor ([7404ec](https://github.com/megapay-labs/megapay/commit/7404ecb33e1289439a3b4b9b5926175fe5d3872d)) * Remove cache, refactor ([7404ec](https://github.com/telecart-labs/telecart/commit/7404ecb33e1289439a3b4b9b5926175fe5d3872d))
* Remove prefilled fields in checkout ([33b350](https://github.com/megapay-labs/megapay/commit/33b3500aa470438963af90ee2edccdff9a27233d)) * Remove prefilled fields in checkout ([33b350](https://github.com/telecart-labs/telecart/commit/33b3500aa470438963af90ee2edccdff9a27233d))
* Safe-top and search ([a8bb5e](https://github.com/megapay-labs/megapay/commit/a8bb5eb493ab329bebca8c7903d4facf4a22d76a)) * Safe-top and search ([a8bb5e](https://github.com/telecart-labs/telecart/commit/a8bb5eb493ab329bebca8c7903d4facf4a22d76a))
* Search component and loading splashscreen ([2fb841](https://github.com/megapay-labs/megapay/commit/2fb841ef08027eeabdade90d9a4725ea602b3f48)) * Search component and loading splashscreen ([2fb841](https://github.com/telecart-labs/telecart/commit/2fb841ef08027eeabdade90d9a4725ea602b3f48))
* Show tg app link ([b1ea16](https://github.com/megapay-labs/megapay/commit/b1ea169e2f83cd3d3108d9d11d2b9bb8ee234211)) * Show tg app link ([b1ea16](https://github.com/telecart-labs/telecart/commit/b1ea169e2f83cd3d3108d9d11d2b9bb8ee234211))
* UI changes ([d522cb](https://github.com/megapay-labs/megapay/commit/d522cbef8389adb05cc6e70ed6665db37915233c)) * UI changes ([d522cb](https://github.com/telecart-labs/telecart/commit/d522cbef8389adb05cc6e70ed6665db37915233c))
* Ui improvements, show only active products, limit max page for infinity scroll ([d499d7](https://github.com/megapay-labs/megapay/commit/d499d7d846d55cc158306160c51d4b871f5b6376)) * Ui improvements, show only active products, limit max page for infinity scroll ([d499d7](https://github.com/telecart-labs/telecart/commit/d499d7d846d55cc158306160c51d4b871f5b6376))
* Update styles ([ca3a59](https://github.com/megapay-labs/megapay/commit/ca3a59f43ae19f9c8417993e45c63f29696f46c8)) * Update styles ([ca3a59](https://github.com/telecart-labs/telecart/commit/ca3a59f43ae19f9c8417993e45c63f29696f46c8))
##### Admin ##### Admin
* Correct getting chat_id ([1e80fd](https://github.com/megapay-labs/megapay/commit/1e80fdb2ebaf47e39a6cbd45438860428146aac6)) * Correct getting chat_id ([1e80fd](https://github.com/telecart-labs/telecart/commit/1e80fdb2ebaf47e39a6cbd45438860428146aac6))
* Correct merge new default settings after initializing app ([469077](https://github.com/megapay-labs/megapay/commit/469077d0c9006f3bcfffcecf4454f2e5e4492fac)) * Correct merge new default settings after initializing app ([469077](https://github.com/telecart-labs/telecart/commit/469077d0c9006f3bcfffcecf4454f2e5e4492fac))
* Update disclaimer text ([133bad](https://github.com/megapay-labs/megapay/commit/133badf45b9727fbf2bee7c9b9f74ff274fa3cc8)) * Update disclaimer text ([133bad](https://github.com/telecart-labs/telecart/commit/133badf45b9727fbf2bee7c9b9f74ff274fa3cc8))
##### App ##### App
* Add maintenance mode ([2752ec](https://github.com/megapay-labs/megapay/commit/2752ec3dd18261af9894c8a28a6775bdb22301c3)) * Add maintenance mode ([2752ec](https://github.com/telecart-labs/telecart/commit/2752ec3dd18261af9894c8a28a6775bdb22301c3))
* Telegram init data signature validator ([350ec4](https://github.com/megapay-labs/megapay/commit/350ec4f64bf6534e57cf613e6b38d39a052fd646)) * Telegram init data signature validator ([350ec4](https://github.com/telecart-labs/telecart/commit/350ec4f64bf6534e57cf613e6b38d39a052fd646))
##### Order ##### Order
* Add success haptic for order created event ([858be6](https://github.com/megapay-labs/megapay/commit/858be67c89130ab291b34d8bd7fb4340b6fff422)) * Add success haptic for order created event ([858be6](https://github.com/telecart-labs/telecart/commit/858be67c89130ab291b34d8bd7fb4340b6fff422))
* Order default status and customer group ([14d42c](https://github.com/megapay-labs/megapay/commit/14d42c6ecb1967cc626c57ae7ccb60f66b361aec)) * Order default status and customer group ([14d42c](https://github.com/telecart-labs/telecart/commit/14d42c6ecb1967cc626c57ae7ccb60f66b361aec))
* Order process enchancements ([85101b](https://github.com/megapay-labs/megapay/commit/85101b988140c1d0114d3176115aab0864011b16)) * Order process enchancements ([85101b](https://github.com/telecart-labs/telecart/commit/85101b988140c1d0114d3176115aab0864011b16))
* WIP: telegram notifications ([454bd3](https://github.com/megapay-labs/megapay/commit/454bd39f1f12a6fa004f80c3b13ebc17032a35f9)) * WIP: telegram notifications ([454bd3](https://github.com/telecart-labs/telecart/commit/454bd39f1f12a6fa004f80c3b13ebc17032a35f9))
##### Orders ##### Orders
* Tg notifications, ya metrika, meta tags ([86d0fa](https://github.com/megapay-labs/megapay/commit/86d0fa95941fd2b1d491de8280817d0e80b461f2)) * Tg notifications, ya metrika, meta tags ([86d0fa](https://github.com/telecart-labs/telecart/commit/86d0fa95941fd2b1d491de8280817d0e80b461f2))
##### Product ##### Product
* Change router history driver, change add to cart behaviour ([ebc352](https://github.com/megapay-labs/megapay/commit/ebc352dcdfcf08694d2590ee94c9e799e795a2fc)) * Change router history driver, change add to cart behaviour ([ebc352](https://github.com/telecart-labs/telecart/commit/ebc352dcdfcf08694d2590ee94c9e799e795a2fc))
* Display attributes ([63adf9](https://github.com/megapay-labs/megapay/commit/63adf96908137ab0c173415f77278ee7483a2fb8)) * Display attributes ([63adf9](https://github.com/telecart-labs/telecart/commit/63adf96908137ab0c173415f77278ee7483a2fb8))
##### Shop ##### Shop
* Change grid image resize algorythm ([c3c256](https://github.com/megapay-labs/megapay/commit/c3c25619326e292575236979e389f8ddb68b6958)) * Change grid image resize algorythm ([c3c256](https://github.com/telecart-labs/telecart/commit/c3c25619326e292575236979e389f8ddb68b6958))
##### Style ##### Style
* Change pagination swiper styles ([50bf90](https://github.com/megapay-labs/megapay/commit/50bf9061be778b37f7f6869f4c39a4833af31b1d)) * Change pagination swiper styles ([50bf90](https://github.com/telecart-labs/telecart/commit/50bf9061be778b37f7f6869f4c39a4833af31b1d))
### Bug Fixes ### Bug Fixes
* Add CORS headers, make ci builds as preleases ([551535](https://github.com/megapay-labs/megapay/commit/55153531fb4899d0f3e699b70231d32290800ee2)) * Add CORS headers, make ci builds as preleases ([551535](https://github.com/telecart-labs/telecart/commit/55153531fb4899d0f3e699b70231d32290800ee2))
* Add route names ([47bb2c](https://github.com/megapay-labs/megapay/commit/47bb2cae85e9a16b0076898cd6265512c3adfc3c)) * Add route names ([47bb2c](https://github.com/telecart-labs/telecart/commit/47bb2cae85e9a16b0076898cd6265512c3adfc3c))
* Change hardcoded axios url ([4bb983](https://github.com/megapay-labs/megapay/commit/4bb983e4af53baf2a7a5aa39f15b5389906a4c71)) * Change hardcoded axios url ([4bb983](https://github.com/telecart-labs/telecart/commit/4bb983e4af53baf2a7a5aa39f15b5389906a4c71))
* Correct back button work ([08af20](https://github.com/megapay-labs/megapay/commit/08af204d7403572dbc45f3a74e13cf5d3d560a42)) * Correct back button work ([08af20](https://github.com/telecart-labs/telecart/commit/08af204d7403572dbc45f3a74e13cf5d3d560a42))
* Correct controller class ([5af66d](https://github.com/megapay-labs/megapay/commit/5af66d228a3defbc6f0b4fd15a9e2a3c192bf41d)) * Correct controller class ([5af66d](https://github.com/telecart-labs/telecart/commit/5af66d228a3defbc6f0b4fd15a9e2a3c192bf41d))
* Corrent telegram mini app url in settings ([ea2a60](https://github.com/megapay-labs/megapay/commit/ea2a60b59b20d2bede9d6884349b09e55e345774)) * Corrent telegram mini app url in settings ([ea2a60](https://github.com/telecart-labs/telecart/commit/ea2a60b59b20d2bede9d6884349b09e55e345774))
* Exception if no images ([9bcf32](https://github.com/megapay-labs/megapay/commit/9bcf32841ebd4663b5c6bd5e855b18e8cd486e45)) * Exception if no images ([9bcf32](https://github.com/telecart-labs/telecart/commit/9bcf32841ebd4663b5c6bd5e855b18e8cd486e45))
* Fullscreen slide index ([4114c3](https://github.com/megapay-labs/megapay/commit/4114c3366e4090e41e29bf6e48fe5f54d0dd4a9c)) * Fullscreen slide index ([4114c3](https://github.com/telecart-labs/telecart/commit/4114c3366e4090e41e29bf6e48fe5f54d0dd4a9c))
* Glitch ([db24be](https://github.com/megapay-labs/megapay/commit/db24be6f92bbe485985892ea017f4e4ef457cd52)) * Glitch ([db24be](https://github.com/telecart-labs/telecart/commit/db24be6f92bbe485985892ea017f4e4ef457cd52))
* Icon error ([19911c](https://github.com/megapay-labs/megapay/commit/19911c8f871e456c51836c3d07add3f066744ace)) * Icon error ([19911c](https://github.com/telecart-labs/telecart/commit/19911c8f871e456c51836c3d07add3f066744ace))
* Infinity scroll, init data in base64 ([f2f161](https://github.com/megapay-labs/megapay/commit/f2f1618e0ee591bc58a830a333b1f759b0a860d6)) * Infinity scroll, init data in base64 ([f2f161](https://github.com/telecart-labs/telecart/commit/f2f1618e0ee591bc58a830a333b1f759b0a860d6))
* Night theme ([06a6dc](https://github.com/megapay-labs/megapay/commit/06a6dca656871a920092dc6767990ab70b9fc6c2)) * Night theme ([06a6dc](https://github.com/telecart-labs/telecart/commit/06a6dca656871a920092dc6767990ab70b9fc6c2))
* Router in opencart ([ad92db](https://github.com/megapay-labs/megapay/commit/ad92dbfad48f993e2393c0e235083614581ae0c6)) * Router in opencart ([ad92db](https://github.com/telecart-labs/telecart/commit/ad92dbfad48f993e2393c0e235083614581ae0c6))
* Router scroll scrollBehavior ([08d245](https://github.com/megapay-labs/megapay/commit/08d2453df92ffc89c5e6c4e264370d8b9c32a432)) * Router scroll scrollBehavior ([08d245](https://github.com/telecart-labs/telecart/commit/08d2453df92ffc89c5e6c4e264370d8b9c32a432))
* Totals ([eb1f1d](https://github.com/megapay-labs/megapay/commit/eb1f1dc9c1de7c4733d0117257f7902f145614b2)) * Totals ([eb1f1d](https://github.com/telecart-labs/telecart/commit/eb1f1dc9c1de7c4733d0117257f7902f145614b2))
* Watch router ([1ffb1c](https://github.com/megapay-labs/megapay/commit/1ffb1cef12df1bde4330a7c9531b6574a07d2fe6)) * Watch router ([1ffb1c](https://github.com/telecart-labs/telecart/commit/1ffb1cef12df1bde4330a7c9531b6574a07d2fe6))
##### Admin ##### Admin
* Fix shop url ([c61dfd](https://github.com/megapay-labs/megapay/commit/c61dfd824a532512703c207c464954b51dbcce5a)) * Fix shop url ([c61dfd](https://github.com/telecart-labs/telecart/commit/c61dfd824a532512703c207c464954b51dbcce5a))
--- ---
## [0.0.2](https://github.com/megapay-labs/megapay/compare/v0.0.1+a26c8ba...v0.0.2) (2025-07-10) ## [0.0.2](https://github.com/telecart-labs/telecart/compare/v0.0.1+a26c8ba...v0.0.2) (2025-07-10)
### Bug Fixes ### Bug Fixes
* Add CORS headers, make ci builds as preleases ([551535](https://github.com/megapay-labs/megapay/commit/55153531fb4899d0f3e699b70231d32290800ee2)) * Add CORS headers, make ci builds as preleases ([551535](https://github.com/telecart-labs/telecart/commit/55153531fb4899d0f3e699b70231d32290800ee2))
* Correct controller class ([5af66d](https://github.com/megapay-labs/megapay/commit/5af66d228a3defbc6f0b4fd15a9e2a3c192bf41d)) * Correct controller class ([5af66d](https://github.com/telecart-labs/telecart/commit/5af66d228a3defbc6f0b4fd15a9e2a3c192bf41d))
--- ---
## [0.0.1+a26c8ba](https://github.com/megapay-labs/megapay/compare/v0.0.1...v0.0.1+a26c8ba) (2025-07-10) ## [0.0.1+a26c8ba](https://github.com/telecart-labs/telecart/compare/v0.0.1...v0.0.1+a26c8ba) (2025-07-10)
### Bug Fixes ### Bug Fixes
* Add CORS headers, make ci builds as preleases ([551535](https://github.com/megapay-labs/megapay/commit/55153531fb4899d0f3e699b70231d32290800ee2)) * Add CORS headers, make ci builds as preleases ([551535](https://github.com/telecart-labs/telecart/commit/55153531fb4899d0f3e699b70231d32290800ee2))
* Correct controller class ([5af66d](https://github.com/megapay-labs/megapay/commit/5af66d228a3defbc6f0b4fd15a9e2a3c192bf41d)) * Correct controller class ([5af66d](https://github.com/telecart-labs/telecart/commit/5af66d228a3defbc6f0b4fd15a9e2a3c192bf41d))
* Move files to the correct folder ([9735d4](https://github.com/megapay-labs/megapay/commit/9735d48957b7d9947be5a1be18edba8aebc45531)) * Move files to the correct folder ([9735d4](https://github.com/telecart-labs/telecart/commit/9735d48957b7d9947be5a1be18edba8aebc45531))
--- ---
## [0.0.1](https://github.com/megapay-labs/megapay/compare/c3664025ba6b608920a0182799102a207980d7be...v0.0.1) (2025-07-10) ## [0.0.1](https://github.com/telecart-labs/telecart/compare/c3664025ba6b608920a0182799102a207980d7be...v0.0.1) (2025-07-10)
### Features ### Features
* WIP ([846fa6](https://github.com/megapay-labs/megapay/commit/846fa64fb4db9760c4264179098c43e7f53b557c)) * WIP ([846fa6](https://github.com/telecart-labs/telecart/commit/846fa64fb4db9760c4264179098c43e7f53b557c))
--- ---

147
CLAUDE.md Normal file
View File

@@ -0,0 +1,147 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Overview
TeleCart — коммерческий модуль для OpenCart, превращающий магазин в Telegram Mini App. Это монорепозиторий из трёх
частей:
- **PHP-модуль OpenCart** — `module/oc_telegram_shop/upload/` (вся бизнес-логика и собственный фреймворк).
- **SPA** — `frontend/spa/` (Telegram Mini App, витрина для покупателей).
- **Admin** — `frontend/admin/` (панель управления настройками модуля).
`src/` в корне — это распакованная установка OpenCart (генерируется скриптами, **не редактировать**; код модуля
«линкуется» в неё через `make link`).
## Документация по фичам (`docs/features/`)
Контекстная документация хранится в `docs/features/` — по одной поддиректории на фичу, внутри индексный
`CLAUDE.md`. Вложенные `CLAUDE.md` Claude Code подхватывает автоматически при чтении файлов из их директории,
поэтому индекс фичи читается «по требованию», когда работа касается этой фичи.
Перед тем как трогать незнакомую часть системы — загляни в её папку в `docs/features/`. Конвенции, шаблон новой
фичи и **реестр всех фич с прямыми путями** — в [`docs/features/CLAUDE.md`](docs/features/CLAUDE.md). Шаблон для
новой фичи — `docs/features/_TEMPLATE.md`. При изменении фичи поддерживай её доку в актуальном состоянии (код —
источник истины; при расхождении правь доку).
## Где живёт код модуля
Сердце проекта — `module/oc_telegram_shop/upload/oc_telegram_shop/` (далее «корень модуля»). Его структура:
| Каталог | Namespace | Назначение |
|------------------------|-------------------------------|-----------------------------------------------------------------------------------------------------------------------------|
| `framework/` | `Openguru\OpenCartFramework\` | Собственный Laravel-подобный фреймворк: Container (DI), Router, QueryBuilder, Cache, Migrations, Telegram, Http, Translator |
| `src/` | `App\` | API витрины/SPA (catalog). Handlers, Services, Models, DTO, Filters, ServiceProviders |
| `bastion/` | `Bastion\` | API админ-панели. Handlers, Services, Tasks, ScheduledTasks |
| `console/` | `Console\` | CLI-команды (Symfony Console), точка входа `upload/cli.php` |
| `database/migrations/` | — | Миграции БД |
| `configs/` | — | `app.php`, `maintenance.php`, `schedule.php` |
| `tests/` | `Tests\` | `Unit/`, `Integration/`, `Telegram/` |
**Маршрутизация.** Запросы приходят через action-параметр и резолвятся по двум картам:
- `src/routes.php` — действия SPA (`products`, `getCart`, `checkout`, `webhook`, …)
- `bastion/routes.php` — действия админки (`getSettingsForm`, `getTelegramCustomers`, `getDashboardStats`, …)
Каждая запись — `'actionName' => [HandlerClass::class, 'method']`.
**Точки входа OpenCart** (тонкие обёртки, грузят autoload и поднимают `App\ApplicationFactory`):
- `module/.../upload/catalog/controller/extension/tgshop/handle.php` — SPA API
- `module/.../upload/admin/controller/extension/module/tgshop.php` — админка
В продакшене модуль собирается в `oc_telegram_shop.phar` и подключается из `system/library/oc_telegram_shop/`; в dev —
из исходников через `vendor/autoload.php`.
## Архитектурные правила (бэкенд)
Паттерн — MVC-L поверх собственного фреймворка. Соблюдай строго:
- **Совместимость с PHP 7.4.** Минимальные требования OpenCart и модуля — PHP 7.4 (`composer.json`: `^7.4`). Не используй синтаксис и функции PHP 8.0+ (named arguments, `match`, constructor property promotion, `enum`, `readonly`, nullsafe `?->`, union types и т.п.).
- **Dependency Injection через конструктор.** Никогда не создавай зависимости через `new` внутри классов — внедряй их.
Регистрация сервисов идёт в Service Providers (`src/ServiceProviders/`) через `$this->app->singleton(...)`.
- **Query Builder вместо сырого SQL.** Используй `$this->builder->newQuery()->from(...)->where(...)->get()`. Прямой
SQL — только в крайних случаях; никогда не конкатенируй значения в запрос (SQL injection).
- **Слои:** Handler (валидация + вызов сервиса + `JsonResponse`) → Service (бизнес-логика) → Model (доступ к данным). Не
смешивай.
- **Строгая типизация:** `declare(strict_types=1)`, типы аргументов и возвращаемых значений, nullable где нужно. PSR-12.
- **Безопасный доступ к массивам:** `Arr::get($data, 'key', $default)` вместо `$data['key']`.
- **Таблицы:** префикс OpenCart (`oc_`) + собственные таблицы модуля.
## Архитектура фронтенда
Оба приложения — Vue 3, **только `<script setup>` + Composition API**, Pinia (setup-stores), Vue Router.
- **SPA** (`frontend/spa/`): Tailwind CSS, FormKit, Swiper, `ofetch`. Сборка →
`module/.../upload/image/catalog/tgshopspa/`. Тесты — Vitest.
- **Admin** (`frontend/admin/`): PrimeVue (основной UI-кит), Tailwind/DaisyUI, axios, CodeMirror, `vuedraggable`.
Сборка → `module/.../upload/admin/view/javascript/telecart/`.
Конвенции: компоненты — PascalCase, файлы логики — camelCase; обработчики событий с префиксом `handle`; `computed` для
производных значений; `??` (не `||`) для дефолтов чисел/булевых; сложную логику выносить в composables. Ошибки API
ловить и показывать через toast (admin: `useToast` из PrimeVue).
**FormBuilder** (admin) — отдельная подсистема визуального конструктора форм. Единственный источник истины — FormKit
Schema JSON (без промежуточных DTO). Drag-and-drop строго на `vuedraggable` (не `@formkit/drag-and-drop`).
Tailwind-классы там идут с префиксом `tw:`.
## Команды
Все `make`-цели выполняются **внутри Docker-контейнера `web`** — нужен запущенный `docker compose`.
**Старт / окружение:**
```bash
make setup # первичная установка: скачать OpenCart, установить, поднять, link
make start # поднять контейнеры
make dev # link + параллельно vite dev для SPA и Admin
make dev-spa # только SPA (vite)
make dev-admin # только Admin (vite)
make link # «слинковать» код модуля в установку OpenCart (после правок структуры)
make ssh # шелл в контейнер в каталоге модуля
```
**Тесты (PHP, PHPUnit в контейнере):**
```bash
make test # все тесты (--testdox)
make test-unit # tests/Unit
make test-integration # tests/Integration
make test-telegram # tests/Telegram
make test-coverage # HTML-покрытие в coverage/
```
Запуск одного теста — через ssh в контейнер:
```bash
make ssh
./vendor/bin/phpunit --filter testMethodName tests/Unit/SomeTest.php
```
**Тесты фронтенда (SPA):** `cd frontend/spa && npm run test` (Vitest), `npm run test:run` для однократного прогона.
**Линтеры / статический анализ:**
```bash
make lint # PHPStan (level 1) по src, bastion, framework, console
make phpcs # PHP_CodeSniffer PSR-12 (проверка)
make phpcbf # PHP_CodeSniffer PSR-12 (автофикс)
```
Admin-фронтенд: `cd frontend/admin && npm run lint` (oxlint + eslint), `npm run format` (prettier).
**Сборка / релиз:**
```bash
make phar # собрать oc_telegram_shop.phar (через docker/build.dockerfile)
make ocmod # применить OCMOD-модификации
make cli ARGS="..." # запустить console-команду модуля
make changelog # сгенерировать CHANGELOG из conventional commits
make release # changelog + commit релиза
```
## Коммиты
Используются **Conventional Commits** (`feat:`, `fix:`, `chore:`, `build(deps):` …) — CHANGELOG и версии генерируются
автоматически. **Не делать `git commit`/`git push` без явного указания пользователя.**

View File

@@ -42,9 +42,12 @@ dev-spa:
dev-admin: dev-admin:
rm -rf module/oc_telegram_shop/upload/admin/view/javascript && \ rm -rf module/oc_telegram_shop/upload/admin/view/javascript && \
rm -rf module/oc_telegram_shop/upload/system/library/oc_telegram_shop && \ rm -rf module/oc_telegram_shop/upload/system/library/oc_telegram_shop && \
rm -rf src/upload/admin/view/javascript/megapay && \ rm -rf src/upload/admin/view/javascript/telecart && \
cd frontend/admin && npm run dev cd frontend/admin && npm run dev
ocmod:
./scripts/apply_ocmod.sh
lint: lint:
docker compose exec -w /module/oc_telegram_shop/upload/oc_telegram_shop web bash -c "./vendor/bin/phpstan analyse" docker compose exec -w /module/oc_telegram_shop/upload/oc_telegram_shop web bash -c "./vendor/bin/phpstan analyse"
@@ -70,8 +73,8 @@ test-coverage:
docker compose exec -w /module/oc_telegram_shop/upload/oc_telegram_shop web bash -c "./vendor/bin/phpunit --coverage-html coverage tests/" docker compose exec -w /module/oc_telegram_shop/upload/oc_telegram_shop web bash -c "./vendor/bin/phpunit --coverage-html coverage tests/"
phar: phar:
docker build -t megapay_local_build -f ./deployment/build.dockerfile . && \ docker build -t telecart_local_build -f ./docker/build.dockerfile . && \
docker run -v "./src/upload/system/library/oc_telegram_shop:/build" megapay_local_build sh -c 'sh /scripts/build_phar.sh' docker run -v "./src/upload/system/library/oc_telegram_shop:/build" telecart_local_build sh -c 'sh /scripts/build_phar.sh'
cli: cli:
docker compose exec -w /module/oc_telegram_shop/upload web bash -c "/usr/local/bin/php cli.php $(ARGS)" docker compose exec -w /module/oc_telegram_shop/upload web bash -c "/usr/local/bin/php cli.php $(ARGS)"

View File

@@ -1 +1,83 @@
# Demo code for interviewing # TeleCart 2.0
[![CI/CD](https://github.com/telecart-labs/telecart/actions/workflows/main.yaml/badge.svg)](https://github.com/telecart-labs/telecart/actions/workflows/main.yaml)
[![License](https://img.shields.io/badge/license-Commercial-red.svg)](LICENSE)
[![PHP Version](https://img.shields.io/badge/PHP-%3E%3D7.4-blue.svg)](https://php.net)
[![Vue.js](https://img.shields.io/badge/Vue.js-3.5.22-4FC08D?logo=vue.js&logoColor=white)](https://vuejs.org)
[![Docker](https://img.shields.io/badge/Docker-Ready-2496ED?logo=docker&logoColor=white)](docker-compose.yaml)
[![OpenCart](https://img.shields.io/badge/OpenCart-Module-FF6B35?logo=opencart)](https://www.opencart.com)
Telegram магазин на базе OpenCart. Позволяет создать полноценный интернет-магазин в виде Telegram Mini App.
## 🚀 Возможности
- 📱 Telegram Mini App интерфейс
- 🛒 Полноценная корзина и оформление заказов
- 🎨 Адаптивный дизайн с поддержкой темной/светлой темы
- 📊 Административная панель для управления настройками
- 🔍 Поиск и фильтрация товаров
- 📦 Интеграция с OpenCart
## 🛠 Технологии
- **Backend**: PHP (OpenCart), Composer
- **Frontend**: Vue.js 3, Vite, Tailwind CSS, Pinia
- **Инфраструктура**: Docker, Docker Compose
- **Тестирование**: PHPUnit, Vitest
## 📦 Структура проекта
```
telecart/
├── frontend/
│ ├── admin/ # Административная панель
│ └── spa/ # Telegram Mini App
├── module/ # Модуль OpenCart
├── docker/ # Docker конфигурации
└── src/ # Исходный код OpenCart
```
## 🚀 Быстрый старт
### Требования
- Docker и Docker Compose
- PHP >= 7.4
- Node.js >= 20.19.0 или >= 22.12.0
### Установка
```bash
# Клонировать репозиторий
git clone git@github.com:telecart-labs/telecart.git
cd telecart
# Настроить проект
make setup
# Запустить в режиме разработки
make dev
```
### Доступные команды
- `make setup` - Первоначальная настройка проекта
- `make start` - Запустить контейнеры
- `make stop` - Остановить контейнеры
- `make dev` - Запустить в режиме разработки
- `make dev-admin` - Запустить админ-панель в режиме разработки
- `make test` - Запустить тесты
- `make lint` - Проверить код линтером
## 📝 Лицензия
Это коммерческий платный модуль. Все права защищены. Использование модуля возможно только после приобретения лицензии.
## 🤝 Вклад в проект
Мы приветствуем вклад в развитие проекта! Пожалуйста, создавайте issue и pull request'ы.
## 📄 Документация
Дополнительная документация находится в папке [`docs/`](docs/).

View File

@@ -1,12 +0,0 @@
APP_DEBUG=true
PULSE_API_HOST=https://pulse.megapay.pro/api/
PULSE_HEARTBEAT_SECRET=c5261f5d-529e-45ad-a69c-9778b755b7cb
MEGAPAY_CACHE_DRIVER=redis
#MEGAPAY_REDIS_HOST=redis
#MEGAPAY_REDIS_PORT=6379
#MEGAPAY_REDIS_DATABASE=0
SENTRY_ENABLED=false
SENTRY_DSN=
SENTRY_ENABLE_LOGS=false

View File

@@ -1,11 +0,0 @@
APP_DEBUG=false
PULSE_API_HOST=https://pulse.megapay.pro/api/
PULSE_HEARTBEAT_SECRET=c5261f5d-529e-45ad-a69c-9778b755b7cb
MEGAPAY_CACHE_DRIVER=mysql
MEGAPAY_REDIS_HOST=redis
MEGAPAY_REDIS_PORT=6379
MEGAPAY_REDIS_DATABASE=0
SENTRY_ENABLED=false
SENTRY_DSN=
SENTRY_ENABLE_LOGS=false

View File

@@ -1,58 +0,0 @@
<?php
namespace App\Handlers;
use Openguru\OpenCartFramework\Config\Settings;
use Openguru\OpenCartFramework\Http\Request;
use Openguru\OpenCartFramework\Scheduler\SchedulerService;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
class CronHandler
{
private Settings $settings;
private SchedulerService $schedulerService;
public function __construct(Settings $settings, SchedulerService $schedulerService)
{
$this->settings = $settings;
$this->schedulerService = $schedulerService;
}
/**
* Запуск планировщика по HTTP (для cron-job.org и аналогов).
* Требует api_key в query, совпадающий с настройкой cron.api_key.
*/
public function runSchedule(Request $request): JsonResponse
{
$mode = $this->settings->get('cron.mode', 'disabled');
if ($mode !== 'cron_job_org') {
return new JsonResponse(['error' => 'Scheduler is not in cron-job.org mode'], Response::HTTP_FORBIDDEN);
}
$apiKey = $request->get('api_key', '');
$expectedKey = $this->settings->get('cron.api_key', '');
if ($expectedKey === '' || $apiKey === '' || !hash_equals($expectedKey, $apiKey)) {
return new JsonResponse(['error' => 'Invalid or missing API key'], Response::HTTP_UNAUTHORIZED);
}
// Увеличиваем лимит времени выполнения при запуске по HTTP, чтобы снизить риск timeout
$limit = 300; // 5 минут
if (function_exists('set_time_limit')) {
@set_time_limit($limit);
}
if (function_exists('ini_set')) {
@ini_set('max_execution_time', (string) $limit);
}
$result = $this->schedulerService->run(true);
$data = [
'success' => true,
'executed' => count($result->executed),
'failed' => count($result->failed),
'skipped' => count($result->skipped),
];
return new JsonResponse($data);
}
}

View File

@@ -1,37 +0,0 @@
<?php
declare(strict_types=1);
namespace Bastion\Services;
use Openguru\OpenCartFramework\Config\Settings;
class CronApiKeyRegenerator
{
private Settings $settings;
private SettingsService $settingsUpdateService;
public function __construct(Settings $settings, SettingsService $settingsUpdateService)
{
$this->settings = $settings;
$this->settingsUpdateService = $settingsUpdateService;
}
/**
* Генерирует новый API-ключ для URL cron-job.org и сохраняет в настройки.
*
* @return string новый api_key
*/
public function regenerate(): string
{
$newApiKey = bin2hex(random_bytes(32));
$all = $this->settings->getAll();
if (! isset($all['cron'])) {
$all['cron'] = [];
}
$all['cron']['api_key'] = $newApiKey;
$this->settingsUpdateService->update($all);
return $newApiKey;
}
}

View File

@@ -1,28 +0,0 @@
<?php
use Openguru\OpenCartFramework\Migrations\Migration;
return new class extends Migration {
public function up(): void
{
$sql = <<<SQL
CREATE TABLE IF NOT EXISTS `megapay_scheduled_jobs` (
`id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`task` varchar(1024) COLLATE utf8mb4_unicode_ci NOT NULL,
`is_enabled` tinyint(1) NOT NULL DEFAULT 1,
`cron_expression` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,
`last_success_at` DATETIME DEFAULT NULL,
`last_duration_seconds` FLOAT UNSIGNED NOT NULL DEFAULT 0.0,
`failed_at` DATETIME DEFAULT NULL,
`failed_reason` varchar(1024) COLLATE utf8mb4_unicode_ci DEFAULT NULL
)
ENGINE=InnoDB
DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_unicode_ci
SQL;
$this->database->statement($sql);
}
};

View File

@@ -1,21 +0,0 @@
<?php
use Bastion\ScheduledTasks\MegaPayPulseSendEventsTask;
use Openguru\OpenCartFramework\Migrations\Migration;
return new class extends Migration {
public function up(): void
{
$this->database->insert('megapay_scheduled_jobs', [
'name' => 'megapay_pulse_send_events',
'task' => MegaPayPulseSendEventsTask::class,
'is_enabled' => 0,
'cron_expression' => '*/10 * * * *',
'last_success_at' => null,
'last_duration_seconds' => 0,
'failed_at' => null,
'failed_reason' => null,
]);
}
};

View File

@@ -1,23 +0,0 @@
<?php
use Bastion\Services\CronApiKeyRegenerator;
use Openguru\OpenCartFramework\Config\Settings;
use Openguru\OpenCartFramework\Migrations\Migration;
return new class extends Migration {
public function up(): void
{
$settings = $this->app->get(Settings::class);
$currentKey = $settings->get('cron.api_key', '');
if ($currentKey !== '') {
$this->logger->info('cron.api_key already set, migration skipped');
return;
}
$regenerator = $this->app->get(CronApiKeyRegenerator::class);
$regenerator->regenerate();
$this->logger->info('cron.api_key initialized');
}
};

View File

@@ -1,93 +0,0 @@
<?php
declare(strict_types=1);
namespace Openguru\OpenCartFramework\Scheduler\Models;
use Carbon\Carbon;
use Carbon\CarbonTimeZone;
use Openguru\OpenCartFramework\QueryBuilder\Builder;
class ScheduledJob
{
private const TABLE_NAME = 'megapay_scheduled_jobs';
private Builder $builder;
public function __construct(Builder $builder)
{
$this->builder = $builder;
}
public function all(): array
{
return $this->builder->newQuery()
->from(self::TABLE_NAME)
->get();
}
public function getEnabledScheduledJobs(): array
{
return $this->builder->newQuery()
->from(self::TABLE_NAME)
->where('is_enabled', '=', 1)
->get();
}
public function enable(int $id): bool
{
return $this->builder->newQuery()
->where('id', '=', $id)
->update(self::TABLE_NAME, [
'is_enabled' => 1,
]);
}
public function disable(int $id): bool
{
return $this->builder->newQuery()
->where('id', '=', $id)
->update(self::TABLE_NAME, [
'is_enabled' => 0,
]);
}
public function updateCronExpression(int $id, string $cronExpression): bool
{
return $this->builder->newQuery()
->where('id', '=', $id)
->update(self::TABLE_NAME, [
'cron_expression' => mb_substr($cronExpression, 0, 64),
]);
}
public function updateLastSuccessAt(int $id, float $durationSeconds): void
{
$this->builder->newQuery()
->where('id', '=', $id)
->update(self::TABLE_NAME, [
'last_success_at' => Carbon::now(),
'last_duration_seconds' => $durationSeconds,
]);
}
public function updateFailedAt(int $id, string $message): void
{
$this->builder->newQuery()
->where('id', '=', $id)
->update(self::TABLE_NAME, [
'failed_at' => Carbon::now(),
'failed_reason' => mb_substr($message, 0, 1000),
]);
}
public function clearFailedInfo(int $id): void
{
$this->builder->newQuery()
->where('id', '=', $id)
->update(self::TABLE_NAME, [
'failed_at' => null,
'failed_reason' => null,
]);
}
}

View File

@@ -1,52 +0,0 @@
<?php
namespace Openguru\OpenCartFramework\Scheduler;
use Openguru\OpenCartFramework\Container\Container;
use Openguru\OpenCartFramework\Scheduler\Models\ScheduledJob;
class ScheduleJobRegistry
{
private Container $container;
/** @var Job[] */
private array $jobs = [];
private ScheduledJob $scheduledJob;
public function __construct(Container $container, ScheduledJob $scheduledJob)
{
$this->container = $container;
$this->scheduledJob = $scheduledJob;
}
/**
* @param string|callable|TaskInterface $job
* @param string|null $name
* @return Job
*/
public function add(int $id, $job, ?string $name = null): Job
{
$newJob = new Job($this->container, $id, $job, $name);
$this->jobs[] = $newJob;
return $newJob;
}
/**
* @return Job[]
*/
public function getJobs(): array
{
return $this->jobs;
}
public function loadJobsFromDatabase(): void
{
$jobs = $this->scheduledJob->getEnabledScheduledJobs();
foreach ($jobs as $job) {
$this->add($job['id'], $job['task'], $job['name'])
->at($job['cron_expression']);
}
}
}

View File

@@ -1,189 +0,0 @@
<?php
declare(strict_types=1);
namespace Openguru\OpenCartFramework\Scheduler;
use DateTime;
use Openguru\OpenCartFramework\Cache\CacheInterface;
use Openguru\OpenCartFramework\Config\Settings;
use Openguru\OpenCartFramework\Scheduler\Models\ScheduledJob;
use Psr\Log\LoggerInterface;
use Throwable;
class SchedulerService
{
private LoggerInterface $logger;
private CacheInterface $cache;
private Settings $settings;
private ScheduleJobRegistry $registry;
private const GLOBAL_LOCK_KEY = 'scheduler.global_lock';
private const GLOBAL_LOCK_TTL = 300; // 5 minutes
private ScheduledJob $scheduledJob;
public function __construct(
LoggerInterface $logger,
CacheInterface $cache,
Settings $settings,
ScheduleJobRegistry $registry,
ScheduledJob $scheduledJob
) {
$this->logger = $logger;
$this->cache = $cache;
$this->settings = $settings;
$this->registry = $registry;
$this->scheduledJob = $scheduledJob;
}
// For testing purposes
public function setRegistry(ScheduleJobRegistry $registry): void
{
$this->registry = $registry;
}
public function run(bool $ignoreGlobalLock = false): SchedulerResult
{
$result = new SchedulerResult();
$mode = $this->settings->get('cron.mode', 'disabled');
if ($mode === 'disabled') {
$result->addSkipped('Global', 'Scheduler is disabled');
return $result;
}
if (! $ignoreGlobalLock && $this->isGlobalLocked()) {
$result->addSkipped('Global', 'Global scheduler lock active');
return $result;
}
if (! $ignoreGlobalLock) {
$this->acquireGlobalLock();
}
$this->updateGlobalLastRun();
try {
$this->registry->loadJobsFromDatabase();
foreach ($this->registry->getJobs() as $job) {
$this->processJob($job, $result);
}
} catch (Throwable $e) {
$this->logger->error('Scheduler run failed: ' . $e->getMessage(), ['exception' => $e]);
$result->addFailed('Scheduler', $e->getMessage());
} finally {
if (! $ignoreGlobalLock) {
$this->releaseGlobalLock();
}
}
return $result;
}
private function processJob(Job $job, SchedulerResult $result): void
{
$name = $job->getName();
$id = $job->getId();
// 1. Check if due by Cron expression
if (! $job->isDue()) {
$result->addSkipped($name, 'Not due');
return;
}
// 2. Check Last Run (Prevent running multiple times in the same minute)
if ($this->hasRanRecently($id)) {
$result->addSkipped($name, 'Already ran recently');
return;
}
// 3. Check Lock (Prevent parallel execution)
if ($this->isJobLocked($id)) {
$result->addSkipped($name, 'Job is locked (running)');
return;
}
$this->lockJob($id);
try {
$this->scheduledJob->clearFailedInfo($id);
$startTime = microtime(true);
$job->run();
$duration = microtime(true) - $startTime;
$this->scheduledJob->updateLastSuccessAt($id, $duration);
$this->logger->debug("Job executed: {$name}", ['duration' => $duration]);
$result->addExecuted($name, $duration);
} catch (Throwable $e) {
$this->logger->error("Job failed: {$name}", ['exception' => $e]);
$this->scheduledJob->updateFailedAt($id, $e->getMessage());
$result->addFailed($name, $e->getMessage());
} finally {
$this->updateLastRun($id);
$this->unlockJob($id);
}
}
private function isGlobalLocked(): bool
{
return (bool) $this->cache->get(self::GLOBAL_LOCK_KEY);
}
private function acquireGlobalLock(): void
{
$this->cache->set(self::GLOBAL_LOCK_KEY, 1, self::GLOBAL_LOCK_TTL);
}
private function releaseGlobalLock(): void
{
$this->cache->delete(self::GLOBAL_LOCK_KEY);
}
private function isJobLocked(int $id): bool
{
return (bool) $this->cache->get("scheduler.lock.{$id}");
}
private function lockJob(int $id): void
{
// 30 minutes max execution time for a single job safe-guard
$this->cache->set("scheduler.lock.{$id}", 1, 1800);
}
private function unlockJob(int $id): void
{
$this->cache->delete("scheduler.lock.{$id}");
}
private function hasRanRecently(int $id): bool
{
$lastRun = $this->getLastRun($id);
if (! $lastRun) {
return false;
}
$lastRunDate = (new DateTime())->setTimestamp($lastRun);
$now = new DateTime();
return $lastRunDate->format('Y-m-d H:i') === $now->format('Y-m-d H:i');
}
private function updateLastRun(int $id): void
{
$this->cache->set("scheduler.last_run.{$id}", time());
}
private function updateGlobalLastRun(): void
{
$this->cache->set("scheduler.global_last_run", time());
}
public function getLastRun(int $id): ?int
{
return $this->cache->get("scheduler.last_run.{$id}");
}
}

View File

@@ -1,13 +0,0 @@
<?php
namespace Openguru\OpenCartFramework\Scheduler;
use Openguru\OpenCartFramework\Container\ServiceProvider;
class SchedulerServiceProvider extends ServiceProvider
{
public function register(): void
{
//
}
}

View File

@@ -1,137 +0,0 @@
<?php
declare(strict_types=1);
namespace Openguru\OpenCartFramework\Sentry;
use Closure;
use Sentry\SentrySdk;
use Sentry\Tracing\Span;
use Sentry\Tracing\SpanContext;
use Sentry\Tracing\Transaction;
use Sentry\Tracing\TransactionContext;
use SplObjectStorage;
use function Sentry\init;
use function Sentry\startTransaction;
class SentryService
{
private static ?string $currentAction = null;
public static ?Transaction $currentTransaction = null;
/**
* Stores parent span for each created child span to correctly restore nesting.
*
* @var SplObjectStorage<Span, Span|null>|null
*/
private static ?SplObjectStorage $spanParents = null;
public static array $excludeActions = [
'health',
];
private static function resolveOptionsFromEnv(): array
{
return [
'dsn' => env('SENTRY_DSN'),
'send_default_pii' => true,
'enable_logs' => (bool) filter_var(env('SENTRY_ENABLE_LOGS', false), FILTER_VALIDATE_BOOLEAN),
'traces_sample_rate' => (float) env('SENTRY_TRACES_SAMPLE_RATE', 1.0),
];
}
private static function isSentryEnabled(): bool
{
return self::$currentAction !== null
&& ! in_array(self::$currentAction, self::$excludeActions, true)
&& env('SENTRY_ENABLED', false);
}
public static function init(?string $action): void
{
$options = self::resolveOptionsFromEnv();
self::$currentAction = $action;
if (! self::isSentryEnabled()) {
return;
}
init($options);
$transactionContext = TransactionContext::make()
->setName(self::$currentAction)
->setOp('http.server');
self::$currentTransaction = startTransaction($transactionContext);
SentrySdk::getCurrentHub()->setSpan(self::$currentTransaction);
}
public static function startSpan(string $description, string $op, ?Closure $closure = null): ?Span
{
$parent = self::resolveParent();
if (! self::isSentryEnabled() || ! $parent) {
return null;
}
$context = SpanContext::make()
->setOp($op)
->setDescription($description);
if ($closure) {
$closure($context);
}
$span = $parent->startChild($context);
if (self::$spanParents === null) {
self::$spanParents = new SplObjectStorage();
}
self::$spanParents[$span] = $parent;
SentrySdk::getCurrentHub()->setSpan($span);
return $span;
}
public static function endSpan(?Span $span = null): void
{
if (! $span || ! self::isSentryEnabled()) {
return;
}
$parent = null;
if (self::$spanParents !== null && self::$spanParents->contains($span)) {
$parent = self::$spanParents[$span] ?? null;
self::$spanParents->detach($span);
}
$span->finish();
SentrySdk::getCurrentHub()->setSpan($parent ?? self::$currentTransaction);
}
public static function resolveParent(): ?Span
{
return SentrySdk::getCurrentHub()->getSpan() ?? self::$currentTransaction;
}
public static function measure(string $description, string $op, Closure $closure, ?Closure $params = null): void
{
$span = self::startSpan($description, $op, $params);
try {
$closure();
} finally {
self::endSpan($span);
}
}
public static function finishTransaction(): void
{
if (self::$currentTransaction) {
self::$currentTransaction->finish();
self::$currentTransaction = null;
}
}
}

2
build/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*
!.gitignore

2
cli Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/bash
docker compose exec -w /module/oc_telegram_shop/upload web php cli.php "$@"

View File

@@ -7,6 +7,7 @@ services:
- "./scripts:/scripts" - "./scripts:/scripts"
- "./module:/module" - "./module:/module"
- "./build:/build" - "./build:/build"
# - "/Users/nikitakiselev/code/italy-moda/image/catalog:/web/upload/image/catalog"
ports: ports:
- "8000:80" - "8000:80"
restart: always restart: always
@@ -28,12 +29,12 @@ services:
- XDEBUG_CLIENT_HOST=host.docker.internal - XDEBUG_CLIENT_HOST=host.docker.internal
- XDEBUG_CLIENT_PORT=9003 - XDEBUG_CLIENT_PORT=9003
- XDEBUG_DISCOVER_CLIENT_HOST=false - XDEBUG_DISCOVER_CLIENT_HOST=false
- PHP_IDE_CONFIG=serverName=megapay - PHP_IDE_CONFIG=serverName=telecart
- php.session.gc_maxlifetime=28800 - php.session.gc_maxlifetime=28800
- php.session.cookie_lifetime=0 - php.session.cookie_lifetime=0
- MEGAPAY_REDIS_HOST=redis - TELECART_REDIS_HOST=redis
- MEGAPAY_REDIS_PORT=6379 - TELECART_REDIS_PORT=6379
- MEGAPAY_REDIS_DATABASE=0 - TELECART_REDIS_DATABASE=0
depends_on: depends_on:
- mysql - mysql
- redis - redis
@@ -60,10 +61,10 @@ services:
volumes: volumes:
- mysql_data:/var/lib/mysql - mysql_data:/var/lib/mysql
- ./sql_dumps:/sql_dumps - ./sql_dumps:/sql_dumps
- ./deployment/mysql/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d - ./docker/mysql/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
cron: cron:
image: ghcr.io/megapay-labs/scheduler:latest image: ghcr.io/telecart-labs/scheduler:latest
platform: linux/amd64 platform: linux/amd64
restart: unless-stopped restart: unless-stopped
environment: environment:

View File

@@ -0,0 +1,7 @@
🚀 Вышла версия Telecart v2.0.0!
Система конфигурации главной страницы через блоки - баннеры и карусели на главной странице. Визуальный конструктор формы оформления заказов на базе FormKit, доступно более 15 типов полей с гибкой настройкой. Ещё больше кастомизации магазина с помощью переработанной админки. Управление клиентами Telegram прямо в админ-панели. Улучшенная Яндекс.Метрика, интеграция с электронной коммерцией. Поддержка купонов и подарочных сертификатов. Улучшение UI/UX, поиска, производительности и многое другое. Более 20 нововведений и улучшений. Весь список изменений можно посмотреть в документации: https://docs.telecart.pro/changelog/2.0.0/
Демонстрация работы: https://t.me/OpenGuruStoreBot?startapp
Купить модуль можно у меня напрямую, либо на https://liveopencart.ru/opencart-moduli-shablony/moduli/telecart

43
docs/features/CLAUDE.md Normal file
View File

@@ -0,0 +1,43 @@
# docs/features — документация по фичам
Эта директория хранит описания **фич** проекта TeleCart: по одной поддиректории на фичу.
Цель — дать агенту (и человеку) возможность быстро восстановить контекст по конкретной
части системы, не перечитывая весь код: что фича делает, через какие точки входит, какие
файлы за неё отвечают и как данные текут по слоям.
Вложенные `CLAUDE.md` подхватываются Claude Code автоматически при чтении файлов из их
директории — поэтому индекс фичи читается «по требованию», когда работа касается этой фичи.
## Структура
```
docs/features/
├── CLAUDE.md # этот файл: конвенции + реестр фич
├── _TEMPLATE.md # шаблон для новой фичи (копировать при описании)
└── <feature-slug>/
└── CLAUDE.md # индексный файл конкретной фичи
```
## Конвенции
- **Имя папки** — `kebab-case`, по сути фичи: `cart`, `telegram-webhook`, `form-builder`.
- **Один индекс на фичу** — `<feature-slug>/CLAUDE.md`, заполняется по `_TEMPLATE.md`.
- **Ссылки на код — обязательны.** Указывай реальные пути (`module/.../src/Handlers/CartHandler.php`)
и `action` из карт маршрутов. Код — источник истины; документация лишь навигирует к нему.
- **Не дублируй код.** Описывай назначение, поток и нюансы, а не построчный пересказ — он устареет.
- **Связывай фичи** через ссылки на их папки (например `[checkout](../checkout/CLAUDE.md)`).
- Если описанное в доке расходится с кодом — прав код; обнови доку.
## Как добавить фичу
1. `cp _TEMPLATE.md <feature-slug>/CLAUDE.md` (создав папку).
2. Заполни секции шаблона, опираясь на реальный код.
3. Добавь строку в реестр ниже.
## Реестр фич
| Фича | Путь | Краткое описание |
|------|----------------------------------|-------------------------------------------------------------|
| cart | [cart/CLAUDE.md](cart/CLAUDE.md) | Корзина: наполнение, просмотр, передача в оформление заказа |
<!-- Новые фичи добавляй строкой в таблицу выше, по алфавиту. -->

View File

@@ -0,0 +1,38 @@
# <Название фичи>
> Скопируй этот файл в `docs/features/<feature-slug>/CLAUDE.md` и заполни секции.
> Удали подсказки в кавычках после заполнения. Не оставляй пустых «TODO».
## Назначение
> 13 предложения: какую задачу решает фича и для кого (SPA-покупатель / админ / системное).
## Точки входа
> `action` из карт маршрутов и где они объявлены. Таблица:
| action | Метод | Где объявлен |
|-----------------|-------------------|-------------------------------------------|
| `exampleAction` | `Handler::method` | `src/routes.php` или `bastion/routes.php` |
## Ключевые файлы по слоям
> Путь от корня репозитория. Только то, что реально относится к фиче.
- **Handler:** `module/.../src/Handlers/XxxHandler.php` — валидация, вызов сервиса, `JsonResponse`.
- **Service:** `module/.../src/Services/XxxService.php` — бизнес-логика.
- **Model / данные:** `module/.../src/Models/Xxx.php` или таблицы БД.
- **Фронтенд:** `frontend/spa/...` или `frontend/admin/...` (store, view, компоненты).
- **Регистрация в DI:** `src/ServiceProviders/...` (если есть нестандартная привязка).
## Поток данных
> Кратко: запрос → слои → ответ. Можно списком шагов или стрелками.
## Связанные фичи
> Ссылки на соседние папки: `[checkout](../checkout/CLAUDE.md)`.
## Нюансы и подводные камни
> Неочевидное: ограничения PHP 7.4, особенности OpenCart-реестра, кэш, edge-cases, известные баги.

View File

@@ -0,0 +1,59 @@
# Cart — корзина
## Назначение
Корзина покупателя в SPA (Telegram Mini App): отдаёт текущее содержимое корзины с
посчитанными ценами/итогами, принимает набор позиций из SPA и наполняет ими корзину
OpenCart перед оформлением заказа. Создание самого заказа — за пределами фичи (см.
[order](../order/CLAUDE.md), если описан).
## Точки входа
| action | Метод | Где объявлен |
|------------|-------------------------|------------------|
| `getCart` | `CartHandler::index` | `src/routes.php` |
| `checkout` | `CartHandler::checkout` | `src/routes.php` |
> `storeOrder` (`OrderHandler::store`) относится к оформлению заказа, не к корзине.
## Ключевые файлы по слоям
- **Handler:** `module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/CartHandler.php`
`index()` возвращает содержимое корзины, `checkout()` принимает позиции из SPA
(`productId`, `quantity`, `options`) и кладёт их в корзину через `Cart\Cart::add()`.
- **Service:** `module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/CartService.php`
`getCart()` собирает данные через OpenCart-реестр (`OcRegistryDecorator`): язык
`checkout/cart`, проверки стока, веса, цен, форматирование через `Cart\Currency`.
- **OpenCart-зависимости:** `Cart\Cart`, `Cart\Currency` — нативные классы OpenCart,
внедряются через DI.
- **Фронтенд (SPA):**
- `frontend/spa/src/stores/CartStore.js` — Pinia-стор корзины.
- `frontend/spa/src/ShoppingCart.js` — клиентская логика корзины.
- `frontend/spa/src/views/Cart.vue` — экран корзины.
- `frontend/spa/src/components/CartButton.vue` — кнопка/индикатор.
## Поток данных
**Просмотр:** SPA → `action=getCart``CartHandler::index``CartService::getCart()`
читает корзину из OpenCart-реестра → ответ оборачивается `Utf8Cleaner::clean()`
`JsonResponse { data }`.
**Передача в оформление:** SPA отправляет массив позиций → `action=checkout`
`CartHandler::checkout` маппит `options` (берёт `product_option_value_id`) и для каждой
позиции вызывает `$cart->add(productId, quantity, options)` → возвращает очищенные позиции.
## Связанные фичи
- Оформление заказа (`OrderHandler::store` / `OrderCreateService`).
- Настройки (`SettingsService`) влияют на отображение цен/стока в `getCart`.
## Нюансы и подводные камни
- **PHP 7.4.** Без синтаксиса 8.0+ (см. корневой `CLAUDE.md`).
- **Весь вывод чистится `App\Support\Utf8Cleaner::clean()`** перед отдачей — невалидный
UTF-8 из товаров OpenCart ломает `json_encode`.
- **`getCart` зависит от состояния OpenCart-сессии и реестра** (сток, валюта, логин
покупателя, язык `checkout/cart`) — это не чистая функция, результат меняется от конфига
магазина (`config_stock_checkout`, `config_customer_price`, `config_cart_weight` и т.д.).
- **`checkout` мутирует корзину OpenCart** (`Cart\Cart::add`), а не локальное состояние —
он переносит выбор из SPA в нативную корзину OpenCart.

56
docs/liveopencart.txt Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,168 @@
# Дизайн: честный и отключаемый heartbeat
Дата: 2026-06-30
Статус: согласовано (ожидает финального ревью)
## Проблема
Модуль скрытно отправляет на сервер разработчика (`pulse.telecart.pro`) сведения об
окружении, где он запущен (heartbeat). Покупатель об этом не уведомляется и не может
это отключить. Это нечестно по отношению к покупателю и создаёт риски (GDPR / 152-ФЗ,
претензии маркетплейсов).
## Цель
Сделать heartbeat **честным**: прозрачным и отключаемым, сохранив поведение по умолчанию.
- **Прозрачность:** в админ-панели покупатель видит, что именно собирается, куда
отправляется и зачем.
- **Контроль:** отдельный переключатель, которым покупатель может выключить heartbeat.
- **По умолчанию включено** (текущее поведение сохраняется, но становится видимым и
управляемым).
## Границы (scope)
- **Только heartbeat.** Остальной TeleCart Pulse (Telegram-рассылки, события кампаний)
**не трогаем** — он гейтится наличием `pulse.api_key` и, по позиции владельца, прав не
нарушает.
- Концептуально heartbeat считаем частью фичи **TeleCart Pulse** (одна папка доков), но
у него **отдельный собственный выключатель**, не завязанный на `pulse.api_key` и не
завязанный на секрет подписи. Pulse и heartbeat включаются/выключаются независимо.
## Терминология
- **heartbeat** — периодическая (раз в час по кэшу) отправка информации об окружении:
`domain`, `bot_name`, `php_version`, `module_version`, `opencart_version`,
`opencart_version_core`. Триггерится из SPA при открытии WebApp.
- **TeleCart Pulse** — фича рассылок и аналитики кампаний; heartbeat живёт внутри её
namespace в коде (`framework/TeleCartPulse/`).
## Текущее устройство (как есть)
- `TeleCartPulseService::handleHeartbeat()`
(`module/.../framework/TeleCartPulse/TeleCartPulseService.php:133`) — ставит кэш-флаг
`telecart_pulse_heartbeat` (ttl 3600), дёргает `getMe()`, формирует payload и шлёт POST
на `PULSE_API_HOST/heartbeat`.
- Отправка фактически происходит только если есть `heartbeatPayloadSigner` (строки 168170),
который создаётся в провайдере при наличии секрета (`pulse.heartbeat_secret` /
`PULSE_HEARTBEAT_SECRET`).
- `TeleCartPulseServiceProvider`
(`module/.../framework/TeleCartPulse/TeleCartPulseServiceProvider.php:22-35`) читает
значения из конфига и инжектит их в `TeleCartPulseService` через конструктор.
- **Пользовательской (per-install) настройки для heartbeat сейчас нет.**
## Решение
### Принцип
Использовать существующий паттерн: провайдер читает значение из конфига и **инжектит**
его в сервис через конструктор (как уже делается для `pulse.api_key` и signer). Не вводить
новых механизмов гейтинга.
### 1. Backend
**`module/.../configs/app.php`** — секция `pulse` (строки 8690): добавить флаг с дефолтом
`true`.
```php
'pulse' => [
'api_key' => '',
'heartbeat_enabled' => true, // новый флаг
'batch_size' => 50,
'max_attempts' => 3,
],
```
**`TeleCartPulseServiceProvider`** — прочитать `pulse.heartbeat_enabled` и передать булевый
флаг в конструктор `TeleCartPulseService` (рядом с `apiKey` и `heartbeatSigner`). Привести
к `bool` явно.
**`TeleCartPulseService`** — принять флаг в конструкторе (новое поле `private bool
$heartbeatEnabled`). В `handleHeartbeat()`**ранний `return` в самом начале метода**, до
проверки/установки кэша и до `getMe()`, если флаг выключен:
```php
public function handleHeartbeat(): void
{
if (! $this->heartbeatEnabled) {
return;
}
// ... существующая логика
}
```
Совместимость с PHP 7.4: без named arguments / промоушена свойств / union-типов. Параметр
конструктора — позиционный, со значением по умолчанию `true` для обратной совместимости
вызовов в тестах.
### 2. Frontend (админка) + прозрачность
**`frontend/admin/src/stores/settings.js`** — секция `pulse` (строки 8992): добавить
`heartbeat_enabled` в дефолтное состояние state.
**`frontend/admin/src/views/TeleCartPulseView.vue`** — добавить `ItemBool` с переключателем,
привязанным к `settings.items.pulse.heartbeat_enabled`. В слот-описание — **честное, но
нейтральное по тону раскрытие**:
- перечислить, что отправляется: домен, имя бота, версия PHP, версия модуля, версии
OpenCart;
- указать адрес назначения: `pulse.telecart.pro`;
- описать цель мягко: для улучшения продукта и техподдержки (без акцента на проверке
лицензии).
Точную формулировку владелец отредактирует; черновик в нейтральном тоне готовит реализация.
### 3. Поведение для существующих установок
Дефолт `true` в конфиге мержится с сохранёнными настройками
(`ApplicationFactory`: `Arr::mergeArraysRecursively`). У установок без сохранённого значения
heartbeat остаётся включённым — поведение не меняется, но теперь переключатель виден.
Когда покупатель выключает тумблер и сохраняет, значение пишется в `oc_setting`
(`module_telecart_settings`) и читается на backend.
### 4. Гейтинг — итоговая логика
heartbeat реально отправляется только если **одновременно**: `heartbeat_enabled = true`
**и** есть signer (секрет настроен). Переключатель — пользовательский контроль поверх
существующего серверного гейта; они независимы.
## Тестирование
Unit-тест на `TeleCartPulseService::handleHeartbeat()`:
- `heartbeat_enabled = false` → метод выходит сразу: HTTP-запрос не выполняется, кэш-флаг
`telecart_pulse_heartbeat` **не** ставится, `getMe()` не вызывается.
- `heartbeat_enabled = true` + signer присутствует → payload формируется и уходит (как
сейчас).
Запуск: `make test-unit` (или `./vendor/bin/phpunit --filter ...` через `make ssh`).
## Документация (в этом порядке)
1. **`docs/features/settings/CLAUDE.md`** — каркас системы настроек: путь поля «форма в
админке → `saveSettingsForm` → таблица `oc_setting` (`module_telecart_settings`) →
`Config\Settings` / DTO → backend», и **чек-лист «как добавить новый toggle»** по слоям
(config-дефолт → провайдер/DTO → store → view-компонент `ItemBool`). По шаблону
`docs/features/_TEMPLATE.md`. Добавить строку в реестр в `docs/features/CLAUDE.md`.
2. **`docs/features/telecart-pulse/CLAUDE.md`** — фича Pulse целиком: рассылки, события
кампаний (гейт `api_key`) и heartbeat с новым выключателем. Ссылка на `[[settings]]`.
Добавить строку в реестр.
## Затрагиваемые файлы (сводка)
- `module/.../configs/app.php` — флаг `pulse.heartbeat_enabled`.
- `module/.../framework/TeleCartPulse/TeleCartPulseServiceProvider.php` — чтение и инжект.
- `module/.../framework/TeleCartPulse/TeleCartPulseService.php` — поле + ранний return.
- `frontend/admin/src/stores/settings.js` — дефолт в state.
- `frontend/admin/src/views/TeleCartPulseView.vue``ItemBool` + текст раскрытия.
- `module/.../tests/Unit/...` — тест на гейтинг heartbeat.
- `docs/features/settings/CLAUDE.md`, `docs/features/telecart-pulse/CLAUDE.md`,
`docs/features/CLAUDE.md` (реестр).
## Явно вне scope (YAGNI)
- Не делаем opt-in (по умолчанию остаётся включено).
- Не делаем отдельный consent-баннер на первой установке (достаточно видимого
переключателя с раскрытием).
- Не трогаем события кампаний и рассылки Pulse.
- Не выносим heartbeat в отдельный namespace/фичу из `TeleCartPulse`.

BIN
dumps/italy.sql.zip Normal file

Binary file not shown.

View File

@@ -0,0 +1,9 @@
{
"recommendations": [
"Vue.volar",
"dbaeumer.vscode-eslint",
"EditorConfig.EditorConfig",
"oxc.oxc-vscode",
"esbenp.prettier-vscode"
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -25,7 +25,7 @@
"@primeuix/themes": "^1.2.5", "@primeuix/themes": "^1.2.5",
"@tailwindcss/vite": "^4.1.16", "@tailwindcss/vite": "^4.1.16",
"@vueuse/core": "^14.0.0", "@vueuse/core": "^14.0.0",
"axios": "^1.13.5", "axios": "^1.15.2",
"codemirror": "^6.0.2", "codemirror": "^6.0.2",
"daisyui": "^5.4.2", "daisyui": "^5.4.2",
"js-md5": "^0.8.3", "js-md5": "^0.8.3",
@@ -51,7 +51,7 @@
"npm-run-all2": "^8.0.4", "npm-run-all2": "^8.0.4",
"oxlint": "~1.23.0", "oxlint": "~1.23.0",
"prettier": "3.6.2", "prettier": "3.6.2",
"vite": "^7.1.11", "vite": "^7.3.3",
"vite-plugin-vue-devtools": "^8.0.3" "vite-plugin-vue-devtools": "^8.0.3"
} }
} }

View File

@@ -58,7 +58,7 @@
<li :class="{active: route.name === 'pulse'}"> <li :class="{active: route.name === 'pulse'}">
<RouterLink :to="{name: 'pulse'}"> <RouterLink :to="{name: 'pulse'}">
<i class="fa fa-heartbeat pulse-icon tw:text-red-200"></i> MegaPay Pulse <span class="pulse-beta-label tw:ml-1 tw:px-1.5 tw:py-0.5 tw:text-xs tw:font-semibold tw:text-white tw:rounded">BETA</span> <i class="fa fa-heartbeat pulse-icon tw:text-red-200"></i> TeleCart Pulse <span class="pulse-beta-label tw:ml-1 tw:px-1.5 tw:py-0.5 tw:text-xs tw:font-semibold tw:text-white tw:rounded">BETA</span>
</RouterLink> </RouterLink>
</li> </li>

View File

@@ -43,7 +43,7 @@ legend.p-fieldset-legend {
margin-bottom: 0; margin-bottom: 0;
} }
.megapay-admin-app { .telecart-admin-app {
color: var(--color-slate-700); color: var(--color-slate-700);
} }

View File

@@ -1,48 +0,0 @@
<template>
<Dropdown
:model-value="modelValue"
:options="options"
option-label="label"
option-value="value"
placeholder="Интервал"
class="tw:w-[14rem] tw:shrink-0"
@update:model-value="$emit('update:modelValue', $event ?? '')"
/>
</template>
<script setup>
import { computed } from 'vue';
import Dropdown from 'primevue/dropdown';
const props = defineProps({
modelValue: {
type: String,
default: '',
},
});
defineEmits(['update:modelValue']);
/** Пресеты интервалов (label — для отображения, value — cron expression) */
const PRESETS = [
{ label: 'Раз в минуту', value: '* * * * *' },
{ label: 'Раз в 5 минут', value: '*/5 * * * *' },
{ label: 'Раз в 10 минут', value: '*/10 * * * *' },
{ label: 'Раз в час', value: '0 * * * *' },
{ label: 'Раз в 3 часа', value: '0 */3 * * *' },
{ label: 'Раз в 6 часов', value: '0 */6 * * *' },
{ label: 'Раз в сутки', value: '0 0 * * *' },
{ label: 'Раз в неделю', value: '0 0 * * 0' },
];
const presetValues = new Set(PRESETS.map((p) => p.value));
/** Только пресеты; если текущее значение не из списка — показываем его в списке (уже сохранённое в БД), чтобы не терять отображение */
const options = computed(() => {
const current = props.modelValue ?? '';
if (!current || presetValues.has(current)) {
return PRESETS;
}
return [{ label: current, value: current }, ...PRESETS];
});
</script>

View File

@@ -1,104 +0,0 @@
<template>
<SettingsItem label="URL для cron-job.org">
<template #default>
<InputGroup>
<Button
icon="fa fa-refresh"
severity="secondary"
:loading="regeneratingUrl"
v-tooltip.top="'Перегенерировать URL'"
@click="confirmRegenerateUrl"
/>
<Button icon="fa fa-copy" severity="secondary" @click="copyToClipboard"/>
<InputText readonly :model-value="cronJobOrgUrl" class="tw:w-full"/>
</InputGroup>
</template>
<template #help>
Создайте задачу на <a href="https://cron-job.org/" target="_blank" rel="noopener" class="tw:underline">cron-job.org</a>, укажите этот URL и интервал (например, каждые 5 минут). Метод: GET. Учитывайте лимиты по времени запроса на вашем хостинге для тяжёлых задач возможны таймауты. При утечке URL нажмите «Перегенерировать URL» и обновите задачу на cron-job.org.
</template>
</SettingsItem>
</template>
<script setup>
import { computed, ref } from 'vue';
import { useSettingsStore } from '@/stores/settings.js';
import SettingsItem from '@/components/SettingsItem.vue';
import InputText from 'primevue/inputtext';
import Button from 'primevue/button';
import InputGroup from 'primevue/inputgroup';
import { useConfirm } from 'primevue/useconfirm';
import { toastBus } from '@/utils/toastHelper.js';
import { apiPost } from '@/utils/http.js';
const settings = useSettingsStore();
const confirm = useConfirm();
const regeneratingUrl = ref(false);
const cronJobOrgUrl = computed(() => settings.items.cron?.schedule_url ?? '');
function confirmRegenerateUrl(event) {
confirm.require({
group: 'popup',
target: event.currentTarget,
message: 'После смены URL его нужно будет обновить в задаче на cron-job.org. Продолжить?',
icon: 'pi pi-exclamation-triangle',
rejectProps: { label: 'Отмена', severity: 'secondary', outlined: true },
acceptProps: { label: 'Перегенерировать', severity: 'secondary' },
accept: () => regenerateUrl(),
});
}
async function regenerateUrl() {
regeneratingUrl.value = true;
try {
const res = await apiPost('regenerateCronScheduleUrl', {});
if (res?.success && res.data?.api_key) {
settings.items.cron.api_key = res.data.api_key;
if (res.data.schedule_url !== undefined) {
settings.items.cron.schedule_url = res.data.schedule_url;
}
toastBus.emit('show', {
severity: 'success',
summary: 'URL обновлён',
detail: 'Обновите URL в задаче на cron-job.org',
life: 4000,
});
} else {
toastBus.emit('show', {
severity: 'error',
summary: 'Ошибка',
detail: res?.data?.error ?? 'Не удалось перегенерировать URL',
life: 4000,
});
}
} catch (err) {
toastBus.emit('show', {
severity: 'error',
summary: 'Ошибка',
detail: err?.response?.data?.error ?? 'Не удалось перегенерировать URL',
life: 4000,
});
} finally {
regeneratingUrl.value = false;
}
}
async function copyToClipboard() {
try {
await navigator.clipboard.writeText(cronJobOrgUrl.value);
toastBus.emit('show', {
severity: 'success',
summary: 'Скопировано',
detail: 'URL скопирован в буфер обмена',
life: 2000,
});
} catch (err) {
toastBus.emit('show', {
severity: 'error',
summary: 'Ошибка',
detail: 'Не удалось скопировать текст',
life: 2000,
});
}
}
</script>

View File

@@ -1,108 +0,0 @@
<template>
<div class="tw:border tw:border-gray-300 tw:rounded-md tw:bg-white tw:overflow-hidden">
<div class="tw:flex tw:flex-col">
<div
v-for="job in scheduledJobs"
:key="job.id"
class="tw:flex tw:items-center tw:gap-4 tw:py-3 tw:px-3 tw:border-b tw:border-gray-200 tw:last:border-b-0"
>
<ToggleSwitch
:model-value="Boolean(job.is_enabled)"
@update:model-value="onJobToggle(job, $event)"
/>
<div class="tw:min-w-0 tw:flex-1 tw:flex tw:flex-col tw:gap-0.5">
<span class="tw:text-[inherit] tw:font-bold">{{ hasJobMeta(job.name) ? jobMeta(job.name).friendlyName : job.name }}</span>
<span v-if="hasJobMeta(job.name) && jobMeta(job.name).description" class="tw:text-sm tw:text-gray-500">
{{ jobMeta(job.name).description }}
</span>
</div>
<span
v-if="job.failed_reason"
class="tw:inline-flex tw:items-center tw:gap-1 tw:shrink-0 tw:px-2 tw:py-1 tw:rounded tw:text-sm tw:bg-red-100 tw:text-red-700 tw:cursor-default"
role="img"
:aria-label="'Ошибка: ' + job.failed_reason"
v-tooltip.top="errorTooltip(job)"
>
<i class="fa fa-exclamation-circle" aria-hidden="true"/>
Ошибка
</span>
<div
v-else
class="tw:flex tw:flex-col tw:gap-0.5 tw:shrink-0 tw:items-end"
>
<span
class="tw:inline-flex tw:items-center tw:shrink-0 tw:px-2 tw:py-1 tw:rounded tw:text-sm tw:bg-green-100 tw:text-green-800 tw:cursor-default"
v-tooltip.top="'Дата последнего успешного запуска'"
>
{{ formatLastRun(job.last_success_at) }}
</span>
<span
v-if="formatDuration(job.last_duration_seconds)"
class="tw:text-xs tw:text-gray-500"
>
Время выполнения: {{ formatDuration(job.last_duration_seconds) }}
</span>
</div>
<div class="tw:shrink-0 tw:w-[14rem]">
<CronExpressionSelect
:model-value="job.cron_expression"
@update:model-value="job.cron_expression = $event"
/>
</div>
</div>
<div v-if="!scheduledJobs.length" class="tw:text-gray-500 tw:py-4 tw:px-3">
Нет запланированных задач
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useSettingsStore } from '@/stores/settings.js';
import ToggleSwitch from 'primevue/toggleswitch';
import CronExpressionSelect from '@/components/CronExpressionSelect.vue';
const settings = useSettingsStore();
/** Человекочитаемое имя и описание задач (ключ — job.name с бэкенда) */
const JOB_META = {
megapay_pulse_send_events: {
friendlyName: 'Отправка данных в MegaPay Pulse',
description: 'Отправка данных телеметрии о действиях в MegaPay. Требуется для сбора метрик по рассылкам и кампаниям, сделанных через сервис MegaPay Pulse',
},
};
function hasJobMeta(jobName) {
return jobName in JOB_META;
}
function jobMeta(jobName) {
return JOB_META[jobName];
}
const scheduledJobs = computed(() => settings.items.scheduled_jobs ?? []);
function formatLastRun(value) {
if (!value) return '—';
const d = typeof value === 'string' ? new Date(value.replace(' ', 'T')) : new Date(value);
return Number.isNaN(d.getTime()) ? value : d.toLocaleString('ru-RU');
}
function formatDuration(seconds) {
if (seconds == null || seconds === '' || Number(seconds) <= 0) return '';
const n = Number(seconds);
if (Number.isNaN(n)) return '';
if (n < 1) return 'менее 1с';
return `${n % 1 ? n.toFixed(1) : Math.round(n)}с`;
}
function errorTooltip(job) {
const dateStr = formatLastRun(job.failed_at);
return `${dateStr}\n${job.failed_reason ?? ''}`.trim();
}
function onJobToggle(job, isEnabled) {
job.is_enabled = isEnabled ? 1 : 0;
}
</script>

View File

@@ -94,7 +94,7 @@ onMounted(() => {
$(searchInput.value).autocomplete({ $(searchInput.value).autocomplete({
source: function (request, response) { source: function (request, response) {
$.ajax({ $.ajax({
url: `/admin/index.php?route=catalog/category/autocomplete&user_token=${window.MegaPay.user_token}&filter_name=${encodeURIComponent(request)}`, url: `/admin/index.php?route=catalog/category/autocomplete&user_token=${window.TeleCart.user_token}&filter_name=${encodeURIComponent(request)}`,
dataType: 'json', dataType: 'json',
success: function (json) { success: function (json) {
response($.map(json, function (item) { response($.map(json, function (item) {

View File

@@ -94,7 +94,7 @@ onMounted(() => {
$(productsInput.value).autocomplete({ $(productsInput.value).autocomplete({
source: function (request, response) { source: function (request, response) {
$.ajax({ $.ajax({
url: `/admin/index.php?route=catalog/product/autocomplete&user_token=${window.MegaPay.user_token}&filter_name=${encodeURIComponent(request)}`, url: `/admin/index.php?route=catalog/product/autocomplete&user_token=${window.TeleCart.user_token}&filter_name=${encodeURIComponent(request)}`,
dataType: 'json', dataType: 'json',
success: function (json) { success: function (json) {
response($.map(json, function (item) { response($.map(json, function (item) {

View File

@@ -40,7 +40,7 @@
<template #help> <template #help>
Подробная инструкция доступна в Подробная инструкция доступна в
<a href="https://megapay-labs.github.io/docs/telegram/telegram/#%D1%81%D0%BE%D0%B7%D0%B4%D0%B0%D0%BD%D0%B8%D0%B5-%D0%B1%D0%BE%D1%82%D0%B0" target="_blank">документации <a href="https://telecart-labs.github.io/docs/telegram/telegram/#%D1%81%D0%BE%D0%B7%D0%B4%D0%B0%D0%BD%D0%B8%D0%B5-%D0%B1%D0%BE%D1%82%D0%B0" target="_blank">документации
<i class="fa fa-external-link"></i> <i class="fa fa-external-link"></i>
</a>. </a>.
</template> </template>

View File

@@ -41,7 +41,7 @@
Telegram поддерживает только часть HTML тегов, которые описаны в их Telegram поддерживает только часть HTML тегов, которые описаны в их
<a href="https://core.telegram.org/bots/api#html-style" target="_blank">документации <i class="fa fa-external-link"></i></a>. <a href="https://core.telegram.org/bots/api#html-style" target="_blank">документации <i class="fa fa-external-link"></i></a>.
</p> </p>
<p>Дополнительно к этому MegaPay добавляет переменные, которые вы можете использовать, чтобы сделать сообщения динамическими.</p> <p>Дополнительно к этому TeleCart добавляет переменные, которые вы можете использовать, чтобы сделать сообщения динамическими.</p>
<ul> <ul>
<li><code>{store_name}</code> название магазина</li> <li><code>{store_name}</code> название магазина</li>
<li><code>{order_id}</code> номер заказа</li> <li><code>{order_id}</code> номер заказа</li>

View File

@@ -6,9 +6,9 @@
:modelValue="model" :modelValue="model"
:allowCopy="true" :allowCopy="true"
> >
Ссылка на сайт с MegaPay витриной, которую нужно указывать в настройках MiniApp в @BotFather.<br> Ссылка на сайт с TeleCart витриной, которую нужно указывать в настройках MiniApp в @BotFather.<br>
Подробная инструкция по настройке в Подробная инструкция по настройке в
<a href="https://docs.megapay.pro/telegram/telegram/" target="_blank"> <a href="https://docs.telecart.pro/telegram/telegram/" target="_blank">
документации <i class="fa fa-external-link"></i> документации <i class="fa fa-external-link"></i>
</a>. </a>.
</ItemInput> </ItemInput>

View File

@@ -1,65 +1,24 @@
<template> <template>
<div class="form-group"> <div class="form-group">
<div class="col-sm-2 tw:flex tw:flex-col tw:gap-1"> <label class="col-sm-2 control-label" for="module_tgshop_status">
<label class="control-label" for="module_tgshop_status">
{{ label }} {{ label }}
</label> </label>
<a
v-if="docHref"
:href="docHref"
target="_blank"
rel="noopener noreferrer"
class="tw:inline-flex tw:items-center tw:gap-1 tw:text-sm tw:text-gray-500 hover:tw:text-gray-700 tw:underline tw:decoration-dotted tw:decoration-1 tw:underline-offset-2 tw:w-fit tw:self-end"
>
<i class="fa fa-external-link tw:text-xs" aria-hidden="true"></i>
<span>Документация</span>
</a>
</div>
<div class="col-sm-10"> <div class="col-sm-10">
<slot name="default"></slot> <slot name="default"></slot>
<div class="help-block"> <div class="help-block">
<slot name="help"></slot> <slot name="help"></slot>
</div> </div>
<div v-if="hasExpandable">
<Button
:label="expandableLabel"
severity="info"
link
size="small"
@click="expanded = !expanded"
/>
<div v-show="expanded" class="tw:mt-2 tw:space-y-2">
<slot name="expandable"></slot>
</div>
</div>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, useSlots, computed } from 'vue';
import Button from 'primevue/button';
const props = defineProps({ const props = defineProps({
label: { label: {
type: String, type: String,
default: '', default: '',
}, },
/** Ссылка на документацию: отображается под label, открывается в новой вкладке */
docHref: {
type: String,
default: '',
},
/** Подпись кнопки раскрытия блока #expandable (по умолчанию «Подробнее») */
expandableLabel: {
type: String,
default: 'Подробнее',
},
}); });
const slots = useSlots();
const hasExpandable = computed(() => !!slots.expandable);
const expanded = ref(false);
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -30,7 +30,7 @@ onMounted(() => {
} }
$.ajax({ $.ajax({
url: `index.php?route=catalog/category/autocomplete&user_token=${window.MegaPay.user_token}&filter_name=` + encodeURIComponent(request), url: `index.php?route=catalog/category/autocomplete&user_token=${window.TeleCart.user_token}&filter_name=` + encodeURIComponent(request),
dataType: 'json', dataType: 'json',
success: function (json) { success: function (json) {
response($.map(json, function (item) { response($.map(json, function (item) {

View File

@@ -29,7 +29,7 @@ onMounted(() => {
} }
$.ajax({ $.ajax({
url: `index.php?route=catalog/product/autocomplete&user_token=${window.MegaPay.user_token}&filter_name=` + encodeURIComponent(request), url: `index.php?route=catalog/product/autocomplete&user_token=${window.TeleCart.user_token}&filter_name=` + encodeURIComponent(request),
dataType: 'json', dataType: 'json',
success: function (json) { success: function (json) {
response($.map(json, function (item) { response($.map(json, function (item) {

View File

@@ -18,7 +18,7 @@
<div class="tw:flex tw:items-center tw:flex-wrap tw:gap-8"> <div class="tw:flex tw:items-center tw:flex-wrap tw:gap-8">
<div> <div>
<span <span
v-tooltip.top="'Общее количество заказов, сделанное через MegaPay за всё время.'" v-tooltip.top="'Общее количество заказов, сделанное через TeleCart за всё время.'"
class="tw:text-surface-500 tw:dark:text-surface-300" class="tw:text-surface-500 tw:dark:text-surface-300"
> >
Количество заказов Количество заказов
@@ -31,7 +31,7 @@
</div> </div>
<div> <div>
<span <span
v-tooltip.top="'Итоговая сумма заказов, сделанных через MegaPay за всё время.'" v-tooltip.top="'Итоговая сумма заказов, сделанных через TeleCart за всё время.'"
class="tw:text-surface-500 tw:dark:text-surface-300" class="tw:text-surface-500 tw:dark:text-surface-300"
>Общая сумма</span> >Общая сумма</span>
<div <div
@@ -101,14 +101,14 @@
/> />
<Button <Button
icon="fa fa-book" icon="fa fa-book"
v-tooltip.top="'Документация по модулю MegaPay'" v-tooltip.top="'Документация по модулю TeleCart'"
as="a" as="a"
target="_blank" target="_blank"
href="https://megapay-labs.github.io/docs/" href="https://telecart-labs.github.io/docs/"
/> />
<Button <Button
icon="fa fa-group" icon="fa fa-group"
v-tooltip.top="'Официальная Telegram группа модуля MegaPay'" v-tooltip.top="'Официальная Telegram группа модуля TeleCart'"
as="a" as="a"
target="_blank" target="_blank"
href="https://t.me/ocstore3" href="https://t.me/ocstore3"

View File

@@ -8,7 +8,7 @@ import StoreView from "@/views/StoreView.vue";
import MainPageView from "@/views/MainPageView.vue"; import MainPageView from "@/views/MainPageView.vue";
import FormBuilderView from "@/views/FormBuilderView.vue"; import FormBuilderView from "@/views/FormBuilderView.vue";
import CustomersView from "@/views/CustomersView.vue"; import CustomersView from "@/views/CustomersView.vue";
import MegaPayPulseView from "@/views/MegaPayPulseView.vue"; import TeleCartPulseView from "@/views/TeleCartPulseView.vue";
import CronView from "@/views/CronView.vue"; import CronView from "@/views/CronView.vue";
const router = createRouter({ const router = createRouter({
@@ -20,7 +20,7 @@ const router = createRouter({
{path: '/mainpage', name: 'mainpage', component: MainPageView}, {path: '/mainpage', name: 'mainpage', component: MainPageView},
{path: '/metrics', name: 'metrics', component: MetricsView}, {path: '/metrics', name: 'metrics', component: MetricsView},
{path: '/orders', name: 'orders', component: OrdersView}, {path: '/orders', name: 'orders', component: OrdersView},
{path: '/pulse', name: 'pulse', component: MegaPayPulseView}, {path: '/pulse', name: 'pulse', component: TeleCartPulseView},
{path: '/store', name: 'store', component: StoreView}, {path: '/store', name: 'store', component: StoreView},
{path: '/telegram', name: 'telegram', component: TelegramView}, {path: '/telegram', name: 'telegram', component: TelegramView},
{path: '/texts', name: 'texts', component: TextsView}, {path: '/texts', name: 'texts', component: TextsView},

View File

@@ -93,11 +93,7 @@ export const useSettingsStore = defineStore('settings', {
cron: { cron: {
mode: 'disabled', mode: 'disabled',
api_key: '',
schedule_url: '',
}, },
scheduled_jobs: [],
}, },
}), }),
@@ -125,14 +121,6 @@ export const useSettingsStore = defineStore('settings', {
...this.items, ...this.items,
...response.data, ...response.data,
}; };
// Нормализуем типы у запланированных задач (is_enabled — число 0/1), чтобы переключение туда-обратно не меняло хеш
const jobs = this.items.scheduled_jobs;
if (Array.isArray(jobs)) {
this.items.scheduled_jobs = jobs.map((job) => ({
...job,
is_enabled: job.is_enabled === true || job.is_enabled === 1 || job.is_enabled === '1' ? 1 : 0,
}));
}
// Сохраняем хеш исходного состояния после загрузки // Сохраняем хеш исходного состояния после загрузки
this.originalItemsHash = md5(JSON.stringify(this.items)); this.originalItemsHash = md5(JSON.stringify(this.items));
} else { } else {

View File

@@ -1,10 +1,10 @@
export function getThumb(imageUrl) { export function getThumb(imageUrl) {
const url = new URL(`${window.MegaPay.shop_base_url}/admin/index.php`); const url = new URL(`${window.TeleCart.shop_base_url}/admin/index.php`);
url.searchParams.set('route', 'extension/module/tgshop/handle'); url.searchParams.set('route', 'extension/module/tgshop/handle');
url.searchParams.set('api_action', 'getImage'); url.searchParams.set('api_action', 'getImage');
url.searchParams.set('path', imageUrl); url.searchParams.set('path', imageUrl);
url.searchParams.set('size', '100x100'); url.searchParams.set('size', '100x100');
url.searchParams.set('user_token', window.MegaPay.user_token); url.searchParams.set('user_token', window.TeleCart.user_token);
return url.toString(); return url.toString();
} }

View File

@@ -1,11 +1,11 @@
import axios from 'axios'; import axios from 'axios';
/** /**
* Получает user_token из глобального объекта MegaPay * Получает user_token из глобального объекта TeleCart
*/ */
function getUserToken() { function getUserToken() {
if (typeof window !== 'undefined' && window.MegaPay?.user_token) { if (typeof window !== 'undefined' && window.TeleCart?.user_token) {
return window.MegaPay.user_token; return window.TeleCart.user_token;
} }
// Fallback: пытаемся получить из URL как запасной вариант // Fallback: пытаемся получить из URL как запасной вариант

View File

@@ -1,5 +1,5 @@
<template> <template>
<SettingsItem label="Режим работы планировщика" doc-href="https://docs.megapay.pro/features/cron/"> <SettingsItem label="Режим работы планировщика">
<template #default> <template #default>
<SelectButton <SelectButton
v-model="settings.items.cron.mode" v-model="settings.items.cron.mode"
@@ -13,35 +13,22 @@
<div v-if="settings.items.cron.mode === 'disabled'" class="tw:text-red-600 tw:font-bold"> <div v-if="settings.items.cron.mode === 'disabled'" class="tw:text-red-600 tw:font-bold">
Все фоновые задачи отключены. Все фоновые задачи отключены.
</div> </div>
<div v-else-if="settings.items.cron.mode === 'cron_job_org'">
Задачи запускаются по вызову URL с сервиса <a href="https://cron-job.org/" target="_blank" rel="noopener" class="tw:underline">cron-job.org</a>. Подходит для лёгких задач; при большом количестве товаров или тяжёлых операциях возможны таймауты.
</div>
<div v-else> <div v-else>
Рекомендуемый режим. Использует системный планировщик задач Linux. Рекомендуемый режим. Использует системный планировщик задач Linux.
</div> </div>
</template>
<template #expandable> <div class="tw:mt-2">
<p> <p>
<strong>Системный CRON (рекомендуется):</strong> Задачи выполняются через команду PHP в оболочке сервера (CLI), без HTTP и без ограничений по времени запроса. Не зависит от посещаемости сайта и подходит для любых объёмов данных, в том числе для тяжёлых задач и больших каталогов. Требует доступа к серверу (SSH или панель с CRON). Добавьте команду ниже в планировщик (обычно <code class="tw:px-1 tw:py-0.5 tw:bg-gray-100 tw:dark:bg-gray-800 tw:rounded">crontab -e</code>) для запуска каждые 5 минут. <strong>Системный CRON (рекомендуется):</strong> Стабильное выполнение задач по расписанию, независимо от
</p> посещаемости сайта. Добавьте команду в CRON для автоматического выполнения каждые 5 минут.
<p>
<strong>cron-job.org:</strong> Внешний сервис по расписанию вызывает URL вашего сайта по HTTP. Не требует доступа к серверу удобно для shared-хостинга без CRON. Ограничения: выполнение идёт через веб-запрос, поэтому есть лимиты по времени (timeout у хостинга и у cron-job.org). <strong>Не подходит для тяжёлых сайтов</strong> (много товаров, большие каталоги, тяжёлые задачи): запрос может обрываться по таймауту, задачи не успеют завершиться. Выбирайте этот способ только если нет доступа к системному CRON и нагрузка на планировщик небольшая.
</p> </p>
<p> <p>
<strong>Выключено:</strong> Все фоновые задачи отключены. Планировщик не будет выполнять никаких задач. <strong>Выключено:</strong> Все фоновые задачи отключены. Планировщик не будет выполнять никаких задач.
</p> </p>
</div>
</template> </template>
</SettingsItem> </SettingsItem>
<div class="tw:relative tw:mt-4">
<div
:class="[
'tw:transition-all tw:duration-200',
settings.items.cron.mode === 'disabled'
? 'tw:blur-[2px] tw:pointer-events-none tw:select-none'
: '',
]"
>
<SettingsItem label="Последний запуск CRON"> <SettingsItem label="Последний запуск CRON">
<template #default> <template #default>
<div v-if="lastRunDate" class="tw:text-green-600 tw:font-bold tw:py-2"> <div v-if="lastRunDate" class="tw:text-green-600 tw:font-bold tw:py-2">
@@ -71,48 +58,22 @@
5 минут. 5 минут.
</template> </template>
</SettingsItem> </SettingsItem>
<CronJobOrgUrlField v-if="settings.items.cron.mode === 'cron_job_org'"/>
<SettingsItem label="Задачи планировщика">
<template #default>
<ScheduledJobsList />
</template>
<template #help>
Включение и отключение задач планировщика. Дата последнего успешного запуска; при ошибке отображается иконка с подсказкой.
</template>
</SettingsItem>
</div>
<div
v-if="settings.items.cron.mode === 'disabled'"
class="tw:absolute tw:inset-0 tw:flex tw:items-center tw:justify-center tw:rounded-lg tw:bg-white/80 tw:dark:bg-gray-900/80 tw:z-10"
aria-hidden="true"
>
<span class="tw:text-lg tw:font-semibold tw:text-gray-600 tw:dark:text-gray-400">
Планировщик выключен
</span>
</div>
</div>
</template> </template>
<script setup> <script setup>
import {computed} from 'vue'; import {computed} from 'vue';
import { useSettingsStore } from '@/stores/settings.js'; import {useSettingsStore} from "@/stores/settings.js";
import SettingsItem from '@/components/SettingsItem.vue'; import SettingsItem from "@/components/SettingsItem.vue";
import ScheduledJobsList from '@/components/ScheduledJobsList.vue'; import SelectButton from "primevue/selectbutton";
import CronJobOrgUrlField from '@/components/CronJobOrgUrlField.vue'; import InputText from "primevue/inputtext";
import SelectButton from 'primevue/selectbutton'; import Button from "primevue/button";
import InputText from 'primevue/inputtext';
import Button from 'primevue/button';
import InputGroup from 'primevue/inputgroup'; import InputGroup from 'primevue/inputgroup';
import { toastBus } from '@/utils/toastHelper.js'; import {toastBus} from "@/utils/toastHelper.js";
const settings = useSettingsStore(); const settings = useSettingsStore();
const cronModes = [ const cronModes = [
{value: 'system', label: 'Системный CRON (Linux)'}, {value: 'system', label: 'Системный CRON (Linux)'},
{value: 'cron_job_org', label: 'cron-job.org'},
{value: 'disabled', label: 'Выключено'}, {value: 'disabled', label: 'Выключено'},
]; ];

View File

@@ -76,7 +76,7 @@ import ItemSelect from "@/components/Settings/ItemSelect.vue";
import ItemInput from "@/components/Settings/ItemInput.vue"; import ItemInput from "@/components/Settings/ItemInput.vue";
const settings = useSettingsStore(); const settings = useSettingsStore();
const themes = JSON.parse(window.MegaPay.themes); const themes = JSON.parse(window.TeleCart.themes);
const aspectRatioOptions = { const aspectRatioOptions = {
'1:1': '1:1 - Квадрат (универсально, аксессуары, мелкие товары)', '1:1': '1:1 - Квадрат (универсально, аксессуары, мелкие товары)',

View File

@@ -26,7 +26,7 @@
<Button <Button
as="a" as="a"
href="https://megapay-labs.github.io/docs/analitycs/start/" href="https://telecart-labs.github.io/docs/analitycs/start/"
target="_blank" target="_blank"
variant="text" variant="text"
> >

View File

@@ -13,5 +13,5 @@ import {useSettingsStore} from "@/stores/settings.js";
import ItemSelect from "@/components/Settings/ItemSelect.vue"; import ItemSelect from "@/components/Settings/ItemSelect.vue";
const settings = useSettingsStore(); const settings = useSettingsStore();
const orderStatuses = JSON.parse(window.MegaPay.order_statuses); const orderStatuses = JSON.parse(window.TeleCart.order_statuses);
</script> </script>

View File

@@ -61,5 +61,5 @@ const productInteractionOptions = {
browser: 'Открытие товара на сайте', browser: 'Открытие товара на сайте',
}; };
const userToken = window.MegaPay.user_token; const userToken = window.TeleCart.user_token;
</script> </script>

View File

@@ -1,27 +1,27 @@
<template> <template>
<div class="tw:space-y-6"> <div class="tw:space-y-6">
<div class="megapay-pulse-info"> <div class="telecart-pulse-info">
<h3>🚀 Расширьте возможности вашего магазина с <strong><a href="https://megapay.pro/" target="_blank">MegaPay Pulse</a>!</strong></h3> <h3>🚀 Расширьте возможности вашего магазина с <strong><a href="https://telecart.pro/" target="_blank">TeleCart Pulse</a>!</strong></h3>
<p> <p>
Если вы хотите не только показывать товары в Telegram, но и активно общаться с клиентами, Если вы хотите не только показывать товары в Telegram, но и активно общаться с клиентами,
рассылать новости, акции и уведомления для этого есть <strong>MegaPay Pulse</strong>. рассылать новости, акции и уведомления для этого есть <strong>TeleCart Pulse</strong>.
Это <strong>SaaS-платформа с месячной подпиской</strong>, которая полностью интегрируется Это <strong>SaaS-платформа с месячной подпиской</strong>, которая полностью интегрируется
с вашим OpenCart-магазином и витриной MegaPay. с вашим OpenCart-магазином и витриной TeleCart.
</p> </p>
<p><strong>С MegaPay Pulse вы сможете:</strong></p> <p><strong>С TeleCart Pulse вы сможете:</strong></p>
<ul> <ul>
<li>📣 Делать массовые рассылки сообщений покупателям прямо в Telegram</li> <li>📣 Делать массовые рассылки сообщений покупателям прямо в Telegram</li>
<li>📊 Анализировать эффективность сообщений и взаимодействие клиентов</li> <li>📊 Анализировать эффективность сообщений и взаимодействие клиентов</li>
<li>🔗 Легко синхронизироваться с вашей витриной MegaPay все данные остаются в одном месте</li> <li>🔗 Легко синхронизироваться с вашей витриной TeleCart все данные остаются в одном месте</li>
</ul> </ul>
<p> <p>
🧪 Платформа <strong>MegaPay Pulse находится на ранней стадии тестирования</strong>. 🧪 Платформа <strong>TeleCart Pulse находится на ранней стадии тестирования</strong>.
Если вам интересно и вы хотите принять участи в тестировании интересно, свяжитесь со мной через Если вам интересно и вы хотите принять участи в тестировании интересно, свяжитесь со мной через
<a href="https://t.me/ocstore3" target="_blank">официальную группу MegaPay в Telegram</a>. <a href="https://t.me/ocstore3" target="_blank">официальную группу TeleCart в Telegram</a>.
</p> </p>
</div> </div>
@@ -91,7 +91,7 @@
type="number" type="number"
placeholder="50" placeholder="50"
> >
Определяет, сколько событий отправляется в MegaPay Pulse за один запуск фоновой задачи. Определяет, сколько событий отправляется в TeleCart Pulse за один запуск фоновой задачи.
При большом значении события обрабатываются быстрее, но увеличивается нагрузка на сервер. При большом значении события обрабатываются быстрее, но увеличивается нагрузка на сервер.
При малом значении нагрузка ниже, но обработка занимает больше времени. При малом значении нагрузка ниже, но обработка занимает больше времени.
Рекомендуемое значение: 50. Рекомендуемое значение: 50.
@@ -110,7 +110,7 @@ const settings = useSettingsStore();
const stats = ref(null); const stats = ref(null);
const loadStats = async () => { const loadStats = async () => {
const response = await apiGet('getMegaPayPulseStats'); const response = await apiGet('getTeleCartPulseStats');
if (response.success) { if (response.success) {
stats.value = response.data; stats.value = response.data;
} }
@@ -122,7 +122,7 @@ onMounted(() => {
</script> </script>
<style scoped> <style scoped>
.megapay-pulse-info { .telecart-pulse-info {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border: 1px solid #dee2e6; border: 1px solid #dee2e6;
border-radius: 8px; border-radius: 8px;
@@ -130,7 +130,7 @@ onMounted(() => {
margin-bottom: 24px; margin-bottom: 24px;
} }
.megapay-pulse-info h3 { .telecart-pulse-info h3 {
font-size: 1.5rem; font-size: 1.5rem;
font-weight: 600; font-weight: 600;
margin-bottom: 16px; margin-bottom: 16px;
@@ -138,43 +138,43 @@ onMounted(() => {
line-height: 1.4; line-height: 1.4;
} }
.megapay-pulse-info h3 a { .telecart-pulse-info h3 a {
color: #dc3545; color: #dc3545;
font-weight: 700; font-weight: 700;
text-decoration: underline; text-decoration: underline;
} }
.megapay-pulse-info h3 a:hover { .telecart-pulse-info h3 a:hover {
text-decoration: none; text-decoration: none;
} }
.megapay-pulse-info p { .telecart-pulse-info p {
margin-bottom: 16px; margin-bottom: 16px;
line-height: 1.6; line-height: 1.6;
color: #495057; color: #495057;
} }
.megapay-pulse-info p:last-child { .telecart-pulse-info p:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
.megapay-pulse-info p strong { .telecart-pulse-info p strong {
color: #212529; color: #212529;
font-weight: 600; font-weight: 600;
} }
.megapay-pulse-info ul { .telecart-pulse-info ul {
margin: 16px 0; margin: 16px 0;
padding-left: 24px; padding-left: 24px;
} }
.megapay-pulse-info ul li { .telecart-pulse-info ul li {
color: #495057; color: #495057;
position: relative; position: relative;
padding-left: 8px; padding-left: 8px;
} }
.megapay-pulse-info ul li:last-child { .telecart-pulse-info ul li:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
</style> </style>

View File

@@ -21,11 +21,11 @@ export default defineConfig({
build: { build: {
manifest: false, manifest: false,
sourcemap: false, sourcemap: false,
outDir: '../../module/oc_telegram_shop/upload/admin/view/javascript/megapay', outDir: '../../module/oc_telegram_shop/upload/admin/view/javascript/telecart',
emptyOutDir: true, // also necessary emptyOutDir: true, // also necessary
rollupOptions: { rollupOptions: {
input: { input: {
megapay: '/src/main.js', telecart: '/src/main.js',
}, },
output: { output: {
entryFileNames: `[name].js`, entryFileNames: `[name].js`,

View File

@@ -4,12 +4,12 @@
<meta charset="UTF-8"/> <meta charset="UTF-8"/>
<link rel="icon" type="image/svg+xml" href="/vite.svg"/> <link rel="icon" type="image/svg+xml" href="/vite.svg"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>MegaPay</title> <title>TeleCart</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<div id="app-error"></div> <div id="app-error"></div>
<script src="https://telegram.org/js/telegram-web-app.js?59"></script> <script src="https://telegram.org/js/telegram-web-app.js?61"></script>
<script type="module" src="/src/main.js"></script> <script type="module" src="/src/main.js"></script>
</body> </body>
</html> </html>

File diff suppressed because it is too large Load Diff

View File

@@ -28,18 +28,19 @@
"vue-router": "^4.6.3" "vue-router": "^4.6.3"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/typography": "^0.5.19",
"@testing-library/jest-dom": "^6.9.1", "@testing-library/jest-dom": "^6.9.1",
"@testing-library/vue": "^8.1.0", "@testing-library/vue": "^8.1.0",
"@vitejs/plugin-vue": "^6.0.1", "@vitejs/plugin-vue": "^6.0.1",
"@vitest/ui": "^4.0.8", "@vitest/ui": "^4.1.9",
"@vue/test-utils": "^2.4.6", "@vue/test-utils": "^2.4.6",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"daisyui": "^5.3.10", "daisyui": "^5.3.10",
"jsdom": "^27.1.0", "jsdom": "^27.1.0",
"postcss": "^8.5.6", "postcss": "^8.5.15",
"tailwindcss": "^4.1.16", "tailwindcss": "^4.1.16",
"terser": "^5.44.0", "terser": "^5.44.0",
"vite": "^7.1.12", "vite": "^7.3.3",
"vitest": "^4.0.8" "vitest": "^4.1.9"
} }
} }

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -2,16 +2,16 @@
<a <a
href="#" href="#"
:key="category.id" :key="category.id"
class="flex items-center" class="flex items-center gap-3"
@click.prevent="$emit('onSelect', category)" @click.prevent="$emit('onSelect', category)"
> >
<div class="avatar"> <div class="avatar shrink-0">
<div class="w-8 h-8 rounded"> <div class="w-10 h-10 rounded">
<img :src="category.image" :alt="category.name" loading="lazy" width="30" height="30" class="bg-base-400"/> <img :src="category.image" :alt="category.name" loading="lazy" width="40" height="40" class="bg-base-400"/>
</div> </div>
</div> </div>
<h3 class="ml-4 text-lg line-clamp-2">{{ category.name }}</h3> <h3 class="text-base font-medium leading-snug line-clamp-2">{{ category.name }}</h3>
</a> </a>
</template> </template>

View File

@@ -1,13 +1,13 @@
<template> <template>
<section class="px-4"> <section class="px-4">
<header class="flex justify-between items-end mb-4"> <header class="flex justify-between items-end mb-4 gap-2">
<div> <div class="min-w-0">
<div v-if="title" class="font-bold uppercase">{{ title }}</div> <div v-if="title" class="font-bold uppercase">{{ title }}</div>
<div v-if="description" class="text-sm text-base-content/50">{{ description }}</div> <div v-if="description" class="text-sm text-base-content/50">{{ description }}</div>
</div> </div>
<div v-if="moreLink"> <div v-if="moreLink" class="shrink-0">
<RouterLink :to="moreLink" class="btn btn-soft btn-xs" @click="haptic.selectionChanged"> <RouterLink :to="moreLink" class="btn btn-soft btn-xs whitespace-nowrap" @click="haptic.selectionChanged">
{{ moreText || 'Смотреть всё' }} {{ moreText || 'Смотреть всё' }}
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
<path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" /> <path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />

View File

@@ -2,10 +2,23 @@
<BaseBlock <BaseBlock
:title="block.title" :title="block.title"
:description="block.description" :description="block.description"
:moreLink="{name: 'product.categories.show', params: { category_id: block.data.category_id }}" :moreLink="hasCategory ? {name: 'product.categories.show', params: { category_id: block.data.category_id }} : null"
:moreText="block.data.all_text" :moreText="block.data.all_text"
> >
<div v-if="!hasCategory" role="alert" class="alert alert-warning alert-soft">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<span>Для блока не выбрана категория.</span>
</div>
<div v-else-if="!hasProducts" role="alert" class="alert alert-soft">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>В этой категории пока нет товаров.</span>
</div>
<Swiper <Swiper
v-else
class="select-none block-products-carousel" class="select-none block-products-carousel"
:slides-per-view="block.data?.carousel?.slides_per_view || 2.5" :slides-per-view="block.data?.carousel?.slides_per_view || 2.5"
:space-between="block.data?.carousel?.space_between || 20" :space-between="block.data?.carousel?.space_between || 20"
@@ -36,6 +49,7 @@
</template> </template>
<script setup> <script setup>
import {computed} from "vue";
import {useYaMetrikaStore} from "@/stores/yaMetrikaStore.js"; import {useYaMetrikaStore} from "@/stores/yaMetrikaStore.js";
import {Swiper, SwiperSlide} from "swiper/vue"; import {Swiper, SwiperSlide} from "swiper/vue";
import {useHapticScroll} from "@/composables/useHapticScroll.js"; import {useHapticScroll} from "@/composables/useHapticScroll.js";
@@ -52,6 +66,9 @@ const props = defineProps({
} }
}); });
const hasCategory = computed(() => Boolean(props.block.data?.category_id));
const hasProducts = computed(() => (props.block.data?.products?.data?.length ?? 0) > 0);
const freeModeSettings = { const freeModeSettings = {
enabled: props.block.data?.carousel?.freemode?.enabled || false, enabled: props.block.data?.carousel?.freemode?.enabled || false,
}; };

View File

@@ -10,7 +10,7 @@
На главной странице не сконфигурировано ни одного блока для отображения. На главной странице не сконфигурировано ни одного блока для отображения.
</p> </p>
<p class="text-sm text-base-content/70 max-w-md"> <p class="text-sm text-base-content/70 max-w-md">
Перейдите в настройки модуля <span class="font-semibold">MegaPay</span> и добавьте блоки на главную страницу. Перейдите в настройки модуля <span class="font-semibold">TeleCart</span> и добавьте блоки на главную страницу.
</p> </p>
<div class="mt-6 p-4 bg-base-200 rounded-lg max-w-md"> <div class="mt-6 p-4 bg-base-200 rounded-lg max-w-md">
<div class="flex items-start gap-3"> <div class="flex items-start gap-3">

View File

@@ -2,10 +2,19 @@
@custom-variant dark (&:where(.dark, .dark *)); @custom-variant dark (&:where(.dark, .dark *));
@plugin "@tailwindcss/typography";
@plugin "daisyui" { @plugin "daisyui" {
themes: all; themes: all;
} }
.prose :where(table):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
display: block;
width: 100%;
max-width: 100%;
overflow-x: auto;
}
/** /**
--color-base-100 - DaisyUI background --color-base-100 - DaisyUI background
*/ */

View File

@@ -9,31 +9,22 @@
</div> </div>
<template v-else> <template v-else>
<button v-if="parentId && parentCategory" class="py-1 px-4 flex items-center mb-3 cursor-pointer" @click="goBack">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6 min-w-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" />
</svg>
<span class="ml-2 line-clamp-2">Назад к "{{ parentCategory.name }}"</span>
</button>
<button <button
v-if="parentCategory && parentCategory.children?.length && settings.show_category_products_button" v-if="parentCategory && parentCategory.children?.length && settings.show_category_products_button"
class="py-2 px-4 flex items-center mb-3 cursor-pointer border-b w-full pb-2 border-base-200" class="btn btn-soft btn-block btn-sm mb-3 justify-start"
@click.prevent="showProductsInParentCategory" @click.prevent="showProductsInParentCategory"
> >
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5 shrink-0">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6A2.25 2.25 0 0 1 6 3.75h2.25A2.25 2.25 0 0 1 10.5 6v2.25a2.25 2.25 0 0 1-2.25 2.25H6a2.25 2.25 0 0 1-2.25-2.25V6ZM3.75 15.75A2.25 2.25 0 0 1 6 13.5h2.25a2.25 2.25 0 0 1 2.25 2.25V18a2.25 2.25 0 0 1-2.25 2.25H6A2.25 2.25 0 0 1 3.75 18v-2.25ZM13.5 6a2.25 2.25 0 0 1 2.25-2.25H18A2.25 2.25 0 0 1 20.25 6v2.25A2.25 2.25 0 0 1 18 10.5h-2.25a2.25 2.25 0 0 1-2.25-2.25V6ZM13.5 15.75a2.25 2.25 0 0 1 2.25-2.25H18a2.25 2.25 0 0 1 2.25 2.25V18A2.25 2.25 0 0 1 18 20.25h-2.25A2.25 2.25 0 0 1 13.5 18v-2.25Z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6A2.25 2.25 0 0 1 6 3.75h2.25A2.25 2.25 0 0 1 10.5 6v2.25a2.25 2.25 0 0 1-2.25 2.25H6a2.25 2.25 0 0 1-2.25-2.25V6ZM3.75 15.75A2.25 2.25 0 0 1 6 13.5h2.25a2.25 2.25 0 0 1 2.25 2.25V18a2.25 2.25 0 0 1-2.25 2.25H6A2.25 2.25 0 0 1 3.75 18v-2.25ZM13.5 6a2.25 2.25 0 0 1 2.25-2.25H18A2.25 2.25 0 0 1 20.25 6v2.25A2.25 2.25 0 0 1 18 10.5h-2.25a2.25 2.25 0 0 1-2.25-2.25V6ZM13.5 15.75a2.25 2.25 0 0 1 2.25-2.25H18a2.25 2.25 0 0 1 2.25 2.25V18A2.25 2.25 0 0 1 18 20.25h-2.25A2.25 2.25 0 0 1 13.5 18v-2.25Z" />
</svg> </svg>
<span>Показать все товары категории</span>
<span class="ml-2">Показать товары из "{{ parentCategory.name }}"</span>
</button> </button>
<TransitionGroup <TransitionGroup
name="stagger" name="stagger"
tag="ul" tag="ul"
appear appear
class="space-y-4" class="space-y-3"
> >
<li <li
v-for="(category, i) in categories" v-for="(category, i) in categories"
@@ -99,10 +90,6 @@ function onSelect(category) {
} }
} }
function goBack() {
router.back();
}
function showProductsInParentCategory() { function showProductsInParentCategory() {
if (parentId.value != null) { if (parentId.value != null) {
router.push({name: "product.categories.show", params: {category_id: parentId.value}}); router.push({name: "product.categories.show", params: {category_id: parentId.value}});

View File

@@ -87,7 +87,11 @@
</svg> </svg>
Описание Описание
</h3> </h3>
<div class="prose prose-sm max-w-none text-base-content/80" v-html="product.description"></div> <div
class="prose prose-sm max-w-none text-base-content/80"
v-html="product.description"
@click="handleDescriptionClick"
></div>
</div> </div>
<!-- Attributes --> <!-- Attributes -->
@@ -306,6 +310,29 @@ async function onCartBtnClick() {
} }
} }
function handleDescriptionClick(event) {
const link = event.target.closest('a');
if (!link) return;
const href = link.getAttribute('href');
if (!href || href.startsWith('#') || href.startsWith('javascript:')) return;
event.preventDefault();
const telegramWebApp = window.Telegram?.WebApp;
if (!telegramWebApp) {
window.open(href, '_blank', 'noopener,noreferrer');
return;
}
if (/^https?:\/\/(t|telegram)\.me\//i.test(href) || href.startsWith('tg://')) {
telegramWebApp.openTelegramLink(href);
return;
}
telegramWebApp.openLink(href, {try_instant_view: false});
}
function openProductInMarketplace() { function openProductInMarketplace() {
if (!product.value.share) { if (!product.value.share) {
return; return;

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<modification>
<name>TeleCart</name>
<code>telecart</code>
<version>1.0</version>
<author>TeleCart</author>
<link>https://docs.telecart.pro/</link>
<file path="admin/controller/common/column_left.php">
<operation>
<search><![CDATA[// System]]></search>
<add position="before"><![CDATA[ // BEGIN TeleCart modification
$telecart = array();
$telecart[] = array(
'name' => 'Настройки',
'href' => $this->url->link('extension/module/tgshop', 'user_token=' . $this->session->data['user_token'], true),
'children' => array()
);
$data['menus'][] = array(
'id' => 'menu-telecart',
'icon' => 'fa-telegram',
'name' => 'TeleCart',
'href' => '',
'children' => $telecart
);
// END TeleCart modification
]]></add>
</operation>
</file>
</modification>

View File

@@ -0,0 +1,282 @@
<?php
use Bastion\ApplicationFactory;
use Cart\User;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Logger;
use Openguru\OpenCartFramework\Application;
use Openguru\OpenCartFramework\OpenCart\Decorators\OcRegistryDecorator;
use Openguru\OpenCartFramework\Support\Arr;
use Psr\Log\LoggerInterface;
$sysLibPath = rtrim(DIR_SYSTEM, '/') . '/library/oc_telegram_shop';
$basePath = rtrim(DIR_APPLICATION, '/') . '/..';
if (is_readable($sysLibPath . '/oc_telegram_shop.phar')) {
require_once "phar://{$sysLibPath}/oc_telegram_shop.phar/vendor/autoload.php";
} elseif (is_dir("$basePath/oc_telegram_shop")) {
require_once "$basePath/oc_telegram_shop/vendor/autoload.php";
} else {
throw new RuntimeException('Unable to locate application directory.');
}
/**
* @property Document $document
* @property Loader $load
* @property Config $config
* @property Language $language
* @property Request $request
* @property Session $session
* @property ModelSettingSetting $model_setting_setting
* @property Response $response
* @property Url $url
* @property ModelToolImage $model_tool_image
* @property ModelCatalogProduct $model_catalog_product
* @property User $user
* @property ModelCustomerCustomerGroup $model_customer_customer_group
* @property ModelLocalisationOrderStatus $model_localisation_order_status
* @property ModelDesignBanner $model_design_banner
* @property DB $db
* @property Log $log
*/
class ControllerExtensionModuleTgshop extends Controller
{
private static array $themes = [
'light' => 'Светлая (light)',
'dark' => 'Тёмная (dark)',
'cupcake' => 'Капкейк (cupcake)',
'bumblebee' => 'Шмель (bumblebee)',
'emerald' => 'Изумруд (emerald)',
'corporate' => 'Корпоративная (corporate)',
'synthwave' => 'Синтвейв (synthwave)',
'retro' => 'Ретро (retro)',
'cyberpunk' => 'Киберпанк (cyberpunk)',
'valentine' => 'Валентинка (valentine)',
'halloween' => 'Хэллоуин (halloween)',
'garden' => 'Сад (garden)',
'forest' => 'Лес (forest)',
'aqua' => 'Аква (aqua)',
'lofi' => 'Лоу-фай (lofi)',
'pastel' => 'Пастель (pastel)',
'fantasy' => 'Фэнтези (fantasy)',
'wireframe' => 'Каркас (wireframe)',
'black' => 'Чёрная (black)',
'luxury' => 'Люкс (luxury)',
'dracula' => 'Дракула (dracula)',
'cmyk' => 'CMYK (cmyk)',
'autumn' => 'Осень (autumn)',
'business' => 'Бизнес (business)',
'acid' => 'Кислотная (acid)',
'lemonade' => 'Лимонад (lemonade)',
'night' => 'Ночная (night)',
'coffee' => 'Кофейная (coffee)',
'winter' => 'Зимняя (winter)',
'dim' => 'Тусклая (dim)',
'nord' => 'Нордическая (nord)',
'sunset' => 'Закат (sunset)',
'caramellatte' => 'Карамель-латте (caramellatte)',
'abyss' => 'Бездна (abyss)',
'silk' => 'Шёлк (silk)',
];
private array $error = [];
public function __construct($registry)
{
parent::__construct($registry);
$this->load->language('extension/module/tgshop');
$this->load->model('setting/setting');
$this->load->model('tool/image');
$this->load->model('catalog/product');
$this->load->model('catalog/category');
$this->load->model('localisation/order_status');
$this->load->model('customer/customer_group');
}
public function index(): void
{
$this->runMaintenanceTasks();
$this->injectAssets();
$this->injectVueJs();
$this->showConfigPage();
}
private function showConfigPage(): void
{
$data = [];
$this->document->setTitle($this->language->get('heading_title'));
$this->baseData($data);
$data['order_statuses'] = $this->getOrderStatuses();
$data['customer_groups'] = $this->getCustomerGroups();
$data['themes'] = self::$themes;
$data['telecart_module_version'] = module_version();
$data['shop_base_url'] = HTTPS_CATALOG;
$data['action'] = $this->url->link(
'extension/module/tgshop',
'user_token=' . $this->session->data['user_token'],
true
);
$this->response->setOutput($this->load->view('extension/module/tgshop', $data));
}
public function handle(): void
{
$logger = $this->createLogger();
$this
->createApplication($logger)
->bootAndHandleRequest();
}
protected function validate(): bool
{
if (! $this->user->hasPermission('modify', 'extension/module/tgshop')) {
$this->error['telecart_error_warning'] = $this->language->get('error_permission');
}
return ! $this->error;
}
private function baseData(array &$data): void
{
$this->document->setTitle($this->language->get('heading_title'));
$data['header'] = $this->load->controller('common/header');
$data['column_left'] = $this->load->controller('common/column_left');
$data['footer'] = $this->load->controller('common/footer');
$data['cancel'] = $this->url->link(
'marketplace/extension',
'user_token=' . $this->session->data['user_token'] . '&type=module',
true
);
$data = array_merge($data, $this->error);
if (isset($this->session->data['success'])) {
$data['success'] = $this->session->data['success'];
unset($this->session->data['success']);
} else {
$data['success'] = '';
}
$data['user_token'] = $this->session->data['user_token'];
}
private function getCustomerGroups(): array
{
$map = [];
foreach ($this->model_customer_customer_group->getCustomerGroups() as $customer_group) {
$map[(int) $customer_group['customer_group_id']] = $customer_group['name'];
}
return $map;
}
private function getOrderStatuses(): array
{
$statuses = $this->model_localisation_order_status->getOrderStatuses();
$map = [];
foreach ($statuses as $item) {
$map[(int) $item['order_status_id']] = $item['name'];
}
return $map;
}
private function injectVueJs(): void
{
$appDir = rtrim(DIR_APPLICATION, '/');
if (file_exists("$appDir/view/javascript/telecart/telecart.js")) {
$hash = file_get_contents($appDir . '/view/javascript/telecart/checksum.hash');
$this->document->addScript('view/javascript/telecart/telecart.js?v=' . $hash);
$this->document->addStyle('view/javascript/telecart/telecart.css?v=' . $hash);
} elseif (file_exists("$appDir/view/integration.js")) {
$this->document->addScript('view/integration.js');
} else {
throw new RuntimeException('Unable to load Vuejs frontend.');
}
}
private function createApplication(LoggerInterface $logger): Application
{
$json = $this->model_setting_setting->getSetting('module_telecart');
if (! isset($json['module_telecart_settings'])) {
$json['module_telecart_settings'] = [];
}
$items = Arr::mergeArraysRecursively($json['module_telecart_settings'], [
'app' => [
'shop_base_url' => HTTPS_CATALOG, // for catalog: HTTPS_SERVER, for admin: HTTPS_CATALOG
'language_id' => (int) $this->config->get('config_language_id'),
],
'paths' => [
'images' => DIR_IMAGE,
],
'logs' => [
'path' => DIR_LOGS,
],
'database' => [
'host' => DB_HOSTNAME,
'database' => DB_DATABASE,
'username' => DB_USERNAME,
'password' => DB_PASSWORD,
'prefix' => DB_PREFIX,
'port' => (int) DB_PORT,
],
'store' => [
'oc_store_id' => 0,
'oc_default_currency' => $this->config->get('config_currency'),
'oc_config_tax' => filter_var($this->config->get('config_tax'), FILTER_VALIDATE_BOOLEAN),
],
'orders' => [
'oc_customer_group_id' => (int) $this->config->get('config_customer_group_id'),
],
'telegram' => [
'mini_app_url' => rtrim(HTTPS_CATALOG, '/') . '/image/catalog/tgshopspa/#/',
],
]);
$app = ApplicationFactory::create($items);
$app->bind(OcRegistryDecorator::class, fn() => new OcRegistryDecorator($this->registry));
$app->setLogger($logger);
return $app;
}
private function runMaintenanceTasks(): void
{
$logger = $this->createLogger();
try {
$this->createApplication($logger)->runMaintenanceTasks();
} catch (Throwable $exception) {
$logger->error($exception->getMessage(), ['exception' => $exception]);
}
}
private function createLogger(bool $debug = false): Logger
{
$log = new Logger('TeleCart_Admin', [], [], new DateTimeZone('UTC'));
$log->pushHandler(
new RotatingFileHandler(
DIR_LOGS . '/telecart.log', 14, $debug ? Logger::DEBUG : Logger::INFO
),
);
return $log;
}
private function injectAssets(): void
{
$this->document->addScript('view/javascript/summernote/summernote.js');
$this->document->addStyle('view/javascript/summernote/summernote.css');
}
}

View File

@@ -0,0 +1,14 @@
<?php
// Heading
$_['heading_title'] = 'TeleCart';
$_['text_module'] = 'Модули';
$_['text_success'] = 'Настройки успешно изменены!';
$_['text_edit'] = 'Настройки';
// Entry
$_['entry_status'] = 'Статус';
// Error
$_['error_permission'] = 'У вас недостаточно прав для внесения изменений!';

View File

@@ -0,0 +1,18 @@
(function() {
function injectVueScript() {
const script = document.createElement('script');
script.type = 'module';
//if (window.vueIntegration && window.vueIntegration.developmentEnv) {
script.src = 'http://localhost:3000/src/main.js';
//} else {
// script.src = '/path/to/built/main.js';
//}
document.body.appendChild(script);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', injectVueScript);
} else { // Документ уже загружен
injectVueScript();
}
})();

View File

@@ -0,0 +1,36 @@
{{ header }}{{ column_left }}
<div id="content">
<div class="container-fluid" style="margin-top: 10px;">
{% if telecart_error_warning %}
<div class="alert alert-danger alert-dismissible"><i
class="fa fa-exclamation-circle"></i> {{ telecart_error_warning }}
<button type="button" class="close" data-dismiss="alert">&times;</button>
</div>
{% endif %}
{% if success %}
<div class="alert alert-success alert-dismissible"><i class="fa fa-check-circle"></i> {{ success }}
<button type="button" class="close" data-dismiss="alert">&times;</button>
</div>
{% endif %}
<div class="panel panel-default">
<div class="panel-body">
<script>
window.TeleCart = {
user_token: '{{ user_token }}',
themes: '{{ themes | json_encode }}',
order_statuses: '{{ order_statuses | json_encode }}',
shop_base_url: '{{ shop_base_url }}',
};
</script>
<div id="app" class="telecart-admin-app">App Loading...</div>
</div>
</div>
<div class="text-muted text-right" style="margin-top: 10px; font-size: 12px;">
Версия модуля: {{ telecart_module_version }}
</div>
</div>
</div>
<script type="text/javascript" src="view/javascript/summernote/summernote.js"></script>
<link href="view/javascript/summernote/summernote.css" rel="stylesheet" />
{{ footer }}

View File

@@ -0,0 +1,137 @@
<?php
use App\Adapters\OcModelCatalogProductAdapter;
use App\ApplicationFactory;
use Cart\Cart;
use Cart\Currency;
use Cart\Tax;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Logger;
use Openguru\OpenCartFramework\OpenCart\Currency as TelecartCurrency;
use Openguru\OpenCartFramework\OpenCart\Decorators\OcRegistryDecorator;
use Openguru\OpenCartFramework\Support\Arr;
use Psr\Log\LoggerInterface;
$sysLibPath = rtrim(DIR_SYSTEM, '/') . '/library/oc_telegram_shop';
$basePath = rtrim(DIR_APPLICATION, '/') . '/..';
if (is_readable($sysLibPath . '/oc_telegram_shop.phar')) {
require_once "phar://{$sysLibPath}/oc_telegram_shop.phar/vendor/autoload.php";
} elseif (is_dir("$basePath/oc_telegram_shop")) {
require_once "$basePath/oc_telegram_shop/vendor/autoload.php";
} else {
throw new RuntimeException('Unable to locate application directory.');
}
/**
* @property Config $config
* @property Log $log
*/
class ControllerExtensionTgshopHandle extends Controller
{
public function __construct($registry)
{
parent::__construct($registry);
$this->load->model('catalog/product');
$this->load->model('checkout/order');
$this->load->model('setting/setting');
}
public function index(): void
{
$this->session->data['language'] = $this->config->get('config_language');
$json = $this->model_setting_setting->getSetting('module_telecart');
if (! isset($json['module_telecart_settings'])) {
$json['module_telecart_settings'] = [];
}
$items = Arr::mergeArraysRecursively($json['module_telecart_settings'], [
'app' => [
'shop_base_url' => HTTPS_SERVER, // for catalog: HTTPS_SERVER, for admin: HTTPS_CATALOG
'language_id' => (int)$this->config->get('config_language_id'),
'oc_timezone' => $this->config->get('config_timezone'),
],
'paths' => [
'images' => DIR_IMAGE,
],
'logs' => [
'path' => DIR_LOGS,
],
'database' => [
'host' => DB_HOSTNAME,
'database' => DB_DATABASE,
'username' => DB_USERNAME,
'password' => DB_PASSWORD,
'prefix' => DB_PREFIX,
'port' => (int)DB_PORT,
],
'store' => [
'oc_store_id' => 0,
'oc_default_currency' => $this->config->get('config_currency'),
'oc_config_tax' => filter_var($this->config->get('config_tax'), FILTER_VALIDATE_BOOLEAN),
],
'orders' => [
'oc_customer_group_id' => (int)$this->config->get('config_customer_group_id'),
],
]);
$appDebug = Arr::get($items, 'app.app_debug', false);
$app = ApplicationFactory::create($items);
$app->bind(OcModelCatalogProductAdapter::class, function () {
return new OcModelCatalogProductAdapter($this->model_catalog_product);
});
$app->bind(Url::class, fn() => $this->url);
$app->bind(Currency::class, fn() => new TelecartCurrency($this->registry));
$app->bind(Tax::class, fn() => $this->tax);
$app->bind(Cart::class, fn() => $this->cart);
$app->bind(OcRegistryDecorator::class, fn() => new OcRegistryDecorator($this->registry));
$app->singleton(Log::class, fn() => $this->log);
$app
->withLogger(fn($app) => $this->createLogger($appDebug))
->bootAndHandleRequest();
}
/** @noinspection PhpUnused */
public function ya_metrika(): void
{
$json = $this->model_setting_setting->getSetting('module_telecart');
if (isset($json['module_telecart_settings'])) {
$yaCounterId = Arr::get($json, 'module_telecart_settings.metrics.yandex_metrika_counter');
$raw = <<<JS
(function(m,e,t,r,i,k,a){
m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)
})(window, document,'script','https://mc.yandex.ru/metrika/tag.js?id={$yaCounterId}', 'ym');
ym({$yaCounterId}, 'init', {ssr: true, defer: true, ecommerce:"dataLayer", accurateTrackBounce:true, trackLinks:true});
JS;
http_response_code(200);
header('Content-Type: application/javascript');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
header('Access-Control-Allow-Credentials: true');
echo $raw;
} else {
http_response_code(404);
}
}
private function createLogger(bool $appDebug = false, string $app = 'TeleCart'): LoggerInterface
{
$log = new Logger($app, [], [], new DateTimeZone('UTC'));
$log->pushHandler(
new RotatingFileHandler(DIR_LOGS . '/telecart.log', 14, $appDebug ? Logger::DEBUG : Logger::INFO),
);
return $log;
}
}

View File

@@ -5,6 +5,7 @@ use Console\ApplicationFactory;
use Console\Commands\CacheClearCommand; use Console\Commands\CacheClearCommand;
use Console\Commands\CustomerCountsCommand; use Console\Commands\CustomerCountsCommand;
use Console\Commands\PulseSendEventsCommand; use Console\Commands\PulseSendEventsCommand;
use Console\Commands\ScheduleListCommand;
use Console\Commands\ScheduleRunCommand; use Console\Commands\ScheduleRunCommand;
use Console\Commands\VersionCommand; use Console\Commands\VersionCommand;
use Console\Commands\ImagesWarmupCacheCommand; use Console\Commands\ImagesWarmupCacheCommand;
@@ -20,7 +21,7 @@ if (PHP_SAPI !== 'cli') {
} }
$baseDir = __DIR__; $baseDir = __DIR__;
$debug = false; $debug = true;
if (is_readable($baseDir . '/oc_telegram_shop.phar')) { if (is_readable($baseDir . '/oc_telegram_shop.phar')) {
require_once "phar://{$baseDir}/oc_telegram_shop.phar/vendor/autoload.php"; require_once "phar://{$baseDir}/oc_telegram_shop.phar/vendor/autoload.php";
@@ -32,6 +33,8 @@ if (is_readable($baseDir . '/oc_telegram_shop.phar')) {
throw new RuntimeException('Unable to locate application directory.'); throw new RuntimeException('Unable to locate application directory.');
} }
date_default_timezone_set('UTC');
// Get Settings from Database // Get Settings from Database
$host = DB_HOSTNAME; $host = DB_HOSTNAME;
$username = DB_USERNAME; $username = DB_USERNAME;
@@ -42,15 +45,12 @@ $prefix = DB_PREFIX;
$dsn = "mysql:host=$host;port=$port;dbname=$dbName"; $dsn = "mysql:host=$host;port=$port;dbname=$dbName";
$pdo = new PDO($dsn, $username, $password); $pdo = new PDO($dsn, $username, $password);
$connection = new MySqlConnection($pdo); $connection = new MySqlConnection($pdo);
$raw = $connection->select("SELECT value FROM `{$prefix}setting` WHERE `key` = 'module_megapay_settings'"); $raw = $connection->select("SELECT value FROM `{$prefix}setting` WHERE `key` = 'module_telecart_settings'");
$timezone = $connection->select("SELECT value FROM `{$prefix}setting` WHERE `key` = 'config_timezone'");
$timezone = $timezone[0]['value'] ?? 'UTC';
$json = json_decode($raw[0]['value'], true, 512, JSON_THROW_ON_ERROR); $json = json_decode($raw[0]['value'], true, 512, JSON_THROW_ON_ERROR);
$items = Arr::mergeArraysRecursively($json, [ $items = Arr::mergeArraysRecursively($json, [
'app' => [ 'app' => [
'shop_base_url' => HTTPS_CATALOG, // for catalog: HTTPS_SERVER, for admin: HTTPS_CATALOG 'shop_base_url' => HTTPS_CATALOG, // for catalog: HTTPS_SERVER, for admin: HTTPS_CATALOG
'language_id' => 1, 'language_id' => 1,
'oc_timezone' => $timezone,
], ],
'paths' => [ 'paths' => [
'images' => DIR_IMAGE, 'images' => DIR_IMAGE,
@@ -79,20 +79,24 @@ $items = Arr::mergeArraysRecursively($json, [
], ],
]); ]);
$logger = new Logger('MegaPay_CLI', [], [], new DateTimeZone('UTC')); // Create logger
$logger = new Logger('TeleCart_CLI', [], [], new DateTimeZone('UTC'));
$logger->pushHandler( $logger->pushHandler(
new RotatingFileHandler( new RotatingFileHandler(
DIR_LOGS . '/megapay.log', 14, $debug ? Logger::DEBUG : Logger::INFO DIR_LOGS . '/telecart.log', 14, $debug ? Logger::DEBUG : Logger::INFO
), ),
); );
// Creates TeleCart application.
$app = ApplicationFactory::create($items); $app = ApplicationFactory::create($items);
$app->setLogger($logger); $app->setLogger($logger);
$app->boot(); $app->boot();
$console = new Application('MegaPay', module_version()); // Creates Console and bind commands.
$console = new Application('TeleCart', module_version());
$console->add($app->get(VersionCommand::class)); $console->add($app->get(VersionCommand::class));
$console->add($app->get(ScheduleRunCommand::class)); $console->add($app->get(ScheduleRunCommand::class));
$console->add($app->get(ScheduleListCommand::class));
$console->add($app->get(PulseSendEventsCommand::class)); $console->add($app->get(PulseSendEventsCommand::class));
$console->add($app->get(ImagesWarmupCacheCommand::class)); $console->add($app->get(ImagesWarmupCacheCommand::class));
$console->add($app->get(ImagesCacheClearCommand::class)); $console->add($app->get(ImagesCacheClearCommand::class));

View File

@@ -0,0 +1,8 @@
APP_DEBUG=true
PULSE_API_HOST=https://pulse.telecart.pro/api/
PULSE_HEARTBEAT_SECRET=c5261f5d-529e-45ad-a69c-9778b755b7cb
TELECART_CACHE_DRIVER=redis
#TELECART_REDIS_HOST=redis
#TELECART_REDIS_PORT=6379
#TELECART_REDIS_DATABASE=0

View File

@@ -0,0 +1,7 @@
APP_DEBUG=false
PULSE_API_HOST=https://pulse.telecart.pro/api/
PULSE_HEARTBEAT_SECRET=c5261f5d-529e-45ad-a69c-9778b755b7cb
TELECART_CACHE_DRIVER=mysql
TELECART_REDIS_HOST=redis
TELECART_REDIS_PORT=6379
TELECART_REDIS_DATABASE=0

View File

@@ -12,7 +12,7 @@ use Openguru\OpenCartFramework\ImageTool\ImageToolServiceProvider;
use Openguru\OpenCartFramework\QueryBuilder\QueryBuilderServiceProvider; use Openguru\OpenCartFramework\QueryBuilder\QueryBuilderServiceProvider;
use Openguru\OpenCartFramework\Router\RouteServiceProvider; use Openguru\OpenCartFramework\Router\RouteServiceProvider;
use Openguru\OpenCartFramework\Support\Arr; use Openguru\OpenCartFramework\Support\Arr;
use Openguru\OpenCartFramework\MegaPayPulse\MegaPayPulseServiceProvider; use Openguru\OpenCartFramework\TeleCartPulse\TeleCartPulseServiceProvider;
use Openguru\OpenCartFramework\Telegram\TelegramServiceProvider; use Openguru\OpenCartFramework\Telegram\TelegramServiceProvider;
class ApplicationFactory class ApplicationFactory
@@ -33,7 +33,7 @@ class ApplicationFactory
AppServiceProvider::class, AppServiceProvider::class,
CacheServiceProvider::class, CacheServiceProvider::class,
TelegramServiceProvider::class, TelegramServiceProvider::class,
MegaPayPulseServiceProvider::class, TeleCartPulseServiceProvider::class,
ImageToolServiceProvider::class, ImageToolServiceProvider::class,
]); ]);
} }

View File

@@ -33,7 +33,7 @@ class FormsHandler
} }
$form = $this->builder->newQuery() $form = $this->builder->newQuery()
->from('megapay_forms') ->from('telecart_forms')
->where('alias', '=', $alias) ->where('alias', '=', $alias)
->firstOrNull(); ->firstOrNull();

View File

@@ -198,7 +198,7 @@ class LogsHandler
private function findLastLogsFileInDir(string $dir): ?string private function findLastLogsFileInDir(string $dir): ?string
{ {
$files = glob($dir . '/megapay-*.log'); $files = glob($dir . '/telecart-*.log');
return $files ? end($files) : null; return $files ? end($files) : null;
} }

View File

@@ -87,7 +87,7 @@ class SendMessageHandler
* Извлечь ID записи из запроса * Извлечь ID записи из запроса
* *
* @param Request $request HTTP запрос * @param Request $request HTTP запрос
* @return int ID записи в таблице megapay_customers * @return int ID записи в таблице telecart_customers
* @throws RuntimeException Если ID отсутствует или невалиден * @throws RuntimeException Если ID отсутствует или невалиден
*/ */
private function extractCustomerId(Request $request): int private function extractCustomerId(Request $request): int

View File

@@ -4,9 +4,7 @@ namespace Bastion\Handlers;
use Bastion\Exceptions\BotTokenConfiguratorException; use Bastion\Exceptions\BotTokenConfiguratorException;
use Bastion\Services\BotTokenConfigurator; use Bastion\Services\BotTokenConfigurator;
use Bastion\Services\CronApiKeyRegenerator;
use Bastion\Services\SettingsService; use Bastion\Services\SettingsService;
use Carbon\Carbon;
use Exception; use Exception;
use Openguru\OpenCartFramework\Cache\CacheInterface; use Openguru\OpenCartFramework\Cache\CacheInterface;
use Openguru\OpenCartFramework\Config\Settings; use Openguru\OpenCartFramework\Config\Settings;
@@ -15,56 +13,35 @@ use Openguru\OpenCartFramework\Http\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Openguru\OpenCartFramework\QueryBuilder\Builder; use Openguru\OpenCartFramework\QueryBuilder\Builder;
use Openguru\OpenCartFramework\QueryBuilder\Connections\ConnectionInterface; use Openguru\OpenCartFramework\QueryBuilder\Connections\ConnectionInterface;
use Openguru\OpenCartFramework\Scheduler\Models\ScheduledJob;
use Openguru\OpenCartFramework\Support\Arr; use Openguru\OpenCartFramework\Support\Arr;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
class SettingsHandler class SettingsHandler
{ {
private BotTokenConfigurator $botTokenConfigurator; private BotTokenConfigurator $botTokenConfigurator;
private CronApiKeyRegenerator $cronApiKeyRegenerator;
private Settings $settings; private Settings $settings;
private SettingsService $settingsUpdateService; private SettingsService $settingsUpdateService;
private CacheInterface $cache; private CacheInterface $cache;
private LoggerInterface $logger; private LoggerInterface $logger;
private Builder $builder; private Builder $builder;
private ConnectionInterface $connection; private ConnectionInterface $connection;
private ScheduledJob $scheduledJob;
public function __construct( public function __construct(
BotTokenConfigurator $botTokenConfigurator, BotTokenConfigurator $botTokenConfigurator,
CronApiKeyRegenerator $cronApiKeyRegenerator,
Settings $settings, Settings $settings,
SettingsService $settingsUpdateService, SettingsService $settingsUpdateService,
CacheInterface $cache, CacheInterface $cache,
LoggerInterface $logger, LoggerInterface $logger,
Builder $builder, Builder $builder,
ConnectionInterface $connection, ConnectionInterface $connection
ScheduledJob $scheduledJob
) { ) {
$this->botTokenConfigurator = $botTokenConfigurator; $this->botTokenConfigurator = $botTokenConfigurator;
$this->cronApiKeyRegenerator = $cronApiKeyRegenerator;
$this->settings = $settings; $this->settings = $settings;
$this->settingsUpdateService = $settingsUpdateService; $this->settingsUpdateService = $settingsUpdateService;
$this->cache = $cache; $this->cache = $cache;
$this->logger = $logger; $this->logger = $logger;
$this->builder = $builder; $this->builder = $builder;
$this->connection = $connection; $this->connection = $connection;
$this->scheduledJob = $scheduledJob;
}
/**
* Перегенерировать секретный ключ в URL для cron-job.org (сохраняет cron.api_key).
*/
public function regenerateCronScheduleUrl(Request $request): JsonResponse
{
$newApiKey = $this->cronApiKeyRegenerator->regenerate();
$scheduleUrl = $this->buildCronScheduleUrl(
$this->settings->get('app.shop_base_url', ''),
$newApiKey
);
return new JsonResponse(['api_key' => $newApiKey, 'schedule_url' => $scheduleUrl]);
} }
public function configureBotToken(Request $request): JsonResponse public function configureBotToken(Request $request): JsonResponse
@@ -104,15 +81,9 @@ class SettingsHandler
// Add CRON system details (read-only) // Add CRON system details (read-only)
$data['cron']['cli_path'] = BP_REAL_BASE_PATH . '/cli.php'; $data['cron']['cli_path'] = BP_REAL_BASE_PATH . '/cli.php';
$data['cron']['last_run'] = $this->getLastCronRunDate(); $data['cron']['last_run'] = $this->getLastCronRunDate();
$data['cron']['schedule_url'] = $this->buildCronScheduleUrl(
$this->settings->get('app.shop_base_url', ''),
$this->settings->get('cron.api_key', '')
);
$data['scheduled_jobs'] = $this->scheduledJob->all();
$forms = $this->builder->newQuery() $forms = $this->builder->newQuery()
->from('megapay_forms') ->from('telecart_forms')
->get(); ->get();
if ($forms) { if ($forms) {
@@ -135,21 +106,6 @@ class SettingsHandler
return new JsonResponse(compact('data')); return new JsonResponse(compact('data'));
} }
private function buildCronScheduleUrl(string $shopBaseUrl, string $apiKey): string
{
$base = rtrim($shopBaseUrl, '/');
if ($base === '') {
return '';
}
$params = http_build_query([
'route' => 'extension/tgshop/handle',
'api_action' => 'runSchedule',
'api_key' => $apiKey,
]);
return $base . '/index.php?' . $params;
}
public function saveSettingsForm(Request $request): JsonResponse public function saveSettingsForm(Request $request): JsonResponse
{ {
$input = $request->json(); $input = $request->json();
@@ -160,7 +116,6 @@ class SettingsHandler
if (isset($input['cron'])) { if (isset($input['cron'])) {
unset($input['cron']['cli_path']); unset($input['cron']['cli_path']);
unset($input['cron']['last_run']); unset($input['cron']['last_run']);
unset($input['cron']['schedule_url']);
} }
$this->settingsUpdateService->update( $this->settingsUpdateService->update(
@@ -184,32 +139,13 @@ class SettingsHandler
$schema = json_encode($form['schema'], JSON_THROW_ON_ERROR); $schema = json_encode($form['schema'], JSON_THROW_ON_ERROR);
$this->builder->newQuery() $this->builder->newQuery()
->where('alias', '=', $form['alias']) ->where('alias', '=', $form['alias'])
->update('megapay_forms', [ ->update('telecart_forms', [
'friendly_name' => $form['friendly_name'], 'friendly_name' => $form['friendly_name'],
'is_custom' => $form['is_custom'], 'is_custom' => $form['is_custom'],
'schema' => $schema, 'schema' => $schema,
]); ]);
} }
// Update scheduled jobs is_enabled and cron_expression
$scheduledJobs = Arr::get($input, 'scheduled_jobs', []);
foreach ($scheduledJobs as $job) {
$id = (int) ($job['id'] ?? 0);
if ($id <= 0) {
continue;
}
$isEnabled = filter_var($job['is_enabled'] ?? false, FILTER_VALIDATE_BOOLEAN);
if ($isEnabled) {
$this->scheduledJob->enable($id);
} else {
$this->scheduledJob->disable($id);
}
$cronExpression = trim((string) ($job['cron_expression'] ?? ''));
if ($cronExpression !== '') {
$this->scheduledJob->updateCronExpression($id, $cronExpression);
}
}
return new JsonResponse([], Response::HTTP_ACCEPTED); return new JsonResponse([], Response::HTTP_ACCEPTED);
} }
@@ -238,7 +174,7 @@ class SettingsHandler
$lastRunTimestamp = $this->cache->get("scheduler.global_last_run"); $lastRunTimestamp = $this->cache->get("scheduler.global_last_run");
if ($lastRunTimestamp) { if ($lastRunTimestamp) {
return Carbon::createFromTimestamp($lastRunTimestamp)->toDateTimeString(); return date('d.m.Y H:i:s', (int)$lastRunTimestamp);
} }
return null; return null;
@@ -267,7 +203,7 @@ class SettingsHandler
$info['MySQL Version'] = 'Error: ' . $e->getMessage(); $info['MySQL Version'] = 'Error: ' . $e->getMessage();
} }
$cacheDriver = env('MEGAPAY_CACHE_DRIVER', 'mysql'); $cacheDriver = env('TELECART_CACHE_DRIVER', 'mysql');
$cacheClass = get_class($this->cache); $cacheClass = get_class($this->cache);
$info['Cache Driver'] = $cacheDriver . ' (' . basename(str_replace('\\', '/', $cacheClass)) . ')'; $info['Cache Driver'] = $cacheDriver . ' (' . basename(str_replace('\\', '/', $cacheClass)) . ')';

View File

@@ -30,12 +30,12 @@ class StatsHandler
new RawExpression('SUM(orders.total) AS orders_total_amount'), new RawExpression('SUM(orders.total) AS orders_total_amount'),
]) ])
->from(db_table('order'), 'orders') ->from(db_table('order'), 'orders')
->join('megapay_customers', function (JoinClause $join) { ->join('telecart_customers', function (JoinClause $join) {
$join->on('orders.customer_id', '=', 'megapay_customers.oc_customer_id'); $join->on('orders.customer_id', '=', 'telecart_customers.oc_customer_id');
}) })
->join('megapay_order_meta', function (JoinClause $join) { ->join('telecart_order_meta', function (JoinClause $join) {
$join->on('orders.order_id', '=', 'megapay_order_meta.oc_order_id') $join->on('orders.order_id', '=', 'telecart_order_meta.oc_order_id')
->whereRaw('orders.store_id = megapay_order_meta.oc_store_id'); ->whereRaw('orders.store_id = telecart_order_meta.oc_store_id');
}) })
->firstOrNull(); ->firstOrNull();
@@ -54,7 +54,7 @@ class StatsHandler
private function countCustomersCount(): int private function countCustomersCount(): int
{ {
return $this->builder->newQuery() return $this->builder->newQuery()
->from('megapay_customers') ->from('telecart_customers')
->count(); ->count();
} }
} }

View File

@@ -3,17 +3,17 @@
namespace Bastion\Handlers; namespace Bastion\Handlers;
use Openguru\OpenCartFramework\Cache\CacheInterface; use Openguru\OpenCartFramework\Cache\CacheInterface;
use Openguru\OpenCartFramework\MegaPayPulse\MegaPayEvent; use Openguru\OpenCartFramework\TeleCartPulse\TeleCartEvent;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
class MegaPayPulseStatsHandler class TeleCartPulseStatsHandler
{ {
private MegaPayEvent $eventModel; private TeleCartEvent $eventModel;
private CacheInterface $cache; private CacheInterface $cache;
private const CACHE_KEY = 'megapay_pulse_stats'; private const CACHE_KEY = 'telecart_pulse_stats';
private const CACHE_TTL = 3600; // 1 час private const CACHE_TTL = 3600; // 1 час
public function __construct(MegaPayEvent $eventModel, CacheInterface $cache) public function __construct(TeleCartEvent $eventModel, CacheInterface $cache)
{ {
$this->eventModel = $eventModel; $this->eventModel = $eventModel;
$this->cache = $cache; $this->cache = $cache;

Some files were not shown because too many files have changed in this diff Show More