fix: harden payment provider handling

This commit is contained in:
2026-06-28 14:12:24 +03:00
parent 0b7bd166c8
commit 612963a606
7 changed files with 31 additions and 19 deletions

View File

@@ -240,7 +240,6 @@ class HandleSbpWithdrawalWalletEventCommand:
withdraw_id=withdraw_id, withdraw_id=withdraw_id,
phone=str(withdrawal.phone or ''), phone=str(withdrawal.phone or ''),
bank_id=str(withdrawal.bank_id or ''), bank_id=str(withdrawal.bank_id or ''),
# amount=Decimal(str(withdrawal.rub_amount or '0')),
amount=Decimal('1'), amount=Decimal('1'),
trace_id=str(withdrawal.trace_id or self._logger.get_trace_id()), trace_id=str(withdrawal.trace_id or self._logger.get_trace_id()),
) )

View File

@@ -147,5 +147,5 @@ class ClaudeKassirClient(IReceipt):
return body return body
except ApplicationException: except ApplicationException:
raise raise
except aiohttp.ClientError: except (TimeoutError,aiohttp.ClientError):
raise ReceiptProviderException(message='Receipt provider unreachable') raise ReceiptProviderException(message='Receipt provider unreachable')

View File

@@ -60,6 +60,8 @@ class Settings(BaseSettings):
CSRF_COOKIE_PATH: str = "/" CSRF_COOKIE_PATH: str = "/"
CSRF_COOKIE_DOMAIN: str | None = None CSRF_COOKIE_DOMAIN: str | None = None
CORS_ALLOW_ORIGIN_REGEX: str = r'https?://([a-z0-9-]+\.)*elcsa\.ru(:\d+)?$'
DOCS_USERNAME: str = "admin" DOCS_USERNAME: str = "admin"
DOCS_PASSWORD: str = "admin" DOCS_PASSWORD: str = "admin"

View File

@@ -1,7 +1,7 @@
import orjson import orjson
from dataclasses import replace from dataclasses import replace
from datetime import datetime, timezone from datetime import datetime, timezone
from decimal import Decimal from decimal import Decimal,InvalidOperation
from typing import Any from typing import Any
import aiohttp import aiohttp
from aiohttp import BasicAuth, ClientTimeout from aiohttp import BasicAuth, ClientTimeout
@@ -72,8 +72,10 @@ class ItPayClient(IItPayService):
body_raw = response_json.get('data') body_raw = response_json.get('data')
body = body_raw if isinstance(body_raw, dict) else response_json body = body_raw if isinstance(body_raw, dict) else response_json
status = str(body['status']).strip().lower() status = str(body.get('status') or '').strip().lower()
itpay_id = str(body['id']) 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': if status == 'cancelled':
return replace(order, status=OrderStatus.CANCELLED, itpay_id=itpay_id) return replace(order, status=OrderStatus.CANCELLED, itpay_id=itpay_id)
@@ -82,19 +84,23 @@ class ItPayClient(IItPayService):
if status == 'error': if status == 'error':
return replace(order, status=OrderStatus.ERROR, itpay_id=itpay_id) return replace(order, status=OrderStatus.ERROR, itpay_id=itpay_id)
qrc_id = str(body['qrc_id']) qrc_id = str(body.get('qrc_id') or '').strip()
itpay_amount = Decimal(str(body['amount'])) 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) 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): if isinstance(payment_qr_urls, str):
payment_qr_urls = orjson.loads(payment_qr_urls) 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): if isinstance(payment_qr_images, str):
payment_qr_images = orjson.loads(payment_qr_images) 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( return replace(
order, order,
@@ -111,5 +117,7 @@ class ItPayClient(IItPayService):
) )
except ApplicationException: except ApplicationException:
raise raise
except aiohttp.ClientError: except (TimeoutError,aiohttp.ClientError):
raise PaymentProviderException(message='Payment provider unreachable') raise PaymentProviderException(message='Payment provider unreachable')
except (KeyError,ValueError,TypeError,InvalidOperation,orjson.JSONDecodeError) as exception:
raise PaymentProviderException(message='Payment provider response invalid') from exception

View File

@@ -61,7 +61,7 @@ class MozenSbpClient(IMozenSbpService):
return banks return banks
except PaymentProviderException: except PaymentProviderException:
raise raise
except aiohttp.ClientError: except (TimeoutError,aiohttp.ClientError):
raise PaymentProviderException(message='Mozen SBP banks provider unreachable') raise PaymentProviderException(message='Mozen SBP banks provider unreachable')
@@ -111,5 +111,5 @@ class MozenSbpClient(IMozenSbpService):
) )
except PaymentProviderException: except PaymentProviderException:
raise raise
except aiohttp.ClientError: except (TimeoutError,aiohttp.ClientError):
raise PaymentProviderException(message='Mozen SBP withdrawal provider unreachable') raise PaymentProviderException(message='Mozen SBP withdrawal provider unreachable')

View File

@@ -9,9 +9,7 @@ class HashService(IHashService):
async def hash(self, value: str) -> str: async def hash(self, value: str) -> str:
hashed_value = bcrypt.hashpw(value.encode(), bcrypt.gensalt()) hashed_value = bcrypt.hashpw(value.encode(), bcrypt.gensalt())
self._logger.info(f'Hash value {hashed_value.decode()}')
return hashed_value.decode() return hashed_value.decode()
async def verify(self, hashed_value: str, plain_value: str) -> bool: 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()) return bcrypt.checkpw(plain_value.encode(), hashed_value.encode())

View File

@@ -66,9 +66,14 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
app.state.jwt_key_store = jwt_store app.state.jwt_key_store = jwt_store
app.state.jwt_keys_scheduler = jwt_scheduler app.state.jwt_keys_scheduler = jwt_scheduler
try:
yield yield
finally:
sched = getattr(app.state,'jwt_keys_scheduler',None)
if sched:
sched.shutdown(wait=False)
await app.state.redis_remote.aclose() await app.state.redis_remote.aclose()
logger.info(f'Users service instance ended with id {instance_id}') logger.info(f'Pay service instance ended with id {instance_id}')
app: FastAPI = FastAPI( app: FastAPI = FastAPI(
@@ -94,7 +99,7 @@ app.include_router(sbp_withdrawal_messaging_router)
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=[], allow_origins=[],
allow_origin_regex='.*', allow_origin_regex=settings.CORS_ALLOW_ORIGIN_REGEX,
allow_credentials=True, allow_credentials=True,
allow_methods=['*'], allow_methods=['*'],
allow_headers=['*'], allow_headers=['*'],