diff --git a/frontend/admin/src/App.vue b/frontend/admin/src/App.vue index 7c72a05..5185a9f 100644 --- a/frontend/admin/src/App.vue +++ b/frontend/admin/src/App.vue @@ -38,6 +38,10 @@ Telegram Покупатели +
  • + TeleCart Pulse +
  • +
  • Журнал событий
  • diff --git a/frontend/admin/src/router/index.js b/frontend/admin/src/router/index.js index 222019c..985d688 100644 --- a/frontend/admin/src/router/index.js +++ b/frontend/admin/src/router/index.js @@ -9,6 +9,7 @@ import MainPageView from "@/views/MainPageView.vue"; import LogsView from "@/views/LogsView.vue"; import FormBuilderView from "@/views/FormBuilderView.vue"; import CustomersView from "@/views/CustomersView.vue"; +import TeleCartPulseView from "@/views/TeleCartPulseView.vue"; const router = createRouter({ history: createMemoryHistory(), @@ -20,6 +21,7 @@ const router = createRouter({ {path: '/mainpage', name: 'mainpage', component: MainPageView}, {path: '/metrics', name: 'metrics', component: MetricsView}, {path: '/orders', name: 'orders', component: OrdersView}, + {path: '/pulse', name: 'pulse', component: TeleCartPulseView}, {path: '/store', name: 'store', component: StoreView}, {path: '/telegram', name: 'telegram', component: TelegramView}, {path: '/texts', name: 'texts', component: TextsView}, diff --git a/frontend/admin/src/stores/settings.js b/frontend/admin/src/stores/settings.js index 4e40bec..4167755 100644 --- a/frontend/admin/src/stores/settings.js +++ b/frontend/admin/src/stores/settings.js @@ -73,6 +73,10 @@ export const useSettingsStore = defineStore('settings', { schema: [], } }, + + pulse: { + api_key: '', + }, }, }), diff --git a/frontend/admin/src/views/TeleCartPulseView.vue b/frontend/admin/src/views/TeleCartPulseView.vue new file mode 100644 index 0000000..12cc762 --- /dev/null +++ b/frontend/admin/src/views/TeleCartPulseView.vue @@ -0,0 +1,15 @@ + + + diff --git a/frontend/spa/src/constants/tPulseEvents.js b/frontend/spa/src/constants/tPulseEvents.js new file mode 100644 index 0000000..59f30b7 --- /dev/null +++ b/frontend/spa/src/constants/tPulseEvents.js @@ -0,0 +1,3 @@ +export const TC_PULSE_EVENTS = { + WEBAPP_OPEN: 'WEBAPP_OPEN', +}; diff --git a/frontend/spa/src/main.js b/frontend/spa/src/main.js index 993a6d8..238816c 100644 --- a/frontend/spa/src/main.js +++ b/frontend/spa/src/main.js @@ -8,18 +8,18 @@ import {useSettingsStore} from "@/stores/SettingsStore.js"; import ApplicationError from "@/ApplicationError.vue"; import AppMetaInitializer from "@/utils/AppMetaInitializer.ts"; import {injectYaMetrika} from "@/utils/yaMetrika.js"; -import {checkIsUserPrivacyConsented, saveTelegramCustomer} from "@/utils/ftch.js"; +import {checkIsUserPrivacyConsented, ingest, saveTelegramCustomer} from "@/utils/ftch.js"; import {register} from 'swiper/element/bundle'; import 'swiper/element/bundle'; import 'swiper/css/bundle'; import AppLoading from "@/AppLoading.vue"; -import {useProductFiltersStore} from "@/stores/ProductFiltersStore.js"; import {useBlocksStore} from "@/stores/BlocksStore.js"; import {getCssVarOklchRgb} from "@/helpers.js"; import {defaultConfig, plugin} from '@formkit/vue'; import config from './formkit.config.js'; +import {TC_PULSE_EVENTS} from "@/constants/tPulseEvents.js"; register(); @@ -51,18 +51,22 @@ settings.load() throw new Error('App disabled (maintenance mode)'); } }) - .then(async () => { + .then(() => { + const webapp = window.Telegram.WebApp; + ingest({ + event: TC_PULSE_EVENTS.WEBAPP_OPEN, + webapp, + }) + .catch(err => console.error('Ingest failed:', err)); + }) + .then(() => { // Сохраняем данные Telegram-пользователя в базу данных const userData = window.Telegram?.WebApp?.initDataUnsafe?.user; if (userData) { - try { - console.debug('[Init] Saving Telegram customer data'); - await saveTelegramCustomer(userData); - console.debug('[Init] Telegram customer data saved successfully'); - } catch (error) { - // Не прерываем загрузку приложения, если не удалось сохранить пользователя - console.warn('[Init] Failed to save Telegram customer data:', error); - } + console.debug('[Init] Saving Telegram customer data'); + saveTelegramCustomer(userData) + .then(() => console.debug('[Init] Telegram customer data saved successfully')) + .catch(() => console.warn('[Init] Failed to save Telegram customer data:', error)); } }) .then(() => { @@ -82,11 +86,11 @@ settings.load() })(); }) .then(() => blocks.processBlocks(settings.mainpage_blocks)) - .then(async () => { - console.debug('Load default filters for the main page'); - const filtersStore = useProductFiltersStore(); - filtersStore.applied = await filtersStore.fetchFiltersForMainPage(); - }) + // .then(async () => { + // console.debug('Load default filters for the main page'); + // const filtersStore = useProductFiltersStore(); + // filtersStore.applied = await filtersStore.fetchFiltersForMainPage(); + // }) .then(() => { console.debug('[Init] Set theme attributes'); document.documentElement.setAttribute('data-theme', settings.theme[window.Telegram.WebApp.colorScheme]); diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/.env b/module/oc_telegram_shop/upload/oc_telegram_shop/.env index ea0466f..5f30767 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/.env +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/.env @@ -1 +1,2 @@ APP_DEBUG=true +PULSE_API_HOST=http://host.docker.internal:8086/api/ diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/bastion/ApplicationFactory.php b/module/oc_telegram_shop/upload/oc_telegram_shop/bastion/ApplicationFactory.php index 40bb263..1daf61b 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/bastion/ApplicationFactory.php +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/bastion/ApplicationFactory.php @@ -11,6 +11,7 @@ use Openguru\OpenCartFramework\Cache\CacheServiceProvider; use Openguru\OpenCartFramework\QueryBuilder\QueryBuilderServiceProvider; use Openguru\OpenCartFramework\Router\RouteServiceProvider; use Openguru\OpenCartFramework\Support\Arr; +use Openguru\OpenCartFramework\TeleCartPulse\TeleCartPulseServiceProvider; use Openguru\OpenCartFramework\Telegram\TelegramServiceProvider; class ApplicationFactory @@ -31,6 +32,7 @@ class ApplicationFactory AppServiceProvider::class, CacheServiceProvider::class, TelegramServiceProvider::class, + TeleCartPulseServiceProvider::class, ]); } } diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/bastion/Handlers/SettingsHandler.php b/module/oc_telegram_shop/upload/oc_telegram_shop/bastion/Handlers/SettingsHandler.php index 4b7abed..40f0765 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/bastion/Handlers/SettingsHandler.php +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/bastion/Handlers/SettingsHandler.php @@ -68,6 +68,7 @@ class SettingsHandler 'texts', 'sliders', 'mainpage_blocks', + 'pulse', ]); $data['forms'] = []; @@ -107,6 +108,7 @@ class SettingsHandler 'texts', 'sliders', 'mainpage_blocks', + 'pulse', ]), ); diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/ErrorHandler.php b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/ErrorHandler.php index 42a52c9..2e62fc8 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/ErrorHandler.php +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/ErrorHandler.php @@ -5,6 +5,7 @@ namespace Openguru\OpenCartFramework; use ErrorException; use Openguru\OpenCartFramework\Contracts\ExceptionHandlerInterface; use Openguru\OpenCartFramework\Exceptions\ActionNotFoundException; +use Openguru\OpenCartFramework\Exceptions\InvalidApiTokenException; use Openguru\OpenCartFramework\Exceptions\NonLoggableExceptionInterface; use Openguru\OpenCartFramework\Http\JsonResponse; use Openguru\OpenCartFramework\Http\Response; @@ -71,6 +72,13 @@ class ErrorHandler exit(1); } + if ($exception instanceof InvalidApiTokenException) { + (new JsonResponse([ + 'message' => $exception->getMessage(), + ], $exception->getCode()))->send(); + exit(1); + } + if (PHP_SAPI === 'cli') { echo $exception->getMessage() . PHP_EOL; } else { diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Exceptions/InvalidApiTokenException.php b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Exceptions/InvalidApiTokenException.php new file mode 100644 index 0000000..2eb185c --- /dev/null +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Exceptions/InvalidApiTokenException.php @@ -0,0 +1,14 @@ +content === null || $this->content === '') { - $this->content = (string) file_get_contents('php://input'); + $this->content = (string)file_get_contents('php://input'); } return $this->content; @@ -115,4 +115,14 @@ final class Request return $this->json($key) !== null; } + + public function getApiKey(): ?string + { + $header = + $_SERVER['HTTP_X_API_KEY'] + ?? $this->header('X-API-KEY') + ?? null; + + return $header ? trim($header) : null; + } } diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Support/Utils.php b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Support/Utils.php index 1ff9a85..2f385df 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Support/Utils.php +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Support/Utils.php @@ -71,4 +71,31 @@ class Utils { return $value === true ? 1 : 0; } + + public static function getCurrentDomain(): string + { + $scheme = 'http'; + if ( + (! empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || + (! empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') + ) { + $scheme = 'https'; + } + + // Определяем хост + $host = $_SERVER['HTTP_X_FORWARDED_HOST'] + ?? $_SERVER['HTTP_HOST'] + ?? $_SERVER['SERVER_NAME'] + ?? 'localhost'; + + // Порт + $port = $_SERVER['SERVER_PORT'] ?? null; + $defaultPort = ($scheme === 'https') ? 443 : 80; + + if ($port && $port != $defaultPort && strpos($host, ':') === false) { + $host .= ':' . $port; + } + + return $scheme . '://' . $host; + } } diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/TeleCartPulse/PayloadSignException.php b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/TeleCartPulse/PayloadSignException.php new file mode 100644 index 0000000..17044c9 --- /dev/null +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/TeleCartPulse/PayloadSignException.php @@ -0,0 +1,9 @@ +secret = $secret; + } + + /** + * @throws PayloadSignException + */ + public function sign(array $payload): string + { + $encoded = $this->encodeJson($payload); + + return hash_hmac('sha256', $encoded, $this->secret); + } + + /** + * @throws PayloadSignException + */ + public function verify(string $signature, array $payload): bool + { + $encoded = $this->encodeJson($payload); + $expected = hash_hmac('sha256', $encoded, $this->secret); + + return hash_equals($signature, $expected); + } + + private function encodeJson(array $payload): string + { + try { + return json_encode($payload, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + } catch (JsonException $e) { + throw new PayloadSignException('Could not encode JSON: ' . $e->getMessage(), 0, $e); + } + } +} diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/TeleCartPulse/PulseEvents.php b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/TeleCartPulse/PulseEvents.php new file mode 100644 index 0000000..c02f66c --- /dev/null +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/TeleCartPulse/PulseEvents.php @@ -0,0 +1,8 @@ + $parameters Массив параметров для передачи + * @return string Сериализованная строка параметров + * @throws \InvalidArgumentException Если не удалось закодировать параметры + */ + public static function serialize(array $parameters): string + { + if (empty($parameters)) { + return ''; + } + + $json = json_encode($parameters, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + + if ($json === false) { + throw new \InvalidArgumentException('Failed to encode parameters to JSON: ' . json_last_error_msg()); + } + + // Используем base64 для безопасной передачи в URL + $encoded = base64_encode($json); + + // Заменяем символы, которые могут вызвать проблемы в URL + // + на -, / на _, убираем = в конце (padding) + return rtrim(strtr($encoded, '+/', '-_'), '='); + } + + /** + * Десериализовать строку параметров обратно в массив + * + * @param string $serialized Сериализованная строка параметров + * @return array Массив параметров + * @throws \InvalidArgumentException Если строка не может быть десериализована + */ + public static function deserialize(string $serialized): array + { + if (empty($serialized)) { + return []; + } + + // Восстанавливаем base64 символы + $encoded = strtr($serialized, '-_', '+/'); + + // Добавляем padding если нужно + $padding = strlen($encoded) % 4; + if ($padding !== 0) { + $encoded .= str_repeat('=', 4 - $padding); + } + + $json = base64_decode($encoded, true); + + if ($json === false) { + throw new \InvalidArgumentException('Failed to decode base64 string'); + } + + $parameters = json_decode($json, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new \InvalidArgumentException('Failed to decode JSON: ' . json_last_error_msg()); + } + + if (! is_array($parameters)) { + throw new \InvalidArgumentException('Decoded value is not an array'); + } + + return $parameters; + } +} diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/TeleCartPulse/TeleCartPulseService.php b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/TeleCartPulse/TeleCartPulseService.php new file mode 100644 index 0000000..1101223 --- /dev/null +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/TeleCartPulse/TeleCartPulseService.php @@ -0,0 +1,111 @@ +initDataDecoder = $initDataDecoder; + $this->payloadSigner = $payloadSigner; + $this->apiKey = $apiKey; + } + + /** + * @throws PulseIngestException + */ + public function handleIngest(array $data): void + { + if (! $this->apiKey) { + return; + } + + $initData = Arr::get($data, 'webapp.initData'); + if (! $initData) { + return; + } + + $event = Arr::get($data, 'event'); + + if (! $event) { + return; + } + + try { + $decoded = $this->initDataDecoder->parseInitDataStringToArray($initData); + $startParam = Arr::get($decoded, 'start_param'); + $deserialized = StartParamSerializer::deserialize($startParam); + + if ($event === PulseEvents::WEBAPP_OPEN) { + $this->handleWebAppInit($data, $deserialized); + } + } catch (ClientException $exception) { + $contents = (string)$exception->getResponse()->getBody(); + $decoded = json_decode($contents, true); + throw new PulseIngestException('TeleCart Pulse API error: ' . $decoded['error'] ?? '', 0, $exception); + } catch (Throwable $exception) { + throw new PulseIngestException('Could not handle ingest: ' . $exception->getMessage(), 0, $exception); + } + } + + /** + * @throws PayloadSignException + * @throws GuzzleException + */ + private function handleWebAppInit(array $data, array $deserialized): void + { + // Campaign Event + if (isset($deserialized['campaign_id'], $deserialized['tracking_id'])) { + $payload = [ + 'event' => PulseEvents::WEBAPP_OPEN, + 'campaign_id' => $deserialized['campaign_id'], + 'tracking_id' => $deserialized['tracking_id'], + 'meta' => [ + 'domain' => Utils::getCurrentDomain(), + 'version' => Arr::get($data, 'webapp.version'), + 'platform' => Arr::get($data, 'webapp.platform'), + ], + 'timestamp' => Carbon::now('UTC')->toJSON(), + ]; + + $dataToSend = [ + 'payload' => $payload, + 'signature' => $this->payloadSigner->sign($payload), + ]; + + $this->pushEvent($dataToSend); + } + } + + /** + * @throws GuzzleException + */ + private function pushEvent(array $json): void + { + $baseUri = rtrim(env('PULSE_API_HOST', 'http://localhost'), '/') . '/'; + + $client = new Client([ + 'base_uri' => $baseUri, + 'timeout' => env('PULSE_TIMEOUT', 5.0), + 'headers' => [ + 'Authorization' => 'Bearer ' . $this->apiKey, + 'X-TELECART-VERSION' => '2.0.0', + ], + ]); + + $client->post('events', compact('json')); + } +} diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/TeleCartPulse/TeleCartPulseServiceProvider.php b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/TeleCartPulse/TeleCartPulseServiceProvider.php new file mode 100644 index 0000000..ef98306 --- /dev/null +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/TeleCartPulse/TeleCartPulseServiceProvider.php @@ -0,0 +1,27 @@ +container->singleton(PayloadSigner::class, function (Container $app) { + return new PayloadSigner( + $app->getConfigValue('pulse.api_key'), + ); + }); + + $this->container->singleton(TeleCartPulseService::class, function (Container $app) { + return new TeleCartPulseService( + $app->get(TelegramInitDataDecoder::class), + $app->get(PayloadSigner::class), + $app->getConfigValue('pulse.api_key'), + ); + }); + } +} \ No newline at end of file diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Telegram/TelegramInitDataDecoder.php b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Telegram/TelegramInitDataDecoder.php old mode 100644 new mode 100755 index 74e2eb9..2f167bd --- a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Telegram/TelegramInitDataDecoder.php +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Telegram/TelegramInitDataDecoder.php @@ -32,7 +32,7 @@ class TelegramInitDataDecoder /** * @throws JsonException */ - private function parseInitDataStringToArray(string $initData): array + public function parseInitDataStringToArray(string $initData): array { parse_str($initData, $parsed); diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Telegram/TelegramValidateInitDataMiddleware.php b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Telegram/TelegramValidateInitDataMiddleware.php index 57fd342..c064871 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Telegram/TelegramValidateInitDataMiddleware.php +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Telegram/TelegramValidateInitDataMiddleware.php @@ -13,6 +13,8 @@ class TelegramValidateInitDataMiddleware 'manifest', 'webhook', 'health', + 'etlCustomers', + 'etlCustomersMeta', ]; public function __construct(SignatureValidator $signatureValidator) diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/src/ApplicationFactory.php b/module/oc_telegram_shop/upload/oc_telegram_shop/src/ApplicationFactory.php index 4309d72..4f6e1d9 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/src/ApplicationFactory.php +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/src/ApplicationFactory.php @@ -9,6 +9,7 @@ use Openguru\OpenCartFramework\Cache\CacheServiceProvider; use Openguru\OpenCartFramework\QueryBuilder\QueryBuilderServiceProvider; use Openguru\OpenCartFramework\Router\RouteServiceProvider; use Openguru\OpenCartFramework\Support\Arr; +use Openguru\OpenCartFramework\TeleCartPulse\TeleCartPulseServiceProvider; use Openguru\OpenCartFramework\Telegram\TelegramServiceProvider; use Openguru\OpenCartFramework\Telegram\TelegramValidateInitDataMiddleware; use Openguru\OpenCartFramework\Validator\ValidatorServiceProvider; @@ -30,6 +31,7 @@ class ApplicationFactory AppServiceProvider::class, TelegramServiceProvider::class, ValidatorServiceProvider::class, + TeleCartPulseServiceProvider::class, ]) ->withMiddlewares([ TelegramValidateInitDataMiddleware::class, diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/ETLHandler.php b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/ETLHandler.php new file mode 100755 index 0000000..72b4a0f --- /dev/null +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/ETLHandler.php @@ -0,0 +1,168 @@ +builder = $builder; + $this->settings = $settings; + $this->logger = $logger; + } + + private function getLastUpdatedAtSql(): string + { + return ' + GREATEST( + COALESCE(( + SELECT MAX(date_modified) + FROM oc_order as o + where o.customer_id = telecart_customers.oc_customer_id + ), 0), + telecart_customers.updated_at + ) + '; + } + + private function getCustomerQuery(?Carbon $updatedAt = null): Builder + { + $lastUpdatedAtSql = $this->getLastUpdatedAtSql(); + + return $this->builder->newQuery() + ->from('telecart_customers') + ->where('allows_write_to_pm', '=', 1) + ->when(! empty($updatedAt), function (Builder $builder) use ($lastUpdatedAtSql, $updatedAt) { + $builder->where(new RawExpression($lastUpdatedAtSql), '>=', $updatedAt); + }); + } + + /** + * @throws InvalidApiTokenException + */ + public function getCustomersMeta(Request $request): JsonResponse + { + $this->validateApiKey($request); + + $updatedAt = $request->get('updated_at'); + if ($updatedAt) { + $updatedAt = Carbon::parse($updatedAt); + } + $query = $this->getCustomerQuery($updatedAt); + $total = $query->count(); + + return new JsonResponse([ + 'data' => [ + 'total' => $total, + ], + ]); + } + + /** + * @throws InvalidApiTokenException + */ + public function customers(Request $request): JsonResponse + { + $this->validateApiKey($request); + + $this->logger->debug('Get customers for ETL'); + + $page = (int)$request->get('page', 1); + $perPage = (int)$request->get('perPage', 10000); + $successOrderStatusIds = '5,3'; + $updatedAt = $request->get('updated_at'); + if ($updatedAt) { + $updatedAt = Carbon::parse($updatedAt); + } + + $lastUpdatedAtSql = $this->getLastUpdatedAtSql(); + $query = $this->getCustomerQuery($updatedAt); + + $query->orderBy('telegram_user_id'); + $query->forPage($page, $perPage); + + $query + ->select([ + new RawExpression('md5(telegram_user_id) AS tracking_id'), + 'telegram_user_id' => 'tg_user_id', + 'telecart_customers.oc_customer_id', + 'is_premium', + 'last_seen_at', + 'orders_count' => 'orders_count_total', + 'created_at' => 'registered_at', + new RawExpression( + '(SELECT MIN(date_added) FROM oc_order WHERE oc_order.customer_id = telecart_customers.oc_customer_id) AS first_order_date' + ), + new RawExpression( + '(SELECT MAX(date_added) FROM oc_order WHERE oc_order.customer_id = telecart_customers.oc_customer_id) AS last_order_date' + ), + new RawExpression( + "COALESCE(( + SELECT + SUM(total) + FROM + oc_order + WHERE + oc_order.customer_id = telecart_customers.oc_customer_id + AND oc_order.order_status_id IN ($successOrderStatusIds) + ), 0) AS total_spent" + ), + new RawExpression( + "COALESCE(( + SELECT + COUNT(*) + FROM + oc_order + WHERE + oc_order.customer_id = telecart_customers.oc_customer_id + AND oc_order.order_status_id IN ($successOrderStatusIds) + ), 0) AS orders_count_success" + ), + new RawExpression("$lastUpdatedAtSql AS updated_at"), + ]); + + $items = $query->get(); + + return new JsonResponse([ + 'data' => array_map(static function ($item) { + $item['is_premium'] = filter_var($item['is_premium'], FILTER_VALIDATE_BOOLEAN); + $item['orders_count_total'] = filter_var($item['orders_count_total'], FILTER_VALIDATE_INT); + $item['oc_customer_id'] = filter_var($item['oc_customer_id'], FILTER_VALIDATE_INT); + $item['tg_user_id'] = filter_var($item['tg_user_id'], FILTER_VALIDATE_INT); + $item['orders_count_success'] = filter_var($item['orders_count_success'], FILTER_VALIDATE_INT); + $item['total_spent'] = (float)$item['total_spent']; + + return $item; + }, $items), + ]); + } + + /** + * @throws InvalidApiTokenException + */ + private function validateApiKey(Request $request): void + { + $token = $request->getApiKey(); + + if (empty($token)) { + throw new InvalidApiTokenException('Invalid API Key.'); + } + + if (strcmp($token, $this->settings->get('pulse.api_key')) !== 0) { + throw new InvalidApiTokenException('Invalid API Key'); + } + } +} diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/TelemetryHandler.php b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/TelemetryHandler.php new file mode 100755 index 0000000..9da8f5a --- /dev/null +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/TelemetryHandler.php @@ -0,0 +1,31 @@ +teleCartPulseService = $teleCartPulseService; + } + + /** + * @throws PulseIngestException + */ + public function ingest(Request $request): JsonResponse + { + $this->teleCartPulseService->handleIngest($request->json()); + + return new JsonResponse([], Response::HTTP_NO_CONTENT); + } +} diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/src/routes.php b/module/oc_telegram_shop/upload/oc_telegram_shop/src/routes.php index 3a8e72f..776a0f3 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/src/routes.php +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/src/routes.php @@ -3,6 +3,7 @@ use App\Handlers\BlocksHandler; use App\Handlers\CartHandler; use App\Handlers\CategoriesHandler; +use App\Handlers\ETLHandler; use App\Handlers\FiltersHandler; use App\Handlers\FormsHandler; use App\Handlers\HealthCheckHandler; @@ -12,6 +13,7 @@ use App\Handlers\ProductsHandler; use App\Handlers\SettingsHandler; use App\Handlers\TelegramCustomerHandler; use App\Handlers\TelegramHandler; +use App\Handlers\TelemetryHandler; return [ 'categoriesList' => [CategoriesHandler::class, 'index'], @@ -21,6 +23,7 @@ return [ 'getCart' => [CartHandler::class, 'index'], 'getForm' => [FormsHandler::class, 'getForm'], 'health' => [HealthCheckHandler::class, 'handle'], + 'ingest' => [TelemetryHandler::class, 'ingest'], 'manifest' => [SettingsHandler::class, 'manifest'], 'processBlock' => [BlocksHandler::class, 'processBlock'], 'product_show' => [ProductsHandler::class, 'show'], @@ -31,4 +34,6 @@ return [ 'testTgMessage' => [SettingsHandler::class, 'testTgMessage'], 'userPrivacyConsent' => [PrivacyPolicyHandler::class, 'userPrivacyConsent'], 'webhook' => [TelegramHandler::class, 'webhook'], + 'etlCustomers' => [ETLHandler::class, 'customers'], + 'etlCustomersMeta' => [ETLHandler::class, 'getCustomersMeta'], ];