diff --git a/src/application/commands/create_kyc_command.py b/src/application/commands/create_kyc_command.py index c769683..16814cd 100644 --- a/src/application/commands/create_kyc_command.py +++ b/src/application/commands/create_kyc_command.py @@ -1,13 +1,25 @@ from __future__ import annotations -from datetime import datetime,timezone +from datetime import datetime,timezone,timedelta from src.application.abstractions import IUnitOfWork from src.application.contracts import IBeorgService,ILogger from src.application.domain.dto import BeorgKycCreateResponse,BeorgKycResultResponse,KycSessionResponse +from src.application.domain.entities import KycEntity from src.application.domain.exceptions import ApplicationException,KycFailedException,KycNotCompletedException,KycSessionExpiredException,KycSessionMissingUserTokenException from src.application.services import ensure_adult,extract_personal_data,parse_birth_date +def _kyc_started_entity_to_create_response(entity: KycEntity) -> BeorgKycCreateResponse: + return BeorgKycCreateResponse( + status=True, + error=entity.error, + link=entity.link, + user_token=entity.user_token, + client_user_token=entity.client_user_token, + qr_code=entity.qr_code, + ) + + class PassKycCommand: def __init__( @@ -16,21 +28,31 @@ class PassKycCommand: unit_of_work: IUnitOfWork, logger: ILogger, beorg_service: IBeorgService, + kyc_session_ttl_seconds: int, ) -> None: self._unit_of_work = unit_of_work self._logger = logger self._beorg_service = beorg_service + self._kyc_session_ttl_seconds = kyc_session_ttl_seconds async def __call__(self,user_id: str) -> BeorgKycCreateResponse: + now = _utc_now() self._logger.info(f'KYC session creation started for user {user_id}') + async with self._unit_of_work as unit_of_work: + await unit_of_work.kyc_repository.expire_started_sessions(user_id=user_id,now=now) + existing = await unit_of_work.kyc_repository.get_active_session(user_id=user_id,now=now) + if existing is not None: + self._logger.info(f'KYC session reuse returned for user {user_id}') + return _kyc_started_entity_to_create_response(existing) + self._logger.info(f'KYC Beorg identification request started for user {user_id}') result = await self._beorg_service.create_identification(client_user_token=user_id) self._logger.info( f'KYC Beorg identification request finished for user {user_id} ' f'status={result.status} user_token={result.user_token} client_user_token={result.client_user_token}' ) - self._logger.info(f'KYC session without expiry persisted for user {user_id}') + expires_at = now + timedelta(seconds=self._kyc_session_ttl_seconds) self._logger.info(f'KYC session database save started for user {user_id}') async with self._unit_of_work as unit_of_work: await unit_of_work.kyc_repository.create_started_session( @@ -39,7 +61,7 @@ class PassKycCommand: client_user_token=result.client_user_token, link=result.link, qr_code=result.qr_code, - expires_at=None, + expires_at=expires_at, error=result.error, ) self._logger.info(f'KYC session database save finished for user {user_id}') @@ -115,13 +137,7 @@ class CompleteKycCommand: await unit_of_work.kyc_repository.expire_started_sessions(user_id=user_id,now=now) session = await unit_of_work.kyc_repository.get_active_session(user_id=user_id,now=now) if session is not None: - return BeorgKycCreateResponse( - status=True, - link=session.link, - user_token=session.user_token, - client_user_token=session.client_user_token, - qr_code=session.qr_code, - ) + return _kyc_started_entity_to_create_response(session) raise KycSessionExpiredException() diff --git a/src/infrastructure/config/settings.py b/src/infrastructure/config/settings.py index 78d8ea3..ad65ce5 100644 --- a/src/infrastructure/config/settings.py +++ b/src/infrastructure/config/settings.py @@ -38,6 +38,7 @@ class Settings(BaseSettings): DATABASE_ECHO: bool = False KYC_POLL_SECONDS: int = 10 KYC_POLL_BATCH_SIZE: int = 20 + KYC_SESSION_TTL_SECONDS: int = 900 EXCLUDED_PATHS: tuple[str,...] = ('/docs','/redoc','/openapi.json','/ping') BEORG_TIMEOUT: int = 120 BEORG_PROCESS_INFO: list[dict[str,Any]] = Field(default_factory=lambda: [ diff --git a/src/infrastructure/database/repositories/kyc_repository.py b/src/infrastructure/database/repositories/kyc_repository.py index a672adb..94d5899 100644 --- a/src/infrastructure/database/repositories/kyc_repository.py +++ b/src/infrastructure/database/repositories/kyc_repository.py @@ -75,8 +75,10 @@ class KycRepository(IKycRepository): .where( KycModel.user_id == user_id, KycModel.status == 'started', - KycModel.expires_at.is_not(None), - KycModel.expires_at <= now, + or_( + KycModel.expires_at.is_(None), + KycModel.expires_at <= now, + ), ) ) for kyc in result.scalars(): @@ -89,8 +91,10 @@ class KycRepository(IKycRepository): select(KycModel) .where( KycModel.status == 'started', - KycModel.expires_at.is_not(None), - KycModel.expires_at <= now, + or_( + KycModel.expires_at.is_(None), + KycModel.expires_at <= now, + ), ) ) for kyc in result.scalars(): @@ -104,7 +108,8 @@ class KycRepository(IKycRepository): .where( KycModel.status == 'started', KycModel.user_token.is_not(None), - or_(KycModel.expires_at.is_(None),KycModel.expires_at > now), + KycModel.expires_at.is_not(None), + KycModel.expires_at > now, ) .order_by(KycModel.created_at.asc()) .limit(limit) @@ -118,7 +123,8 @@ class KycRepository(IKycRepository): .where( KycModel.user_id == user_id, KycModel.status == 'started', - or_(KycModel.expires_at.is_(None),KycModel.expires_at > now), + KycModel.expires_at.is_not(None), + KycModel.expires_at > now, ) .order_by(KycModel.created_at.desc()) .limit(1) diff --git a/src/presentation/dependencies/commands.py b/src/presentation/dependencies/commands.py index eae0409..db94441 100644 --- a/src/presentation/dependencies/commands.py +++ b/src/presentation/dependencies/commands.py @@ -28,6 +28,7 @@ def get_pass_kyc_command( unit_of_work=unit_of_work, logger=logger, beorg_service=beorg_service, + kyc_session_ttl_seconds=settings.KYC_SESSION_TTL_SECONDS, ) diff --git a/src/presentation/routing/kyc.py b/src/presentation/routing/kyc.py index 3d2d3b5..5fd9344 100644 --- a/src/presentation/routing/kyc.py +++ b/src/presentation/routing/kyc.py @@ -56,7 +56,7 @@ GET_KYC_SESSION_RESPONSES = { response_model=BeorgKycCreateResponse, responses=CREATE_KYC_RESPONSES, summary='Start KYC session', - description='Creates a Beorg KYC session without link expiry at the issuer and persists it without local expiration.', + description='Starts or resumes Beorg identification: one active session per user expires after KYC_SESSION_TTL_SECONDS (default 15 minutes); after expiry or failure a new identification can be created.', ) async def create_kyc( auth: AuthContext = Depends(require_access_token), @@ -71,7 +71,7 @@ async def create_kyc( response_model=KycSessionResponse, responses=GET_KYC_SESSION_RESPONSES, summary='Get KYC session', - description='Returns latest KYC session; expires_at and expires_in are omitted when the session has no expiry.', + description='Returns latest KYC session row; TTL sessions include expires_at and expires_in.', ) async def get_kyc_session( auth: AuthContext = Depends(require_access_token),