feat: add TeleCartPulse telemetry system and ETL endpoints
- Add TeleCartPulse service for event tracking and analytics - Implement PayloadSigner for secure payload signing/verification - Add StartParamSerializer for campaign parameter handling - Create TeleCartPulseServiceProvider for dependency injection - Add PulseEvents constants and exception classes - Add TelemetryHandler for ingesting client-side events - Implement /ingest endpoint for receiving webapp events - Support WEBAPP_OPEN event tracking with campaign metadata - Add ETLHandler for customer data export - Implement /customers endpoint for ETL processes - Add /customers/meta endpoint for pagination metadata - Support filtering by updated_at timestamp - Include customer metrics: orders count, total spent, etc. - Add InvalidApiTokenException for API key validation - Update Request class to support API key extraction - Add Utils helper methods for domain extraction - Integrate telemetry in frontend SPA (webapp open event) - Add TeleCartPulseView in admin panel for API key configuration - Update routes to include new telemetry and ETL endpoints
This commit is contained in:
@@ -1 +1,2 @@
|
||||
APP_DEBUG=true
|
||||
PULSE_API_HOST=http://host.docker.internal:8086/api/
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ class SettingsHandler
|
||||
'texts',
|
||||
'sliders',
|
||||
'mainpage_blocks',
|
||||
'pulse',
|
||||
]);
|
||||
|
||||
$data['forms'] = [];
|
||||
@@ -107,6 +108,7 @@ class SettingsHandler
|
||||
'texts',
|
||||
'sliders',
|
||||
'mainpage_blocks',
|
||||
'pulse',
|
||||
]),
|
||||
);
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Openguru\OpenCartFramework\Exceptions;
|
||||
|
||||
use Exception;
|
||||
use Throwable;
|
||||
|
||||
class InvalidApiTokenException extends Exception
|
||||
{
|
||||
public function __construct($message = "Token is invalid", $code = 401, Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ final class Request
|
||||
public function getContent(): string
|
||||
{
|
||||
if ($this->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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Openguru\OpenCartFramework\TeleCartPulse;
|
||||
|
||||
use Exception;
|
||||
|
||||
class PayloadSignException extends Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Openguru\OpenCartFramework\TeleCartPulse;
|
||||
|
||||
use JsonException;
|
||||
|
||||
class PayloadSigner
|
||||
{
|
||||
private string $secret;
|
||||
|
||||
public function __construct(string $secret)
|
||||
{
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Openguru\OpenCartFramework\TeleCartPulse;
|
||||
|
||||
final class PulseEvents
|
||||
{
|
||||
public const WEBAPP_OPEN = 'WEBAPP_OPEN';
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Openguru\OpenCartFramework\TeleCartPulse;
|
||||
|
||||
use Exception;
|
||||
|
||||
class PulseIngestException extends Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace Openguru\OpenCartFramework\TeleCartPulse;
|
||||
|
||||
class StartParamSerializer
|
||||
{
|
||||
/**
|
||||
* Сериализовать массив параметров в строку для передачи в startapp
|
||||
*
|
||||
* Используется base64 кодирование JSON для безопасной передачи данных в URL
|
||||
*
|
||||
* @param array<string, mixed> $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<string, mixed> Массив параметров
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
namespace Openguru\OpenCartFramework\TeleCartPulse;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use Openguru\OpenCartFramework\Support\Arr;
|
||||
use Openguru\OpenCartFramework\Support\Utils;
|
||||
use Openguru\OpenCartFramework\Telegram\TelegramInitDataDecoder;
|
||||
use Throwable;
|
||||
|
||||
class TeleCartPulseService
|
||||
{
|
||||
private TelegramInitDataDecoder $initDataDecoder;
|
||||
private PayloadSigner $payloadSigner;
|
||||
private string $apiKey;
|
||||
|
||||
public function __construct(TelegramInitDataDecoder $initDataDecoder, PayloadSigner $payloadSigner, string $apiKey)
|
||||
{
|
||||
$this->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'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Openguru\OpenCartFramework\TeleCartPulse;
|
||||
|
||||
use Openguru\OpenCartFramework\Container\Container;
|
||||
use Openguru\OpenCartFramework\Container\ServiceProvider;
|
||||
use Openguru\OpenCartFramework\Telegram\TelegramInitDataDecoder;
|
||||
|
||||
class TeleCartPulseServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
$this->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'),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
2
module/oc_telegram_shop/upload/oc_telegram_shop/framework/Telegram/TelegramInitDataDecoder.php
Normal file → Executable file
2
module/oc_telegram_shop/upload/oc_telegram_shop/framework/Telegram/TelegramInitDataDecoder.php
Normal file → Executable file
@@ -32,7 +32,7 @@ class TelegramInitDataDecoder
|
||||
/**
|
||||
* @throws JsonException
|
||||
*/
|
||||
private function parseInitDataStringToArray(string $initData): array
|
||||
public function parseInitDataStringToArray(string $initData): array
|
||||
{
|
||||
parse_str($initData, $parsed);
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ class TelegramValidateInitDataMiddleware
|
||||
'manifest',
|
||||
'webhook',
|
||||
'health',
|
||||
'etlCustomers',
|
||||
'etlCustomersMeta',
|
||||
];
|
||||
|
||||
public function __construct(SignatureValidator $signatureValidator)
|
||||
|
||||
@@ -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,
|
||||
|
||||
168
module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/ETLHandler.php
Executable file
168
module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/ETLHandler.php
Executable file
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
namespace App\Handlers;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Openguru\OpenCartFramework\Config\Settings;
|
||||
use Openguru\OpenCartFramework\Exceptions\InvalidApiTokenException;
|
||||
use Openguru\OpenCartFramework\Http\JsonResponse;
|
||||
use Openguru\OpenCartFramework\Http\Request;
|
||||
use Openguru\OpenCartFramework\QueryBuilder\Builder;
|
||||
use Openguru\OpenCartFramework\QueryBuilder\RawExpression;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class ETLHandler
|
||||
{
|
||||
private Builder $builder;
|
||||
private Settings $settings;
|
||||
private LoggerInterface $logger;
|
||||
|
||||
public function __construct(Builder $builder, Settings $settings, LoggerInterface $logger)
|
||||
{
|
||||
$this->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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Handlers;
|
||||
|
||||
use Openguru\OpenCartFramework\Http\JsonResponse;
|
||||
use Openguru\OpenCartFramework\Http\Request;
|
||||
use Openguru\OpenCartFramework\Http\Response;
|
||||
use Openguru\OpenCartFramework\TeleCartPulse\PulseIngestException;
|
||||
use Openguru\OpenCartFramework\TeleCartPulse\TeleCartPulseService;
|
||||
|
||||
class TelemetryHandler
|
||||
{
|
||||
private TeleCartPulseService $teleCartPulseService;
|
||||
|
||||
public function __construct(TeleCartPulseService $teleCartPulseService)
|
||||
{
|
||||
$this->teleCartPulseService = $teleCartPulseService;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws PulseIngestException
|
||||
*/
|
||||
public function ingest(Request $request): JsonResponse
|
||||
{
|
||||
$this->teleCartPulseService->handleIngest($request->json());
|
||||
|
||||
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
}
|
||||
@@ -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'],
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user