Improve TeleCart Pulse ETL, time handling and customer tracking

- Ensure Pulse store always prefers tracking_id from backend response when saving Telegram customer data.
- Route storeOrder calls through shared ftch helper instead of manual fetch.
- Remove legacy catalog controller extension/tgshop/handle and obsolete telecart_cache migration and setting mapping for Yandex Metrika.
- Update migrations framework to rely on Carbon, sort migration files deterministically and store executed_at as a Carbon timestamp without DEFAULT CURRENT_TIMESTAMP.
- Introduce DateUtils helper to convert timestamps between UTC and system time zone and use it in ETLHandler responses.
- Make TeleCartPulse PayloadSigner and TeleCartPulseService accept nullable secrets/API keys but fail fast when secret is missing.
- Normalize ETL customer payload shape and convert all date fields to UTC JSON timestamps.
- Default created order customer_id to 0 when no OpenCart customer id is available.
This commit is contained in:
2025-12-01 16:11:40 +03:00
parent ef785654b9
commit 49b0201b5f
13 changed files with 79 additions and 38 deletions

View File

@@ -51,7 +51,7 @@ class ControllerExtensionTgshopHandle extends Controller
'app' => [
'shop_base_url' => HTTPS_SERVER, // for catalog: HTTPS_SERVER, for admin: HTTPS_CATALOG
'language_id' => (int)$this->config->get('config_language_id'),
'timezone' => $this->config->get('config_timezone'),
'oc_timezone' => $this->config->get('config_timezone'),
],
'logs' => [
'path' => DIR_LOGS,
@@ -74,7 +74,7 @@ class ControllerExtensionTgshopHandle extends Controller
],
]);
$appDebug = Arr::get($items, 'app.app_debug');
$appDebug = Arr::get($items, 'app.app_debug', false);
$app = ApplicationFactory::create($items);

View File

@@ -29,7 +29,6 @@ return new class extends Migration {
'module_tgshop_owner_notification_template' => 'telegram.owner_notification_template',
'module_tgshop_text_order_created_success' => 'texts.text_order_created_success',
'module_tgshop_enable_store' => 'store.enable_store',
'module_tgshop_yandex_metrika' => 'metrics.yandex_metrika_counter',
'module_tgshop_customer_notification_template' => 'telegram.customer_notification_template',
'module_tgshop_feature_vouchers' => 'store.feature_vouchers',
'module_tgshop_order_default_status_id' => 'orders.order_default_status_id',

View File

@@ -1,11 +0,0 @@
<?php
use Openguru\OpenCartFramework\Migrations\Migration;
use Symfony\Component\Cache\Adapter\DoctrineDbalAdapter;
return new class extends Migration {
public function up(): void
{
$this->app->get(DoctrineDbalAdapter::class)->createTable();
}
};

View File

@@ -2,6 +2,7 @@
namespace Openguru\OpenCartFramework;
use Carbon\Carbon;
use Closure;
use Dotenv\Dotenv;
use InvalidArgumentException;

View File

@@ -2,6 +2,7 @@
namespace Openguru\OpenCartFramework\Migrations;
use Carbon\Carbon;
use Exception;
use Openguru\OpenCartFramework\QueryBuilder\Connections\ConnectionInterface;
use Psr\Log\LoggerInterface;
@@ -61,7 +62,7 @@ class MigratorService
return <<<SQL
CREATE TABLE IF NOT EXISTS `{$this->getMigrationsTableName()}` (
migration VARCHAR(191) NOT NULL,
executed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
executed_at DATETIME NOT NULL,
PRIMARY KEY (migration)
) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB;
SQL;
@@ -91,6 +92,7 @@ SQL;
private function applyMigrations(array $files): int
{
$count = 0;
sort($files);
foreach ($files as $file) {
try {
@@ -120,7 +122,7 @@ SQL;
{
$this->connection->insert($this->getMigrationsTableName(), [
'migration' => $file,
'executed_at' => date('Y-m-d H:i:s'),
'executed_at' => Carbon::now(),
]);
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Openguru\OpenCartFramework\Support;
use Carbon\Carbon;
use DateTimeZone;
class DateUtils
{
public static function toUTC(?string $date = null): ?string
{
if (! $date) {
return null;
}
return Carbon::parse($date)->setTimezone('UTC')->toJSON();
}
/**
* @param string|null $date
* @param DateTimeZone|string|null $tz
* @return Carbon|null
*/
public static function toSystemTimezone(?string $date = null, $tz = 'UTC'): ?Carbon
{
if (! $date) {
return null;
}
$systemTimezone = date_default_timezone_get();
return Carbon::parse($date, $tz)->timezone($systemTimezone);
}
}

View File

@@ -6,9 +6,9 @@ use JsonException;
class PayloadSigner
{
private string $secret;
private ?string $secret;
public function __construct(string $secret)
public function __construct(?string $secret = null)
{
$this->secret = $secret;
}
@@ -18,6 +18,7 @@ class PayloadSigner
*/
public function sign(array $payload): string
{
$this->ensureSecretExists();
$encoded = $this->encodeJson($payload);
return hash_hmac('sha256', $encoded, $this->secret);
@@ -28,6 +29,7 @@ class PayloadSigner
*/
public function verify(string $signature, array $payload): bool
{
$this->ensureSecretExists();
$encoded = $this->encodeJson($payload);
$expected = hash_hmac('sha256', $encoded, $this->secret);
@@ -42,4 +44,11 @@ class PayloadSigner
throw new PayloadSignException('Could not encode JSON: ' . $e->getMessage(), 0, $e);
}
}
private function ensureSecretExists(): void
{
if (! $this->secret) {
throw new PayloadSignException('No secret provided');
}
}
}

View File

@@ -15,10 +15,13 @@ class TeleCartPulseService
{
private TelegramInitDataDecoder $initDataDecoder;
private PayloadSigner $payloadSigner;
private string $apiKey;
private ?string $apiKey;
public function __construct(TelegramInitDataDecoder $initDataDecoder, PayloadSigner $payloadSigner, string $apiKey)
{
public function __construct(
TelegramInitDataDecoder $initDataDecoder,
PayloadSigner $payloadSigner,
?string $apiKey = null
) {
$this->initDataDecoder = $initDataDecoder;
$this->payloadSigner = $payloadSigner;
$this->apiKey = $apiKey;

View File

@@ -9,6 +9,7 @@ use Openguru\OpenCartFramework\Http\JsonResponse;
use Openguru\OpenCartFramework\Http\Request;
use Openguru\OpenCartFramework\QueryBuilder\Builder;
use Openguru\OpenCartFramework\QueryBuilder\RawExpression;
use Openguru\OpenCartFramework\Support\DateUtils;
use Psr\Log\LoggerInterface;
class ETLHandler
@@ -45,7 +46,7 @@ class ETLHandler
return $this->builder->newQuery()
->from('telecart_customers')
->where('allows_write_to_pm', '=', 1)
->when(! empty($updatedAt), function (Builder $builder) use ($lastUpdatedAtSql, $updatedAt) {
->when($updatedAt !== null, function (Builder $builder) use ($lastUpdatedAtSql, $updatedAt) {
$builder->where(new RawExpression($lastUpdatedAtSql), '>=', $updatedAt);
});
}
@@ -59,7 +60,7 @@ class ETLHandler
$updatedAt = $request->get('updated_at');
if ($updatedAt) {
$updatedAt = Carbon::parse($updatedAt);
$updatedAt = DateUtils::toSystemTimezone($updatedAt);
}
$query = $this->getCustomerQuery($updatedAt);
$total = $query->count();
@@ -85,7 +86,7 @@ class ETLHandler
$successOrderStatusIds = '5,3';
$updatedAt = $request->get('updated_at');
if ($updatedAt) {
$updatedAt = Carbon::parse($updatedAt);
$updatedAt = DateUtils::toSystemTimezone($updatedAt);
}
$lastUpdatedAtSql = $this->getLastUpdatedAtSql();
@@ -138,14 +139,20 @@ class ETLHandler
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;
return [
'tracking_id' => $item['tracking_id'],
'tg_user_id' => filter_var($item['tg_user_id'], FILTER_VALIDATE_INT),
'oc_customer_id' => filter_var($item['oc_customer_id'], FILTER_VALIDATE_INT),
'is_premium' => filter_var($item['is_premium'], FILTER_VALIDATE_BOOLEAN),
'last_seen_at' => DateUtils::toUTC($item['last_seen_at']),
'orders_count_total' => filter_var($item['orders_count_total'], FILTER_VALIDATE_INT),
'registered_at' => DateUtils::toUTC($item['registered_at']),
'first_order_date' => DateUtils::toUTC($item['first_order_date']),
'last_order_date' => DateUtils::toUTC($item['last_order_date']),
'total_spent' => (float)$item['total_spent'],
'orders_count_success' => filter_var($item['orders_count_success'], FILTER_VALIDATE_INT),
'updated_at' => DateUtils::toUTC($item['updated_at']),
];
}, $items),
]);
}

View File

@@ -118,7 +118,7 @@ class OrderCreateService
);
}
$orderData['customer_id'] = $ocCustomerId;
$orderData['customer_id'] = $ocCustomerId ?? 0;
$this->database->insert(db_table('order'), $orderData);
$orderId = $this->database->lastInsertId();