feat: add time of kyc session
This commit is contained in:
@@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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: [
|
||||||
|
|||||||
@@ -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.is_(None),
|
||||||
KycModel.expires_at <= now,
|
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.is_(None),
|
||||||
KycModel.expires_at <= now,
|
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)
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
Reference in New Issue
Block a user