diff --git a/src/application/domain/exceptions/__init__.py b/src/application/domain/exceptions/__init__.py index 6d6ca18..c773402 100644 --- a/src/application/domain/exceptions/__init__.py +++ b/src/application/domain/exceptions/__init__.py @@ -1 +1,21 @@ -from src.application.domain.exceptions.application_exceptions import ApplicationException \ No newline at end of file +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', +] \ No newline at end of file diff --git a/src/application/domain/exceptions/application_exceptions.py b/src/application/domain/exceptions/application_exception.py similarity index 88% rename from src/application/domain/exceptions/application_exceptions.py rename to src/application/domain/exceptions/application_exception.py index 7396ceb..f896ae7 100644 --- a/src/application/domain/exceptions/application_exceptions.py +++ b/src/application/domain/exceptions/application_exception.py @@ -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}' diff --git a/src/application/domain/exceptions/bad_gateway_exception.py b/src/application/domain/exceptions/bad_gateway_exception.py new file mode 100644 index 0000000..93fead3 --- /dev/null +++ b/src/application/domain/exceptions/bad_gateway_exception.py @@ -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) diff --git a/src/application/domain/exceptions/bad_request_exception.py b/src/application/domain/exceptions/bad_request_exception.py new file mode 100644 index 0000000..2b34dc6 --- /dev/null +++ b/src/application/domain/exceptions/bad_request_exception.py @@ -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) diff --git a/src/application/domain/exceptions/conflict_exception.py b/src/application/domain/exceptions/conflict_exception.py new file mode 100644 index 0000000..23e9193 --- /dev/null +++ b/src/application/domain/exceptions/conflict_exception.py @@ -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) diff --git a/src/application/domain/exceptions/forbidden_exception.py b/src/application/domain/exceptions/forbidden_exception.py new file mode 100644 index 0000000..4b65989 --- /dev/null +++ b/src/application/domain/exceptions/forbidden_exception.py @@ -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) diff --git a/src/application/domain/exceptions/internal_server_exception.py b/src/application/domain/exceptions/internal_server_exception.py new file mode 100644 index 0000000..b290b85 --- /dev/null +++ b/src/application/domain/exceptions/internal_server_exception.py @@ -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) diff --git a/src/application/domain/exceptions/not_found_exception.py b/src/application/domain/exceptions/not_found_exception.py new file mode 100644 index 0000000..a80bedd --- /dev/null +++ b/src/application/domain/exceptions/not_found_exception.py @@ -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) diff --git a/src/application/domain/exceptions/service_unavailable_exception.py b/src/application/domain/exceptions/service_unavailable_exception.py new file mode 100644 index 0000000..dbd4f41 --- /dev/null +++ b/src/application/domain/exceptions/service_unavailable_exception.py @@ -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) diff --git a/src/application/domain/exceptions/unauthorized_exception.py b/src/application/domain/exceptions/unauthorized_exception.py new file mode 100644 index 0000000..dd9e6b0 --- /dev/null +++ b/src/application/domain/exceptions/unauthorized_exception.py @@ -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) diff --git a/src/main.py b/src/main.py index 3bbb294..76c8e49 100644 --- a/src/main.py +++ b/src/main.py @@ -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'}, ) diff --git a/src/presentation/handler/__init__.py b/src/presentation/handler/__init__.py new file mode 100644 index 0000000..10924fe --- /dev/null +++ b/src/presentation/handler/__init__.py @@ -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', +] diff --git a/src/presentation/handlers/application_handler.py b/src/presentation/handler/application_exception_handler.py similarity index 79% rename from src/presentation/handlers/application_handler.py rename to src/presentation/handler/application_exception_handler.py index 2835ac5..b1bb593 100644 --- a/src/presentation/handlers/application_handler.py +++ b/src/presentation/handler/application_exception_handler.py @@ -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, ) - - diff --git a/src/presentation/handlers/unhandled_handler.py b/src/presentation/handler/unhandled_exception_handler.py similarity index 77% rename from src/presentation/handlers/unhandled_handler.py rename to src/presentation/handler/unhandled_exception_handler.py index c6f1d52..e456f44 100644 --- a/src/presentation/handlers/unhandled_handler.py +++ b/src/presentation/handler/unhandled_exception_handler.py @@ -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'}, - ) \ No newline at end of file + content={'detail': 'Internal Server Error','status_code': status.HTTP_500_INTERNAL_SERVER_ERROR}, + ) diff --git a/src/presentation/handlers/__init__.py b/src/presentation/handlers/__init__.py deleted file mode 100644 index cb6cbad..0000000 --- a/src/presentation/handlers/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from src.presentation.handlers.unhandled_handler import unhandled_exception_handler -from src.presentation.handlers.application_handler import application_exception_handler \ No newline at end of file diff --git a/src/presentation/routing/order.py b/src/presentation/routing/order.py index c30320b..012f528 100644 --- a/src/presentation/routing/order.py +++ b/src/presentation/routing/order.py @@ -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') diff --git a/src/presentation/schemas/order.py b/src/presentation/schemas/order.py index 26e0e3c..f5406e4 100644 --- a/src/presentation/schemas/order.py +++ b/src/presentation/schemas/order.py @@ -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 +