249 lines
9.1 KiB
Python
249 lines
9.1 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import func, or_, select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
|
|
|
|
from src.application.abstractions.repositories import IUserRepository
|
|
from src.application.contracts import ILogger
|
|
from src.application.domain.entities import UserEntity
|
|
from src.application.domain.entities.party_search import PartySearchEntity
|
|
from src.application.domain.enums.account_type import AccountType
|
|
from src.application.domain.exceptions import InternalServerException, ApplicationException
|
|
from src.infrastructure.database.models import UserModel
|
|
|
|
|
|
class UserRepository(IUserRepository):
|
|
def __init__(self, session: AsyncSession, logger: ILogger):
|
|
self._session = session
|
|
self._logger = logger
|
|
|
|
def _to_entity(self, user: UserModel) -> UserEntity:
|
|
return UserEntity(
|
|
id=user.id,
|
|
email=user.email,
|
|
password_hash=user.password_hash,
|
|
first_name=user.first_name,
|
|
middle_name=user.middle_name,
|
|
last_name=user.last_name,
|
|
birth_date=user.birth_date,
|
|
encrypted_mnemonic=user.encrypted_mnemonic,
|
|
phone=user.phone,
|
|
passport_data=user.passport_data,
|
|
inn=user.inn,
|
|
erc20=user.erc20,
|
|
avatar_link=user.avatar_link,
|
|
kyc_verified=user.kyc_verified,
|
|
is_deleted=user.is_deleted,
|
|
created_at=user.created_at,
|
|
updated_at=user.updated_at,
|
|
kyc_verified_at=user.kyc_verified_at,
|
|
account_type=user.account_type,
|
|
provisioned_by=user.provisioned_by,
|
|
provisioned_at=user.provisioned_at,
|
|
)
|
|
|
|
async def get_by_id(self, user_id: str) -> UserEntity:
|
|
try:
|
|
user = await self._get_user(user_id)
|
|
return self._to_entity(user)
|
|
except ApplicationException:
|
|
raise
|
|
except SQLAlchemyError as exception:
|
|
self._logger.exception(str(exception))
|
|
raise InternalServerException(message=f'Database error: {str(exception)}')
|
|
|
|
async def create_legal_entity_user(
|
|
self,
|
|
*,
|
|
email: str,
|
|
password_hash: str,
|
|
provisioned_by: str,
|
|
provisioned_at: datetime,
|
|
kyc_verified: bool,
|
|
kyc_verified_at: datetime,
|
|
) -> UserEntity:
|
|
user = UserModel(
|
|
email=email,
|
|
password_hash=password_hash,
|
|
account_type=AccountType.LEGAL_ENTITY,
|
|
provisioned_by=provisioned_by,
|
|
provisioned_at=provisioned_at,
|
|
kyc_verified=kyc_verified,
|
|
kyc_verified_at=kyc_verified_at,
|
|
)
|
|
self._session.add(user)
|
|
try:
|
|
await self._session.flush()
|
|
return self._to_entity(user)
|
|
except IntegrityError:
|
|
raise ApplicationException(status_code=409, message='User with this email already exists')
|
|
except SQLAlchemyError as exc:
|
|
self._logger.exception(str(exc))
|
|
raise ApplicationException(status_code=500, message='Database error')
|
|
|
|
async def _get_user(self, user_id: str) -> UserModel:
|
|
stmt = (
|
|
select(UserModel)
|
|
.where(
|
|
UserModel.id == user_id,
|
|
UserModel.is_deleted.is_(False),
|
|
)
|
|
)
|
|
result = await self._session.execute(stmt)
|
|
user: UserModel | None = result.scalar_one_or_none()
|
|
if user is None:
|
|
self._logger.warning(f'User not found with user_id {user_id}')
|
|
raise ApplicationException(status_code=404, message='User not found')
|
|
return user
|
|
|
|
async def _update_field(self, user_id: str, **fields: object) -> UserEntity:
|
|
try:
|
|
user = await self._get_user(user_id)
|
|
for key, value in fields.items():
|
|
setattr(user, key, value)
|
|
await self._session.flush()
|
|
await self._session.refresh(user)
|
|
return self._to_entity(user)
|
|
except ApplicationException:
|
|
raise
|
|
except SQLAlchemyError as exception:
|
|
self._logger.exception(str(exception))
|
|
raise InternalServerException(message=f'Database error: {str(exception)}')
|
|
|
|
async def set_password(self, user_id: str, password_hash: str) -> UserEntity:
|
|
return await self._update_field(user_id, password_hash=password_hash)
|
|
|
|
async def get_user_by_email(self, email: str) -> UserEntity:
|
|
try:
|
|
stmt = select(UserModel).where(UserModel.email == email, UserModel.is_deleted.is_(False))
|
|
result = await self._session.execute(stmt)
|
|
user = result.scalar_one_or_none()
|
|
if user is None:
|
|
raise ApplicationException(status_code=404, message='User not found')
|
|
return self._to_entity(user)
|
|
except ApplicationException:
|
|
raise
|
|
except SQLAlchemyError as exc:
|
|
self._logger.exception(str(exc))
|
|
raise ApplicationException(status_code=500, message='Database error')
|
|
|
|
async def exists_by_email(self, email: str) -> bool:
|
|
stmt = select(UserModel.id).where(UserModel.email == email, UserModel.is_deleted.is_(False)).limit(1)
|
|
result = await self._session.execute(stmt)
|
|
return result.scalar_one_or_none() is not None
|
|
|
|
def _search_filter(self, search: str | None):
|
|
if not search or not search.strip():
|
|
return None
|
|
|
|
pattern = f'%{search.strip()}%'
|
|
return or_(
|
|
UserModel.id.ilike(pattern),
|
|
UserModel.email.ilike(pattern),
|
|
UserModel.last_name.ilike(pattern),
|
|
UserModel.first_name.ilike(pattern),
|
|
UserModel.middle_name.ilike(pattern),
|
|
UserModel.phone.ilike(pattern),
|
|
UserModel.inn.ilike(pattern),
|
|
)
|
|
|
|
async def list_all(
|
|
self,
|
|
*,
|
|
limit: int,
|
|
offset: int,
|
|
search: str | None = None,
|
|
) -> list[UserEntity]:
|
|
stmt = (
|
|
select(UserModel)
|
|
.where(UserModel.is_deleted.is_(False))
|
|
.order_by(UserModel.created_at.desc())
|
|
.limit(limit)
|
|
.offset(offset)
|
|
)
|
|
search_filter = self._search_filter(search)
|
|
if search_filter is not None:
|
|
stmt = stmt.where(search_filter)
|
|
res = await self._session.execute(stmt)
|
|
return [self._to_entity(m) for m in res.scalars().all()]
|
|
|
|
def _individual_search_filter(self, query: str):
|
|
pattern = f'%{query.strip()}%'
|
|
full_name = func.concat_ws(
|
|
' ',
|
|
UserModel.last_name,
|
|
UserModel.first_name,
|
|
UserModel.middle_name,
|
|
)
|
|
return or_(
|
|
UserModel.id.ilike(pattern),
|
|
UserModel.email.ilike(pattern),
|
|
UserModel.last_name.ilike(pattern),
|
|
UserModel.first_name.ilike(pattern),
|
|
UserModel.middle_name.ilike(pattern),
|
|
UserModel.phone.ilike(pattern),
|
|
UserModel.passport_data.ilike(pattern),
|
|
UserModel.inn.ilike(pattern),
|
|
UserModel.erc20.ilike(pattern),
|
|
full_name.ilike(pattern),
|
|
)
|
|
|
|
def _to_search_entity(self, user: UserModel) -> PartySearchEntity:
|
|
name = ' '.join(
|
|
part for part in (user.last_name, user.first_name, user.middle_name)
|
|
if part
|
|
) or None
|
|
return PartySearchEntity(
|
|
id=user.id,
|
|
account_type=user.account_type,
|
|
user_id=user.id,
|
|
email=user.email,
|
|
name=name,
|
|
inn=user.inn,
|
|
phone=user.phone,
|
|
status=None,
|
|
kyc_verified=user.kyc_verified,
|
|
created_at=user.created_at,
|
|
)
|
|
|
|
async def search_individuals(self, *, query: str, limit: int, offset: int) -> list[PartySearchEntity]:
|
|
stmt = (
|
|
select(UserModel)
|
|
.where(
|
|
UserModel.is_deleted.is_(False),
|
|
UserModel.account_type == AccountType.INDIVIDUAL,
|
|
self._individual_search_filter(query),
|
|
)
|
|
.order_by(UserModel.created_at.desc())
|
|
.limit(limit)
|
|
.offset(offset)
|
|
)
|
|
result = await self._session.execute(stmt)
|
|
return [self._to_search_entity(user) for user in result.scalars().all()]
|
|
|
|
async def count_individuals(self, *, query: str) -> int:
|
|
stmt = (
|
|
select(func.count(UserModel.id))
|
|
.where(
|
|
UserModel.is_deleted.is_(False),
|
|
UserModel.account_type == AccountType.INDIVIDUAL,
|
|
self._individual_search_filter(query),
|
|
)
|
|
)
|
|
result = await self._session.execute(stmt)
|
|
return int(result.scalar_one())
|
|
|
|
async def count_all(self, *, search: str | None = None) -> int:
|
|
stmt = (
|
|
select(func.count(UserModel.id))
|
|
.where(UserModel.is_deleted.is_(False))
|
|
)
|
|
search_filter = self._search_filter(search)
|
|
if search_filter is not None:
|
|
stmt = stmt.where(search_filter)
|
|
res = await self._session.execute(stmt)
|
|
return int(res.scalar_one())
|