import asyncio import json from typing import Any import structlog from aiohttp import web from app.config import settings from app.database.database import AsyncSessionLocal from app.external.heleket import HeleketService from app.services.payment_service import PaymentService logger = structlog.get_logger(__name__) class HeleketWebhookHandler: def __init__(self, payment_service: PaymentService) -> None: self.payment_service = payment_service self.service = HeleketService() async def handle(self, request: web.Request) -> web.Response: if not settings.is_heleket_enabled(): logger.warning('Получен Heleket webhook, но сервис отключен') return web.json_response({'status': 'error', 'reason': 'disabled'}, status=503) try: payload: dict[str, Any] = await request.json() except json.JSONDecodeError: logger.error('Некорректный JSON Heleket webhook') return web.json_response({'status': 'error', 'reason': 'invalid_json'}, status=400) if not self.service.verify_webhook_signature(payload): return web.json_response({'status': 'error', 'reason': 'invalid_signature'}, status=401) processed: bool | None = None async with AsyncSessionLocal() as db: try: processed = await self.payment_service.process_heleket_webhook(db, payload) await db.commit() except Exception as e: logger.error('Ошибка обработки Heleket webhook', error=e) await db.rollback() return web.json_response({'status': 'error', 'reason': 'internal_error'}, status=500) if processed: return web.json_response({'status': 'ok'}, status=200) return web.json_response({'status': 'error', 'reason': 'not_processed'}, status=400) async def health_check(self, _: web.Request) -> web.Response: return web.json_response( { 'status': 'ok', 'service': 'heleket_webhook', 'enabled': settings.is_heleket_enabled(), 'path': settings.HELEKET_WEBHOOK_PATH, } ) async def options_handler(self, _: web.Request) -> web.Response: return web.Response( status=200, headers={ 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization', }, ) def create_heleket_app(payment_service: PaymentService) -> web.Application: handler = HeleketWebhookHandler(payment_service) app = web.Application() app.router.add_post(settings.HELEKET_WEBHOOK_PATH, handler.handle) app.router.add_get('/heleket/health', handler.health_check) app.router.add_get('/health', handler.health_check) app.router.add_options(settings.HELEKET_WEBHOOK_PATH, handler.options_handler) return app async def start_heleket_webhook_server(payment_service: PaymentService) -> None: if not settings.is_heleket_enabled(): logger.info('Heleket отключен, webhook сервер не запускается') return app = create_heleket_app(payment_service) runner = web.AppRunner(app) await runner.setup() site = web.TCPSite( runner, host=settings.HELEKET_WEBHOOK_HOST, port=settings.HELEKET_WEBHOOK_PORT, ) try: await site.start() logger.info( 'Heleket webhook сервер запущен на', HELEKET_WEBHOOK_HOST=settings.HELEKET_WEBHOOK_HOST, HELEKET_WEBHOOK_PORT=settings.HELEKET_WEBHOOK_PORT, ) logger.info( 'Heleket webhook URL: http://', HELEKET_WEBHOOK_HOST=settings.HELEKET_WEBHOOK_HOST, HELEKET_WEBHOOK_PORT=settings.HELEKET_WEBHOOK_PORT, HELEKET_WEBHOOK_PATH=settings.HELEKET_WEBHOOK_PATH, ) while True: await asyncio.sleep(1) except asyncio.CancelledError: logger.info('Heleket webhook сервер остановлен по запросу') finally: await site.stop() await runner.cleanup() logger.info('Heleket webhook сервер корректно остановлен')