feat: add docs and custom exc

This commit is contained in:
2026-05-12 17:24:10 +03:00
parent 85cdf1f720
commit 4c6761d4c4
15 changed files with 242 additions and 56 deletions

View File

@@ -3,7 +3,7 @@ from datetime import datetime,timedelta,timezone
from src.application.abstractions import IUnitOfWork from src.application.abstractions import IUnitOfWork
from src.application.contracts import IBeorgService,ILogger from src.application.contracts import IBeorgService,ILogger
from src.application.domain.dto import BeorgKycCreateResponse,BeorgKycResultResponse,KycSessionResponse from src.application.domain.dto import BeorgKycCreateResponse,BeorgKycResultResponse,KycSessionResponse
from src.application.domain.exceptions import ApplicationException from src.application.domain.exceptions import ApplicationException,KycFailedException,KycNotCompletedException,KycSessionExpiredException,KycSessionMissingUserTokenException
from src.application.services import ensure_adult,extract_personal_data,parse_birth_date from src.application.services import ensure_adult,extract_personal_data,parse_birth_date
@@ -58,11 +58,11 @@ class CompleteKycCommand:
async def __call__(self,user_id: str) -> BeorgKycResultResponse: async def __call__(self,user_id: str) -> BeorgKycResultResponse:
session = await self._get_session(user_id) session = await self._get_session(user_id)
if not session.user_token: if not session.user_token:
raise ApplicationException(status_code=409,message='KYC session has no user token') raise KycSessionMissingUserTokenException()
result = await self._beorg_service.get_result(user_token=session.user_token) result = await self._beorg_service.get_result(user_token=session.user_token)
if result.done_state is None: if result.done_state is None:
raise ApplicationException(status_code=409,message='KYC is not completed yet') raise KycNotCompletedException()
if result.done_state is False: if result.done_state is False:
async with self._unit_of_work as unit_of_work: async with self._unit_of_work as unit_of_work:
await unit_of_work.kyc_repository.update_session_result( await unit_of_work.kyc_repository.update_session_result(
@@ -74,7 +74,7 @@ class CompleteKycCommand:
result_data=result.data, result_data=result.data,
error='KYC failed', error='KYC failed',
) )
raise ApplicationException(status_code=400,message='KYC failed') raise KycFailedException()
personal_data = extract_personal_data(result.data) personal_data = extract_personal_data(result.data)
birth_date = parse_birth_date(personal_data.birth_date) birth_date = parse_birth_date(personal_data.birth_date)
@@ -115,7 +115,7 @@ class CompleteKycCommand:
client_user_token=session.client_user_token, client_user_token=session.client_user_token,
qr_code=session.qr_code, qr_code=session.qr_code,
) )
raise ApplicationException(status_code=404,message='KYC session expired') raise KycSessionExpiredException()
class GetKycSessionCommand: class GetKycSessionCommand:
@@ -134,7 +134,7 @@ class GetKycSessionCommand:
await unit_of_work.kyc_repository.expire_started_sessions(user_id=user_id,now=now) await unit_of_work.kyc_repository.expire_started_sessions(user_id=user_id,now=now)
session = await unit_of_work.kyc_repository.get_latest_session(user_id=user_id) session = await unit_of_work.kyc_repository.get_latest_session(user_id=user_id)
if session is None or session.expires_at is None: if session is None or session.expires_at is None:
raise ApplicationException(status_code=404,message='KYC session expired') raise KycSessionExpiredException()
expires_in = max(int((session.expires_at - now).total_seconds()),0) expires_in = max(int((session.expires_at - now).total_seconds()),0)
return KycSessionResponse( return KycSessionResponse(

View File

@@ -1 +1,2 @@
from src.application.domain.exceptions.application_exceptions import ApplicationException from src.application.domain.exceptions.application_exceptions import ApplicationException
from src.application.domain.exceptions.kyc_exceptions import BeorgConfigException,BeorgRejectedException,BeorgUnavailableException,CsrfRequestRequiredException,InvalidTokenException,JwtDecodeFailedException,KycAgeRestrictedException,KycBirthDateInvalidException,KycFailedException,KycNotCompletedException,KycPersonalDataIncompleteException,KycSessionExpiredException,KycSessionMissingUserTokenException,NotAuthenticatedException,UnauthorizedException,UserNotFoundException

View File

@@ -7,11 +7,13 @@ class ApplicationException(Exception):
self, self,
status_code: int, status_code: int,
message: str, message: str,
error_code: str = 'application_error',
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.error_code = error_code
self.headers = headers self.headers = headers
def __str__(self): def __str__(self):

View File

@@ -0,0 +1,98 @@
from __future__ import annotations
from src.application.domain.exceptions.application_exceptions import ApplicationException
class UnauthorizedException(ApplicationException):
def __init__(self,message: str = 'Unauthorized') -> None:
super().__init__(status_code=401,message=message,error_code='unauthorized')
class NotAuthenticatedException(ApplicationException):
def __init__(self) -> None:
super().__init__(status_code=401,message='Not authenticated',error_code='not_authenticated')
class InvalidTokenException(ApplicationException):
def __init__(self,message: str = 'Invalid token') -> None:
super().__init__(status_code=401,message=message,error_code='invalid_token')
class JwtDecodeFailedException(ApplicationException):
def __init__(self) -> None:
super().__init__(status_code=500,message='JWT decode failed',error_code='jwt_decode_failed')
class CsrfRequestRequiredException(ApplicationException):
def __init__(self) -> None:
super().__init__(status_code=500,message='Request is required for CSRF protection',error_code='csrf_request_required')
class UserNotFoundException(ApplicationException):
def __init__(self) -> None:
super().__init__(status_code=404,message='User not found',error_code='user_not_found')
class KycSessionMissingUserTokenException(ApplicationException):
def __init__(self) -> None:
super().__init__(status_code=409,message='KYC session has no user token',error_code='kyc_session_missing_user_token')
class KycNotCompletedException(ApplicationException):
def __init__(self) -> None:
super().__init__(status_code=409,message='KYC is not completed yet',error_code='kyc_not_completed')
class KycFailedException(ApplicationException):
def __init__(self) -> None:
super().__init__(status_code=400,message='KYC failed',error_code='kyc_failed')
class KycSessionExpiredException(ApplicationException):
def __init__(self) -> None:
super().__init__(status_code=404,message='KYC session expired',error_code='kyc_session_expired')
class KycPersonalDataIncompleteException(ApplicationException):
def __init__(self) -> None:
super().__init__(status_code=422,message='KYC personal data is incomplete',error_code='kyc_personal_data_incomplete')
class KycBirthDateInvalidException(ApplicationException):
def __init__(self) -> None:
super().__init__(status_code=422,message='KYC birth date has invalid format',error_code='kyc_birth_date_invalid')
class KycAgeRestrictedException(ApplicationException):
def __init__(self) -> None:
super().__init__(status_code=403,message='KYC is unavailable for users under 18',error_code='kyc_age_restricted')
class BeorgUnavailableException(ApplicationException):
def __init__(self) -> None:
super().__init__(status_code=502,message='Beorg service unavailable',error_code='beorg_unavailable')
class BeorgRejectedException(ApplicationException):
def __init__(self,message: str = 'Beorg rejected kyc request') -> None:
super().__init__(status_code=400,message=message,error_code='beorg_rejected')
class BeorgConfigException(ApplicationException):
def __init__(self) -> None:
super().__init__(status_code=500,message='Beorg service is not configured',error_code='beorg_not_configured')

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
from datetime import date,datetime from datetime import date,datetime
from typing import Any from typing import Any
from src.application.domain.dto import KycPersonalData from src.application.domain.dto import KycPersonalData
from src.application.domain.exceptions import ApplicationException from src.application.domain.exceptions import KycAgeRestrictedException,KycBirthDateInvalidException,KycPersonalDataIncompleteException
FIELD_ALIASES = { FIELD_ALIASES = {
@@ -33,7 +33,7 @@ def extract_personal_data(data: Any) -> KycPersonalData:
missing = [field for field in ('first_name','last_name','birth_date') if not values.get(field)] missing = [field for field in ('first_name','last_name','birth_date') if not values.get(field)]
if missing: if missing:
raise ApplicationException(status_code=422,message='KYC personal data is incomplete') raise KycPersonalDataIncompleteException()
return KycPersonalData( return KycPersonalData(
first_name=values['first_name'], first_name=values['first_name'],
@@ -51,7 +51,7 @@ def ensure_adult(birth_date: date) -> None:
except ValueError: except ValueError:
adult_from = date(today.year - 18,2,28) adult_from = date(today.year - 18,2,28)
if birth_date > adult_from: if birth_date > adult_from:
raise ApplicationException(status_code=403,message='KYC is unavailable for users under 18') raise KycAgeRestrictedException()
def parse_birth_date(value: str) -> date: def parse_birth_date(value: str) -> date:
@@ -84,4 +84,4 @@ def _parse_date(value: str) -> date:
return datetime.strptime(clean,date_format).date() return datetime.strptime(clean,date_format).date()
except ValueError: except ValueError:
continue continue
raise ApplicationException(status_code=422,message='KYC birth date has invalid format') raise KycBirthDateInvalidException()

View File

@@ -3,7 +3,7 @@ from typing import Any
import aiohttp import aiohttp
from src.application.contracts import IBeorgService from src.application.contracts import IBeorgService
from src.application.domain.dto import BeorgKycCreateResponse,BeorgKycResultResponse from src.application.domain.dto import BeorgKycCreateResponse,BeorgKycResultResponse
from src.application.domain.exceptions import ApplicationException from src.application.domain.exceptions import BeorgConfigException,BeorgRejectedException,BeorgUnavailableException
class BeorgService(IBeorgService): class BeorgService(IBeorgService):
@@ -49,11 +49,11 @@ class BeorgService(IBeorgService):
data = await response.json(content_type=None) data = await response.json(content_type=None)
if response.status >= 500: if response.status >= 500:
raise ApplicationException(status_code=502,message='Beorg service unavailable') raise BeorgUnavailableException()
result = BeorgKycCreateResponse.model_validate(data) result = BeorgKycCreateResponse.model_validate(data)
if not result.status: if not result.status:
raise ApplicationException(status_code=400,message=result.error or 'Beorg rejected kyc request') raise BeorgRejectedException(result.error or 'Beorg rejected kyc request')
return result return result
@@ -73,11 +73,11 @@ class BeorgService(IBeorgService):
data = await response.json(content_type=None) data = await response.json(content_type=None)
if response.status >= 500: if response.status >= 500:
raise ApplicationException(status_code=502,message='Beorg service unavailable') raise BeorgUnavailableException()
return BeorgKycResultResponse.model_validate(data) return BeorgKycResultResponse.model_validate(data)
def _ensure_configured(self) -> None: def _ensure_configured(self) -> None:
if not self._project_id or not self._machine_uid or not self._token or not self._process_info: if not self._project_id or not self._machine_uid or not self._token or not self._process_info:
raise ApplicationException(status_code=500,message='Beorg service is not configured') raise BeorgConfigException()

View File

@@ -4,7 +4,7 @@ from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from src.application.abstractions.repositories import IUserRepository from src.application.abstractions.repositories import IUserRepository
from src.application.domain.entities import UserEntity from src.application.domain.entities import UserEntity
from src.application.domain.exceptions import ApplicationException from src.application.domain.exceptions import UserNotFoundException
from src.infrastructure.database.models.user import UserModel from src.infrastructure.database.models.user import UserModel
@@ -25,7 +25,7 @@ class UserRepository(IUserRepository):
result = await self._session.execute(select(UserModel).where(UserModel.email == email)) result = await self._session.execute(select(UserModel).where(UserModel.email == email))
user = result.scalar_one_or_none() user = result.scalar_one_or_none()
if user is None: if user is None:
raise ApplicationException(status_code=404,message='User not found') raise UserNotFoundException()
return self._to_entity(user) return self._to_entity(user)
@@ -46,7 +46,7 @@ class UserRepository(IUserRepository):
) -> UserEntity: ) -> UserEntity:
user = await self._session.get(UserModel,user_id) user = await self._session.get(UserModel,user_id)
if user is None: if user is None:
raise ApplicationException(status_code=404,message='User not found') raise UserNotFoundException()
user.first_name = first_name user.first_name = first_name
user.last_name = last_name user.last_name = last_name

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
from jose import jwt, ExpiredSignatureError, JWTError from jose import jwt, ExpiredSignatureError, JWTError
from src.application.contracts import ILogger, IJwtService from src.application.contracts import ILogger, IJwtService
from src.application.domain.dto import AccessTokenPayload from src.application.domain.dto import AccessTokenPayload
from src.application.domain.exceptions import ApplicationException from src.application.domain.exceptions import ApplicationException,InvalidTokenException,JwtDecodeFailedException
from src.infrastructure.config.settings import settings from src.infrastructure.config.settings import settings
from src.infrastructure.vault import JwtKeyStore from src.infrastructure.vault import JwtKeyStore
@@ -17,7 +17,7 @@ class JwtService(IJwtService):
if payload.get('type') != 'access': if payload.get('type') != 'access':
self._logger.warning(f'Access token invalid type received_type={payload.get("type")}') self._logger.warning(f'Access token invalid type received_type={payload.get("type")}')
raise ApplicationException(status_code=401, message='Invalid token type') raise InvalidTokenException('Invalid token type')
try: try:
return AccessTokenPayload( return AccessTokenPayload(
@@ -32,7 +32,7 @@ class JwtService(IJwtService):
) )
except KeyError as exception: except KeyError as exception:
self._logger.warning(f'Access token missing claim error={str(exception)}') self._logger.warning(f'Access token missing claim error={str(exception)}')
raise ApplicationException(status_code=401, message=f'Missing token claim: {exception}') raise InvalidTokenException(f'Missing token claim: {exception}')
async def _decode_and_verify(self, token: str) -> dict: async def _decode_and_verify(self, token: str) -> dict:
kid: str | None = None kid: str | None = None
@@ -42,12 +42,12 @@ class JwtService(IJwtService):
kid = header.get('kid') kid = header.get('kid')
if not kid: if not kid:
self._logger.warning(f'JWT header missing kid header={header}') self._logger.warning(f'JWT header missing kid header={header}')
raise ApplicationException(status_code=401, message='Missing token header: kid') raise InvalidTokenException('Missing token header: kid')
received_alg = header.get('alg') received_alg = header.get('alg')
if received_alg != settings.JWT_ALGORITHM: if received_alg != settings.JWT_ALGORITHM:
self._logger.warning(f'JWT invalid algorithm kid={kid} received_alg={received_alg} expected_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 InvalidTokenException('Invalid token algorithm')
public_pem = await self._key_store.get_public_key_for_kid(str(kid)) public_pem = await self._key_store.get_public_key_for_kid(str(kid))
@@ -58,7 +58,7 @@ class JwtService(IJwtService):
if not public_pem: if not public_pem:
self._logger.warning(f'JWT unknown kid kid={kid}') self._logger.warning(f'JWT unknown kid kid={kid}')
raise ApplicationException(status_code=401, message='Unknown token kid') raise InvalidTokenException('Unknown token kid')
options = { options = {
'verify_signature': True, 'verify_signature': True,
@@ -85,25 +85,25 @@ class JwtService(IJwtService):
if 'sid' not in payload: if 'sid' not in payload:
self._logger.warning(f'JWT missing sid claim kid={kid}') self._logger.warning(f'JWT missing sid claim kid={kid}')
raise ApplicationException(status_code=401, message='Missing token claim: sid') raise InvalidTokenException('Missing token claim: sid')
if 'type' not in payload: if 'type' not in payload:
self._logger.warning(f'JWT missing type claim kid={kid}') self._logger.warning(f'JWT missing type claim kid={kid}')
raise ApplicationException(status_code=401, message='Missing token claim: type') raise InvalidTokenException('Missing token claim: type')
return payload return payload
except ExpiredSignatureError as exception: except ExpiredSignatureError as exception:
self._logger.info(f'JWT expired kid={kid} error={str(exception)}') self._logger.info(f'JWT expired kid={kid} error={str(exception)}')
raise ApplicationException(status_code=401, message='Token expired') raise InvalidTokenException('Token expired')
except ApplicationException: except ApplicationException:
raise raise
except JWTError as exception: except JWTError as exception:
self._logger.warning(f'JWT decode failed kid={kid} error={str(exception)}') self._logger.warning(f'JWT decode failed kid={kid} error={str(exception)}')
raise ApplicationException(status_code=401, message='Invalid token') raise InvalidTokenException()
except Exception as exception: except Exception as exception:
self._logger.error(f'Unexpected JWT decode error kid={kid} error={str(exception)}') self._logger.error(f'Unexpected JWT decode error kid={kid} error={str(exception)}')
raise ApplicationException(status_code=500, message='JWT decode failed') raise JwtDecodeFailedException()

View File

@@ -3,13 +3,14 @@ from contextlib import asynccontextmanager
import secrets import secrets
from typing import AsyncGenerator from typing import AsyncGenerator
from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.schedulers.asyncio import AsyncIOScheduler
from fastapi import Depends, FastAPI, status from fastapi import Depends,FastAPI
from fastapi.exceptions import RequestValidationError
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.commands import PollKycSessionsCommand from src.application.commands import PollKycSessionsCommand
from src.application.domain.enums import LogFormat,LogLevel from src.application.domain.enums import LogFormat,LogLevel
from src.application.domain.exceptions import ApplicationException from src.application.domain.exceptions import ApplicationException,UnauthorizedException
from src.infrastructure.beorg import BeorgService from src.infrastructure.beorg import BeorgService
from src.infrastructure.config.settings import get_settings from src.infrastructure.config.settings import get_settings
from src.infrastructure.database.context import async_session_maker from src.infrastructure.database.context import async_session_maker
@@ -18,7 +19,7 @@ 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.handlers import application_exception_handler,unhandled_exception_handler,validation_exception_handler
from src.presentation.middleware import TraceIDMiddleware, SecurityHeadersMiddleware from src.presentation.middleware import TraceIDMiddleware, SecurityHeadersMiddleware
from src.presentation.routing import kyc_router from src.presentation.routing import kyc_router
@@ -29,11 +30,9 @@ 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( exception = UnauthorizedException()
status_code=status.HTTP_401_UNAUTHORIZED, exception.headers = {'WWW-Authenticate': 'Basic'}
message='Unauthorized', raise exception
headers={'WWW-Authenticate': 'Basic'},
)
return credentials return credentials
@@ -97,6 +96,7 @@ app: FastAPI = FastAPI(
) )
app.add_exception_handler(ApplicationException, application_exception_handler) app.add_exception_handler(ApplicationException, application_exception_handler)
app.add_exception_handler(RequestValidationError, validation_exception_handler)
app.add_exception_handler(Exception, unhandled_exception_handler) app.add_exception_handler(Exception, unhandled_exception_handler)
app.include_router(kyc_router) app.include_router(kyc_router)

View File

@@ -1,7 +1,7 @@
from fastapi import Depends, Request from fastapi import Depends, Request
from fastapi.security.utils import get_authorization_scheme_param from fastapi.security.utils import get_authorization_scheme_param
from src.application.contracts import IJwtService from src.application.contracts import IJwtService
from src.application.domain.exceptions import ApplicationException from src.application.domain.exceptions import InvalidTokenException,NotAuthenticatedException
from src.application.domain.dto import AccessTokenPayload, AuthContext from src.application.domain.dto import AccessTokenPayload, AuthContext
from src.presentation.dependencies import get_jwt_service from src.presentation.dependencies import get_jwt_service
@@ -27,10 +27,10 @@ async def require_access_token(
) -> AuthContext: ) -> AuthContext:
token = _extract_access_token(request) token = _extract_access_token(request)
if not token: if not token:
raise ApplicationException(status_code=401, message='Not authenticated') raise NotAuthenticatedException()
payload: AccessTokenPayload = await jwt_service.decode_access_token(token) payload: AccessTokenPayload = await jwt_service.decode_access_token(token)
if payload.type != 'access': if payload.type != 'access':
raise ApplicationException(status_code=401, message='Invalid token type') raise InvalidTokenException('Invalid token type')
return AuthContext(user_id=payload.sub, sid=payload.sid, token=payload) return AuthContext(user_id=payload.sub, sid=payload.sid, token=payload)

View File

@@ -3,7 +3,7 @@ import inspect
from functools import wraps from functools import wraps
from typing import Callable, Awaitable, Any, Optional, Annotated from typing import Callable, Awaitable, Any, Optional, Annotated
from fastapi import Request, Header from fastapi import Request, Header
from src.application.domain.exceptions import ApplicationException from src.application.domain.exceptions import CsrfRequestRequiredException
from src.infrastructure.security import CsrfService from src.infrastructure.security import CsrfService
@@ -39,10 +39,7 @@ def csrf_protect(
break break
if request is None: if request is None:
raise ApplicationException( raise CsrfRequestRequiredException()
status_code=500,
message='Request is required for CSRF protection',
)
csrf = CsrfService() csrf = CsrfService()

View File

@@ -1,4 +1,5 @@
from fastapi import Request from fastapi import Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import ORJSONResponse from fastapi.responses import ORJSONResponse
from src.application.domain.exceptions import ApplicationException from src.application.domain.exceptions import ApplicationException
@@ -6,13 +7,35 @@ from src.application.domain.exceptions import ApplicationException
async def application_exception_handler(request: Request,exception: ApplicationException) -> ORJSONResponse: async def application_exception_handler(request: Request,exception: ApplicationException) -> ORJSONResponse:
return ORJSONResponse( return ORJSONResponse(
status_code=exception.status_code, status_code=exception.status_code,
content={'detail': exception.message}, content={
'error': {
'code': exception.error_code,
'message': exception.message,
},
},
headers=exception.headers, headers=exception.headers,
) )
async def validation_exception_handler(request: Request,exception: RequestValidationError) -> ORJSONResponse:
return ORJSONResponse(
status_code=422,
content={
'error': {
'code': 'request_validation_error',
'message': 'Request validation error',
},
},
)
async def unhandled_exception_handler(request: Request,exception: Exception) -> ORJSONResponse: async def unhandled_exception_handler(request: Request,exception: Exception) -> ORJSONResponse:
return ORJSONResponse( return ORJSONResponse(
status_code=500, status_code=500,
content={'detail': 'Internal server error'}, content={
'error': {
'code': 'internal_server_error',
'message': 'Internal server error',
},
},
) )

View File

@@ -1,27 +1,81 @@
from fastapi import APIRouter,Depends from fastapi import APIRouter,Depends
from fastapi.responses import ORJSONResponse
from src.application.commands import GetKycSessionCommand,PassKycCommand from src.application.commands import GetKycSessionCommand,PassKycCommand
from src.application.domain.dto import AuthContext from src.application.domain.dto import AuthContext,BeorgKycCreateResponse,KycSessionResponse
from src.presentation.decorators.auth import require_access_token from src.presentation.decorators.auth import require_access_token
from src.presentation.dependencies.commands import get_kyc_session_command,get_pass_kyc_command from src.presentation.dependencies.commands import get_kyc_session_command,get_pass_kyc_command
from src.presentation.schemas import ErrorResponse
kyc_router = APIRouter(prefix='/kyc', tags=['Kyc']) kyc_router = APIRouter(prefix='/kyc', tags=['Kyc'])
CREATE_KYC_RESPONSES = {
400: {
'model': ErrorResponse,
'description': 'Beorg rejected request. error.code: beorg_rejected',
},
401: {
'model': ErrorResponse,
'description': 'Authentication error. error.code: not_authenticated, invalid_token',
},
500: {
'model': ErrorResponse,
'description': 'Configuration or internal error. error.code: beorg_not_configured, internal_server_error',
},
502: {
'model': ErrorResponse,
'description': 'Beorg is unavailable. error.code: beorg_unavailable',
},
422: {
'model': ErrorResponse,
'description': 'Request validation error. error.code: request_validation_error',
},
}
@kyc_router.post('/create') GET_KYC_SESSION_RESPONSES = {
401: {
'model': ErrorResponse,
'description': 'Authentication error. error.code: not_authenticated, invalid_token',
},
404: {
'model': ErrorResponse,
'description': 'Active KYC session was not found. error.code: kyc_session_expired',
},
500: {
'model': ErrorResponse,
'description': 'Internal error. error.code: internal_server_error',
},
422: {
'model': ErrorResponse,
'description': 'Request validation error. error.code: request_validation_error',
},
}
@kyc_router.post(
'/create',
response_model=BeorgKycCreateResponse,
responses=CREATE_KYC_RESPONSES,
summary='Start KYC session',
description='Creates a Beorg KYC session for one hour and returns link, user token and QR code.',
)
async def create_kyc( async def create_kyc(
#auth: AuthContext = Depends(require_access_token), #auth: AuthContext = Depends(require_access_token),
command: PassKycCommand = Depends(get_pass_kyc_command), command: PassKycCommand = Depends(get_pass_kyc_command),
) -> ORJSONResponse: ) -> BeorgKycCreateResponse:
result = await command(user_id='01KPKAFN6J1NJBY15DX8JE2QYB') result = await command(user_id='01KPKAFN6J1NJBY15DX8JE2QYB')
return ORJSONResponse(result.model_dump()) return result
@kyc_router.get('/session') @kyc_router.get(
'/session',
response_model=KycSessionResponse,
responses=GET_KYC_SESSION_RESPONSES,
summary='Get KYC session',
description='Returns latest KYC session status, link, QR code and expiration data.',
)
async def get_kyc_session( async def get_kyc_session(
#auth: AuthContext = Depends(require_access_token), #auth: AuthContext = Depends(require_access_token),
command: GetKycSessionCommand = Depends(get_kyc_session_command), command: GetKycSessionCommand = Depends(get_kyc_session_command),
) -> ORJSONResponse: ) -> KycSessionResponse:
result = await command(user_id='01KPKAFN6J1NJBY15DX8JE2QYB') result = await command(user_id='01KPKAFN6J1NJBY15DX8JE2QYB')
return ORJSONResponse(result.model_dump()) return result

View File

@@ -0,0 +1 @@
from src.presentation.schemas.error import ErrorDetail,ErrorResponse

View File

@@ -0,0 +1,10 @@
from pydantic import BaseModel
class ErrorDetail(BaseModel):
code: str
message: str
class ErrorResponse(BaseModel):
error: ErrorDetail