feat(customers): track order meta and OC sync
- add telecart_order_meta table and orders_count column for customers - introduce OcCustomerService and OrderMetaService for syncing OC data - rework OrderCreateService transaction flow, metadata handling and tests - increment telegram customer orders_count and expose it via handlers/UI - update stats dashboard with rub formatting, tooltips and customers count - sync SPA theme colors with Telegram WebApp and fix dark variant behavior - add helpers for RUB formatting and bool casting; simplify logs handler
This commit is contained in:
@@ -4,17 +4,14 @@ namespace Bastion\Handlers;
|
||||
|
||||
use Openguru\OpenCartFramework\Config\Settings;
|
||||
use Openguru\OpenCartFramework\Http\JsonResponse;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class LogsHandler
|
||||
{
|
||||
private Settings $settings;
|
||||
private LoggerInterface $logger;
|
||||
|
||||
public function __construct(Settings $settings, LoggerInterface $logger)
|
||||
public function __construct(Settings $settings)
|
||||
{
|
||||
$this->settings = $settings;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
public function getLogs(): JsonResponse
|
||||
@@ -36,29 +33,26 @@ class LogsHandler
|
||||
private function parseLogLines(array $lines): array
|
||||
{
|
||||
$parsed = [];
|
||||
|
||||
// Регулярка для формата Monolog с ISO 8601 датой: [YYYY-MM-DDTHH:MM:SS.microseconds+timezone] channel.LEVEL: message {context} [extra]
|
||||
// Пример: [2025-11-23T14:28:21.772518+00:00] TeleCart.ERROR: Invalid Telegram Signature. {"exception":"..."} []
|
||||
// Поддерживает также формат без контекста и extra: [2025-11-23T14:28:21.772518+00:00] TeleCart.INFO: Message text
|
||||
|
||||
$pattern = '/^\[([^\]]+)\]\s+([^.]+)\.(\w+):\s+(.+)$/s';
|
||||
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if (empty($line)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
if (preg_match($pattern, $line, $matches)) {
|
||||
$datetime = $matches[1] ?? '';
|
||||
$channel = $matches[2] ?? '';
|
||||
$level = $matches[3] ?? '';
|
||||
$rest = $matches[4] ?? '';
|
||||
|
||||
|
||||
// Извлекаем сообщение и контекст
|
||||
// Контекст начинается с { и заканчивается соответствующим }
|
||||
$message = $rest;
|
||||
$context = null;
|
||||
|
||||
|
||||
// Ищем JSON контекст (начинается с {, может быть после пробела или сразу)
|
||||
$jsonStart = strpos($rest, ' {');
|
||||
if ($jsonStart === false) {
|
||||
@@ -66,11 +60,11 @@ class LogsHandler
|
||||
} else {
|
||||
$jsonStart++; // Пропускаем пробел перед {
|
||||
}
|
||||
|
||||
|
||||
if ($jsonStart !== false) {
|
||||
$message = trim(substr($rest, 0, $jsonStart));
|
||||
$jsonPart = substr($rest, $jsonStart);
|
||||
|
||||
|
||||
// Находим конец JSON объекта, учитывая вложенность
|
||||
$jsonEnd = $this->findJsonEnd($jsonPart);
|
||||
if ($jsonEnd !== false) {
|
||||
@@ -81,10 +75,10 @@ class LogsHandler
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Форматируем дату для отображения (убираем микросекунды и временную зону для читаемости)
|
||||
$formattedDatetime = $this->formatDateTime($datetime);
|
||||
|
||||
|
||||
$parsed[] = [
|
||||
'datetime' => $formattedDatetime,
|
||||
'datetime_raw' => $datetime,
|
||||
@@ -107,7 +101,7 @@ class LogsHandler
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return $parsed;
|
||||
}
|
||||
|
||||
@@ -122,29 +116,29 @@ class LogsHandler
|
||||
$inString = false;
|
||||
$escape = false;
|
||||
$len = strlen($json);
|
||||
|
||||
|
||||
for ($i = 0; $i < $len; $i++) {
|
||||
$char = $json[$i];
|
||||
|
||||
|
||||
if ($escape) {
|
||||
$escape = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
if ($char === '\\') {
|
||||
$escape = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
if ($char === '"') {
|
||||
$inString = !$inString;
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
if ($inString) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
if ($char === '{') {
|
||||
$depth++;
|
||||
} elseif ($char === '}') {
|
||||
@@ -154,7 +148,7 @@ class LogsHandler
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -165,7 +159,7 @@ class LogsHandler
|
||||
if (preg_match('/^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})/', $datetime, $dateMatches)) {
|
||||
return $dateMatches[1] . ' ' . $dateMatches[2];
|
||||
}
|
||||
|
||||
|
||||
return $datetime;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ use Openguru\OpenCartFramework\Http\JsonResponse;
|
||||
use Openguru\OpenCartFramework\QueryBuilder\Builder;
|
||||
use Openguru\OpenCartFramework\QueryBuilder\JoinClause;
|
||||
use Openguru\OpenCartFramework\QueryBuilder\RawExpression;
|
||||
use Openguru\OpenCartFramework\QueryBuilder\Table;
|
||||
|
||||
class StatsHandler
|
||||
{
|
||||
@@ -21,28 +20,32 @@ class StatsHandler
|
||||
{
|
||||
$ordersTotalAmount = $this->builder->newQuery()
|
||||
->select([
|
||||
new RawExpression('COUNT(DISTINCT orders.order_id) AS orders_count'),
|
||||
new RawExpression('COUNT(DISTINCT orders.order_id) AS orders_total_count'),
|
||||
new RawExpression('SUM(orders.total) AS orders_total_amount'),
|
||||
new RawExpression('COUNT(DISTINCT order_product.product_id) AS order_products_count'),
|
||||
])
|
||||
->from(db_table('order'), 'orders')
|
||||
->join(new Table(db_table('order_history'), 'order_history'), function (JoinClause $join) {
|
||||
$join->on('orders.order_id', '=', 'order_history.order_id')
|
||||
->where('order_history.comment', '=', 'Заказ оформлен через Telegram Mini App');
|
||||
})
|
||||
->join(new Table(db_table('order_product'), 'order_product'), function (JoinClause $join) {
|
||||
$join->on('orders.order_id', '=', 'order_product.order_id');
|
||||
->join('telecart_order_meta', function (JoinClause $join) {
|
||||
$join->on('orders.order_id', '=', 'telecart_order_meta.oc_order_id')
|
||||
->whereRaw('orders.store_id = telecart_order_meta.oc_store_id');
|
||||
})
|
||||
->firstOrNull();
|
||||
|
||||
|
||||
if ($ordersTotalAmount) {
|
||||
$data = [
|
||||
'orders_count' => (int) $ordersTotalAmount['orders_count'],
|
||||
'orders_count' => (int) $ordersTotalAmount['orders_total_count'],
|
||||
'orders_total_amount' => (int) $ordersTotalAmount['orders_total_amount'],
|
||||
'order_products_count' => (int) $ordersTotalAmount['order_products_count'],
|
||||
'customers_count' => $this->countCustomersCount(),
|
||||
];
|
||||
}
|
||||
|
||||
return new JsonResponse(compact('data'));
|
||||
}
|
||||
|
||||
private function countCustomersCount(): int
|
||||
{
|
||||
return $this->builder->newQuery()
|
||||
->from('telecart_customers')
|
||||
->count();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ class TelegramCustomersHandler
|
||||
'photo_url',
|
||||
'last_seen_at',
|
||||
'referral',
|
||||
'orders_count',
|
||||
'privacy_consented_at',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
@@ -254,6 +255,14 @@ class TelegramCustomersHandler
|
||||
$query->where($field, '=', $value);
|
||||
} elseif ($matchMode === 'notEquals') {
|
||||
$query->where($field, '!=', $value);
|
||||
} elseif ($matchMode === 'gt') {
|
||||
$query->where($field, '>', $value);
|
||||
} elseif ($matchMode === 'lt') {
|
||||
$query->where($field, '<', $value);
|
||||
} elseif ($matchMode === 'gte') {
|
||||
$query->where($field, '>=', $value);
|
||||
} elseif ($matchMode === 'lte') {
|
||||
$query->where($field, '<=', $value);
|
||||
} elseif ($matchMode === 'dateIs') {
|
||||
// Для точного совпадения даты используем диапазон от 00:00:00 до 23:59:59
|
||||
$date = date('Y-m-d', strtotime($value));
|
||||
@@ -323,6 +332,7 @@ class TelegramCustomersHandler
|
||||
'photo_url' => $customer['photo_url'],
|
||||
'last_seen_at' => $customer['last_seen_at'],
|
||||
'referral' => $customer['referral'],
|
||||
'orders_count' => (int) $customer['orders_count'],
|
||||
'privacy_consented_at' => $customer['privacy_consented_at'],
|
||||
'created_at' => $customer['created_at'],
|
||||
'updated_at' => $customer['updated_at'],
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use Openguru\OpenCartFramework\Migrations\Migration;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
$tableName = 'telecart_order_meta';
|
||||
|
||||
$sql = <<<SQL
|
||||
CREATE TABLE IF NOT EXISTS `{$tableName}` (
|
||||
`id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`oc_order_id` INT(11) UNSIGNED NOT NULL,
|
||||
`oc_store_id` INT(11) UNSIGNED NOT NULL,
|
||||
`telecart_customer_id` INT(11) UNSIGNED DEFAULT NULL,
|
||||
`meta_data` JSON DEFAULT NULL,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `unique_oc_order_id` (`oc_order_id`),
|
||||
KEY `idx_oc_store_id` (`oc_store_id`),
|
||||
KEY `idx_telecart_customer_id` (`telecart_customer_id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB;
|
||||
SQL;
|
||||
|
||||
$this->database->statement($sql);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
use Openguru\OpenCartFramework\Migrations\Migration;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
$sql = <<<SQL
|
||||
ALTER TABLE `telecart_customers`
|
||||
ADD COLUMN `orders_count` INT(11) UNSIGNED NOT NULL DEFAULT 0 AFTER `referral`;
|
||||
SQL;
|
||||
|
||||
$this->database->statement($sql);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -66,4 +66,9 @@ class Utils
|
||||
{
|
||||
return $needle === '' || strpos($haystack, $needle) !== false;
|
||||
}
|
||||
|
||||
public static function boolToInt(bool $value): int
|
||||
{
|
||||
return $value === true ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,4 +120,12 @@ class TelegramCustomer
|
||||
'last_seen_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function increase(int $id, string $field): bool
|
||||
{
|
||||
$table = self::TABLE_NAME;
|
||||
$sql = "UPDATE `$table` SET `$field` = `$field` + 1 WHERE id = ?";
|
||||
|
||||
return $this->database->statement($sql, [$id]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Openguru\OpenCartFramework\QueryBuilder\Builder;
|
||||
use Openguru\OpenCartFramework\QueryBuilder\Connections\ConnectionInterface;
|
||||
use Openguru\OpenCartFramework\Support\Arr;
|
||||
|
||||
class OcCustomerService
|
||||
{
|
||||
private Builder $builder;
|
||||
private ConnectionInterface $database;
|
||||
|
||||
public function __construct(Builder $builder, ConnectionInterface $database)
|
||||
{
|
||||
$this->builder = $builder;
|
||||
$this->database = $database;
|
||||
}
|
||||
|
||||
public function findOrCreate(array $orderData): ?int
|
||||
{
|
||||
$email = Arr::get($orderData, 'email');
|
||||
$phone = Arr::get($orderData, 'telephone');
|
||||
|
||||
if (! $email && ! $phone) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$customer = $this->builder->newQuery()
|
||||
->from(db_table('customer'))
|
||||
->where('email', '=', $email)
|
||||
->where('telephone', '=', $phone)
|
||||
->firstOrNull();
|
||||
|
||||
if ($customer) {
|
||||
return (int) $customer['customer_id'];
|
||||
}
|
||||
|
||||
$customerData = [
|
||||
'customer_group_id' => $orderData['customer_group_id'],
|
||||
'store_id' => $orderData['store_id'],
|
||||
'language_id' => $orderData['language_id'],
|
||||
'firstname' => $orderData['firstname'] ?? '',
|
||||
'lastname' => $orderData['lastname'] ?? '',
|
||||
'email' => $orderData['email'] ?? '',
|
||||
'telephone' => $orderData['telephone'] ?? '',
|
||||
'fax' => $orderData['fax'] ?? '',
|
||||
'password' => bin2hex(random_bytes(16)),
|
||||
'salt' => bin2hex(random_bytes(9)),
|
||||
'ip' => $orderData['ip'] ?? '',
|
||||
'status' => 1,
|
||||
'safe' => 0,
|
||||
'token' => bin2hex(random_bytes(32)),
|
||||
'code' => '',
|
||||
'date_added' => $orderData['date_added'],
|
||||
];
|
||||
|
||||
$this->database->insert(db_table('customer'), $customerData);
|
||||
|
||||
return $this->database->lastInsertId();
|
||||
}
|
||||
}
|
||||
@@ -6,13 +6,14 @@ namespace App\Services;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use JsonException;
|
||||
use Openguru\OpenCartFramework\OpenCart\Decorators\OcRegistryDecorator;
|
||||
use Openguru\OpenCartFramework\QueryBuilder\Connections\ConnectionInterface;
|
||||
use Openguru\OpenCartFramework\Support\Arr;
|
||||
use Openguru\OpenCartFramework\Telegram\TelegramService;
|
||||
use Openguru\OpenCartFramework\Validator\ValidatorInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
class OrderCreateService
|
||||
{
|
||||
@@ -22,6 +23,9 @@ class OrderCreateService
|
||||
private SettingsService $settings;
|
||||
private TelegramService $telegramService;
|
||||
private LoggerInterface $logger;
|
||||
private TelegramCustomerService $telegramCustomerService;
|
||||
private OcCustomerService $ocCustomerService;
|
||||
private OrderMetaService $orderMetaService;
|
||||
|
||||
public function __construct(
|
||||
ConnectionInterface $database,
|
||||
@@ -29,7 +33,10 @@ class OrderCreateService
|
||||
OcRegistryDecorator $registry,
|
||||
SettingsService $settings,
|
||||
TelegramService $telegramService,
|
||||
LoggerInterface $logger
|
||||
LoggerInterface $logger,
|
||||
TelegramCustomerService $telegramCustomerService,
|
||||
OcCustomerService $ocCustomerService,
|
||||
OrderMetaService $orderMetaService
|
||||
) {
|
||||
$this->database = $database;
|
||||
$this->cartService = $cartService;
|
||||
@@ -37,8 +44,15 @@ class OrderCreateService
|
||||
$this->settings = $settings;
|
||||
$this->telegramService = $telegramService;
|
||||
$this->logger = $logger;
|
||||
$this->telegramCustomerService = $telegramCustomerService;
|
||||
$this->ocCustomerService = $ocCustomerService;
|
||||
$this->orderMetaService = $orderMetaService;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
* @throws JsonException
|
||||
*/
|
||||
public function create(array $data, array $meta = []): array
|
||||
{
|
||||
$now = Carbon::now();
|
||||
@@ -56,6 +70,15 @@ class OrderCreateService
|
||||
$products = $cart['products'] ?? [];
|
||||
$totals = $cart['totals'] ?? [];
|
||||
|
||||
// Получаем telegram_user_id из tgData
|
||||
$telegramUserId = Arr::get($data['tgData'] ?? [], 'user.id');
|
||||
|
||||
if (! $telegramUserId) {
|
||||
throw new RuntimeException('Telegram user id is required.');
|
||||
}
|
||||
|
||||
$customOrderFields = $this->customOrderFields($data);
|
||||
|
||||
$orderData = [
|
||||
'store_id' => $storeId,
|
||||
'store_name' => $storeName,
|
||||
@@ -83,88 +106,40 @@ class OrderCreateService
|
||||
'customer_group_id' => $customerGroupId,
|
||||
];
|
||||
|
||||
$orderId = null;
|
||||
try {
|
||||
$this->database->beginTransaction();
|
||||
|
||||
$this->database->transaction(
|
||||
function () use (&$orderData, $products, $totals, $orderStatusId, $now, &$orderId, $data) {
|
||||
$success = $this->database->insert(db_table('order'), $orderData);
|
||||
|
||||
if (! $success) {
|
||||
[, $error] = $this->database->getLastError();
|
||||
throw new RuntimeException("Failed to insert row into order. Error: $error");
|
||||
}
|
||||
|
||||
$orderId = $this->database->lastInsertId();
|
||||
|
||||
// Insert products
|
||||
foreach ($products as $product) {
|
||||
$success = $this->database->insert(db_table('order_product'), [
|
||||
'order_id' => $orderId,
|
||||
'product_id' => $product['product_id'],
|
||||
'name' => $product['name'],
|
||||
'model' => $product['model'],
|
||||
'quantity' => $product['quantity'],
|
||||
'price' => $product['price_numeric'],
|
||||
'total' => $product['total_numeric'],
|
||||
'reward' => $product['reward_numeric'],
|
||||
]);
|
||||
|
||||
if (! $success) {
|
||||
[, $error] = $this->database->getLastError();
|
||||
throw new RuntimeException("Failed to insert row into order_product. Error: $error");
|
||||
}
|
||||
|
||||
$orderProductId = $this->database->lastInsertId();
|
||||
foreach ($product['option'] as $option) {
|
||||
$success = $this->database->insert(db_table('order_option'), [
|
||||
'order_id' => $orderId,
|
||||
'order_product_id' => $orderProductId,
|
||||
'product_option_id' => $option['product_option_id'],
|
||||
'product_option_value_id' => $option['product_option_value_id'],
|
||||
'name' => $option['name'],
|
||||
'value' => $option['value'],
|
||||
'type' => $option['type'],
|
||||
]);
|
||||
|
||||
if (! $success) {
|
||||
[, $error] = $this->database->getLastError();
|
||||
throw new RuntimeException("Failed to insert row into order_option. Error: $error");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Insert totals
|
||||
foreach ($totals as $total) {
|
||||
$success = $this->database->insert(db_table('order_total'), [
|
||||
'order_id' => $orderId,
|
||||
'code' => $total['code'],
|
||||
'title' => $total['title'],
|
||||
'value' => $total['value'],
|
||||
'sort_order' => $total['sort_order'],
|
||||
]);
|
||||
|
||||
if (! $success) {
|
||||
[, $error] = $this->database->getLastError();
|
||||
throw new RuntimeException("Failed to insert row into order_total. Error: $error");
|
||||
}
|
||||
}
|
||||
|
||||
// Insert history
|
||||
$history = [
|
||||
'order_id' => $orderId,
|
||||
'order_status_id' => $orderStatusId,
|
||||
'notify' => 0,
|
||||
'comment' => $this->formatHistoryComment($data),
|
||||
'date_added' => $now,
|
||||
];
|
||||
$success = $this->database->insert(db_table('order_history'), $history);
|
||||
|
||||
if (! $success) {
|
||||
[, $error] = $this->database->getLastError();
|
||||
throw new RuntimeException("Failed to insert row into order_history. Error: $error");
|
||||
}
|
||||
$ocCustomerId = $this->ocCustomerService->findOrCreate($orderData);
|
||||
$telecartCustomerId = null;
|
||||
if ($ocCustomerId) {
|
||||
$telecartCustomerId = $this->telegramCustomerService->assignOcCustomer(
|
||||
$telegramUserId,
|
||||
$ocCustomerId
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
$this->database->insert(db_table('order'), $orderData);
|
||||
$orderId = $this->database->lastInsertId();
|
||||
|
||||
// Insert products
|
||||
$this->insertProducts($products, $orderId);
|
||||
|
||||
// Insert totals
|
||||
$this->insertTotals($totals, $orderId);
|
||||
|
||||
// Insert history
|
||||
$this->insertHistory($orderId, $orderStatusId, $customOrderFields, $now);
|
||||
|
||||
// Insert order meta data
|
||||
if ($customOrderFields) {
|
||||
$this->orderMetaService->insert($orderId, $storeId, $customOrderFields, $telecartCustomerId);
|
||||
}
|
||||
|
||||
$this->database->commitTransaction();
|
||||
} catch (Throwable $exception) {
|
||||
$this->database->rollBackTransaction();
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
$this->cartService->flush();
|
||||
|
||||
@@ -174,6 +149,10 @@ class OrderCreateService
|
||||
|
||||
$this->sendNotifications($orderData, $data['tgData']);
|
||||
|
||||
if ($telecartCustomerId) {
|
||||
$this->telegramCustomerService->increaseOrdersCount($telecartCustomerId);
|
||||
}
|
||||
|
||||
$dateTimeFormatted = '';
|
||||
try {
|
||||
$dateTimeFormatted = $now->format('d.m.Y H:i');
|
||||
@@ -212,8 +191,8 @@ class OrderCreateService
|
||||
if ($chatId && $template) {
|
||||
$message = $this->telegramService->prepareMessage($template, $variables);
|
||||
try {
|
||||
$this->telegramService->sendMessage((int) $chatId, $message);
|
||||
} catch (Exception $exception) {
|
||||
$this->telegramService->sendMessage($chatId, $message);
|
||||
} catch (Throwable $exception) {
|
||||
$this->logger->error(
|
||||
"Telegram sendMessage to owner error. ChatID: $chatId, Message: $message",
|
||||
['exception' => $exception],
|
||||
@@ -229,7 +208,7 @@ class OrderCreateService
|
||||
$message = $this->telegramService->prepareMessage($template, $variables);
|
||||
try {
|
||||
$this->telegramService->sendMessage($customerChatId, $message);
|
||||
} catch (Exception $exception) {
|
||||
} catch (Throwable $exception) {
|
||||
$this->logger->error(
|
||||
"Telegram sendMessage to customer error. ChatID: $chatId, Message: $message",
|
||||
['exception' => $exception]
|
||||
@@ -238,9 +217,22 @@ class OrderCreateService
|
||||
}
|
||||
}
|
||||
|
||||
private function formatHistoryComment(array $data): string
|
||||
private function formatHistoryComment(array $customFields): string
|
||||
{
|
||||
$customFields = Arr::except($data, [
|
||||
$additionalString = '';
|
||||
if ($customFields) {
|
||||
$additionalString = "\n\nДополнительная информация по заказу:\n";
|
||||
foreach ($customFields as $field => $value) {
|
||||
$additionalString .= $field . ': ' . $value . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
return "Заказ оформлен через Telegram Mini App.$additionalString";
|
||||
}
|
||||
|
||||
private function customOrderFields(array $data): array
|
||||
{
|
||||
return Arr::except($data, [
|
||||
'firstname',
|
||||
'lastname',
|
||||
'email',
|
||||
@@ -253,15 +245,67 @@ class OrderCreateService
|
||||
'payment_method',
|
||||
'tgData',
|
||||
]);
|
||||
}
|
||||
|
||||
$additionalString = '';
|
||||
if ($customFields) {
|
||||
$additionalString = "\n\nДополнительная информация по заказу:\n";
|
||||
foreach ($customFields as $field => $value) {
|
||||
$additionalString .= $field . ': ' . $value . "\n";
|
||||
public function insertTotals(array $totals, int $orderId): void
|
||||
{
|
||||
foreach ($totals as $total) {
|
||||
$this->database->insert(db_table('order_total'), [
|
||||
'order_id' => $orderId,
|
||||
'code' => $total['code'],
|
||||
'title' => $total['title'],
|
||||
'value' => $total['value'],
|
||||
'sort_order' => $total['sort_order'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $orderId
|
||||
* @param int $orderStatusId
|
||||
* @param array $customOrderFields
|
||||
* @param Carbon $now
|
||||
* @return void
|
||||
*/
|
||||
public function insertHistory(int $orderId, int $orderStatusId, array $customOrderFields, Carbon $now): void
|
||||
{
|
||||
$history = [
|
||||
'order_id' => $orderId,
|
||||
'order_status_id' => $orderStatusId,
|
||||
'notify' => 0,
|
||||
'comment' => $this->formatHistoryComment($customOrderFields),
|
||||
'date_added' => $now,
|
||||
];
|
||||
|
||||
$this->database->insert(db_table('order_history'), $history);
|
||||
}
|
||||
|
||||
private function insertProducts($products, int $orderId): void
|
||||
{
|
||||
foreach ($products as $product) {
|
||||
$this->database->insert(db_table('order_product'), [
|
||||
'order_id' => $orderId,
|
||||
'product_id' => $product['product_id'],
|
||||
'name' => $product['name'],
|
||||
'model' => $product['model'],
|
||||
'quantity' => $product['quantity'],
|
||||
'price' => $product['price_numeric'],
|
||||
'total' => $product['total_numeric'],
|
||||
'reward' => $product['reward_numeric'],
|
||||
]);
|
||||
|
||||
$orderProductId = $this->database->lastInsertId();
|
||||
foreach ($product['option'] as $option) {
|
||||
$this->database->insert(db_table('order_option'), [
|
||||
'order_id' => $orderId,
|
||||
'order_product_id' => $orderProductId,
|
||||
'product_option_id' => $option['product_option_id'],
|
||||
'product_option_value_id' => $option['product_option_value_id'],
|
||||
'name' => $option['name'],
|
||||
'value' => $option['value'],
|
||||
'type' => $option['type'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return "Заказ оформлен через Telegram Mini App.{$additionalString}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Openguru\OpenCartFramework\QueryBuilder\Connections\ConnectionInterface;
|
||||
|
||||
class OrderMetaService
|
||||
{
|
||||
private ConnectionInterface $connection;
|
||||
|
||||
public function __construct(ConnectionInterface $connection)
|
||||
{
|
||||
$this->connection = $connection;
|
||||
}
|
||||
|
||||
public function insert(int $orderId, int $storeId, array $fields, ?int $telecartCustomerId = null): void
|
||||
{
|
||||
$orderMeta = [
|
||||
'oc_order_id' => $orderId,
|
||||
'oc_store_id' => $storeId,
|
||||
'telecart_customer_id' => $telecartCustomerId,
|
||||
'meta_data' => json_encode($fields, JSON_THROW_ON_ERROR),
|
||||
];
|
||||
|
||||
$this->connection->insert('telecart_order_meta', $orderMeta);
|
||||
}
|
||||
}
|
||||
@@ -5,21 +5,18 @@ declare(strict_types=1);
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\TelegramCustomer;
|
||||
use Carbon\Carbon;
|
||||
use Openguru\OpenCartFramework\Support\Arr;
|
||||
use Openguru\OpenCartFramework\Support\Utils;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Сервис для работы с Telegram-кастомерами
|
||||
*
|
||||
* @package App\Services
|
||||
*/
|
||||
class TelegramCustomerService
|
||||
{
|
||||
private TelegramCustomer $telegramCustomerModel;
|
||||
private TelegramCustomer $telegramCustomer;
|
||||
|
||||
public function __construct(TelegramCustomer $telegramCustomerModel)
|
||||
public function __construct(TelegramCustomer $telegramCustomer)
|
||||
{
|
||||
$this->telegramCustomerModel = $telegramCustomerModel;
|
||||
$this->telegramCustomer = $telegramCustomer;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -34,12 +31,12 @@ class TelegramCustomerService
|
||||
$telegramUserId = $this->extractTelegramUserId($telegramUserData);
|
||||
$telegramCustomerData = $this->prepareCustomerData($telegramUserData, $telegramUserId);
|
||||
|
||||
$existingRecord = $this->telegramCustomerModel->findByTelegramUserId($telegramUserId);
|
||||
$existingRecord = $this->telegramCustomer->findByTelegramUserId($telegramUserId);
|
||||
|
||||
if ($existingRecord) {
|
||||
$this->telegramCustomerModel->updateByTelegramUserId($telegramUserId, $telegramCustomerData);
|
||||
$this->telegramCustomer->updateByTelegramUserId($telegramUserId, $telegramCustomerData);
|
||||
} else {
|
||||
$this->telegramCustomerModel->create($telegramCustomerData);
|
||||
$this->telegramCustomer->create($telegramCustomerData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,21 +73,40 @@ class TelegramCustomerService
|
||||
'first_name' => Arr::get($telegramUserData, 'first_name'),
|
||||
'last_name' => Arr::get($telegramUserData, 'last_name'),
|
||||
'language_code' => Arr::get($telegramUserData, 'language_code'),
|
||||
'is_premium' => $this->convertToInt(Arr::get($telegramUserData, 'is_premium', false)),
|
||||
'allows_write_to_pm' => $this->convertToInt(Arr::get($telegramUserData, 'allows_write_to_pm', false)),
|
||||
'is_premium' => Utils::boolToInt(Arr::get($telegramUserData, 'is_premium', false)),
|
||||
'allows_write_to_pm' => Utils::boolToInt(Arr::get($telegramUserData, 'allows_write_to_pm', false)),
|
||||
'photo_url' => Arr::get($telegramUserData, 'photo_url'),
|
||||
'last_seen_at' => date('Y-m-d H:i:s'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Конвертировать булево значение в int для БД
|
||||
* Assign OpenCart Customer to Telegram User ID and return Telecart Customer ID if it exists.
|
||||
*
|
||||
* @param mixed $value Значение для конвертации
|
||||
* @return int 1 или 0
|
||||
* @param $telegramUserId
|
||||
* @param int $ocCustomerId
|
||||
* @return int|null
|
||||
*/
|
||||
private function convertToInt($value): int
|
||||
public function assignOcCustomer($telegramUserId, int $ocCustomerId): ?int
|
||||
{
|
||||
return ($value === true || $value === 1 || $value === '1') ? 1 : 0;
|
||||
$customer = $this->telegramCustomer->findByTelegramUserId($telegramUserId);
|
||||
|
||||
if (! $customer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($customer['oc_customer_id'] === null) {
|
||||
$this->telegramCustomer->updateByTelegramUserId($telegramUserId, [
|
||||
'oc_customer_id' => $ocCustomerId,
|
||||
'updated_at' => Carbon::now()->toDateTimeString(),
|
||||
]);
|
||||
}
|
||||
|
||||
return (int) $customer['id'];
|
||||
}
|
||||
|
||||
public function increaseOrdersCount(int $telecartCustomerId): void
|
||||
{
|
||||
$this->telegramCustomer->increase($telecartCustomerId, 'orders_count');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,16 @@
|
||||
namespace Tests\Unit\Services;
|
||||
|
||||
use App\Services\CartService;
|
||||
use App\Services\OcCustomerService;
|
||||
use App\Services\OrderCreateService;
|
||||
use App\Services\OrderMetaService;
|
||||
use App\Services\SettingsService;
|
||||
use App\Services\TelegramCustomerService;
|
||||
use Carbon\Carbon;
|
||||
use Mockery as m;
|
||||
use Openguru\OpenCartFramework\OpenCart\Decorators\OcRegistryDecorator;
|
||||
use Openguru\OpenCartFramework\QueryBuilder\Connections\ConnectionInterface;
|
||||
use Openguru\OpenCartFramework\Telegram\TelegramService;
|
||||
use Openguru\OpenCartFramework\Validator\ValidatorInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Tests\TestCase;
|
||||
|
||||
@@ -18,6 +20,18 @@ class OrderCreateServiceTest extends TestCase
|
||||
{
|
||||
public function testCreateNewOrder(): void
|
||||
{
|
||||
$telegramUserId = 9999;
|
||||
$customOrderFields = [
|
||||
'field_1' => 'кирилица',
|
||||
'field_2' => 'hello',
|
||||
];
|
||||
|
||||
$tgData = [
|
||||
'user' => [
|
||||
'id' => $telegramUserId,
|
||||
],
|
||||
];
|
||||
|
||||
$data = [
|
||||
'firstname' => 'John',
|
||||
'lastname' => 'Doe',
|
||||
@@ -29,16 +43,18 @@ class OrderCreateServiceTest extends TestCase
|
||||
'shipping_zone' => 'Rostov',
|
||||
'shipping_postcode' => 'Rostov',
|
||||
'payment_method' => 'Cash',
|
||||
'field_1' => 'кирилица',
|
||||
'field_2' => 'hello',
|
||||
'tgData' => [],
|
||||
];
|
||||
|
||||
$data = array_merge($data, $customOrderFields);
|
||||
|
||||
$data['tgData'] = $tgData;
|
||||
|
||||
$meta = [
|
||||
'ip' => '127.0.0.1',
|
||||
'user_agent' => 'UnitTests',
|
||||
];
|
||||
|
||||
$storeId = $this->app->getConfigValue('store.oc_store_id');
|
||||
$dateAdded = '2026-01-01 00:00:00';
|
||||
$dateAddedFormatted = '01.01.2026 00:00';
|
||||
Carbon::setTestNow($dateAdded);
|
||||
@@ -50,6 +66,8 @@ class OrderCreateServiceTest extends TestCase
|
||||
$currencyValue = 222;
|
||||
$orderId = 1111;
|
||||
$orderProductId = 223;
|
||||
$ocCustomerId = 333;
|
||||
$telecartCustomerId = 444;
|
||||
|
||||
$product = [
|
||||
'product_id' => 93,
|
||||
@@ -63,14 +81,16 @@ class OrderCreateServiceTest extends TestCase
|
||||
$products = [$product];
|
||||
|
||||
$connection = m::mock(ConnectionInterface::class);
|
||||
$connection->shouldReceive('transaction')->once()->andReturnUsing(fn($c) => $c());
|
||||
$connection->shouldReceive('beginTransaction')->once()->andReturnTrue();
|
||||
$connection->shouldReceive('commitTransaction')->once()->andReturnTrue();
|
||||
// $connection->shouldReceive('rollBackTransaction')->once()->andReturnTrue();
|
||||
$connection->shouldReceive('lastInsertId')->once()->andReturn($orderId)->ordered();
|
||||
$connection->shouldReceive('lastInsertId')->once()->andReturn($orderProductId)->ordered();
|
||||
|
||||
$connection->shouldReceive('insert')->once()->with(
|
||||
db_table('order'),
|
||||
[
|
||||
'store_id' => $this->app->getConfigValue('store.oc_store_id'),
|
||||
'store_id' => $storeId,
|
||||
'store_name' => $this->app->getConfigValue('app.app_name'),
|
||||
'firstname' => $data['firstname'],
|
||||
'lastname' => $data['lastname'],
|
||||
@@ -154,7 +174,24 @@ class OrderCreateServiceTest extends TestCase
|
||||
|
||||
$telegramServiceMock = m::mock(TelegramService::class);
|
||||
$loggerMock = m::mock(LoggerInterface::class);
|
||||
$validatorMock = m::mock(ValidatorInterface::class);
|
||||
|
||||
$telegramCustomerService = m::mock(TelegramCustomerService::class);
|
||||
$telegramCustomerService->shouldReceive('assignOcCustomer')->once()
|
||||
->with($telegramUserId, $ocCustomerId)
|
||||
->andReturn($telecartCustomerId);
|
||||
|
||||
$telegramCustomerService->shouldReceive('increaseOrdersCount')->once()
|
||||
->with($telecartCustomerId)
|
||||
->andReturnNull();
|
||||
|
||||
$ocCustomerService = m::mock(OcCustomerService::class);
|
||||
$ocCustomerService->shouldReceive('findOrCreate')->once()->andReturn($ocCustomerId);
|
||||
|
||||
$orderMetaService = m::mock(OrderMetaService::class);
|
||||
$orderMetaService->shouldReceive('insert')
|
||||
->once()
|
||||
->with($orderId, $storeId, $customOrderFields, $telecartCustomerId)
|
||||
->andReturnNull();
|
||||
|
||||
$service = new OrderCreateService(
|
||||
$connection,
|
||||
@@ -163,7 +200,9 @@ class OrderCreateServiceTest extends TestCase
|
||||
$this->app->get(SettingsService::class),
|
||||
$telegramServiceMock,
|
||||
$loggerMock,
|
||||
$validatorMock,
|
||||
$telegramCustomerService,
|
||||
$ocCustomerService,
|
||||
$orderMetaService
|
||||
);
|
||||
|
||||
$order = $service->create($data, $meta);
|
||||
|
||||
Reference in New Issue
Block a user