fix: update endpoints
This commit is contained in:
@@ -42,6 +42,21 @@ class IKycRepository(ABC):
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def expire_all_started_sessions(self,*,now: datetime) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def get_started_sessions(self,*,now: datetime,limit: int) -> list[KycEntity]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def get_active_session(self,*,user_id: str,now: datetime) -> KycEntity | None:
|
async def get_active_session(self,*,user_id: str,now: datetime) -> KycEntity | None:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def get_latest_session(self,*,user_id: str) -> KycEntity | None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
from src.application.commands.create_kyc_command import CompleteKycCommand,GetKycSessionCommand,PassKycCommand
|
from src.application.commands.create_kyc_command import CompleteKycCommand,GetKycSessionCommand,PassKycCommand,PollKycSessionsCommand
|
||||||
@@ -132,7 +132,7 @@ class GetKycSessionCommand:
|
|||||||
now = _utc_now()
|
now = _utc_now()
|
||||||
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_active_session(user_id=user_id,now=now)
|
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 or session.expires_at is None:
|
||||||
raise ApplicationException(status_code=404,message='KYC session expired')
|
raise ApplicationException(status_code=404,message='KYC session expired')
|
||||||
|
|
||||||
@@ -142,10 +142,99 @@ class GetKycSessionCommand:
|
|||||||
link=session.link,
|
link=session.link,
|
||||||
qr_code=session.qr_code,
|
qr_code=session.qr_code,
|
||||||
user_token=session.user_token,
|
user_token=session.user_token,
|
||||||
|
done_state=session.done_state,
|
||||||
|
error=session.error,
|
||||||
expires_at=session.expires_at,
|
expires_at=session.expires_at,
|
||||||
expires_in=expires_in,
|
expires_in=expires_in,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PollKycSessionsCommand:
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
unit_of_work: IUnitOfWork,
|
||||||
|
logger: ILogger,
|
||||||
|
beorg_service: IBeorgService,
|
||||||
|
batch_size: int,
|
||||||
|
) -> None:
|
||||||
|
self._unit_of_work = unit_of_work
|
||||||
|
self._logger = logger
|
||||||
|
self._beorg_service = beorg_service
|
||||||
|
self._batch_size = batch_size
|
||||||
|
|
||||||
|
|
||||||
|
async def __call__(self) -> None:
|
||||||
|
now = _utc_now()
|
||||||
|
async with self._unit_of_work as unit_of_work:
|
||||||
|
await unit_of_work.kyc_repository.expire_all_started_sessions(now=now)
|
||||||
|
sessions = await unit_of_work.kyc_repository.get_started_sessions(now=now,limit=self._batch_size)
|
||||||
|
|
||||||
|
for session in sessions:
|
||||||
|
if not session.user_id or not session.user_token:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
await self._poll_session(user_id=session.user_id,user_token=session.user_token)
|
||||||
|
except ApplicationException as exc:
|
||||||
|
if exc.status_code in (400,403,422):
|
||||||
|
async with self._unit_of_work as unit_of_work:
|
||||||
|
await unit_of_work.kyc_repository.update_session_result(
|
||||||
|
user_id=session.user_id,
|
||||||
|
user_token=session.user_token,
|
||||||
|
status='failed',
|
||||||
|
done_state=True,
|
||||||
|
set_id=None,
|
||||||
|
result_data=session.result_data,
|
||||||
|
error=exc.message,
|
||||||
|
)
|
||||||
|
self._logger.error(f'KYC polling failed for user {session.user_id}: {exc.message}')
|
||||||
|
except Exception as exc:
|
||||||
|
self._logger.error(f'KYC polling failed for user {session.user_id}: {str(exc)}')
|
||||||
|
|
||||||
|
|
||||||
|
async def _poll_session(self,*,user_id: str,user_token: str) -> None:
|
||||||
|
result = await self._beorg_service.get_result(user_token=user_token)
|
||||||
|
if result.done_state is None:
|
||||||
|
return
|
||||||
|
if result.done_state is False:
|
||||||
|
async with self._unit_of_work as unit_of_work:
|
||||||
|
await unit_of_work.kyc_repository.update_session_result(
|
||||||
|
user_id=user_id,
|
||||||
|
user_token=user_token,
|
||||||
|
status='failed',
|
||||||
|
done_state=result.done_state,
|
||||||
|
set_id=result.set_id,
|
||||||
|
result_data=result.data,
|
||||||
|
error='KYC failed',
|
||||||
|
)
|
||||||
|
self._logger.info(f'KYC failed for user {user_id}')
|
||||||
|
return
|
||||||
|
|
||||||
|
personal_data = extract_personal_data(result.data)
|
||||||
|
birth_date = parse_birth_date(personal_data.birth_date)
|
||||||
|
ensure_adult(birth_date)
|
||||||
|
|
||||||
|
async with self._unit_of_work as unit_of_work:
|
||||||
|
await unit_of_work.user_repository.update_kyc_data(
|
||||||
|
user_id=user_id,
|
||||||
|
first_name=personal_data.first_name,
|
||||||
|
last_name=personal_data.last_name,
|
||||||
|
middle_name=personal_data.middle_name,
|
||||||
|
birth_date=birth_date,
|
||||||
|
inn=personal_data.inn,
|
||||||
|
)
|
||||||
|
await unit_of_work.kyc_repository.update_session_result(
|
||||||
|
user_id=user_id,
|
||||||
|
user_token=user_token,
|
||||||
|
status='completed',
|
||||||
|
done_state=result.done_state,
|
||||||
|
set_id=result.set_id,
|
||||||
|
result_data=result.data,
|
||||||
|
error=None,
|
||||||
|
)
|
||||||
|
self._logger.info(f'KYC completed for user {user_id}')
|
||||||
|
|
||||||
|
|
||||||
def _utc_now() -> datetime:
|
def _utc_now() -> datetime:
|
||||||
return datetime.now(timezone.utc)
|
return datetime.now(timezone.utc)
|
||||||
|
|||||||
@@ -33,5 +33,7 @@ class KycSessionResponse(BaseModel):
|
|||||||
link: str | None = None
|
link: str | None = None
|
||||||
qr_code: str | None = None
|
qr_code: str | None = None
|
||||||
user_token: str | None = None
|
user_token: str | None = None
|
||||||
|
done_state: bool | None = None
|
||||||
|
error: str | None = None
|
||||||
expires_at: datetime
|
expires_at: datetime
|
||||||
expires_in: int
|
expires_in: int
|
||||||
|
|||||||
@@ -6,11 +6,12 @@ from src.application.domain.exceptions import ApplicationException
|
|||||||
|
|
||||||
|
|
||||||
FIELD_ALIASES = {
|
FIELD_ALIASES = {
|
||||||
'first_name': {'first_name','name','given_name','имя'},
|
'full_name': {'full_name','fullname','fio','фио'},
|
||||||
'last_name': {'last_name','surname','family_name','фамилия'},
|
'first_name': {'first_name','firstname','name','given_name','givenname','имя'},
|
||||||
'middle_name': {'middle_name','patronymic','отчество'},
|
'last_name': {'last_name','lastname','surname','family_name','familyname','фамилия'},
|
||||||
'birth_date': {'birth_date','birthdate','date_birth','birthday','дата рождения'},
|
'middle_name': {'middle_name','middlename','patronymic','отчество'},
|
||||||
'inn': {'inn','tax_id','инн'},
|
'birth_date': {'birth_date','birthdate','date_birth','datebirth','birthday','дата_рождения'},
|
||||||
|
'inn': {'inn','tax_id','taxid','инн'},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -22,6 +23,14 @@ def extract_personal_data(data: Any) -> KycPersonalData:
|
|||||||
if field not in values and normalized in aliases and value not in (None,''):
|
if field not in values and normalized in aliases and value not in (None,''):
|
||||||
values[field] = str(value).strip()
|
values[field] = str(value).strip()
|
||||||
|
|
||||||
|
if values.get('full_name') and (not values.get('first_name') or not values.get('last_name')):
|
||||||
|
parts = values['full_name'].split()
|
||||||
|
if len(parts) >= 2:
|
||||||
|
values.setdefault('last_name',parts[0])
|
||||||
|
values.setdefault('first_name',parts[1])
|
||||||
|
if len(parts) >= 3:
|
||||||
|
values.setdefault('middle_name',' '.join(parts[2:]))
|
||||||
|
|
||||||
missing = [field for field in ('first_name','last_name','birth_date') if not values.get(field)]
|
missing = [field for field in ('first_name','last_name','birth_date') if not values.get(field)]
|
||||||
if missing:
|
if missing:
|
||||||
raise ApplicationException(status_code=422,message='KYC personal data is incomplete')
|
raise ApplicationException(status_code=422,message='KYC personal data is incomplete')
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ class Settings(BaseSettings):
|
|||||||
DATABASE_POOL_TIMEOUT: int = 30
|
DATABASE_POOL_TIMEOUT: int = 30
|
||||||
DATABASE_POOL_RECYCLE: int = 1800
|
DATABASE_POOL_RECYCLE: int = 1800
|
||||||
DATABASE_ECHO: bool = False
|
DATABASE_ECHO: bool = False
|
||||||
|
KYC_POLL_SECONDS: int = 10
|
||||||
|
KYC_POLL_BATCH_SIZE: int = 20
|
||||||
EXCLUDED_PATHS: tuple[str,...] = ('/docs','/redoc','/openapi.json','/ping')
|
EXCLUDED_PATHS: tuple[str,...] = ('/docs','/redoc','/openapi.json','/ping')
|
||||||
BEORG_TIMEOUT: int = 15
|
BEORG_TIMEOUT: int = 15
|
||||||
BEORG_PROCESS_INFO: list[dict[str,Any]] = Field(default_factory=lambda: [
|
BEORG_PROCESS_INFO: list[dict[str,Any]] = Field(default_factory=lambda: [
|
||||||
|
|||||||
@@ -83,6 +83,33 @@ class KycRepository(IKycRepository):
|
|||||||
await self._session.flush()
|
await self._session.flush()
|
||||||
|
|
||||||
|
|
||||||
|
async def expire_all_started_sessions(self,*,now: datetime) -> None:
|
||||||
|
result = await self._session.execute(
|
||||||
|
select(KycModel)
|
||||||
|
.where(
|
||||||
|
KycModel.status == 'started',
|
||||||
|
KycModel.expires_at <= now,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for kyc in result.scalars():
|
||||||
|
kyc.status = 'expired'
|
||||||
|
await self._session.flush()
|
||||||
|
|
||||||
|
|
||||||
|
async def get_started_sessions(self,*,now: datetime,limit: int) -> list[KycEntity]:
|
||||||
|
result = await self._session.execute(
|
||||||
|
select(KycModel)
|
||||||
|
.where(
|
||||||
|
KycModel.status == 'started',
|
||||||
|
KycModel.expires_at > now,
|
||||||
|
KycModel.user_token.is_not(None),
|
||||||
|
)
|
||||||
|
.order_by(KycModel.created_at.asc())
|
||||||
|
.limit(limit)
|
||||||
|
)
|
||||||
|
return [self._to_entity(kyc) for kyc in result.scalars()]
|
||||||
|
|
||||||
|
|
||||||
async def get_active_session(self,*,user_id: str,now: datetime) -> KycEntity | None:
|
async def get_active_session(self,*,user_id: str,now: datetime) -> KycEntity | None:
|
||||||
result = await self._session.execute(
|
result = await self._session.execute(
|
||||||
select(KycModel)
|
select(KycModel)
|
||||||
@@ -100,6 +127,19 @@ class KycRepository(IKycRepository):
|
|||||||
return self._to_entity(kyc)
|
return self._to_entity(kyc)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_latest_session(self,*,user_id: str) -> KycEntity | None:
|
||||||
|
result = await self._session.execute(
|
||||||
|
select(KycModel)
|
||||||
|
.where(KycModel.user_id == user_id)
|
||||||
|
.order_by(KycModel.created_at.desc())
|
||||||
|
.limit(1)
|
||||||
|
)
|
||||||
|
kyc = result.scalar_one_or_none()
|
||||||
|
if kyc is None:
|
||||||
|
return None
|
||||||
|
return self._to_entity(kyc)
|
||||||
|
|
||||||
|
|
||||||
def _to_entity(self,kyc: KycModel) -> KycEntity:
|
def _to_entity(self,kyc: KycModel) -> KycEntity:
|
||||||
return KycEntity(
|
return KycEntity(
|
||||||
id=kyc.id,
|
id=kyc.id,
|
||||||
|
|||||||
28
src/main.py
28
src/main.py
@@ -2,13 +2,18 @@ from __future__ import annotations
|
|||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
import secrets
|
import secrets
|
||||||
from typing import AsyncGenerator
|
from typing import AsyncGenerator
|
||||||
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||||
from fastapi import Depends, FastAPI, status
|
from fastapi import Depends, FastAPI, status
|
||||||
from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html
|
from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html
|
||||||
from fastapi.responses import HTMLResponse
|
from fastapi.responses import HTMLResponse
|
||||||
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||||
|
from src.application.commands import PollKycSessionsCommand
|
||||||
from src.application.domain.enums import LogFormat,LogLevel
|
from src.application.domain.enums import LogFormat,LogLevel
|
||||||
from src.application.domain.exceptions import ApplicationException
|
from src.application.domain.exceptions import ApplicationException
|
||||||
|
from src.infrastructure.beorg import BeorgService
|
||||||
from src.infrastructure.config.settings import get_settings
|
from src.infrastructure.config.settings import get_settings
|
||||||
|
from src.infrastructure.database.context import async_session_maker
|
||||||
|
from src.infrastructure.database.unit_of_work import UnitOfWork
|
||||||
from src.infrastructure.vault import JwtKeyStore, start_jwt_keys_scheduler
|
from src.infrastructure.vault import JwtKeyStore, start_jwt_keys_scheduler
|
||||||
from src.infrastructure.utils import generate_instance_id
|
from src.infrastructure.utils import generate_instance_id
|
||||||
from src.infrastructure.logger import logger
|
from src.infrastructure.logger import logger
|
||||||
@@ -54,10 +59,33 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|||||||
await jwt_store.refresh()
|
await jwt_store.refresh()
|
||||||
|
|
||||||
jwt_scheduler = start_jwt_keys_scheduler(jwt_store, refresh_seconds=settings.JWT_KEYS_REFRESH_SECONDS)
|
jwt_scheduler = start_jwt_keys_scheduler(jwt_store, refresh_seconds=settings.JWT_KEYS_REFRESH_SECONDS)
|
||||||
|
kyc_poll_command = PollKycSessionsCommand(
|
||||||
|
unit_of_work=UnitOfWork(session_factory=async_session_maker,logger=logger),
|
||||||
|
logger=logger,
|
||||||
|
beorg_service=BeorgService(
|
||||||
|
project_id=settings.BEORG_PROJECT_ID,
|
||||||
|
machine_uid=settings.BEORG_MACHINE_UID,
|
||||||
|
token=settings.BEORG_TOKEN,
|
||||||
|
process_info=settings.BEORG_PROCESS_INFO,
|
||||||
|
timeout=settings.BEORG_TIMEOUT,
|
||||||
|
),
|
||||||
|
batch_size=settings.KYC_POLL_BATCH_SIZE,
|
||||||
|
)
|
||||||
|
kyc_scheduler = AsyncIOScheduler()
|
||||||
|
kyc_scheduler.add_job(
|
||||||
|
kyc_poll_command.__call__,
|
||||||
|
'interval',
|
||||||
|
seconds=settings.KYC_POLL_SECONDS,
|
||||||
|
max_instances=1,
|
||||||
|
)
|
||||||
|
kyc_scheduler.start()
|
||||||
|
|
||||||
app.state.jwt_key_store = jwt_store
|
app.state.jwt_key_store = jwt_store
|
||||||
app.state.jwt_keys_scheduler = jwt_scheduler
|
app.state.jwt_keys_scheduler = jwt_scheduler
|
||||||
|
app.state.kyc_scheduler = kyc_scheduler
|
||||||
yield
|
yield
|
||||||
|
app.state.kyc_scheduler.shutdown(wait=False)
|
||||||
|
app.state.jwt_keys_scheduler.shutdown(wait=False)
|
||||||
logger.info(f'Users service instance ended with id {instance_id}')
|
logger.info(f'Users service instance ended with id {instance_id}')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
from src.application.abstractions import IUnitOfWork
|
from src.application.abstractions import IUnitOfWork
|
||||||
from src.application.commands import CompleteKycCommand,GetKycSessionCommand,PassKycCommand
|
from src.application.commands import GetKycSessionCommand,PassKycCommand
|
||||||
from src.application.contracts import IBeorgService,ILogger
|
from src.application.contracts import IBeorgService,ILogger
|
||||||
from src.infrastructure.config import settings
|
from src.infrastructure.config import settings
|
||||||
from src.infrastructure.beorg import BeorgService
|
from src.infrastructure.beorg import BeorgService
|
||||||
@@ -37,15 +37,3 @@ def get_kyc_session_command(
|
|||||||
return GetKycSessionCommand(
|
return GetKycSessionCommand(
|
||||||
unit_of_work=unit_of_work,
|
unit_of_work=unit_of_work,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_complete_kyc_command(
|
|
||||||
logger: ILogger = Depends(get_logger),
|
|
||||||
unit_of_work: IUnitOfWork = Depends(get_unit_of_work),
|
|
||||||
beorg_service: IBeorgService = Depends(get_beorg_service),
|
|
||||||
) -> CompleteKycCommand:
|
|
||||||
return CompleteKycCommand(
|
|
||||||
unit_of_work=unit_of_work,
|
|
||||||
logger=logger,
|
|
||||||
beorg_service=beorg_service,
|
|
||||||
)
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
from fastapi import APIRouter,Depends
|
from fastapi import APIRouter,Depends
|
||||||
from fastapi.responses import ORJSONResponse
|
from fastapi.responses import ORJSONResponse
|
||||||
from src.application.commands import CompleteKycCommand,GetKycSessionCommand,PassKycCommand
|
from src.application.commands import GetKycSessionCommand,PassKycCommand
|
||||||
from src.application.domain.dto import AuthContext
|
from src.application.domain.dto import AuthContext
|
||||||
from src.presentation.decorators.auth import require_access_token
|
from src.presentation.decorators.auth import require_access_token
|
||||||
from src.presentation.dependencies.commands import get_complete_kyc_command,get_kyc_session_command,get_pass_kyc_command
|
from src.presentation.dependencies.commands import get_kyc_session_command,get_pass_kyc_command
|
||||||
|
|
||||||
|
|
||||||
kyc_router = APIRouter(prefix='/kyc', tags=['Kyc'])
|
kyc_router = APIRouter(prefix='/kyc', tags=['Kyc'])
|
||||||
@@ -11,29 +11,17 @@ kyc_router = APIRouter(prefix='/kyc', tags=['Kyc'])
|
|||||||
|
|
||||||
@kyc_router.post('/create')
|
@kyc_router.post('/create')
|
||||||
async def create_kyc(
|
async def create_kyc(
|
||||||
#auth: AuthContext = Depends(require_access_token),
|
auth: AuthContext = Depends(require_access_token),
|
||||||
command: PassKycCommand = Depends(get_pass_kyc_command),
|
command: PassKycCommand = Depends(get_pass_kyc_command),
|
||||||
) -> ORJSONResponse:
|
) -> ORJSONResponse:
|
||||||
#result = await command(user_id=user_id)
|
result = await command(user_id=auth.user_id)
|
||||||
result = await command(user_id='01KPKAFN6J1NJBY15DX8JE2QYB')
|
|
||||||
return ORJSONResponse(result.model_dump())
|
return ORJSONResponse(result.model_dump())
|
||||||
|
|
||||||
|
|
||||||
@kyc_router.get('/session')
|
@kyc_router.get('/session')
|
||||||
async def get_kyc_session(
|
async def get_kyc_session(
|
||||||
#auth: AuthContext = Depends(require_access_token),
|
auth: AuthContext = Depends(require_access_token),
|
||||||
command: GetKycSessionCommand = Depends(get_kyc_session_command),
|
command: GetKycSessionCommand = Depends(get_kyc_session_command),
|
||||||
) -> ORJSONResponse:
|
) -> ORJSONResponse:
|
||||||
#result = await command(user_id=user_id)
|
result = await command(user_id=auth.user_id)
|
||||||
result = await command(user_id='01KPKAFN6J1NJBY15DX8JE2QYB')
|
|
||||||
return ORJSONResponse(result.model_dump())
|
|
||||||
|
|
||||||
|
|
||||||
@kyc_router.post('/complete')
|
|
||||||
async def complete_kyc(
|
|
||||||
#auth: AuthContext = Depends(require_access_token),
|
|
||||||
command: CompleteKycCommand = Depends(get_complete_kyc_command),
|
|
||||||
) -> ORJSONResponse:
|
|
||||||
#result = await command(user_id=user_id)
|
|
||||||
result = await command(user_id='01KPKAFN6J1NJBY15DX8JE2QYB')
|
|
||||||
return ORJSONResponse(result.model_dump())
|
return ORJSONResponse(result.model_dump())
|
||||||
Reference in New Issue
Block a user