feat: add endpoints desc
This commit is contained in:
@@ -3,7 +3,7 @@ from decimal import Decimal
|
||||
from src.application.abstractions import IUnitOfWork
|
||||
from src.application.contracts import ILogger,IReceipt
|
||||
from src.application.domain.enums import PaymentStatus
|
||||
from src.application.domain.exceptions import ApplicationException
|
||||
from src.application.domain.exceptions import ApplicationException,NotFoundException,PaymentMetadataException,ReceiptDataException
|
||||
from src.infrastructure.database.decorators import transactional
|
||||
|
||||
|
||||
@@ -17,19 +17,19 @@ class CreateCryptoTransferCompletedCommand:
|
||||
@transactional
|
||||
async def __call__(self, *, order_id: str, user_id: str, web3_transaction_hash: str | None = None) -> None:
|
||||
if not order_id:
|
||||
raise ApplicationException(status_code=400, message='Crypto transfer completed message missing order_id')
|
||||
raise PaymentMetadataException(message='Crypto transfer completed message missing order_id')
|
||||
if not user_id:
|
||||
raise ApplicationException(status_code=400, message='Crypto transfer completed message missing user_id')
|
||||
raise PaymentMetadataException(message='Crypto transfer completed message missing user_id')
|
||||
await self._unit_of_work.payment_repository.update_crypto_transfer_completed(
|
||||
order_id=order_id,
|
||||
web3_transaction_hash=web3_transaction_hash,
|
||||
)
|
||||
user = await self._unit_of_work.user_repository.get(user_id)
|
||||
if user is None:
|
||||
raise ApplicationException(status_code=404, message='User not found')
|
||||
raise NotFoundException(message='User not found')
|
||||
email = str(user.email or '').strip()
|
||||
if not email:
|
||||
raise ApplicationException(status_code=400, message='User email missing')
|
||||
raise ReceiptDataException(message='User email missing')
|
||||
customer_info = ' '.join(
|
||||
part
|
||||
for part in (
|
||||
@@ -40,28 +40,28 @@ class CreateCryptoTransferCompletedCommand:
|
||||
if part
|
||||
)
|
||||
if not customer_info:
|
||||
raise ApplicationException(status_code=400, message='User full name missing')
|
||||
raise ReceiptDataException(message='User full name missing')
|
||||
customer_inn = str(user.inn or '').strip()
|
||||
if not customer_inn:
|
||||
raise ApplicationException(status_code=400, message='User inn missing')
|
||||
raise ReceiptDataException(message='User inn missing')
|
||||
if user.birth_date is None:
|
||||
raise ApplicationException(status_code=400, message='User birth date missing')
|
||||
raise ReceiptDataException(message='User birth date missing')
|
||||
customer_birthday = f'{user.birth_date.isoformat()}T12:00:00.000Z'
|
||||
|
||||
order = await self._unit_of_work.order_repository.get_by_id(order_id)
|
||||
if order is None:
|
||||
raise ApplicationException(status_code=404, message='Order not found')
|
||||
raise NotFoundException(message='Order not found')
|
||||
if order.total_price is None:
|
||||
raise ApplicationException(status_code=400, message='Order total price missing for receipt')
|
||||
raise ReceiptDataException(message='Order total price missing for receipt')
|
||||
if order.service_fee is None:
|
||||
raise ApplicationException(status_code=400, message='Order service fee missing for receipt')
|
||||
raise ReceiptDataException(message='Order service fee missing for receipt')
|
||||
|
||||
total_amount = Decimal(str(order.total_price)).quantize(Decimal('0.01'))
|
||||
service_fee = Decimal(str(order.service_fee)).quantize(Decimal('0.01'))
|
||||
principal_amount = (total_amount - service_fee).quantize(Decimal('0.01'))
|
||||
|
||||
if principal_amount < 0:
|
||||
raise ApplicationException(status_code=400, message='Invalid receipt amounts: principal negative')
|
||||
raise ReceiptDataException(message='Invalid receipt amounts: principal negative')
|
||||
|
||||
try:
|
||||
receipt_response = await self._receipt.create_receipt(
|
||||
|
||||
@@ -7,7 +7,7 @@ from src.application.contracts import ILogger
|
||||
from src.application.contracts import IItPayService
|
||||
from src.application.domain.entities.order import OrderEntity
|
||||
from src.application.domain.enums import OrderStatus
|
||||
from src.application.domain.exceptions import ApplicationException
|
||||
from src.application.domain.exceptions import PriceChangedException
|
||||
from src.application.services import PaymentQuoteService
|
||||
from src.infrastructure.database.decorators import transactional
|
||||
from src.presentation.schemas.order import CreateOrder
|
||||
@@ -40,7 +40,7 @@ class CreateOrderCommand:
|
||||
actual_total_price = quote.total_price
|
||||
if actual_total_price > payment_data.total_price * Decimal('1.01'):
|
||||
self._logger.error('Price has changed, please refresh and try again')
|
||||
raise ApplicationException(status_code=409, message='Price has changed, please refresh and try again')
|
||||
raise PriceChangedException()
|
||||
|
||||
order = OrderEntity(
|
||||
user_id=user_id,
|
||||
|
||||
@@ -3,7 +3,7 @@ from ulid import ULID
|
||||
from src.application.abstractions import IUnitOfWork
|
||||
from src.application.contracts import ILogger,IQueueMessanger
|
||||
from src.application.domain.enums import PaymentStatus
|
||||
from src.application.domain.exceptions import ApplicationException
|
||||
from src.application.domain.exceptions import PaymentMetadataException
|
||||
from src.infrastructure.config import settings
|
||||
from src.infrastructure.database.decorators import transactional
|
||||
from src.presentation.schemas.itpay_payment_models import ItpayPaymentData
|
||||
@@ -25,9 +25,9 @@ class CreatePaymentCommand:
|
||||
trace_id = str(metadata.get('trace_id') or self._logger.get_trace_id())
|
||||
self._logger.set_trace_id(trace_id)
|
||||
if not order_id:
|
||||
raise ApplicationException(status_code=400, message='Itpay webhook metadata missing order_id')
|
||||
raise PaymentMetadataException(message='Itpay webhook metadata missing order_id')
|
||||
if not user_id:
|
||||
raise ApplicationException(status_code=400, message='Itpay webhook metadata missing user_id')
|
||||
raise PaymentMetadataException(message='Itpay webhook metadata missing user_id')
|
||||
payment_created = await self._unit_of_work.payment_repository.create_completed(
|
||||
user_id=user_id,
|
||||
order_id=order_id,
|
||||
|
||||
@@ -2,9 +2,17 @@ from src.application.domain.exceptions.application_exception import ApplicationE
|
||||
from src.application.domain.exceptions.bad_gateway_exception import BadGatewayException
|
||||
from src.application.domain.exceptions.bad_request_exception import BadRequestException
|
||||
from src.application.domain.exceptions.conflict_exception import ConflictException
|
||||
from src.application.domain.exceptions.csrf_exception import CsrfException
|
||||
from src.application.domain.exceptions.forbidden_exception import ForbiddenException
|
||||
from src.application.domain.exceptions.internal_server_exception import InternalServerException
|
||||
from src.application.domain.exceptions.jwt_exception import JwtException
|
||||
from src.application.domain.exceptions.not_found_exception import NotFoundException
|
||||
from src.application.domain.exceptions.payment_metadata_exception import PaymentMetadataException
|
||||
from src.application.domain.exceptions.payment_provider_exception import PaymentProviderException
|
||||
from src.application.domain.exceptions.price_changed_exception import PriceChangedException
|
||||
from src.application.domain.exceptions.receipt_data_exception import ReceiptDataException
|
||||
from src.application.domain.exceptions.receipt_provider_exception import ReceiptProviderException
|
||||
from src.application.domain.exceptions.rate_limit_exception import RateLimitException
|
||||
from src.application.domain.exceptions.service_unavailable_exception import ServiceUnavailableException
|
||||
from src.application.domain.exceptions.unauthorized_exception import UnauthorizedException
|
||||
|
||||
@@ -13,9 +21,17 @@ __all__ = [
|
||||
'BadGatewayException',
|
||||
'BadRequestException',
|
||||
'ConflictException',
|
||||
'CsrfException',
|
||||
'ForbiddenException',
|
||||
'InternalServerException',
|
||||
'JwtException',
|
||||
'NotFoundException',
|
||||
'PaymentMetadataException',
|
||||
'PaymentProviderException',
|
||||
'PriceChangedException',
|
||||
'ReceiptDataException',
|
||||
'ReceiptProviderException',
|
||||
'RateLimitException',
|
||||
'ServiceUnavailableException',
|
||||
'UnauthorizedException',
|
||||
]
|
||||
12
src/application/domain/exceptions/csrf_exception.py
Normal file
12
src/application/domain/exceptions/csrf_exception.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from __future__ import annotations
|
||||
from typing import Mapping
|
||||
from src.application.domain.exceptions.application_exception import ApplicationException
|
||||
|
||||
|
||||
class CsrfException(ApplicationException):
|
||||
def __init__(
|
||||
self,
|
||||
message: str = 'CSRF token invalid',
|
||||
headers: Mapping[str,str] | None = None,
|
||||
):
|
||||
super().__init__(status_code=403,message=message,headers=headers)
|
||||
13
src/application/domain/exceptions/jwt_exception.py
Normal file
13
src/application/domain/exceptions/jwt_exception.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from __future__ import annotations
|
||||
from typing import Mapping
|
||||
from src.application.domain.exceptions.application_exception import ApplicationException
|
||||
|
||||
|
||||
class JwtException(ApplicationException):
|
||||
def __init__(
|
||||
self,
|
||||
message: str = 'Invalid token',
|
||||
status_code: int = 401,
|
||||
headers: Mapping[str,str] | None = None,
|
||||
):
|
||||
super().__init__(status_code=status_code,message=message,headers=headers)
|
||||
@@ -0,0 +1,12 @@
|
||||
from __future__ import annotations
|
||||
from typing import Mapping
|
||||
from src.application.domain.exceptions.application_exception import ApplicationException
|
||||
|
||||
|
||||
class PaymentMetadataException(ApplicationException):
|
||||
def __init__(
|
||||
self,
|
||||
message: str = 'Payment metadata invalid',
|
||||
headers: Mapping[str,str] | None = None,
|
||||
):
|
||||
super().__init__(status_code=400,message=message,headers=headers)
|
||||
@@ -0,0 +1,12 @@
|
||||
from __future__ import annotations
|
||||
from typing import Mapping
|
||||
from src.application.domain.exceptions.application_exception import ApplicationException
|
||||
|
||||
|
||||
class PaymentProviderException(ApplicationException):
|
||||
def __init__(
|
||||
self,
|
||||
message: str = 'Payment provider error',
|
||||
headers: Mapping[str,str] | None = None,
|
||||
):
|
||||
super().__init__(status_code=502,message=message,headers=headers)
|
||||
12
src/application/domain/exceptions/price_changed_exception.py
Normal file
12
src/application/domain/exceptions/price_changed_exception.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from __future__ import annotations
|
||||
from typing import Mapping
|
||||
from src.application.domain.exceptions.application_exception import ApplicationException
|
||||
|
||||
|
||||
class PriceChangedException(ApplicationException):
|
||||
def __init__(
|
||||
self,
|
||||
message: str = 'Price has changed, please refresh and try again',
|
||||
headers: Mapping[str,str] | None = None,
|
||||
):
|
||||
super().__init__(status_code=409,message=message,headers=headers)
|
||||
13
src/application/domain/exceptions/rate_limit_exception.py
Normal file
13
src/application/domain/exceptions/rate_limit_exception.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from __future__ import annotations
|
||||
from typing import Mapping
|
||||
from src.application.domain.exceptions.application_exception import ApplicationException
|
||||
|
||||
|
||||
class RateLimitException(ApplicationException):
|
||||
def __init__(
|
||||
self,
|
||||
message: str = 'Too Many Requests',
|
||||
status_code: int = 429,
|
||||
headers: Mapping[str,str] | None = None,
|
||||
):
|
||||
super().__init__(status_code=status_code,message=message,headers=headers)
|
||||
12
src/application/domain/exceptions/receipt_data_exception.py
Normal file
12
src/application/domain/exceptions/receipt_data_exception.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from __future__ import annotations
|
||||
from typing import Mapping
|
||||
from src.application.domain.exceptions.application_exception import ApplicationException
|
||||
|
||||
|
||||
class ReceiptDataException(ApplicationException):
|
||||
def __init__(
|
||||
self,
|
||||
message: str = 'Receipt data invalid',
|
||||
headers: Mapping[str,str] | None = None,
|
||||
):
|
||||
super().__init__(status_code=400,message=message,headers=headers)
|
||||
@@ -0,0 +1,13 @@
|
||||
from __future__ import annotations
|
||||
from typing import Mapping
|
||||
from src.application.domain.exceptions.application_exception import ApplicationException
|
||||
|
||||
|
||||
class ReceiptProviderException(ApplicationException):
|
||||
def __init__(
|
||||
self,
|
||||
message: str = 'Receipt provider error',
|
||||
status_code: int = 502,
|
||||
headers: Mapping[str,str] | None = None,
|
||||
):
|
||||
super().__init__(status_code=status_code,message=message,headers=headers)
|
||||
@@ -7,7 +7,7 @@ import aiohttp
|
||||
import orjson
|
||||
from aiohttp import BasicAuth, ClientTimeout
|
||||
from src.application.contracts import IReceipt
|
||||
from src.application.domain.exceptions import ApplicationException
|
||||
from src.application.domain.exceptions import ApplicationException,ReceiptProviderException
|
||||
from src.infrastructure.cloud_kassir.constants import CLOUD_KASSIR_API_BASE_URL
|
||||
|
||||
|
||||
@@ -122,30 +122,30 @@ class ClaudeKassirClient(IReceipt):
|
||||
async with session.post(url, json=payload, headers=headers, auth=auth) as resp:
|
||||
raw = (await resp.text()).strip()
|
||||
if not raw:
|
||||
raise ApplicationException(
|
||||
status_code=502,
|
||||
raise ReceiptProviderException(
|
||||
message=f'Receipt provider empty response (HTTP {resp.status})',
|
||||
)
|
||||
try:
|
||||
parsed: Any = orjson.loads(raw)
|
||||
except orjson.JSONDecodeError:
|
||||
preview = raw[:240].replace('\n', ' ')
|
||||
raise ApplicationException(
|
||||
status_code=502,
|
||||
raise ReceiptProviderException(
|
||||
message=f'Receipt provider non-JSON response (HTTP {resp.status}): {preview}',
|
||||
)
|
||||
if not isinstance(parsed, dict):
|
||||
raise ApplicationException(status_code=502, message='Receipt provider invalid response')
|
||||
raise ReceiptProviderException(message='Receipt provider invalid response')
|
||||
body = parsed
|
||||
if resp.status >= 400:
|
||||
raise ApplicationException(
|
||||
status_code=502,
|
||||
raise ReceiptProviderException(
|
||||
message=str(body.get('Message') or 'Receipt provider error'),
|
||||
)
|
||||
if body.get('Success') is False:
|
||||
raise ApplicationException(status_code=409, message=str(body.get('Message') or 'Receipt provider rejected receipt'))
|
||||
raise ReceiptProviderException(
|
||||
status_code=409,
|
||||
message=str(body.get('Message') or 'Receipt provider rejected receipt'),
|
||||
)
|
||||
return body
|
||||
except ApplicationException:
|
||||
raise
|
||||
except aiohttp.ClientError:
|
||||
raise ApplicationException(status_code=502, message='Receipt provider unreachable')
|
||||
raise ReceiptProviderException(message='Receipt provider unreachable')
|
||||
|
||||
@@ -8,7 +8,7 @@ from aiohttp import BasicAuth, ClientTimeout
|
||||
from src.application.contracts.i_itpay_service import IItPayService
|
||||
from src.application.domain.entities.order import OrderEntity
|
||||
from src.application.domain.enums import OrderStatus
|
||||
from src.application.domain.exceptions import ApplicationException
|
||||
from src.application.domain.exceptions import ApplicationException,PaymentProviderException
|
||||
|
||||
|
||||
class ItPayClient(IItPayService):
|
||||
@@ -68,7 +68,7 @@ class ItPayClient(IItPayService):
|
||||
except orjson.JSONDecodeError:
|
||||
response_json = {'raw': response_text}
|
||||
if resp.status >= 400:
|
||||
raise ApplicationException(status_code=502, message='Payment provider error')
|
||||
raise PaymentProviderException(message='Payment provider error')
|
||||
body_raw = response_json.get('data')
|
||||
body = body_raw if isinstance(body_raw, dict) else response_json
|
||||
|
||||
@@ -112,4 +112,4 @@ class ItPayClient(IItPayService):
|
||||
except ApplicationException:
|
||||
raise
|
||||
except aiohttp.ClientError:
|
||||
raise ApplicationException(status_code=502, message='Payment provider unreachable')
|
||||
raise PaymentProviderException(message='Payment provider unreachable')
|
||||
|
||||
@@ -3,7 +3,7 @@ import secrets
|
||||
from typing import Any, Optional, Mapping
|
||||
from itsdangerous import URLSafeTimedSerializer, SignatureExpired, BadSignature
|
||||
from src.application.contracts import ICsrfService
|
||||
from src.application.domain.exceptions import ApplicationException
|
||||
from src.application.domain.exceptions import CsrfException
|
||||
from src.infrastructure.config.settings import settings
|
||||
|
||||
|
||||
@@ -42,21 +42,12 @@ class CsrfService(ICsrfService):
|
||||
try:
|
||||
data = self._serializer.loads(token, max_age=self.TTL_SECONDS)
|
||||
except SignatureExpired:
|
||||
raise ApplicationException(
|
||||
status_code=403,
|
||||
message='CSRF token expired',
|
||||
)
|
||||
raise CsrfException(message='CSRF token expired')
|
||||
except BadSignature:
|
||||
raise ApplicationException(
|
||||
status_code=403,
|
||||
message='CSRF token invalid',
|
||||
)
|
||||
raise CsrfException(message='CSRF token invalid')
|
||||
|
||||
if expected_subject is not None and data.get('sub') != expected_subject:
|
||||
raise ApplicationException(
|
||||
status_code=403,
|
||||
message='CSRF token subject mismatch',
|
||||
)
|
||||
raise CsrfException(message='CSRF token subject mismatch')
|
||||
|
||||
return data
|
||||
|
||||
@@ -67,15 +58,9 @@ class CsrfService(ICsrfService):
|
||||
|
||||
def verify_pair(self, cookie_token: Optional[str], header_token: Optional[str], expected_subject: Optional[str] = None) -> None:
|
||||
if not cookie_token or not header_token:
|
||||
raise ApplicationException(
|
||||
status_code=403,
|
||||
message='CSRF token missing',
|
||||
)
|
||||
raise CsrfException(message='CSRF token missing')
|
||||
|
||||
if not secrets.compare_digest(cookie_token, header_token):
|
||||
raise ApplicationException(
|
||||
status_code=403,
|
||||
message='CSRF token mismatch',
|
||||
)
|
||||
raise CsrfException(message='CSRF token mismatch')
|
||||
|
||||
self.verify(cookie_token, expected_subject=expected_subject)
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
from jose import jwt, ExpiredSignatureError, JWTError
|
||||
from src.application.contracts import ILogger, IJwtService
|
||||
from src.application.domain.dto import AccessTokenPayload
|
||||
from src.application.domain.exceptions import ApplicationException
|
||||
from src.application.domain.exceptions import ApplicationException,InternalServerException,JwtException
|
||||
from src.infrastructure.config.settings import settings
|
||||
from src.infrastructure.vault import JwtKeyStore
|
||||
|
||||
@@ -17,7 +17,7 @@ class JwtService(IJwtService):
|
||||
|
||||
if payload.get('type') != 'access':
|
||||
self._logger.warning(f'Access token invalid type received_type={payload.get('type')}')
|
||||
raise ApplicationException(status_code=401, message='Invalid token type')
|
||||
raise JwtException(message='Invalid token type')
|
||||
|
||||
try:
|
||||
return AccessTokenPayload(
|
||||
@@ -32,7 +32,7 @@ class JwtService(IJwtService):
|
||||
)
|
||||
except KeyError as exception:
|
||||
self._logger.warning(f'Access token missing claim error={str(exception)}')
|
||||
raise ApplicationException(status_code=401, message=f'Missing token claim: {exception}')
|
||||
raise JwtException(message=f'Missing token claim: {exception}')
|
||||
|
||||
async def _decode_and_verify(self, token: str) -> dict:
|
||||
kid: str | None = None
|
||||
@@ -42,12 +42,12 @@ class JwtService(IJwtService):
|
||||
kid = header.get('kid')
|
||||
if not kid:
|
||||
self._logger.warning(f'JWT header missing kid header={header}')
|
||||
raise ApplicationException(status_code=401, message='Missing token header: kid')
|
||||
raise JwtException(message='Missing token header: kid')
|
||||
|
||||
received_alg = header.get('alg')
|
||||
if received_alg != settings.JWT_ALGORITHM:
|
||||
self._logger.warning(f'JWT invalid algorithm kid={kid} received_alg={received_alg} expected_alg={settings.JWT_ALGORITHM}')
|
||||
raise ApplicationException(status_code=401, message='Invalid token algorithm')
|
||||
raise JwtException(message='Invalid token algorithm')
|
||||
|
||||
public_pem = await self._key_store.get_public_key_for_kid(str(kid))
|
||||
|
||||
@@ -58,7 +58,7 @@ class JwtService(IJwtService):
|
||||
|
||||
if not public_pem:
|
||||
self._logger.warning(f'JWT unknown kid kid={kid}')
|
||||
raise ApplicationException(status_code=401, message='Unknown token kid')
|
||||
raise JwtException(message='Unknown token kid')
|
||||
|
||||
options = {
|
||||
'verify_signature': True,
|
||||
@@ -85,25 +85,25 @@ class JwtService(IJwtService):
|
||||
|
||||
if 'sid' not in payload:
|
||||
self._logger.warning(f'JWT missing sid claim kid={kid}')
|
||||
raise ApplicationException(status_code=401, message='Missing token claim: sid')
|
||||
raise JwtException(message='Missing token claim: sid')
|
||||
|
||||
if 'type' not in payload:
|
||||
self._logger.warning(f'JWT missing type claim kid={kid}')
|
||||
raise ApplicationException(status_code=401, message='Missing token claim: type')
|
||||
raise JwtException(message='Missing token claim: type')
|
||||
|
||||
return payload
|
||||
|
||||
except ExpiredSignatureError as exception:
|
||||
self._logger.info(f'JWT expired kid={kid} error={str(exception)}')
|
||||
raise ApplicationException(status_code=401, message='Token expired')
|
||||
raise JwtException(message='Token expired')
|
||||
|
||||
except ApplicationException:
|
||||
raise
|
||||
|
||||
except JWTError as exception:
|
||||
self._logger.warning(f'JWT decode failed kid={kid} error={str(exception)}')
|
||||
raise ApplicationException(status_code=401, message='Invalid token')
|
||||
raise JwtException(message='Invalid token')
|
||||
|
||||
except Exception as exception:
|
||||
self._logger.error(f'Unexpected JWT decode error kid={kid} error={str(exception)}')
|
||||
raise ApplicationException(status_code=500, message='JWT decode failed')
|
||||
raise InternalServerException(message='JWT decode failed')
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
from datetime import datetime, timezone
|
||||
from src.application.domain.dto import JwtPublicKeySet, JwtPublicKey
|
||||
from src.application.domain.exceptions import ApplicationException
|
||||
from src.application.domain.exceptions import InternalServerException
|
||||
from src.infrastructure.vault import create_hvac_client_from_approle,read_kv2_secret
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ class JwtKeyStore:
|
||||
@classmethod
|
||||
def get_instance(cls) -> 'JwtKeyStore':
|
||||
if cls._instance is None:
|
||||
raise ApplicationException(status_code=500, message='JwtKeyStore not initialized')
|
||||
raise InternalServerException(message='JwtKeyStore not initialized')
|
||||
return cls._instance
|
||||
|
||||
def _read_keyset_sync(self) -> JwtPublicKeySet:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from fastapi import Depends, Request
|
||||
from fastapi.security.utils import get_authorization_scheme_param
|
||||
from src.application.contracts import IJwtService
|
||||
from src.application.domain.exceptions import ApplicationException
|
||||
from src.application.domain.exceptions import UnauthorizedException
|
||||
from src.application.domain.dto import AccessTokenPayload, AuthContext
|
||||
from src.presentation.dependencies import get_jwt_service
|
||||
|
||||
@@ -27,10 +27,10 @@ async def require_access_token(
|
||||
) -> AuthContext:
|
||||
token = _extract_access_token(request)
|
||||
if not token:
|
||||
raise ApplicationException(status_code=401, message='Not authenticated')
|
||||
raise UnauthorizedException(message='Not authenticated')
|
||||
|
||||
payload: AccessTokenPayload = await jwt_service.decode_access_token(token)
|
||||
if payload.type != 'access':
|
||||
raise ApplicationException(status_code=401, message='Invalid token type')
|
||||
raise UnauthorizedException(message='Invalid token type')
|
||||
|
||||
return AuthContext(user_id=payload.sub, sid=payload.sid, token=payload)
|
||||
|
||||
@@ -3,7 +3,7 @@ import inspect
|
||||
from functools import wraps
|
||||
from typing import Callable, Awaitable, Any, Optional, Annotated
|
||||
from fastapi import Request, Header
|
||||
from src.application.domain.exceptions import ApplicationException
|
||||
from src.application.domain.exceptions import InternalServerException
|
||||
from src.infrastructure.security import CsrfService
|
||||
|
||||
|
||||
@@ -39,10 +39,7 @@ def csrf_protect(
|
||||
break
|
||||
|
||||
if request is None:
|
||||
raise ApplicationException(
|
||||
status_code=500,
|
||||
message='Request is required for CSRF protection',
|
||||
)
|
||||
raise InternalServerException(message='Request is required for CSRF protection')
|
||||
|
||||
csrf = CsrfService()
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ from typing import Any, Awaitable, Callable, Literal, Optional, Protocol, runtim
|
||||
from fastapi import Request
|
||||
from redis.asyncio.client import Redis
|
||||
from src.application.contracts import ILogger
|
||||
from src.application.domain.exceptions import ApplicationException
|
||||
from src.application.domain.exceptions import InternalServerException,RateLimitException
|
||||
from src.infrastructure.logger import get_logger
|
||||
from src.presentation.dependencies import get_redis
|
||||
|
||||
@@ -124,7 +124,7 @@ def rate_limit(
|
||||
ident = _call_key_builder(key_builder, request, args, kwargs) # type: ignore[arg-type]
|
||||
except Exception as e:
|
||||
logger.error(f'RateLimit key_builder failed error={str(e)}')
|
||||
raise ApplicationException(500, 'Rate limiter key_builder failed')
|
||||
raise InternalServerException(message='Rate limiter key_builder failed')
|
||||
|
||||
route = request.url.path
|
||||
method = request.method
|
||||
@@ -153,13 +153,12 @@ def rate_limit(
|
||||
logger.warning(f'RateLimit fail-open activated key={redis_key}')
|
||||
return await func(*args, **kwargs)
|
||||
|
||||
raise ApplicationException(503, 'Rate limiter unavailable')
|
||||
raise RateLimitException(status_code=503,message='Rate limiter unavailable')
|
||||
|
||||
if count > limit:
|
||||
retry_after = max(ttl, 0)
|
||||
logger.warning(f'RateLimit exceeded key={redis_key} count={count} limit={limit} retry_after={retry_after}')
|
||||
raise ApplicationException(
|
||||
status_code=429,
|
||||
raise RateLimitException(
|
||||
message='Too Many Requests',
|
||||
headers={'Retry-After': str(retry_after)},
|
||||
)
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
from fastapi import Request
|
||||
from fastapi.responses import ORJSONResponse
|
||||
from src.application.domain.exceptions import ApplicationException
|
||||
from src.infrastructure.logger import logger
|
||||
|
||||
|
||||
async def application_exception_handler(_request: Request, exc: ApplicationException) -> ORJSONResponse:
|
||||
async def application_exception_handler(request: Request, exc: ApplicationException) -> ORJSONResponse:
|
||||
logger.warning({
|
||||
'event':'application_exception',
|
||||
'path':request.url.path,
|
||||
'status_code':exc.status_code,
|
||||
'detail':exc.message,
|
||||
})
|
||||
detail = exc.message
|
||||
if 500 <= exc.status_code:
|
||||
detail = 'Internal Server Error'
|
||||
|
||||
@@ -26,6 +26,19 @@ orders_router = APIRouter(tags=['orders'])
|
||||
payment_router = APIRouter(prefix='/payment', tags=['payments'])
|
||||
payments_router = APIRouter(tags=['payments'])
|
||||
|
||||
ERROR_RESPONSES = {
|
||||
400: {'model': ErrorResponse, 'description': 'Bad Request'},
|
||||
401: {'model': ErrorResponse, 'description': 'Unauthorized'},
|
||||
403: {'model': ErrorResponse, 'description': 'Forbidden'},
|
||||
404: {'model': ErrorResponse, 'description': 'Not Found'},
|
||||
409: {'model': ErrorResponse, 'description': 'Conflict'},
|
||||
422: {'model': ErrorResponse, 'description': 'Validation Error'},
|
||||
429: {'model': ErrorResponse, 'description': 'Too Many Requests'},
|
||||
500: {'model': ErrorResponse, 'description': 'Internal Server Error'},
|
||||
502: {'model': ErrorResponse, 'description': 'Bad Gateway'},
|
||||
503: {'model': ErrorResponse, 'description': 'Service Unavailable'},
|
||||
}
|
||||
|
||||
|
||||
def _payment_config_response(quote: PaymentQuote) -> PaymentConfigResponse:
|
||||
return PaymentConfigResponse(
|
||||
@@ -156,16 +169,7 @@ async def _websocket_auth_context(websocket: WebSocket, jwt_service: IJwtService
|
||||
'/create',
|
||||
response_model=CreateOrderResponse,
|
||||
status_code=201,
|
||||
responses={
|
||||
400: {'model': ErrorResponse, 'description': 'Bad Request'},
|
||||
401: {'model': ErrorResponse, 'description': 'Unauthorized'},
|
||||
403: {'model': ErrorResponse, 'description': 'Forbidden'},
|
||||
404: {'model': ErrorResponse, 'description': 'Not Found'},
|
||||
409: {'model': ErrorResponse, 'description': 'Conflict'},
|
||||
500: {'model': ErrorResponse, 'description': 'Internal Server Error'},
|
||||
502: {'model': ErrorResponse, 'description': 'Bad Gateway'},
|
||||
503: {'model': ErrorResponse, 'description': 'Service Unavailable'},
|
||||
},
|
||||
responses=ERROR_RESPONSES,
|
||||
)
|
||||
@csrf_protect()
|
||||
async def create_order(
|
||||
@@ -200,7 +204,12 @@ async def create_order(
|
||||
return content
|
||||
|
||||
|
||||
@payment_router.get('/config', response_model=PaymentConfigResponse)
|
||||
@payment_router.get(
|
||||
'/config',
|
||||
response_model=PaymentConfigResponse,
|
||||
status_code=200,
|
||||
responses=ERROR_RESPONSES,
|
||||
)
|
||||
async def payment_config(
|
||||
command: GetPaymentConfigCommand = Depends(get_payment_config_command),
|
||||
) -> PaymentConfigResponse:
|
||||
@@ -208,7 +217,12 @@ async def payment_config(
|
||||
return _payment_config_response(quote)
|
||||
|
||||
|
||||
@payment_router.get('/quote', response_model=PaymentQuoteResponse)
|
||||
@payment_router.get(
|
||||
'/quote',
|
||||
response_model=PaymentQuoteResponse,
|
||||
status_code=200,
|
||||
responses=ERROR_RESPONSES,
|
||||
)
|
||||
async def payment_quote(
|
||||
usdt_amount: Decimal = Query(gt=0, decimal_places=2, max_digits=20),
|
||||
command: GetPaymentQuoteCommand = Depends(get_payment_quote_command),
|
||||
@@ -217,7 +231,12 @@ async def payment_quote(
|
||||
return _payment_quote_response(quote)
|
||||
|
||||
|
||||
@orders_router.get('/orders', response_model=OrdersResponse)
|
||||
@orders_router.get(
|
||||
'/orders',
|
||||
response_model=OrdersResponse,
|
||||
status_code=200,
|
||||
responses=ERROR_RESPONSES,
|
||||
)
|
||||
async def list_orders(
|
||||
limit: int = Query(default=20, ge=1, le=100),
|
||||
offset: int = Query(default=0, ge=0),
|
||||
@@ -229,7 +248,12 @@ async def list_orders(
|
||||
return OrdersResponse(status_code=200,orders=items,limit=limit,offset=offset)
|
||||
|
||||
|
||||
@payments_router.get('/payments', response_model=PaymentsResponse)
|
||||
@payments_router.get(
|
||||
'/payments',
|
||||
response_model=PaymentsResponse,
|
||||
status_code=200,
|
||||
responses=ERROR_RESPONSES,
|
||||
)
|
||||
async def list_payments(
|
||||
limit: int = Query(default=20, ge=1, le=100),
|
||||
offset: int = Query(default=0, ge=0),
|
||||
@@ -245,7 +269,12 @@ async def list_payments(
|
||||
)
|
||||
|
||||
|
||||
@order_router.get('/{order_id}/status', response_model=OrderStatusResponse)
|
||||
@order_router.get(
|
||||
'/{order_id}/status',
|
||||
response_model=OrderStatusResponse,
|
||||
status_code=200,
|
||||
responses=ERROR_RESPONSES,
|
||||
)
|
||||
async def order_status(
|
||||
order_id: str,
|
||||
auth: AuthContext = Depends(require_access_token),
|
||||
@@ -292,7 +321,12 @@ async def order_events(
|
||||
trace_id_var.reset(token)
|
||||
|
||||
|
||||
@order_router.get('/{order_id}', response_model=OrderDetailResponse)
|
||||
@order_router.get(
|
||||
'/{order_id}',
|
||||
response_model=OrderDetailResponse,
|
||||
status_code=200,
|
||||
responses=ERROR_RESPONSES,
|
||||
)
|
||||
async def order_detail(
|
||||
order_id: str,
|
||||
auth: AuthContext = Depends(require_access_token),
|
||||
@@ -302,7 +336,11 @@ async def order_detail(
|
||||
return _order_detail_response(result.order,result.payment)
|
||||
|
||||
|
||||
@order_router.post('/webhook/itpay')
|
||||
@order_router.post(
|
||||
'/webhook/itpay',
|
||||
status_code=200,
|
||||
responses=ERROR_RESPONSES,
|
||||
)
|
||||
async def itpay_webhook(
|
||||
request: Request,
|
||||
payment_command: CreatePaymentCommand = Depends(get_create_payment_command),
|
||||
|
||||
Reference in New Issue
Block a user