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:
@@ -37,7 +37,7 @@ export const usePulseStore = defineStore('pulse', {
|
||||
console.debug('[Pulse] Saving Telegram customer data');
|
||||
saveTelegramCustomer(userData)
|
||||
.then((response) => {
|
||||
this.tracking_id = this.tracking_id || response?.data?.tracking_id || null;
|
||||
this.tracking_id = response?.data?.tracking_id || this.tracking_id || null;
|
||||
console.debug(
|
||||
'[Pulse] Telegram customer data saved successfully. Tracking ID: ',
|
||||
toRaw(this.tracking_id)
|
||||
|
||||
@@ -43,10 +43,7 @@ async function ftchPost(action, json = {}) {
|
||||
}
|
||||
|
||||
export async function storeOrder(data) {
|
||||
return await apiFetch(`${BASE_URL}index.php?route=extension/tgshop/handle&api_action=storeOrder`, {
|
||||
method: 'POST',
|
||||
body: data,
|
||||
});
|
||||
return ftch('storeOrder', null, data);
|
||||
}
|
||||
|
||||
export async function getCart() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace Openguru\OpenCartFramework;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Closure;
|
||||
use Dotenv\Dotenv;
|
||||
use InvalidArgumentException;
|
||||
|
||||
@@ -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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
0
module/oc_telegram_shop/upload/oc_telegram_shop/framework/TeleCartPulse/TrackingIdGenerator.php
Normal file → Executable file
0
module/oc_telegram_shop/upload/oc_telegram_shop/framework/TeleCartPulse/TrackingIdGenerator.php
Normal file → Executable 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),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user