feat: add custom exceptions

This commit is contained in:
2026-05-09 14:49:15 +03:00
parent e929133db8
commit 499947e44e
17 changed files with 204 additions and 48 deletions

View File

@@ -1 +1,21 @@
from src.application.domain.exceptions.application_exceptions import ApplicationException
from src.application.domain.exceptions.application_exception import ApplicationException
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.forbidden_exception import ForbiddenException
from src.application.domain.exceptions.internal_server_exception import InternalServerException
from src.application.domain.exceptions.not_found_exception import NotFoundException
from src.application.domain.exceptions.service_unavailable_exception import ServiceUnavailableException
from src.application.domain.exceptions.unauthorized_exception import UnauthorizedException
__all__ = [
'ApplicationException',
'BadGatewayException',
'BadRequestException',
'ConflictException',
'ForbiddenException',
'InternalServerException',
'NotFoundException',
'ServiceUnavailableException',
'UnauthorizedException',
]

View File

@@ -7,12 +7,13 @@ class ApplicationException(Exception):
self,
status_code: int,
message: str,
headers: Mapping[str, str] | None = None,
headers: Mapping[str,str] | None = None,
):
super().__init__(message)
self.status_code = status_code
self.message = message
self.headers = headers
def __str__(self):
return f'{self.status_code}: {self.message}'

View File

@@ -0,0 +1,12 @@
from __future__ import annotations
from typing import Mapping
from src.application.domain.exceptions.application_exception import ApplicationException
class BadGatewayException(ApplicationException):
def __init__(
self,
message: str = 'Bad Gateway',
headers: Mapping[str,str] | None = None,
):
super().__init__(status_code=502,message=message,headers=headers)

View File

@@ -0,0 +1,12 @@
from __future__ import annotations
from typing import Mapping
from src.application.domain.exceptions.application_exception import ApplicationException
class BadRequestException(ApplicationException):
def __init__(
self,
message: str = 'Bad Request',
headers: Mapping[str,str] | None = None,
):
super().__init__(status_code=400,message=message,headers=headers)

View File

@@ -0,0 +1,12 @@
from __future__ import annotations
from typing import Mapping
from src.application.domain.exceptions.application_exception import ApplicationException
class ConflictException(ApplicationException):
def __init__(
self,
message: str = 'Conflict',
headers: Mapping[str,str] | None = None,
):
super().__init__(status_code=409,message=message,headers=headers)

View File

@@ -0,0 +1,12 @@
from __future__ import annotations
from typing import Mapping
from src.application.domain.exceptions.application_exception import ApplicationException
class ForbiddenException(ApplicationException):
def __init__(
self,
message: str = 'Forbidden',
headers: Mapping[str,str] | None = None,
):
super().__init__(status_code=403,message=message,headers=headers)

View File

@@ -0,0 +1,12 @@
from __future__ import annotations
from typing import Mapping
from src.application.domain.exceptions.application_exception import ApplicationException
class InternalServerException(ApplicationException):
def __init__(
self,
message: str = 'Internal Server Error',
headers: Mapping[str,str] | None = None,
):
super().__init__(status_code=500,message=message,headers=headers)

View File

@@ -0,0 +1,12 @@
from __future__ import annotations
from typing import Mapping
from src.application.domain.exceptions.application_exception import ApplicationException
class NotFoundException(ApplicationException):
def __init__(
self,
message: str = 'Not Found',
headers: Mapping[str,str] | None = None,
):
super().__init__(status_code=404,message=message,headers=headers)

View File

@@ -0,0 +1,12 @@
from __future__ import annotations
from typing import Mapping
from src.application.domain.exceptions.application_exception import ApplicationException
class ServiceUnavailableException(ApplicationException):
def __init__(
self,
message: str = 'Service Unavailable',
headers: Mapping[str,str] | None = None,
):
super().__init__(status_code=503,message=message,headers=headers)

View File

@@ -0,0 +1,12 @@
from __future__ import annotations
from typing import Mapping
from src.application.domain.exceptions.application_exception import ApplicationException
class UnauthorizedException(ApplicationException):
def __init__(
self,
message: str = 'Unauthorized',
headers: Mapping[str,str] | None = None,
):
super().__init__(status_code=401,message=message,headers=headers)

View File

@@ -2,18 +2,18 @@ from __future__ import annotations
from contextlib import asynccontextmanager
import secrets
from typing import AsyncGenerator
from fastapi import Depends, FastAPI, status
from fastapi import Depends, FastAPI
from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html
from fastapi.responses import HTMLResponse
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from src.application.domain.exceptions import ApplicationException
from src.application.domain.exceptions import ApplicationException,UnauthorizedException
from src.infrastructure.cache import create_redis_client
from src.infrastructure.config.settings import get_settings
from src.infrastructure.vault import JwtKeyStore, start_jwt_keys_scheduler
from src.infrastructure.utils import generate_instance_id
from src.infrastructure.logger import logger
from src.infrastructure.config import settings
from src.presentation.handlers import application_exception_handler, unhandled_exception_handler
from src.presentation.handler import application_exception_handler, unhandled_exception_handler
from src.presentation.messaging import crypto_transfer_router
from src.presentation.middleware import TraceIDMiddleware, SecurityHeadersMiddleware
from src.presentation.routing import order_router
@@ -25,8 +25,7 @@ async def verify_credentials(credentials: HTTPBasicCredentials = Depends(securit
user_ok = secrets.compare_digest(credentials.username, settings.DOCS_USERNAME)
pass_ok = secrets.compare_digest(credentials.password, settings.DOCS_PASSWORD)
if not (user_ok and pass_ok):
raise ApplicationException(
status_code=status.HTTP_401_UNAUTHORIZED,
raise UnauthorizedException(
message='Unauthorized',
headers={'WWW-Authenticate': 'Basic'},
)

View File

@@ -0,0 +1,7 @@
from src.presentation.handler.application_exception_handler import application_exception_handler
from src.presentation.handler.unhandled_exception_handler import unhandled_exception_handler
__all__ = [
'application_exception_handler',
'unhandled_exception_handler',
]

View File

@@ -1,17 +1,15 @@
from fastapi.responses import ORJSONResponse
from fastapi import Request
from fastapi.responses import ORJSONResponse
from src.application.domain.exceptions import ApplicationException
async def application_exception_handler(_request: Request, exc: ApplicationException) -> ORJSONResponse:
detail = exc.message
if 500 <= exc.status_code:
detail = "Internal Server Error"
detail = 'Internal Server Error'
return ORJSONResponse(
status_code=exc.status_code,
content={'detail': detail, 'status_code': exc.status_code},
content={'detail': detail,'status_code': exc.status_code},
headers=dict(exc.headers) if exc.headers else None,
)

View File

@@ -1,5 +1,5 @@
from fastapi.responses import ORJSONResponse
from fastapi import Request
from fastapi.responses import ORJSONResponse
from starlette import status
from src.infrastructure.logger import logger
@@ -8,5 +8,5 @@ async def unhandled_exception_handler(_request: Request, exc: Exception) -> ORJS
logger.exception(f'Unhandled exception: {type(exc).__name__}')
return ORJSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={'detail': 'Internal Server Error'},
)
content={'detail': 'Internal Server Error','status_code': status.HTTP_500_INTERNAL_SERVER_ERROR},
)

View File

@@ -1,2 +0,0 @@
from src.presentation.handlers.unhandled_handler import unhandled_exception_handler
from src.presentation.handlers.application_handler import application_exception_handler

View File

@@ -1,6 +1,6 @@
from urllib.parse import parse_qs
import orjson
from fastapi import APIRouter, Depends, Request
from fastapi import APIRouter, Depends, Request, Response
from fastapi.responses import ORJSONResponse
from src.application.commands import CreateOrderCommand
from src.application.commands import CreatePaymentCloudkassirCommand
@@ -10,20 +10,26 @@ from src.application.domain.enums import OrderStatus
from src.presentation.decorators import require_access_token, csrf_protect
from src.presentation.dependencies.commands import get_create_order_command, get_create_payment_cloudkassir_command
from src.presentation.dependencies.logger import get_logger
from src.presentation.schemas.order import CreateOrder
from src.presentation.schemas.order import CreateOrder,CreateOrderResponse,OrderPaymentResponse
from src.presentation.schemas.itpay_payment_models import ItpayPaymentData
order_router = APIRouter(prefix='/order', tags=['orders'])
@order_router.post('/create')
@order_router.post(
'/create',
response_model=CreateOrderResponse,
status_code=201,
responses={409: {'model': CreateOrderResponse}},
)
#@csrf_protect()
async def create_order(
payment_data: CreateOrder,
response: Response,
#auth: AuthContext = Depends(require_access_token),
command: CreateOrderCommand = Depends(get_create_order_command),
logger: ILogger = Depends(get_logger),
) -> ORJSONResponse:
) -> CreateOrderResponse:
#o = await command(payment_data, auth.user_id)
o = await command(payment_data, '01KPKAFN6J1NJBY15DX8JE2QYB')
itpay_error = o.status in (
@@ -32,32 +38,33 @@ async def create_order(
OrderStatus.ERROR,
)
http_code = 409 if itpay_error else 201
content: dict = {
'status_code': http_code,
'order': {
'id': o.id,
'created_at': o.created_at.isoformat() if o.created_at is not None else None,
'updated_at': o.updated_at.isoformat() if o.updated_at is not None else None,
'user_id': o.user_id,
'usdt_amount': str(o.usdt_amount) if o.usdt_amount is not None else None,
'usdt_exchange_rate': str(o.usdt_exchange_rate) if o.usdt_exchange_rate is not None else None,
'gas_fee': str(o.gas_fee) if o.gas_fee is not None else None,
'total_price': str(o.total_price) if o.total_price is not None else None,
'service_fee': str(o.service_fee) if o.service_fee is not None else None,
'status': o.status.value if o.status is not None else None,
'client_payment_id': o.client_payment_id,
'itpay_payment_qr_url_desktop': o.itpay_payment_qr_url_desktop,
'itpay_payment_qr_url_android': o.itpay_payment_qr_url_android,
'itpay_payment_qr_url_ios': o.itpay_payment_qr_url_ios,
'itpay_payment_qr_image_desktop': o.itpay_payment_qr_image_desktop,
'itpay_payment_qr_image_android': o.itpay_payment_qr_image_android,
'itpay_payment_qr_image_ios': o.itpay_payment_qr_image_ios,
'itpay_id': o.itpay_id,
'itpay_qr_id': o.itpay_qr_id,
'itpay_amount': str(o.itpay_amount) if o.itpay_amount is not None else None,
'itpay_created_at': o.itpay_created_at.isoformat() if o.itpay_created_at is not None else None,
}
}
response.status_code = http_code
content = CreateOrderResponse(
status_code=http_code,
order=OrderPaymentResponse(
id=o.id,
created_at=o.created_at.isoformat() if o.created_at is not None else None,
updated_at=o.updated_at.isoformat() if o.updated_at is not None else None,
user_id=o.user_id,
usdt_amount=str(o.usdt_amount) if o.usdt_amount is not None else None,
usdt_exchange_rate=str(o.usdt_exchange_rate) if o.usdt_exchange_rate is not None else None,
gas_fee=str(o.gas_fee) if o.gas_fee is not None else None,
total_price=str(o.total_price) if o.total_price is not None else None,
service_fee=str(o.service_fee) if o.service_fee is not None else None,
status=o.status,
client_payment_id=o.client_payment_id,
itpay_payment_qr_url_desktop=o.itpay_payment_qr_url_desktop,
itpay_payment_qr_url_android=o.itpay_payment_qr_url_android,
itpay_payment_qr_url_ios=o.itpay_payment_qr_url_ios,
itpay_payment_qr_image_desktop=o.itpay_payment_qr_image_desktop,
itpay_payment_qr_image_android=o.itpay_payment_qr_image_android,
itpay_payment_qr_image_ios=o.itpay_payment_qr_image_ios,
itpay_id=o.itpay_id,
itpay_qr_id=o.itpay_qr_id,
itpay_amount=str(o.itpay_amount) if o.itpay_amount is not None else None,
itpay_created_at=o.itpay_created_at.isoformat() if o.itpay_created_at is not None else None,
),
)
log_ids = {
'event': 'order_create_itpay_failed' if itpay_error else 'order_created',
'order_id': o.id,
@@ -67,7 +74,7 @@ async def create_order(
'order_status': o.status.value if o.status is not None else None,
}
logger.info(log_ids)
return ORJSONResponse(content=content, status_code=http_code)
return content
@order_router.post('/webhook/itpay')

View File

@@ -1,5 +1,6 @@
from decimal import Decimal
from pydantic import BaseModel,Field
from src.application.domain.enums import OrderStatus
class CreateOrder(BaseModel):
@@ -8,3 +9,32 @@ class CreateOrder(BaseModel):
gas_fee: Decimal = Field(gt=0, decimal_places=2, max_digits=20)
total_price: Decimal = Field(gt=0, decimal_places=2, max_digits=20)
class OrderPaymentResponse(BaseModel):
id: str | None = None
created_at: str | None = None
updated_at: str | None = None
user_id: str | None = None
usdt_amount: str | None = None
usdt_exchange_rate: str | None = None
gas_fee: str | None = None
total_price: str | None = None
service_fee: str | None = None
status: OrderStatus | None = None
client_payment_id: str | None = None
itpay_payment_qr_url_desktop: str | None = None
itpay_payment_qr_url_android: str | None = None
itpay_payment_qr_url_ios: str | None = None
itpay_payment_qr_image_desktop: str | None = None
itpay_payment_qr_image_android: str | None = None
itpay_payment_qr_image_ios: str | None = None
itpay_id: str | None = None
itpay_qr_id: str | None = None
itpay_amount: str | None = None
itpay_created_at: str | None = None
class CreateOrderResponse(BaseModel):
status_code: int
order: OrderPaymentResponse