feat: добавлена функциональность политики конфиденциальности и согласия на обработку ПД

Основные изменения:

Backend:
- Добавлена миграция для поля privacy_consented_at в таблицу telecart_customers
- Создан PrivacyPolicyHandler с методами:
  * checkIsUserPrivacyConsented - проверка наличия согласия пользователя
  * userPrivacyConsent - сохранение согласия пользователя
- Обновлен TelegramService для извлечения userId из initData
- Обновлен TelegramServiceProvider для внедрения зависимостей
- Добавлены новые маршруты в routes.php
- Обновлен SettingsHandler для возврата privacy_policy_link
- Обновлен TelegramCustomersHandler для включения privacy_consented_at в ответы
- Обновлены тесты TelegramServiceTest

Frontend (SPA):
- Создан компонент PrivacyPolicy.vue для отображения запроса согласия
- Добавлена проверка согласия при инициализации приложения (main.js)
- Обновлен App.vue для отображения компонента PrivacyPolicy
- Добавлены функции checkIsUserPrivacyConsented и userPrivacyConsent в ftch.js
- Обновлен SettingsStore для хранения privacy_policy_link и is_privacy_consented

Frontend (Admin):
- Добавлено поле privacy_policy_link в настройки (settings.js)
- Добавлена настройка ссылки на политику конфиденциальности в GeneralView.vue
- Обновлен CustomersView.vue:
  * Добавлена колонка privacy_consented_at с отображением даты согласия
  * Добавлена поддержка help-текста для колонок с иконкой вопроса и tooltip
  * Добавлены help-тексты для колонок last_seen_at, privacy_consented_at, created_at
  * Улучшено форматирование кода
This commit is contained in:
2025-11-23 23:17:21 +03:00
committed by Nikita Kiselev
parent 9a93cc7342
commit 7a5eebec91
16 changed files with 378 additions and 52 deletions

View File

@@ -17,6 +17,7 @@ export const useSettingsStore = defineStore('settings', {
theme_light: 'light',
theme_dark: 'dark',
app_debug: false,
privacy_policy_link: null,
},
telegram: {

View File

@@ -67,13 +67,16 @@
</div>
</div>
</OverlayPanel>
<Button icon="fa fa-refresh" @click="loadCustomers" v-tooltip.top="'Обновить таблицу'" size="small"/>
<Button icon="fa fa-times-circle" label="Сбросить фильтры" @click="resetFilters" v-tooltip.top="'Сбросить все фильтры'" size="small"/>
<Button icon="fa fa-refresh" @click="loadCustomers" v-tooltip.top="'Обновить таблицу'"
size="small"/>
<Button icon="fa fa-times-circle" label="Сбросить фильтры" @click="resetFilters"
v-tooltip.top="'Сбросить все фильтры'" size="small"/>
</div>
<IconField>
<InputIcon class="fa fa-search" />
<InputText v-model="globalSearchValue" placeholder="Поиск по таблице..." @input="onGlobalSearch" />
<InputIcon class="fa fa-search"/>
<InputText v-model="globalSearchValue" placeholder="Поиск по таблице..."
@input="onGlobalSearch"/>
</IconField>
</div>
</template>
@@ -91,10 +94,25 @@
</template>
</Column>
<Column v-for="col in selectedColumns" :key="col.field" :field="col.field" :header="col.header" :sortable="col.sortable" :dataType="col.dataType" :showFilterMenu="col.filterable !== false">
<Column v-for="col in selectedColumns" :key="col.field" :field="col.field"
:sortable="col.sortable" :dataType="col.dataType"
:showFilterMenu="col.filterable !== false">
<template #header>
<div class="tw:flex tw:items-center tw:gap-1">
<span>{{ col.header }}</span>
<i
v-if="col.help"
class="fa fa-question-circle tw:text-gray-400 tw:cursor-help"
v-tooltip.top="col.help"
></i>
</div>
</template>
<template #body="{ data }">
<template v-if="col.field === 'id'">{{ data.id }}</template>
<template v-else-if="col.field === 'telegram_user_id'">{{ data.telegram_user_id }}</template>
<template v-else-if="col.field === 'telegram_user_id'">{{
data.telegram_user_id
}}
</template>
<template v-else-if="col.field === 'username'">
<div class="tw:flex tw:items-center tw:gap-2">
<div v-if="data.photo_url" class="tw:relative">
@@ -132,25 +150,35 @@
</span>
<span v-else></span>
</template>
<template v-else-if="col.field === 'privacy_consented_at'">
<span v-if="data.privacy_consented_at">
<i class="fa fa-clock-o"></i> {{ formatDate(data.privacy_consented_at) }}
</span>
<span v-else></span>
</template>
<template v-else-if="col.field === 'created_at'">
<i class="fa fa-calendar"></i> {{ formatDate(data.created_at) }}
</template>
</template>
<template #filter="{ filterModel }">
<template v-if="col.field === 'telegram_user_id'">
<InputText v-model="filterModel.value" type="text" placeholder="Поиск по ID" class="p-column-filter" />
<InputText v-model="filterModel.value" type="text" placeholder="Поиск по ID"
class="p-column-filter"/>
</template>
<template v-else-if="col.field === 'username'">
<InputText v-model="filterModel.value" type="text" placeholder="Поиск по имени пользователя" class="p-column-filter" />
<InputText v-model="filterModel.value" type="text"
placeholder="Поиск по имени пользователя" class="p-column-filter"/>
</template>
<template v-else-if="col.field === 'first_name'">
<InputText v-model="filterModel.value" type="text" placeholder="Поиск по имени" class="p-column-filter" />
<InputText v-model="filterModel.value" type="text" placeholder="Поиск по имени"
class="p-column-filter"/>
</template>
<template v-else-if="col.field === 'last_name'">
<InputText v-model="filterModel.value" type="text" placeholder="Поиск по фамилии" class="p-column-filter" />
<InputText v-model="filterModel.value" type="text" placeholder="Поиск по фамилии"
class="p-column-filter"/>
</template>
<template v-else-if="['last_seen_at', 'created_at'].includes(col.field)">
<DatePicker v-model="filterModel.value" dateFormat="dd.mm.yy" placeholder="dd.mm.yyyy" />
<DatePicker v-model="filterModel.value" dateFormat="dd.mm.yy" placeholder="dd.mm.yyyy"/>
</template>
<template v-else-if="col.field === 'is_premium'">
<Dropdown
@@ -204,7 +232,8 @@
<div style="font-size: 0.9rem; color: #666;">
ID: {{ selectedCustomer.telegram_user_id }}
</div>
<div v-if="!selectedCustomer.allows_write_to_pm" style="color: #f59e0b; margin-top: 0.5rem;">
<div v-if="!selectedCustomer.allows_write_to_pm"
style="color: #f59e0b; margin-top: 0.5rem;">
<i class="fa fa-exclamation-triangle"></i> Пользователь не разрешил писать ему в PM
</div>
</div>
@@ -245,8 +274,8 @@
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { FilterMatchMode, FilterOperator } from '@primevue/core/api';
import {onMounted, ref} from 'vue';
import {FilterMatchMode, FilterOperator} from '@primevue/core/api';
import DataTable from 'primevue/datatable';
import Column from 'primevue/column';
import DatePicker from 'primevue/datepicker';
@@ -257,8 +286,8 @@ import Textarea from 'primevue/textarea';
import OverlayPanel from 'primevue/overlaypanel';
import Checkbox from 'primevue/checkbox';
import Button from 'primevue/button';
import { apiPost } from '@/utils/http.js';
import { useToast, IconField, InputIcon } from 'primevue';
import {apiPost} from '@/utils/http.js';
import {IconField, InputIcon, useToast} from 'primevue';
const toast = useToast();
const customers = ref([]);
@@ -270,16 +299,59 @@ const messageText = ref('');
const sendingMessage = ref(false);
const columns = ref([
{ field: 'id', header: '№', sortable: true, filterable: false, visible: true },
{ field: 'telegram_user_id', header: 'ID в Telegram', sortable: true, filterable: true, visible: false },
{ field: 'username', header: 'Имя пользователя', sortable: true, filterable: true, visible: true },
{ field: 'first_name', header: 'Имя', sortable: true, filterable: true, visible: true },
{ field: 'last_name', header: 'Фамилия', sortable: true, filterable: true, visible: true },
{ field: 'language_code', header: 'Язык интерфейса', sortable: true, filterable: false, visible: false },
{ field: 'is_premium', header: 'Премиум статус', sortable: true, filterable: true, visible: true },
{ field: 'oc_customer_id', header: 'ID покупателя', sortable: true, filterable: false, visible: false },
{ field: 'last_seen_at', header: 'Последний визит', sortable: true, dataType: 'date', filterable: true, visible: true },
{ field: 'created_at', header: 'Дата регистрации', sortable: true, dataType: 'date', filterable: true, visible: true },
{field: 'id', header: '№', sortable: true, filterable: false, visible: true},
{
field: 'telegram_user_id',
header: 'ID в Telegram',
sortable: true,
filterable: true,
visible: false
},
{field: 'username', header: 'Имя пользователя', sortable: true, filterable: true, visible: true},
{field: 'first_name', header: 'Имя', sortable: true, filterable: true, visible: true},
{field: 'last_name', header: 'Фамилия', sortable: true, filterable: true, visible: true},
{
field: 'language_code',
header: 'Язык интерфейса',
sortable: true,
filterable: false,
visible: false
},
{field: 'is_premium', header: 'Премиум статус', sortable: true, filterable: true, visible: true},
{
field: 'oc_customer_id',
header: 'ID покупателя',
sortable: true,
filterable: false,
visible: false
},
{
field: 'last_seen_at',
header: 'Последний визит',
sortable: true,
dataType: 'date',
filterable: true,
visible: true,
help: 'Показывает, когда пользователь последний раз открывал Telegram магазин.',
},
{
field: 'privacy_consented_at',
header: 'Согласие ПД',
sortable: true,
dataType: 'date',
filterable: true,
visible: true,
help: 'Показывает, когда пользователь дал согласие на обработку персональных данных.',
},
{
field: 'created_at',
header: 'Дата регистрации',
sortable: true,
dataType: 'date',
filterable: true,
visible: true,
help: 'Показывает, когда пользователь впервые открыл Telegram магазин.',
},
]);
const selectedColumns = ref(columns.value.filter(col => col.visible !== false));
@@ -314,9 +386,9 @@ function deselectAllColumns() {
}
const premiumFilterOptions = [
{ label: 'Любой', value: null },
{ label: 'Нет', value: false },
{ label: 'Да', value: true },
{label: 'Любой', value: null},
{label: 'Нет', value: false},
{label: 'Да', value: true},
];
const lazyParams = ref({
@@ -329,14 +401,35 @@ const lazyParams = ref({
});
const filters = ref({
global: { value: null, matchMode: FilterMatchMode.CONTAINS },
telegram_user_id: { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.STARTS_WITH }] },
username: { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.CONTAINS }] },
first_name: { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.CONTAINS }] },
last_name: { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.CONTAINS }] },
is_premium: { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.EQUALS }] },
created_at: { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.DATE_IS }] },
last_seen_at: { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.DATE_IS }] },
global: {value: null, matchMode: FilterMatchMode.CONTAINS},
telegram_user_id: {
operator: FilterOperator.AND,
constraints: [{value: null, matchMode: FilterMatchMode.STARTS_WITH}]
},
username: {
operator: FilterOperator.AND,
constraints: [{value: null, matchMode: FilterMatchMode.CONTAINS}]
},
first_name: {
operator: FilterOperator.AND,
constraints: [{value: null, matchMode: FilterMatchMode.CONTAINS}]
},
last_name: {
operator: FilterOperator.AND,
constraints: [{value: null, matchMode: FilterMatchMode.CONTAINS}]
},
is_premium: {
operator: FilterOperator.AND,
constraints: [{value: null, matchMode: FilterMatchMode.EQUALS}]
},
created_at: {
operator: FilterOperator.AND,
constraints: [{value: null, matchMode: FilterMatchMode.DATE_IS}]
},
last_seen_at: {
operator: FilterOperator.AND,
constraints: [{value: null, matchMode: FilterMatchMode.DATE_IS}]
},
});
function processFiltersForBackend(filtersObj) {
@@ -447,14 +540,35 @@ function onGlobalSearch() {
function resetFilters() {
globalSearchValue.value = '';
filters.value = {
global: { value: null, matchMode: FilterMatchMode.CONTAINS },
telegram_user_id: { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.STARTS_WITH }] },
username: { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.CONTAINS }] },
first_name: { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.CONTAINS }] },
last_name: { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.CONTAINS }] },
is_premium: { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.EQUALS }] },
created_at: { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.DATE_IS }] },
last_seen_at: { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.DATE_IS }] },
global: {value: null, matchMode: FilterMatchMode.CONTAINS},
telegram_user_id: {
operator: FilterOperator.AND,
constraints: [{value: null, matchMode: FilterMatchMode.STARTS_WITH}]
},
username: {
operator: FilterOperator.AND,
constraints: [{value: null, matchMode: FilterMatchMode.CONTAINS}]
},
first_name: {
operator: FilterOperator.AND,
constraints: [{value: null, matchMode: FilterMatchMode.CONTAINS}]
},
last_name: {
operator: FilterOperator.AND,
constraints: [{value: null, matchMode: FilterMatchMode.CONTAINS}]
},
is_premium: {
operator: FilterOperator.AND,
constraints: [{value: null, matchMode: FilterMatchMode.EQUALS}]
},
created_at: {
operator: FilterOperator.AND,
constraints: [{value: null, matchMode: FilterMatchMode.DATE_IS}]
},
last_seen_at: {
operator: FilterOperator.AND,
constraints: [{value: null, matchMode: FilterMatchMode.DATE_IS}]
},
};
lazyParams.value.page = 1;
lazyParams.value.first = 0;

View File

@@ -18,6 +18,17 @@
Изображение, которое будет отображаться в Telegram Mini App.
</ItemImage>
<ItemInput label="Политика конфиденциальности"
v-model="settings.items.app.privacy_policy_link"
placeholder="https://your-site/privacy-polisy"
>
Укажите ссылку на страницу с вашей Политикой конфиденциальности.
Она будет показываться пользователям для получения согласия на обработку персональных данных.
Вы сможете видеть, кто согласился, на странице с
<RouterLink to="/customers">Telegram покупателями</RouterLink>
.
</ItemInput>
<ItemSelect label="Светлая тема" v-model="settings.items.app.theme_light" :items="themes">
Выберите стиль, который будет использоваться при отображении вашего магазина
в Telegram для дневного режима.

View File

@@ -17,6 +17,8 @@
</KeepAlive>
</RouterView>
<PrivacyPolicy v-if="! settings.is_privacy_consented"/>
<CartButton v-if="settings.store_enabled"/>
<Dock v-if="isAppDockShown"/>
</section>
@@ -42,8 +44,8 @@ import {useSettingsStore} from "@/stores/SettingsStore.js";
import {useProductFiltersStore} from "@/stores/ProductFiltersStore.js";
import CartButton from "@/components/CartButton.vue";
import Dock from "@/components/Dock.vue";
import Navbar from "@/components/Navbar.vue";
import AppDebugMessage from "@/components/AppDebugMessage.vue";
import PrivacyPolicy from "@/components/PrivacyPolicy.vue";
const tg = useMiniApp();
const platform = ref();

View File

@@ -0,0 +1,42 @@
<template>
<div v-if="isShown" class="toast toast-center bottom-20 z-50">
<div class="alert alert-info">
<span>
Используя магазин, вы соглашаетесь с
<a v-if="settings.privacy_policy_link"
href="#" class="underline"
@click.prevent="showPrivacyPolicy"
>обработкой персональных данных</a>
<span v-else>обработкой персональных данных</span>.
</span>
<button
class="btn btn-outline"
@click="privacyConsent"
>
OK
</button>
</div>
</div>
</template>
<script setup>
import {userPrivacyConsent} from "@/utils/ftch.js";
import {ref} from "vue";
import {useSettingsStore} from "@/stores/SettingsStore.js";
const isShown = ref(true);
const settings = useSettingsStore();
async function privacyConsent() {
isShown.value = false;
await userPrivacyConsent();
}
function showPrivacyPolicy() {
if (settings.privacy_policy_link) {
window.Telegram.WebApp.openLink(settings.privacy_policy_link, {
try_instant_view: true,
});
}
}
</script>

View File

@@ -8,7 +8,7 @@ import {useSettingsStore} from "@/stores/SettingsStore.js";
import ApplicationError from "@/ApplicationError.vue";
import AppMetaInitializer from "@/utils/AppMetaInitializer.ts";
import {injectYaMetrika} from "@/utils/yaMetrika.js";
import {saveTelegramCustomer} from "@/utils/ftch.js";
import {checkIsUserPrivacyConsented, saveTelegramCustomer} from "@/utils/ftch.js";
import {register} from 'swiper/element/bundle';
import 'swiper/element/bundle';
@@ -59,6 +59,22 @@ settings.load()
}
}
})
.then(() => {
(async () => {
try {
const response = await checkIsUserPrivacyConsented();
settings.is_privacy_consented = response?.data?.is_privacy_consented;
if (settings.is_privacy_consented) {
console.info('[Init] Privacy Policy consent given by user.');
} else {
console.info('[Init] Privacy Policy consent NOT given by user.');
}
} catch (error) {
console.error('[Init] Failed to check Telegram user consent.');
settings.is_privacy_consented = false;
}
})();
})
.then(() => blocks.processBlocks(settings.mainpage_blocks))
.then(async () => {
console.debug('Load default filters for the main page');

View File

@@ -29,6 +29,8 @@ export const useSettingsStore = defineStore('settings', {
text_order_created_success: 'Заказ успешно оформлен.',
},
mainpage_blocks: [],
is_privacy_consented: false,
privacy_policy_link: false,
}),
actions: {
@@ -53,6 +55,7 @@ export const useSettingsStore = defineStore('settings', {
this.currency_code = settings.currency_code;
this.texts = settings.texts;
this.mainpage_blocks = settings.mainpage_blocks;
this.privacy_policy_link = settings.privacy_policy_link;
}
}
});

View File

@@ -107,4 +107,12 @@ export async function saveTelegramCustomer(userData) {
});
}
export async function checkIsUserPrivacyConsented() {
return await ftch('checkIsUserPrivacyConsented');
}
export async function userPrivacyConsent() {
return await ftch('userPrivacyConsent');
}
export default ftch;

View File

@@ -88,6 +88,7 @@ class TelegramCustomersHandler
'photo_url',
'last_seen_at',
'referral',
'privacy_consented_at',
'created_at',
'updated_at',
])
@@ -322,6 +323,7 @@ class TelegramCustomersHandler
'photo_url' => $customer['photo_url'],
'last_seen_at' => $customer['last_seen_at'],
'referral' => $customer['referral'],
'privacy_consented_at' => $customer['privacy_consented_at'],
'created_at' => $customer['created_at'],
'updated_at' => $customer['updated_at'],
];

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 `privacy_consented_at` TIMESTAMP NULL DEFAULT NULL AFTER `referral`;
SQL;
$this->database->statement($sql);
}
};

View File

@@ -7,15 +7,24 @@ use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\GuzzleException;
use Openguru\OpenCartFramework\Support\Arr;
use Openguru\OpenCartFramework\Telegram\Enums\ChatAction;
use Openguru\OpenCartFramework\Telegram\Exceptions\DecodeTelegramInitDataException;
use Openguru\OpenCartFramework\Telegram\Exceptions\TelegramClientException;
use Psr\Log\LoggerInterface;
class TelegramService
{
private ?string $botToken;
private TelegramInitDataDecoder $initDataDecoder;
private LoggerInterface $logger;
public function __construct(?string $botToken = null)
{
public function __construct(
TelegramInitDataDecoder $initDataDecoder,
LoggerInterface $logger,
?string $botToken = null
) {
$this->botToken = $botToken;
$this->initDataDecoder = $initDataDecoder;
$this->logger = $logger;
}
public function escapeTelegramMarkdownV2(string $text): string
@@ -194,4 +203,21 @@ class TelegramService
return $response['result'];
}
public function userId(?string $initDataRaw = null): ?int
{
if (! $initDataRaw) {
return null;
}
try {
$decoded = $this->initDataDecoder->decode($initDataRaw);
return Arr::get($decoded, 'user.id');
} catch (DecodeTelegramInitDataException $e) {
$this->logger->error($e->getMessage(), ['exception' => $e]);
}
return null;
}
}

View File

@@ -14,7 +14,11 @@ class TelegramServiceProvider extends ServiceProvider
$this->container->singleton(TelegramService::class, function (Application $app) {
$botToken = $app->getConfigValue('telegram.bot_token');
return new TelegramService($botToken);
return new TelegramService(
$app->get(TelegramInitDataDecoder::class),
$app->get(LoggerInterface::class),
$botToken,
);
});
$this->container->singleton(SignatureValidator::class, function (Application $app) {

View File

@@ -0,0 +1,73 @@
<?php
namespace App\Handlers;
use App\Models\TelegramCustomer;
use Carbon\Carbon;
use Openguru\OpenCartFramework\Http\JsonResponse;
use Openguru\OpenCartFramework\Http\Request;
use Openguru\OpenCartFramework\Http\Response;
use Openguru\OpenCartFramework\Support\Arr;
use Openguru\OpenCartFramework\Telegram\Enums\TelegramHeader;
use Openguru\OpenCartFramework\Telegram\TelegramService;
use Psr\Log\LoggerInterface;
class PrivacyPolicyHandler
{
private TelegramService $telegramService;
private TelegramCustomer $telegramCustomer;
private LoggerInterface $logger;
public function __construct(
TelegramService $telegramService,
TelegramCustomer $telegramCustomer,
LoggerInterface $logger
) {
$this->telegramService = $telegramService;
$this->telegramCustomer = $telegramCustomer;
$this->logger = $logger;
}
public function checkIsUserPrivacyConsented(Request $request): JsonResponse
{
$isPrivacyConsented = false;
$telegramUserId = $this->telegramService->userId($request->header(TelegramHeader::INIT_DATA));
if (! $telegramUserId) {
return new JsonResponse([
'data' => [
'is_privacy_consented' => false,
],
]);
}
$customer = $this->telegramCustomer->findByTelegramUserId($telegramUserId);
if ($customer) {
$isPrivacyConsented = Arr::get($customer, 'privacy_consented_at') !== null;
}
return new JsonResponse([
'data' => [
'is_privacy_consented' => $isPrivacyConsented,
],
]);
}
public function userPrivacyConsent(Request $request): JsonResponse
{
$telegramUserId = $this->telegramService->userId($request->header(TelegramHeader::INIT_DATA));
if ($telegramUserId) {
$this->telegramCustomer->updateByTelegramUserId($telegramUserId, [
'privacy_consented_at' => Carbon::now()->toDateTimeString(),
]);
} else {
$this->logger->warning(
'Could not find customer with telegram user_id: ' . $telegramUserId . ' to give privacy consent.'
);
}
return new JsonResponse([], Response::HTTP_NO_CONTENT);
}
}

View File

@@ -65,6 +65,7 @@ class SettingsHandler
'currency_code' => $this->settings->config()->getStore()->getOcDefaultCurrency(),
'texts' => $this->settings->config()->getTexts()->toArray(),
'mainpage_blocks' => $this->settings->get('mainpage_blocks', []),
'privacy_policy_link' => $this->settings->get('app.privacy_policy_link'),
]);
}

View File

@@ -7,13 +7,15 @@ use App\Handlers\FiltersHandler;
use App\Handlers\FormsHandler;
use App\Handlers\HealthCheckHandler;
use App\Handlers\OrderHandler;
use App\Handlers\PrivacyPolicyHandler;
use App\Handlers\ProductsHandler;
use App\Handlers\SettingsHandler;
use App\Handlers\TelegramHandler;
use App\Handlers\TelegramCustomerHandler;
use App\Handlers\TelegramHandler;
return [
'categoriesList' => [CategoriesHandler::class, 'index'],
'checkIsUserPrivacyConsented' => [PrivacyPolicyHandler::class, 'checkIsUserPrivacyConsented'],
'checkout' => [CartHandler::class, 'checkout'],
'filtersForMainPage' => [FiltersHandler::class, 'getFiltersForMainPage'],
'getCart' => [CartHandler::class, 'index'],
@@ -27,5 +29,6 @@ return [
'settings' => [SettingsHandler::class, 'index'],
'storeOrder' => [OrderHandler::class, 'store'],
'testTgMessage' => [SettingsHandler::class, 'testTgMessage'],
'userPrivacyConsent' => [PrivacyPolicyHandler::class, 'userPrivacyConsent'],
'webhook' => [TelegramHandler::class, 'webhook'],
];

View File

@@ -2,6 +2,7 @@
namespace Telegram;
use Openguru\OpenCartFramework\Telegram\TelegramInitDataDecoder;
use Openguru\OpenCartFramework\Telegram\TelegramService;
use Tests\TestCase;
@@ -12,7 +13,10 @@ class TelegramServiceTest extends TestCase
protected function setUp(): void
{
parent::setUp();
$this->service = new TelegramService();
$this->service = new TelegramService(
new TelegramInitDataDecoder(),
$this->getNullLogger(),
);
}
public function testDoesNotEscapeNormalCharacters(): void