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:
2025-11-24 14:08:56 +03:00
committed by Nikita Kiselev
parent b39a344a7d
commit 952d8e58da
18 changed files with 489 additions and 172 deletions

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

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

View File

@@ -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);
}
};

View File

@@ -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);
}
};

View File

@@ -66,4 +66,9 @@ class Utils
{
return $needle === '' || strpos($haystack, $needle) !== false;
}
public static function boolToInt(bool $value): int
{
return $value === true ? 1 : 0;
}
}

View File

@@ -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]);
}
}

View File

@@ -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();
}
}

View File

@@ -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}";
}
}

View File

@@ -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);
}
}

View File

@@ -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');
}
}

View File

@@ -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);