Нужно написать код с нуля. Конечно, всегда есть куда улучшать до бесконечности, но для оптимального обсуждения стоит ориентироваться на примерно 4 часа работы. Использование библиотек или готовых компонентов приветствуется, изобретать велосипеды не нужно. Наш проект написан на Symfony поэтому использование их фреймворка или компонентов будет плюсом, но это не принципиально и решения на других компонентах тоже принимаются.
Задача. Нужен сервис, который представляет из себя обработчик платежей с логикой общения с покупателем. Платёж в данном случае это POST-запрос к сервису на любой придуманный эндпоинт в определённом формате:
{
"token": "{uuid}",
"status": "(authorized|confirmed|rejected|refunded)",
"order_id": 1234567890,
"amount": 20000,
"currency": "(RUB|USD|EUR)",
"error_code": "(76543|null)",
"pan": "12341********234",
"user_id": "876123654",
"language_code": "(ru|en)"
}
Где user_id
это уникальный идентификатор пользователя:
- если платежей от него ранее не было, то это означает "человек оплатил подписку";
- если платежи от него были ранее, то это означает "человек продлил подписку". NB: в задании можно не добавлять реальное хранилище данных (опционально), достаточно иметь решение в коде на этот счёт.
После получения платежа нужно посылать ему уведомление в Телеграм. Нюансы для тестового задания:
user_id
это его идентификатор в ТГ.- Нужно иметь код для работы с ТГ, в тестовом задании реальных запросов можно не слать, токен тг-бота можно захардкодить случайным.
- Сообщения должны быть различные в зависимости от статуса платежа (успех, ошибка).
- Сообщения должны быть различные в зависимости от того это новая подписка и продление.
- Сообщения должны быть переведён на два языка: en, ru.
- Сообщения должны поддерживать форматирование (жирный, курсив, и т.д.).
- Сам текст не принципиален, можно рыбу, главное чтобы он различался исходя из параметров выше.
Следует учесть что хайлоада здесь нет и нагрузка небольшая, однако бывают острые пики нагрузки: например, кто-то резко запустил рекламу на большую аудиторию. У Телеграм есть ограничения на запросы к их серверам в единицу времени (ориентируйтесь на 10 запросов / сек), однако надо в решении учесть, что в пиковой нагрузке количество платежей точно больше этого ограничения.
Бонусом в архитектуре решения будет учесть наличие другого платёжного шлюза: то есть, хотя бы теоретическое наличие другого формата данных на другой эндпоинт, но с сохранением логики обработки платежа из описания выше.
symfony new payment-service --webapp
cd payment-service
symfony server:start
[OK] Web server listening on https://127.0.0.1:8000
Установка зависимостей
composer require symfony/messenger symfony/serializer symfony/translation guzzlehttp/guzzle
messenger – обработка очередей.
serializer – работа с JSON.
translation – мультиязычность.
guzzlehttp/guzzle – HTTP-запросы к Telegram API.
php bin/console make:controller PaymentController
src\Controller\PaymentController.php
Что делает этот код?
Принимает POST /payment с JSON-данными.
Проверяет, что JSON корректный.
Передаёт данные в PaymentProcessor.
Возвращает JSON-ответ.
src/Service/PaymentProcessor.php
Что делает этот код?
Логирует платеж.
Проверяет user_id.
Определяет, новая подписка или продление.
Возвращает ответ.
symfony server:start
Протестируем запросом powershell
Invoke-WebRequest -Uri "http://127.0.0.1:8000/payment" `
-Method Post `
-Headers @{ "Content-Type" = "application/json" } `
-Body '{"token": "123e4567-e89b-12d3-a456-426614174000", "status": "confirmed", "order_id": 1234567890, "amount": 20000, "currency": "RUB", "error_code": null, "pan": "12341********234", "user_id": "876123654", "language_code": "ru"}'
Ответ
StatusCode : 200
StatusDescription : OK
Content : {"message":"New subscription processed","status":"confirmed"}
RawContent : HTTP/1.1 200 OK
Cache-Control: no-cache, private
Content-Type: application/json
Date: Fri, 28 Feb 2025 17:27:30 GMT
Set-Cookie: main_deauth_profile_token=d59ad4; path=/; httponly; samesite=lax,mai...
Forms : {}
Headers : {[Cache-Control, no-cache, private], [Content-Type, application/json], [Date, Fri, 28 Feb 2025 17:27:30 GMT], [Set-Cookie, main_deauth_profile_token=d59ad4; path=/; httponly; samesite=lax,main_auth_pro
file_token=deleted; expires=Thu, 29 Feb 2024 17:27:29 GMT; Max-Age=0; path=/; httponly]...}
Images : {}
InputFields : {}
Links : {}
ParsedHtml : System.__ComObject
RawContentLength : 61
Symfony использует HttpClient для работы с внешними API. Установим его:
composer require symfony/http-client
src/Service/TelegramNotifier.php
Что делает этот код?
Использует HttpClientInterface для отправки запросов в Telegram API.
Логирует успешную отправку или ошибки.
Форматирует сообщение в MarkdownV2.
src/Service/PaymentProcessor.php отредактируем код
Что изменилось?
Теперь при обработке платежа отправляется сообщение в Telegram.
Форматируется текст в зависимости от статуса и языка.
POST-запрос через PowerShell:
Invoke-RestMethod -Uri "http://127.0.0.1:8000/payment" `
-Method Post `
-Headers @{ "Content-Type" = "application/json" } `
-Body '{"token": "123e4567-e89b-12d3-a456-426614174000", "status": "confirmed", "order_id": 1234567890, "amount": 20000, "currency": "RUB", "error_code": null, "pan": "12341********234", "user_id": "876123654", "language_code": "ru"}'
Ответ
message status
------- ------
Subscription renewed confirmed
Symfony Messenger позволит:
Обрабатывать уведомления асинхронно, не задерживая ответ на API-запрос.
Учитывать ограничения Telegram API (не более 10 запросов в секунду).
Обрабатывать высокую нагрузку (пики платежей).
Установим Messenger с поддержкой очередей:
composer require symfony/messenger
Symfony Messenger может работать с разными очередями:
Doctrine (БД) – просто.
Установим:
composer require symfony/doctrine-messenger
Включим в .env поддержку Doctrine:
MESSENGER_TRANSPORT_DSN=doctrine://default
Подключим sqlite, отредактируем .env
DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
Обновим схему БД:
php bin/console doctrine:migrations:diff
php bin/console doctrine:migrations:migrate
Создание сообщения для отправки уведомлений Добавим класс Message, этот класс просто хранит данные для обработки в очереди:
src/Message/SendTelegramNotification.php
Создание обработчика сообщений (Message Handler)
Добавим Handler, этот класс обрабатывает сообщения из очереди и отправляет уведомление:
src/MessageHandler/SendTelegramNotificationHandler.php
Изменение PaymentProcessor для работы с очередью. Теперь мы не отправляем уведомления сразу, а ставим их в очередь.
src/Service/PaymentProcessor.php
Конфигурация Symfony Messenger В файле config/packages/messenger.yaml добавим настройки транспорта:
framework:
messenger:
failure_transport: failed
transports:
async: doctrine://default
failed: doctrine://default?queue_name=failed
default_bus: messenger.bus.default
buses:
messenger.bus.default: ~
routing:
App\Message\SendTelegramNotification: async
async: doctrine://default — Основная очередь сообщений в базе данных (Doctrine).
failed: doctrine://default?queue_name=failed — Очередь для неудачных сообщений.
default_bus: messenger.bus.default — Основная шина сообщений.
buses: messenger.bus.default — Объявление шины (без лишних параметров).
routing: App\Message\SendTelegramNotification: async — Указано, что наши сообщения SendTelegramNotification попадут в очередь.
Создаем таблицы для очередей в базе данных:
php bin/console messenger:setup-transports
Запуск обработчика очереди (чтобы сообщения реально отправлялись):
php bin/console messenger:consume async --limit=10 --time-limit=60 --memory-limit=128M
php bin/console messenger:stats
php bin/console messenger:consume async --limit=10
postman http://127.0.0.1:8000/payment raw
{
"token": "550e8400-e29b-41d4-a716-446655440000",
"status": "confirmed",
"order_id": 1234567890,
"amount": 20000,
"currency": "RUB",
"error_code": null,
"pan": "12341********234",
"user_id": "876123654",
"language_code": "ru"
}
powershell
Invoke-RestMethod -Uri "http://127.0.0.1:8000/payment" `
-Method Post `
-Headers @{ "Content-Type" = "application/json" } `
-Body '{"token": "123e4567-e89b-12d3-a456-426614174000", "status": "confirmed", "order_id": 1234567890, "amount": 20000, "currency": "RUB", "error_code": null, "pan": "12341********234", "user_id": "876123654", "language_code": "ru"}'