From 612963a60640c4eaa7e2aa0624df37103c79e478 Mon Sep 17 00:00:00 2001 From: Noloquideus Date: Sun, 28 Jun 2026 14:12:24 +0300 Subject: [PATCH] fix: harden payment provider handling --- .../commands/sbp_withdrawal_commands.py | 1 - src/infrastructure/cloud_kassir/client.py | 2 +- src/infrastructure/config/settings.py | 2 ++ src/infrastructure/itpay/client.py | 26 ++++++++++++------- src/infrastructure/mozen/sbp_client.py | 4 +-- src/infrastructure/security/hash.py | 2 -- src/main.py | 13 +++++++--- 7 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/application/commands/sbp_withdrawal_commands.py b/src/application/commands/sbp_withdrawal_commands.py index d607cbd..82f1b2c 100644 --- a/src/application/commands/sbp_withdrawal_commands.py +++ b/src/application/commands/sbp_withdrawal_commands.py @@ -240,7 +240,6 @@ class HandleSbpWithdrawalWalletEventCommand: withdraw_id=withdraw_id, phone=str(withdrawal.phone or ''), bank_id=str(withdrawal.bank_id or ''), - # amount=Decimal(str(withdrawal.rub_amount or '0')), amount=Decimal('1'), trace_id=str(withdrawal.trace_id or self._logger.get_trace_id()), ) diff --git a/src/infrastructure/cloud_kassir/client.py b/src/infrastructure/cloud_kassir/client.py index a25fc16..55be8ee 100644 --- a/src/infrastructure/cloud_kassir/client.py +++ b/src/infrastructure/cloud_kassir/client.py @@ -147,5 +147,5 @@ class ClaudeKassirClient(IReceipt): return body except ApplicationException: raise - except aiohttp.ClientError: + except (TimeoutError,aiohttp.ClientError): raise ReceiptProviderException(message='Receipt provider unreachable') diff --git a/src/infrastructure/config/settings.py b/src/infrastructure/config/settings.py index c464e63..41753ab 100644 --- a/src/infrastructure/config/settings.py +++ b/src/infrastructure/config/settings.py @@ -60,6 +60,8 @@ class Settings(BaseSettings): CSRF_COOKIE_PATH: str = "/" CSRF_COOKIE_DOMAIN: str | None = None + CORS_ALLOW_ORIGIN_REGEX: str = r'https?://([a-z0-9-]+\.)*elcsa\.ru(:\d+)?$' + DOCS_USERNAME: str = "admin" DOCS_PASSWORD: str = "admin" diff --git a/src/infrastructure/itpay/client.py b/src/infrastructure/itpay/client.py index 7b52f31..db3cecd 100644 --- a/src/infrastructure/itpay/client.py +++ b/src/infrastructure/itpay/client.py @@ -1,7 +1,7 @@ import orjson from dataclasses import replace from datetime import datetime, timezone -from decimal import Decimal +from decimal import Decimal,InvalidOperation from typing import Any import aiohttp from aiohttp import BasicAuth, ClientTimeout @@ -72,8 +72,10 @@ class ItPayClient(IItPayService): body_raw = response_json.get('data') body = body_raw if isinstance(body_raw, dict) else response_json - status = str(body['status']).strip().lower() - itpay_id = str(body['id']) + status = str(body.get('status') or '').strip().lower() + itpay_id = str(body.get('id') or '').strip() + if not status or not itpay_id: + raise PaymentProviderException(message='Payment provider response invalid') if status == 'cancelled': return replace(order, status=OrderStatus.CANCELLED, itpay_id=itpay_id) @@ -82,19 +84,23 @@ class ItPayClient(IItPayService): if status == 'error': return replace(order, status=OrderStatus.ERROR, itpay_id=itpay_id) - qrc_id = str(body['qrc_id']) - itpay_amount = Decimal(str(body['amount'])) + qrc_id = str(body.get('qrc_id') or '').strip() + if not qrc_id: + raise PaymentProviderException(message='Payment provider response invalid') + itpay_amount = Decimal(str(body.get('amount'))) - created_norm = str(body['created']).replace('Z', '+00:00') + created_norm = str(body.get('created') or '').replace('Z', '+00:00') itpay_created_at = datetime.fromisoformat(created_norm) - payment_qr_urls = body['payment_qr_urls'] + payment_qr_urls = body.get('payment_qr_urls') if isinstance(payment_qr_urls, str): payment_qr_urls = orjson.loads(payment_qr_urls) - payment_qr_images = body['payment_qr_images'] + payment_qr_images = body.get('payment_qr_images') if isinstance(payment_qr_images, str): payment_qr_images = orjson.loads(payment_qr_images) + if not isinstance(payment_qr_urls,dict) or not isinstance(payment_qr_images,dict): + raise PaymentProviderException(message='Payment provider response invalid') return replace( order, @@ -111,5 +117,7 @@ class ItPayClient(IItPayService): ) except ApplicationException: raise - except aiohttp.ClientError: + except (TimeoutError,aiohttp.ClientError): raise PaymentProviderException(message='Payment provider unreachable') + except (KeyError,ValueError,TypeError,InvalidOperation,orjson.JSONDecodeError) as exception: + raise PaymentProviderException(message='Payment provider response invalid') from exception diff --git a/src/infrastructure/mozen/sbp_client.py b/src/infrastructure/mozen/sbp_client.py index 736ba60..439d36e 100644 --- a/src/infrastructure/mozen/sbp_client.py +++ b/src/infrastructure/mozen/sbp_client.py @@ -61,7 +61,7 @@ class MozenSbpClient(IMozenSbpService): return banks except PaymentProviderException: raise - except aiohttp.ClientError: + except (TimeoutError,aiohttp.ClientError): raise PaymentProviderException(message='Mozen SBP banks provider unreachable') @@ -111,5 +111,5 @@ class MozenSbpClient(IMozenSbpService): ) except PaymentProviderException: raise - except aiohttp.ClientError: + except (TimeoutError,aiohttp.ClientError): raise PaymentProviderException(message='Mozen SBP withdrawal provider unreachable') diff --git a/src/infrastructure/security/hash.py b/src/infrastructure/security/hash.py index 94d92b8..a321532 100644 --- a/src/infrastructure/security/hash.py +++ b/src/infrastructure/security/hash.py @@ -9,9 +9,7 @@ class HashService(IHashService): async def hash(self, value: str) -> str: hashed_value = bcrypt.hashpw(value.encode(), bcrypt.gensalt()) - self._logger.info(f'Hash value {hashed_value.decode()}') return hashed_value.decode() async def verify(self, hashed_value: str, plain_value: str) -> bool: - self._logger.info(f'Hash value {hashed_value[:10]}') return bcrypt.checkpw(plain_value.encode(), hashed_value.encode()) \ No newline at end of file diff --git a/src/main.py b/src/main.py index 1f570ec..6807b6d 100644 --- a/src/main.py +++ b/src/main.py @@ -66,9 +66,14 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: app.state.jwt_key_store = jwt_store app.state.jwt_keys_scheduler = jwt_scheduler - yield - await app.state.redis_remote.aclose() - logger.info(f'Users service instance ended with id {instance_id}') + try: + yield + finally: + sched = getattr(app.state,'jwt_keys_scheduler',None) + if sched: + sched.shutdown(wait=False) + await app.state.redis_remote.aclose() + logger.info(f'Pay service instance ended with id {instance_id}') app: FastAPI = FastAPI( @@ -94,7 +99,7 @@ app.include_router(sbp_withdrawal_messaging_router) app.add_middleware( CORSMiddleware, allow_origins=[], - allow_origin_regex='.*', + allow_origin_regex=settings.CORS_ALLOW_ORIGIN_REGEX, allow_credentials=True, allow_methods=['*'], allow_headers=['*'],