@@ -102,6 +115,7 @@ import OcImagePicker from "@/components/OcImagePicker.vue";
import {apiGet} from "@/utils/http.js";
import ResetCacheBtn from "@/components/Form/ResetCacheBtn.vue";
import {Button, ButtonGroup} from "primevue";
+import {rub} from "@/utils/helpers.js";
const settings = useSettingsStore();
const stats = useStatsStore();
diff --git a/frontend/admin/src/stores/stats.js b/frontend/admin/src/stores/stats.js
index 2afac9a..16f361b 100644
--- a/frontend/admin/src/stores/stats.js
+++ b/frontend/admin/src/stores/stats.js
@@ -6,7 +6,7 @@ export const useStatsStore = defineStore('stats', {
items: {
orders_count: null,
orders_total_amount: null,
- order_products_count: null,
+ customers_count: null,
}
}),
@@ -15,7 +15,7 @@ export const useStatsStore = defineStore('stats', {
const response = await apiPost('getDashboardStats');
this.items.orders_count = response.data?.data?.orders_count;
this.items.orders_total_amount = response.data?.data?.orders_total_amount;
- this.items.order_products_count = response.data?.data?.order_products_count;
+ this.items.customers_count = response.data?.data?.customers_count;
}
},
diff --git a/frontend/admin/src/utils/helpers.js b/frontend/admin/src/utils/helpers.js
index 338adc0..1aea486 100644
--- a/frontend/admin/src/utils/helpers.js
+++ b/frontend/admin/src/utils/helpers.js
@@ -5,3 +5,11 @@ export function getThumb(imageUrl) {
const filename = imageUrl.substring(0, extIndex);
return `/image/cache/${filename}-100x100${ext}`;
}
+
+export function rub(value) {
+ return new Intl.NumberFormat('ru-RU', {
+ style: 'currency',
+ currency: 'RUB',
+ maximumFractionDigits: 0
+ }).format(value);
+}
diff --git a/frontend/admin/src/views/CustomersView.vue b/frontend/admin/src/views/CustomersView.vue
index 14a992d..13215ea 100644
--- a/frontend/admin/src/views/CustomersView.vue
+++ b/frontend/admin/src/views/CustomersView.vue
@@ -140,6 +140,9 @@
—
+
+ {{ data.orders_count }}
+
{{ data.oc_customer_id }}
—
@@ -177,9 +180,12 @@
-
+
+
+
+
{
if (processed[field] && processed[field].constraints) {
processed[field].constraints.forEach(constraint => {
@@ -557,6 +581,10 @@ function resetFilters() {
operator: FilterOperator.AND,
constraints: [{value: null, matchMode: FilterMatchMode.CONTAINS}]
},
+ orders_count: {
+ operator: FilterOperator.AND,
+ constraints: [{value: null, matchMode: FilterMatchMode.EQUALS}]
+ },
is_premium: {
operator: FilterOperator.AND,
constraints: [{value: null, matchMode: FilterMatchMode.EQUALS}]
@@ -569,6 +597,10 @@ function resetFilters() {
operator: FilterOperator.AND,
constraints: [{value: null, matchMode: FilterMatchMode.DATE_IS}]
},
+ privacy_consented_at: {
+ operator: FilterOperator.AND,
+ constraints: [{value: null, matchMode: FilterMatchMode.DATE_IS}]
+ },
};
lazyParams.value.page = 1;
lazyParams.value.first = 0;
diff --git a/frontend/spa/src/main.js b/frontend/spa/src/main.js
index ee31107..993a6d8 100644
--- a/frontend/spa/src/main.js
+++ b/frontend/spa/src/main.js
@@ -38,6 +38,12 @@ const blocks = useBlocksStore();
const appLoading = createApp(AppLoading);
appLoading.mount('#app');
+function setTelegramUIColors() {
+ const daisyUIBgColor = getCssVarOklchRgb('--color-base-100');
+ window.Telegram.WebApp.setHeaderColor(daisyUIBgColor);
+ window.Telegram.WebApp.setBackgroundColor(daisyUIBgColor);
+}
+
settings.load()
.then(() => window.Telegram.WebApp.lockOrientation())
.then(() => {
@@ -87,18 +93,22 @@ settings.load()
if (settings.night_auto) {
window.Telegram.WebApp.onEvent('themeChanged', function () {
document.documentElement.setAttribute('data-theme', settings.theme[this.colorScheme]);
+ setTelegramUIColors();
});
+ }
+ const tgColorScheme = getComputedStyle(document.documentElement)
+ .getPropertyValue('--tg-color-scheme')
+ .trim();
+ if (tgColorScheme) {
+ document.documentElement.classList.add(tgColorScheme);
}
for (const key in settings.theme.variables) {
document.documentElement.style.setProperty(key, settings.theme.variables[key]);
-
}
- const daisyUIBgColor = getCssVarOklchRgb('--color-base-100');
- window.Telegram.WebApp.setHeaderColor(daisyUIBgColor);
- window.Telegram.WebApp.setBackgroundColor(daisyUIBgColor);
+ setTelegramUIColors();
}
)
.then(() => new AppMetaInitializer(settings).init())
diff --git a/frontend/spa/src/style.css b/frontend/spa/src/style.css
index e5ac242..5c604af 100644
--- a/frontend/spa/src/style.css
+++ b/frontend/spa/src/style.css
@@ -1,5 +1,5 @@
@import "tailwindcss";
-@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
+@custom-variant dark (&:where(.dark, .dark *));
@plugin "daisyui" {
themes: all;
diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/bastion/Handlers/LogsHandler.php b/module/oc_telegram_shop/upload/oc_telegram_shop/bastion/Handlers/LogsHandler.php
index 823ac15..8a035f4 100755
--- a/module/oc_telegram_shop/upload/oc_telegram_shop/bastion/Handlers/LogsHandler.php
+++ b/module/oc_telegram_shop/upload/oc_telegram_shop/bastion/Handlers/LogsHandler.php
@@ -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;
}
diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/bastion/Handlers/StatsHandler.php b/module/oc_telegram_shop/upload/oc_telegram_shop/bastion/Handlers/StatsHandler.php
index 3e8987d..15be8dd 100755
--- a/module/oc_telegram_shop/upload/oc_telegram_shop/bastion/Handlers/StatsHandler.php
+++ b/module/oc_telegram_shop/upload/oc_telegram_shop/bastion/Handlers/StatsHandler.php
@@ -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();
+ }
}
diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/bastion/Handlers/TelegramCustomersHandler.php b/module/oc_telegram_shop/upload/oc_telegram_shop/bastion/Handlers/TelegramCustomersHandler.php
index b6e3c1e..45dee59 100644
--- a/module/oc_telegram_shop/upload/oc_telegram_shop/bastion/Handlers/TelegramCustomersHandler.php
+++ b/module/oc_telegram_shop/upload/oc_telegram_shop/bastion/Handlers/TelegramCustomersHandler.php
@@ -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'],
diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/database/migrations/20260101000007_create_telecart_order_meta_table.php b/module/oc_telegram_shop/upload/oc_telegram_shop/database/migrations/20260101000007_create_telecart_order_meta_table.php
new file mode 100644
index 0000000..b643b5e
--- /dev/null
+++ b/module/oc_telegram_shop/upload/oc_telegram_shop/database/migrations/20260101000007_create_telecart_order_meta_table.php
@@ -0,0 +1,29 @@
+database->statement($sql);
+ }
+};
+
diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/database/migrations/20260101000008_add_orders_count_to_telecart_customers_table.php b/module/oc_telegram_shop/upload/oc_telegram_shop/database/migrations/20260101000008_add_orders_count_to_telecart_customers_table.php
new file mode 100644
index 0000000..4a1978f
--- /dev/null
+++ b/module/oc_telegram_shop/upload/oc_telegram_shop/database/migrations/20260101000008_add_orders_count_to_telecart_customers_table.php
@@ -0,0 +1,16 @@
+database->statement($sql);
+ }
+};
+
diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Support/Utils.php b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Support/Utils.php
index 979e18e..1ff9a85 100755
--- a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Support/Utils.php
+++ b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Support/Utils.php
@@ -66,4 +66,9 @@ class Utils
{
return $needle === '' || strpos($haystack, $needle) !== false;
}
+
+ public static function boolToInt(bool $value): int
+ {
+ return $value === true ? 1 : 0;
+ }
}
diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Models/TelegramCustomer.php b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Models/TelegramCustomer.php
index c4a0aca..974c70f 100644
--- a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Models/TelegramCustomer.php
+++ b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Models/TelegramCustomer.php
@@ -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]);
+ }
}
diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/OcCustomerService.php b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/OcCustomerService.php
new file mode 100644
index 0000000..6177457
--- /dev/null
+++ b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/OcCustomerService.php
@@ -0,0 +1,62 @@
+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();
+ }
+}
diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/OrderCreateService.php b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/OrderCreateService.php
index 82797ea..6570f97 100755
--- a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/OrderCreateService.php
+++ b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/OrderCreateService.php
@@ -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}";
}
}
diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/OrderMetaService.php b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/OrderMetaService.php
new file mode 100644
index 0000000..4eb01bf
--- /dev/null
+++ b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/OrderMetaService.php
@@ -0,0 +1,27 @@
+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);
+ }
+}
diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/TelegramCustomerService.php b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/TelegramCustomerService.php
index b766e32..a850cf9 100644
--- a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/TelegramCustomerService.php
+++ b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/TelegramCustomerService.php
@@ -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');
}
}
diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/tests/Unit/Services/OrderCreateServiceTest.php b/module/oc_telegram_shop/upload/oc_telegram_shop/tests/Unit/Services/OrderCreateServiceTest.php
index c44c252..ee96b8a 100755
--- a/module/oc_telegram_shop/upload/oc_telegram_shop/tests/Unit/Services/OrderCreateServiceTest.php
+++ b/module/oc_telegram_shop/upload/oc_telegram_shop/tests/Unit/Services/OrderCreateServiceTest.php
@@ -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);