feat: add custom exceptions
This commit is contained in:
@@ -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',
|
||||||
|
]
|
||||||
@@ -7,12 +7,13 @@ class ApplicationException(Exception):
|
|||||||
self,
|
self,
|
||||||
status_code: int,
|
status_code: int,
|
||||||
message: str,
|
message: str,
|
||||||
headers: Mapping[str, str] | None = None,
|
headers: Mapping[str,str] | None = None,
|
||||||
):
|
):
|
||||||
super().__init__(message)
|
super().__init__(message)
|
||||||
self.status_code = status_code
|
self.status_code = status_code
|
||||||
self.message = message
|
self.message = message
|
||||||
self.headers = headers
|
self.headers = headers
|
||||||
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'{self.status_code}: {self.message}'
|
return f'{self.status_code}: {self.message}'
|
||||||
12
src/application/domain/exceptions/bad_gateway_exception.py
Normal file
12
src/application/domain/exceptions/bad_gateway_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 BadGatewayException(ApplicationException):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
message: str = 'Bad Gateway',
|
||||||
|
headers: Mapping[str,str] | None = None,
|
||||||
|
):
|
||||||
|
super().__init__(status_code=502,message=message,headers=headers)
|
||||||
12
src/application/domain/exceptions/bad_request_exception.py
Normal file
12
src/application/domain/exceptions/bad_request_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 BadRequestException(ApplicationException):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
message: str = 'Bad Request',
|
||||||
|
headers: Mapping[str,str] | None = None,
|
||||||
|
):
|
||||||
|
super().__init__(status_code=400,message=message,headers=headers)
|
||||||
12
src/application/domain/exceptions/conflict_exception.py
Normal file
12
src/application/domain/exceptions/conflict_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 ConflictException(ApplicationException):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
message: str = 'Conflict',
|
||||||
|
headers: Mapping[str,str] | None = None,
|
||||||
|
):
|
||||||
|
super().__init__(status_code=409,message=message,headers=headers)
|
||||||
12
src/application/domain/exceptions/forbidden_exception.py
Normal file
12
src/application/domain/exceptions/forbidden_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 ForbiddenException(ApplicationException):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
message: str = 'Forbidden',
|
||||||
|
headers: Mapping[str,str] | None = None,
|
||||||
|
):
|
||||||
|
super().__init__(status_code=403,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 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)
|
||||||
12
src/application/domain/exceptions/not_found_exception.py
Normal file
12
src/application/domain/exceptions/not_found_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 NotFoundException(ApplicationException):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
message: str = 'Not Found',
|
||||||
|
headers: Mapping[str,str] | None = None,
|
||||||
|
):
|
||||||
|
super().__init__(status_code=404,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 ServiceUnavailableException(ApplicationException):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
message: str = 'Service Unavailable',
|
||||||
|
headers: Mapping[str,str] | None = None,
|
||||||
|
):
|
||||||
|
super().__init__(status_code=503,message=message,headers=headers)
|
||||||
12
src/application/domain/exceptions/unauthorized_exception.py
Normal file
12
src/application/domain/exceptions/unauthorized_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 UnauthorizedException(ApplicationException):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
message: str = 'Unauthorized',
|
||||||
|
headers: Mapping[str,str] | None = None,
|
||||||
|
):
|
||||||
|
super().__init__(status_code=401,message=message,headers=headers)
|
||||||
@@ -2,18 +2,18 @@ from __future__ import annotations
|
|||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
import secrets
|
import secrets
|
||||||
from typing import AsyncGenerator
|
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.openapi.docs import get_redoc_html, get_swagger_ui_html
|
||||||
from fastapi.responses import HTMLResponse
|
from fastapi.responses import HTMLResponse
|
||||||
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
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.cache import create_redis_client
|
||||||
from src.infrastructure.config.settings import get_settings
|
from src.infrastructure.config.settings import get_settings
|
||||||
from src.infrastructure.vault import JwtKeyStore, start_jwt_keys_scheduler
|
from src.infrastructure.vault import JwtKeyStore, start_jwt_keys_scheduler
|
||||||
from src.infrastructure.utils import generate_instance_id
|
from src.infrastructure.utils import generate_instance_id
|
||||||
from src.infrastructure.logger import logger
|
from src.infrastructure.logger import logger
|
||||||
from src.infrastructure.config import settings
|
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.messaging import crypto_transfer_router
|
||||||
from src.presentation.middleware import TraceIDMiddleware, SecurityHeadersMiddleware
|
from src.presentation.middleware import TraceIDMiddleware, SecurityHeadersMiddleware
|
||||||
from src.presentation.routing import order_router
|
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)
|
user_ok = secrets.compare_digest(credentials.username, settings.DOCS_USERNAME)
|
||||||
pass_ok = secrets.compare_digest(credentials.password, settings.DOCS_PASSWORD)
|
pass_ok = secrets.compare_digest(credentials.password, settings.DOCS_PASSWORD)
|
||||||
if not (user_ok and pass_ok):
|
if not (user_ok and pass_ok):
|
||||||
raise ApplicationException(
|
raise UnauthorizedException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
||||||
message='Unauthorized',
|
message='Unauthorized',
|
||||||
headers={'WWW-Authenticate': 'Basic'},
|
headers={'WWW-Authenticate': 'Basic'},
|
||||||
)
|
)
|
||||||
|
|||||||
7
src/presentation/handler/__init__.py
Normal file
7
src/presentation/handler/__init__.py
Normal 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',
|
||||||
|
]
|
||||||
@@ -1,17 +1,15 @@
|
|||||||
from fastapi.responses import ORJSONResponse
|
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
|
from fastapi.responses import ORJSONResponse
|
||||||
from src.application.domain.exceptions import ApplicationException
|
from src.application.domain.exceptions import ApplicationException
|
||||||
|
|
||||||
|
|
||||||
async def application_exception_handler(_request: Request, exc: ApplicationException) -> ORJSONResponse:
|
async def application_exception_handler(_request: Request, exc: ApplicationException) -> ORJSONResponse:
|
||||||
detail = exc.message
|
detail = exc.message
|
||||||
if 500 <= exc.status_code:
|
if 500 <= exc.status_code:
|
||||||
detail = "Internal Server Error"
|
detail = 'Internal Server Error'
|
||||||
|
|
||||||
return ORJSONResponse(
|
return ORJSONResponse(
|
||||||
status_code=exc.status_code,
|
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,
|
headers=dict(exc.headers) if exc.headers else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
from fastapi.responses import ORJSONResponse
|
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
|
from fastapi.responses import ORJSONResponse
|
||||||
from starlette import status
|
from starlette import status
|
||||||
from src.infrastructure.logger import logger
|
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__}')
|
logger.exception(f'Unhandled exception: {type(exc).__name__}')
|
||||||
return ORJSONResponse(
|
return ORJSONResponse(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
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},
|
||||||
)
|
)
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
from src.presentation.handlers.unhandled_handler import unhandled_exception_handler
|
|
||||||
from src.presentation.handlers.application_handler import application_exception_handler
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
from urllib.parse import parse_qs
|
from urllib.parse import parse_qs
|
||||||
import orjson
|
import orjson
|
||||||
from fastapi import APIRouter, Depends, Request
|
from fastapi import APIRouter, Depends, Request, Response
|
||||||
from fastapi.responses import ORJSONResponse
|
from fastapi.responses import ORJSONResponse
|
||||||
from src.application.commands import CreateOrderCommand
|
from src.application.commands import CreateOrderCommand
|
||||||
from src.application.commands import CreatePaymentCloudkassirCommand
|
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.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.commands import get_create_order_command, get_create_payment_cloudkassir_command
|
||||||
from src.presentation.dependencies.logger import get_logger
|
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
|
from src.presentation.schemas.itpay_payment_models import ItpayPaymentData
|
||||||
|
|
||||||
order_router = APIRouter(prefix='/order', tags=['orders'])
|
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()
|
#@csrf_protect()
|
||||||
async def create_order(
|
async def create_order(
|
||||||
payment_data: CreateOrder,
|
payment_data: CreateOrder,
|
||||||
|
response: Response,
|
||||||
#auth: AuthContext = Depends(require_access_token),
|
#auth: AuthContext = Depends(require_access_token),
|
||||||
command: CreateOrderCommand = Depends(get_create_order_command),
|
command: CreateOrderCommand = Depends(get_create_order_command),
|
||||||
logger: ILogger = Depends(get_logger),
|
logger: ILogger = Depends(get_logger),
|
||||||
) -> ORJSONResponse:
|
) -> CreateOrderResponse:
|
||||||
#o = await command(payment_data, auth.user_id)
|
#o = await command(payment_data, auth.user_id)
|
||||||
o = await command(payment_data, '01KPKAFN6J1NJBY15DX8JE2QYB')
|
o = await command(payment_data, '01KPKAFN6J1NJBY15DX8JE2QYB')
|
||||||
itpay_error = o.status in (
|
itpay_error = o.status in (
|
||||||
@@ -32,32 +38,33 @@ async def create_order(
|
|||||||
OrderStatus.ERROR,
|
OrderStatus.ERROR,
|
||||||
)
|
)
|
||||||
http_code = 409 if itpay_error else 201
|
http_code = 409 if itpay_error else 201
|
||||||
content: dict = {
|
response.status_code = http_code
|
||||||
'status_code': http_code,
|
content = CreateOrderResponse(
|
||||||
'order': {
|
status_code=http_code,
|
||||||
'id': o.id,
|
order=OrderPaymentResponse(
|
||||||
'created_at': o.created_at.isoformat() if o.created_at is not None else None,
|
id=o.id,
|
||||||
'updated_at': o.updated_at.isoformat() if o.updated_at is not None else None,
|
created_at=o.created_at.isoformat() if o.created_at is not None else None,
|
||||||
'user_id': o.user_id,
|
updated_at=o.updated_at.isoformat() if o.updated_at is not None else None,
|
||||||
'usdt_amount': str(o.usdt_amount) if o.usdt_amount is not None else None,
|
user_id=o.user_id,
|
||||||
'usdt_exchange_rate': str(o.usdt_exchange_rate) if o.usdt_exchange_rate is not None else None,
|
usdt_amount=str(o.usdt_amount) if o.usdt_amount is not None else None,
|
||||||
'gas_fee': str(o.gas_fee) if o.gas_fee is not None else None,
|
usdt_exchange_rate=str(o.usdt_exchange_rate) if o.usdt_exchange_rate is not None else None,
|
||||||
'total_price': str(o.total_price) if o.total_price is not None else None,
|
gas_fee=str(o.gas_fee) if o.gas_fee is not None else None,
|
||||||
'service_fee': str(o.service_fee) if o.service_fee is not None else None,
|
total_price=str(o.total_price) if o.total_price is not None else None,
|
||||||
'status': o.status.value if o.status is not None else None,
|
service_fee=str(o.service_fee) if o.service_fee is not None else None,
|
||||||
'client_payment_id': o.client_payment_id,
|
status=o.status,
|
||||||
'itpay_payment_qr_url_desktop': o.itpay_payment_qr_url_desktop,
|
client_payment_id=o.client_payment_id,
|
||||||
'itpay_payment_qr_url_android': o.itpay_payment_qr_url_android,
|
itpay_payment_qr_url_desktop=o.itpay_payment_qr_url_desktop,
|
||||||
'itpay_payment_qr_url_ios': o.itpay_payment_qr_url_ios,
|
itpay_payment_qr_url_android=o.itpay_payment_qr_url_android,
|
||||||
'itpay_payment_qr_image_desktop': o.itpay_payment_qr_image_desktop,
|
itpay_payment_qr_url_ios=o.itpay_payment_qr_url_ios,
|
||||||
'itpay_payment_qr_image_android': o.itpay_payment_qr_image_android,
|
itpay_payment_qr_image_desktop=o.itpay_payment_qr_image_desktop,
|
||||||
'itpay_payment_qr_image_ios': o.itpay_payment_qr_image_ios,
|
itpay_payment_qr_image_android=o.itpay_payment_qr_image_android,
|
||||||
'itpay_id': o.itpay_id,
|
itpay_payment_qr_image_ios=o.itpay_payment_qr_image_ios,
|
||||||
'itpay_qr_id': o.itpay_qr_id,
|
itpay_id=o.itpay_id,
|
||||||
'itpay_amount': str(o.itpay_amount) if o.itpay_amount is not None else None,
|
itpay_qr_id=o.itpay_qr_id,
|
||||||
'itpay_created_at': o.itpay_created_at.isoformat() if o.itpay_created_at is not None else None,
|
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 = {
|
log_ids = {
|
||||||
'event': 'order_create_itpay_failed' if itpay_error else 'order_created',
|
'event': 'order_create_itpay_failed' if itpay_error else 'order_created',
|
||||||
'order_id': o.id,
|
'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,
|
'order_status': o.status.value if o.status is not None else None,
|
||||||
}
|
}
|
||||||
logger.info(log_ids)
|
logger.info(log_ids)
|
||||||
return ORJSONResponse(content=content, status_code=http_code)
|
return content
|
||||||
|
|
||||||
|
|
||||||
@order_router.post('/webhook/itpay')
|
@order_router.post('/webhook/itpay')
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from pydantic import BaseModel,Field
|
from pydantic import BaseModel,Field
|
||||||
|
from src.application.domain.enums import OrderStatus
|
||||||
|
|
||||||
|
|
||||||
class CreateOrder(BaseModel):
|
class CreateOrder(BaseModel):
|
||||||
@@ -8,3 +9,32 @@ class CreateOrder(BaseModel):
|
|||||||
gas_fee: Decimal = Field(gt=0, decimal_places=2, max_digits=20)
|
gas_fee: Decimal = Field(gt=0, decimal_places=2, max_digits=20)
|
||||||
total_price: 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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user