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 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()

View File

@@ -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: [

View File

@@ -75,8 +75,10 @@ class KycRepository(IKycRepository):
.where(
KycModel.user_id == user_id,
KycModel.status == 'started',
KycModel.expires_at.is_not(None),
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),
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)

View File

@@ -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,
)

View File

@@ -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),