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

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