featL add mre
This commit is contained in:
@@ -16,7 +16,7 @@ class IKycRepository(ABC):
|
|||||||
client_user_token: str | None,
|
client_user_token: str | None,
|
||||||
link: str | None,
|
link: str | None,
|
||||||
qr_code: str | None,
|
qr_code: str | None,
|
||||||
expires_at: datetime,
|
expires_at: datetime | None,
|
||||||
error: str | None,
|
error: str | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from datetime import datetime,timedelta,timezone
|
from datetime import datetime,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
|
||||||
@@ -7,9 +8,6 @@ from src.application.domain.exceptions import ApplicationException,KycFailedExce
|
|||||||
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
|
||||||
|
|
||||||
|
|
||||||
KYC_SESSION_TTL = 3600
|
|
||||||
|
|
||||||
|
|
||||||
class PassKycCommand:
|
class PassKycCommand:
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -32,8 +30,7 @@ class PassKycCommand:
|
|||||||
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}'
|
||||||
)
|
)
|
||||||
expires_at = _utc_now() + timedelta(seconds=KYC_SESSION_TTL)
|
self._logger.info(f'KYC session without expiry persisted for user {user_id}')
|
||||||
self._logger.info(f'KYC session expiration calculated for user {user_id} ttl={KYC_SESSION_TTL} expires_at={expires_at.isoformat()}')
|
|
||||||
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(
|
||||||
@@ -42,7 +39,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=expires_at,
|
expires_at=None,
|
||||||
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}')
|
||||||
@@ -143,10 +140,11 @@ class GetKycSessionCommand:
|
|||||||
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.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:
|
||||||
raise KycSessionExpiredException()
|
raise KycSessionExpiredException()
|
||||||
|
|
||||||
expires_in = max(int((session.expires_at - now).total_seconds()),0)
|
expires_at = session.expires_at
|
||||||
|
expires_in = None if expires_at is None else max(int((expires_at - now).total_seconds()),0)
|
||||||
return KycSessionResponse(
|
return KycSessionResponse(
|
||||||
status=session.status or 'started',
|
status=session.status or 'started',
|
||||||
link=session.link,
|
link=session.link,
|
||||||
@@ -154,7 +152,7 @@ class GetKycSessionCommand:
|
|||||||
user_token=session.user_token,
|
user_token=session.user_token,
|
||||||
done_state=session.done_state,
|
done_state=session.done_state,
|
||||||
error=session.error,
|
error=session.error,
|
||||||
expires_at=session.expires_at,
|
expires_at=expires_at,
|
||||||
expires_in=expires_in,
|
expires_in=expires_in,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -36,5 +36,5 @@ class KycSessionResponse(BaseModel):
|
|||||||
user_token: str | None = None
|
user_token: str | None = None
|
||||||
done_state: bool | None = None
|
done_state: bool | None = None
|
||||||
error: str | None = None
|
error: str | None = None
|
||||||
expires_at: datetime
|
expires_at: datetime | None = None
|
||||||
expires_in: int
|
expires_in: int | None = None
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class UserEntity:
|
|||||||
|
|
||||||
passport_data: str | None = None
|
passport_data: str | None = None
|
||||||
inn: str | None = None
|
inn: str | None = None
|
||||||
|
erc20: str | None = None
|
||||||
|
|
||||||
kyc_verified: bool | None = None
|
kyc_verified: bool | None = None
|
||||||
is_deleted: bool | None = None
|
is_deleted: bool | None = None
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ FIELD_ALIASES = {
|
|||||||
|
|
||||||
|
|
||||||
def extract_personal_data(data: Any) -> KycPersonalData:
|
def extract_personal_data(data: Any) -> KycPersonalData:
|
||||||
|
beorg = _try_extract_beorg_documents(data)
|
||||||
|
if beorg is not None:
|
||||||
|
return beorg
|
||||||
values: dict[str,str] = {}
|
values: dict[str,str] = {}
|
||||||
for key,value in _walk(data):
|
for key,value in _walk(data):
|
||||||
normalized = _normalize_key(key)
|
normalized = _normalize_key(key)
|
||||||
@@ -46,7 +49,93 @@ def extract_personal_data(data: Any) -> KycPersonalData:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def ensure_adult(birth_date: date) -> None:
|
def _try_extract_beorg_documents(data: Any) -> KycPersonalData | None:
|
||||||
|
if isinstance(data,dict) and 'documents' not in data:
|
||||||
|
inner = data.get('data')
|
||||||
|
if isinstance(inner,dict):
|
||||||
|
nested = _try_extract_beorg_documents(inner)
|
||||||
|
if nested is not None:
|
||||||
|
return nested
|
||||||
|
if not isinstance(data,dict):
|
||||||
|
return None
|
||||||
|
documents = data.get('documents')
|
||||||
|
if not isinstance(documents,list) or not documents:
|
||||||
|
return None
|
||||||
|
|
||||||
|
passport: dict[str,Any] | None = None
|
||||||
|
for item in documents:
|
||||||
|
if not isinstance(item,dict):
|
||||||
|
continue
|
||||||
|
dtype = str(item.get('type') or '').strip().upper()
|
||||||
|
dkey = str(item.get('key') or '').strip().upper()
|
||||||
|
if dtype == 'PASSPORT' or 'PASSPORT' in dtype or dkey.startswith('PASSPORT'):
|
||||||
|
passport = item
|
||||||
|
break
|
||||||
|
|
||||||
|
if passport is None:
|
||||||
|
for item in documents:
|
||||||
|
if not isinstance(item,dict):
|
||||||
|
continue
|
||||||
|
pdata = item.get('data')
|
||||||
|
if isinstance(pdata,dict) and any(k in pdata for k in ('LastName','FirstName','Series','Number')):
|
||||||
|
passport = item
|
||||||
|
break
|
||||||
|
|
||||||
|
if passport is None or not isinstance(passport.get('data'),dict):
|
||||||
|
return None
|
||||||
|
|
||||||
|
pdata = passport['data']
|
||||||
|
last_name = _norm_text(pdata.get('LastName'),pdata.get('last_name'))
|
||||||
|
first_name = _norm_text(pdata.get('FirstName'),pdata.get('first_name'))
|
||||||
|
middle_raw = pdata.get('MiddleName')
|
||||||
|
if middle_raw is None:
|
||||||
|
middle_raw = pdata.get('middle_name')
|
||||||
|
ms = _norm_scalar(middle_raw)
|
||||||
|
middle_name = ms if ms else None
|
||||||
|
birth_raw = pdata.get('BirthDate') or pdata.get('birth_date')
|
||||||
|
birth_ts = _norm_scalar(birth_raw)
|
||||||
|
birth_date_raw = birth_ts if birth_ts else ''
|
||||||
|
series = (_norm_scalar(pdata.get('Series')) or '').strip()
|
||||||
|
number = (_norm_scalar(pdata.get('Number')) or '').strip()
|
||||||
|
passport_data = f'{series}{number}'.strip() or None
|
||||||
|
|
||||||
|
inn_val = None
|
||||||
|
meta = passport.get('metadata')
|
||||||
|
if isinstance(meta,dict):
|
||||||
|
ext = meta.get('external_integrations')
|
||||||
|
if isinstance(ext,dict):
|
||||||
|
inn_block = ext.get('inn')
|
||||||
|
if isinstance(inn_block,dict):
|
||||||
|
val = inn_block.get('value')
|
||||||
|
if val not in (None,'',False):
|
||||||
|
inn_val = str(val).strip()
|
||||||
|
|
||||||
|
if not first_name or not last_name or not birth_date_raw:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return KycPersonalData(
|
||||||
|
first_name=first_name,
|
||||||
|
last_name=last_name,
|
||||||
|
middle_name=middle_name,
|
||||||
|
birth_date=str(_parse_date(birth_date_raw)),
|
||||||
|
inn=inn_val,
|
||||||
|
passport_data=passport_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _norm_scalar(value: Any) -> str | None:
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
s = str(value).strip()
|
||||||
|
return s if s else None
|
||||||
|
|
||||||
|
|
||||||
|
def _norm_text(*candidates: Any) -> str:
|
||||||
|
for cand in candidates:
|
||||||
|
s = _norm_scalar(cand)
|
||||||
|
if s:
|
||||||
|
return s
|
||||||
|
return ''
|
||||||
today = date.today()
|
today = date.today()
|
||||||
try:
|
try:
|
||||||
adult_from = date(today.year - 18,today.month,today.day)
|
adult_from = date(today.year - 18,today.month,today.day)
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ class BeorgService(IBeorgService):
|
|||||||
self._machine_uid = machine_uid
|
self._machine_uid = machine_uid
|
||||||
self._token = token
|
self._token = token
|
||||||
self._process_info = process_info
|
self._process_info = process_info
|
||||||
self._expires = 3600
|
|
||||||
self._timeout = timeout
|
self._timeout = timeout
|
||||||
|
|
||||||
|
|
||||||
@@ -36,7 +35,6 @@ class BeorgService(IBeorgService):
|
|||||||
'token': self._token,
|
'token': self._token,
|
||||||
'process_info': self._process_info,
|
'process_info': self._process_info,
|
||||||
'client_user_token': client_user_token,
|
'client_user_token': client_user_token,
|
||||||
'expires': self._expires,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
timeout = aiohttp.ClientTimeout(total=self._timeout)
|
timeout = aiohttp.ClientTimeout(total=self._timeout)
|
||||||
|
|||||||
@@ -20,5 +20,5 @@ class KycModel(Base,UlidPrimaryKeyMixin,AuditTimestampsMixin,SoftDeleteMixin):
|
|||||||
set_id: Mapped[str | None] = mapped_column(String(255),nullable=True,index=True)
|
set_id: Mapped[str | None] = mapped_column(String(255),nullable=True,index=True)
|
||||||
error: Mapped[str | None] = mapped_column(Text,nullable=True)
|
error: Mapped[str | None] = mapped_column(Text,nullable=True)
|
||||||
result_data: Mapped[Any | None] = mapped_column(JSON,nullable=True)
|
result_data: Mapped[Any | None] = mapped_column(JSON,nullable=True)
|
||||||
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True),nullable=False,index=True)
|
expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True),nullable=True,index=True)
|
||||||
completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True),nullable=True)
|
completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True),nullable=True)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class UserModel(Base, UlidPrimaryKeyMixin, AuditTimestampsMixin, SoftDeleteMixin
|
|||||||
|
|
||||||
passport_data: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
passport_data: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||||
inn: Mapped[str | None] = mapped_column(String(12), nullable=True)
|
inn: Mapped[str | None] = mapped_column(String(12), nullable=True)
|
||||||
|
erc20: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||||
|
|
||||||
kyc_verified: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='false', default=False)
|
kyc_verified: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='false', default=False)
|
||||||
kyc_verified_at: Mapped[DateTime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
kyc_verified_at: Mapped[DateTime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from datetime import datetime,timezone
|
from datetime import datetime,timezone
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from sqlalchemy import select
|
from sqlalchemy import or_,select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from src.application.abstractions.repositories import IKycRepository
|
from src.application.abstractions.repositories import IKycRepository
|
||||||
from src.application.domain.entities import KycEntity
|
from src.application.domain.entities import KycEntity
|
||||||
@@ -22,7 +22,7 @@ class KycRepository(IKycRepository):
|
|||||||
client_user_token: str | None,
|
client_user_token: str | None,
|
||||||
link: str | None,
|
link: str | None,
|
||||||
qr_code: str | None,
|
qr_code: str | None,
|
||||||
expires_at: datetime,
|
expires_at: datetime | None,
|
||||||
error: str | None,
|
error: str | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
kyc = KycModel(
|
kyc = KycModel(
|
||||||
@@ -75,6 +75,7 @@ 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),
|
||||||
KycModel.expires_at <= now,
|
KycModel.expires_at <= now,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -88,6 +89,7 @@ class KycRepository(IKycRepository):
|
|||||||
select(KycModel)
|
select(KycModel)
|
||||||
.where(
|
.where(
|
||||||
KycModel.status == 'started',
|
KycModel.status == 'started',
|
||||||
|
KycModel.expires_at.is_not(None),
|
||||||
KycModel.expires_at <= now,
|
KycModel.expires_at <= now,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -101,8 +103,8 @@ class KycRepository(IKycRepository):
|
|||||||
select(KycModel)
|
select(KycModel)
|
||||||
.where(
|
.where(
|
||||||
KycModel.status == 'started',
|
KycModel.status == 'started',
|
||||||
KycModel.expires_at > now,
|
|
||||||
KycModel.user_token.is_not(None),
|
KycModel.user_token.is_not(None),
|
||||||
|
or_(KycModel.expires_at.is_(None),KycModel.expires_at > now),
|
||||||
)
|
)
|
||||||
.order_by(KycModel.created_at.asc())
|
.order_by(KycModel.created_at.asc())
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
@@ -116,7 +118,7 @@ class KycRepository(IKycRepository):
|
|||||||
.where(
|
.where(
|
||||||
KycModel.user_id == user_id,
|
KycModel.user_id == user_id,
|
||||||
KycModel.status == 'started',
|
KycModel.status == 'started',
|
||||||
KycModel.expires_at > now,
|
or_(KycModel.expires_at.is_(None),KycModel.expires_at > now),
|
||||||
)
|
)
|
||||||
.order_by(KycModel.created_at.desc())
|
.order_by(KycModel.created_at.desc())
|
||||||
.limit(1)
|
.limit(1)
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ class UserRepository(IUserRepository):
|
|||||||
phone=user.phone,
|
phone=user.phone,
|
||||||
passport_data=user.passport_data,
|
passport_data=user.passport_data,
|
||||||
inn=user.inn,
|
inn=user.inn,
|
||||||
|
erc20=user.erc20,
|
||||||
kyc_verified=user.kyc_verified,
|
kyc_verified=user.kyc_verified,
|
||||||
is_deleted=user.is_deleted,
|
is_deleted=user.is_deleted,
|
||||||
created_at=user.created_at,
|
created_at=user.created_at,
|
||||||
|
|||||||
@@ -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 for one hour and returns link, user token and QR code.',
|
description='Creates a Beorg KYC session without link expiry at the issuer and persists it without local expiration.',
|
||||||
)
|
)
|
||||||
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 status, link, QR code and expiration data.',
|
description='Returns latest KYC session; expires_at and expires_in are omitted when the session has no expiry.',
|
||||||
)
|
)
|
||||||
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