feat: add time of kyc session

This commit is contained in:
2026-05-14 15:17:58 +03:00
parent 57807c105d
commit 0eb894c1ee
5 changed files with 42 additions and 18 deletions

View File

@@ -1,13 +1,25 @@
from __future__ import annotations from __future__ import annotations
from datetime import datetime,timezone from datetime import datetime,timezone,timedelta
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.entities import KycEntity
from src.application.domain.exceptions import ApplicationException,KycFailedException,KycNotCompletedException,KycSessionExpiredException,KycSessionMissingUserTokenException 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
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: class PassKycCommand:
def __init__( def __init__(
@@ -16,21 +28,31 @@ class PassKycCommand:
unit_of_work: IUnitOfWork, unit_of_work: IUnitOfWork,
logger: ILogger, logger: ILogger,
beorg_service: IBeorgService, beorg_service: IBeorgService,
kyc_session_ttl_seconds: int,
) -> None: ) -> None:
self._unit_of_work = unit_of_work self._unit_of_work = unit_of_work
self._logger = logger self._logger = logger
self._beorg_service = beorg_service self._beorg_service = beorg_service
self._kyc_session_ttl_seconds = kyc_session_ttl_seconds
async def __call__(self,user_id: str) -> BeorgKycCreateResponse: async def __call__(self,user_id: str) -> BeorgKycCreateResponse:
now = _utc_now()
self._logger.info(f'KYC session creation started for user {user_id}') 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}') 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) result = await self._beorg_service.create_identification(client_user_token=user_id)
self._logger.info( self._logger.info(
f'KYC Beorg identification request finished for user {user_id} ' 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}' 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}') self._logger.info(f'KYC session database save started for user {user_id}')
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.create_started_session( await unit_of_work.kyc_repository.create_started_session(
@@ -39,7 +61,7 @@ class PassKycCommand:
client_user_token=result.client_user_token, client_user_token=result.client_user_token,
link=result.link, link=result.link,
qr_code=result.qr_code, qr_code=result.qr_code,
expires_at=None, expires_at=expires_at,
error=result.error, error=result.error,
) )
self._logger.info(f'KYC session database save finished for user {user_id}') 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) 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) session = await unit_of_work.kyc_repository.get_active_session(user_id=user_id,now=now)
if session is not None: if session is not None:
return BeorgKycCreateResponse( return _kyc_started_entity_to_create_response(session)
status=True,
link=session.link,
user_token=session.user_token,
client_user_token=session.client_user_token,
qr_code=session.qr_code,
)
raise KycSessionExpiredException() raise KycSessionExpiredException()

View File

@@ -38,6 +38,7 @@ class Settings(BaseSettings):
DATABASE_ECHO: bool = False DATABASE_ECHO: bool = False
KYC_POLL_SECONDS: int = 10 KYC_POLL_SECONDS: int = 10
KYC_POLL_BATCH_SIZE: int = 20 KYC_POLL_BATCH_SIZE: int = 20
KYC_SESSION_TTL_SECONDS: int = 900
EXCLUDED_PATHS: tuple[str,...] = ('/docs','/redoc','/openapi.json','/ping') EXCLUDED_PATHS: tuple[str,...] = ('/docs','/redoc','/openapi.json','/ping')
BEORG_TIMEOUT: int = 120 BEORG_TIMEOUT: int = 120
BEORG_PROCESS_INFO: list[dict[str,Any]] = Field(default_factory=lambda: [ BEORG_PROCESS_INFO: list[dict[str,Any]] = Field(default_factory=lambda: [

View File

@@ -75,8 +75,10 @@ class KycRepository(IKycRepository):
.where( .where(
KycModel.user_id == user_id, KycModel.user_id == user_id,
KycModel.status == 'started', KycModel.status == 'started',
KycModel.expires_at.is_not(None), or_(
KycModel.expires_at <= now, KycModel.expires_at.is_(None),
KycModel.expires_at <= now,
),
) )
) )
for kyc in result.scalars(): for kyc in result.scalars():
@@ -89,8 +91,10 @@ class KycRepository(IKycRepository):
select(KycModel) select(KycModel)
.where( .where(
KycModel.status == 'started', KycModel.status == 'started',
KycModel.expires_at.is_not(None), or_(
KycModel.expires_at <= now, KycModel.expires_at.is_(None),
KycModel.expires_at <= now,
),
) )
) )
for kyc in result.scalars(): for kyc in result.scalars():
@@ -104,7 +108,8 @@ class KycRepository(IKycRepository):
.where( .where(
KycModel.status == 'started', KycModel.status == 'started',
KycModel.user_token.is_not(None), 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()) .order_by(KycModel.created_at.asc())
.limit(limit) .limit(limit)
@@ -118,7 +123,8 @@ class KycRepository(IKycRepository):
.where( .where(
KycModel.user_id == user_id, KycModel.user_id == user_id,
KycModel.status == 'started', 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()) .order_by(KycModel.created_at.desc())
.limit(1) .limit(1)

View File

@@ -28,6 +28,7 @@ def get_pass_kyc_command(
unit_of_work=unit_of_work, unit_of_work=unit_of_work,
logger=logger, logger=logger,
beorg_service=beorg_service, beorg_service=beorg_service,
kyc_session_ttl_seconds=settings.KYC_SESSION_TTL_SECONDS,
) )

View File

@@ -56,7 +56,7 @@ GET_KYC_SESSION_RESPONSES = {
response_model=BeorgKycCreateResponse, response_model=BeorgKycCreateResponse,
responses=CREATE_KYC_RESPONSES, responses=CREATE_KYC_RESPONSES,
summary='Start KYC session', 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( async def create_kyc(
auth: AuthContext = Depends(require_access_token), auth: AuthContext = Depends(require_access_token),
@@ -71,7 +71,7 @@ async def create_kyc(
response_model=KycSessionResponse, response_model=KycSessionResponse,
responses=GET_KYC_SESSION_RESPONSES, responses=GET_KYC_SESSION_RESPONSES,
summary='Get KYC session', 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( async def get_kyc_session(
auth: AuthContext = Depends(require_access_token), auth: AuthContext = Depends(require_access_token),