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'],
];