feat: add user endpoints

This commit is contained in:
2026-06-11 18:46:21 +03:00
parent e0f044b455
commit 4aae631c73
21 changed files with 503 additions and 15 deletions

View File

@@ -5,6 +5,7 @@ from src.infrastructure.database.models.admin_session import AdminSessionModel
from src.infrastructure.database.models.legal_entity import LegalEntityModel
from src.infrastructure.database.models.organization_wallet import OrganizationWalletModel
from src.infrastructure.database.models.purchase_request import PurchaseRequestModel
from src.infrastructure.database.models.wallets import WalletModel
__all__ = [
'Base',
@@ -14,4 +15,5 @@ __all__ = [
'LegalEntityModel',
'OrganizationWalletModel',
'PurchaseRequestModel',
'WalletModel',
]

View File

@@ -0,0 +1,28 @@
from __future__ import annotations
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, String, func
from sqlalchemy.orm import Mapped, mapped_column
from src.infrastructure.database.models.base import Base
from src.infrastructure.database.models.mixins import UlidPrimaryKeyMixin
class WalletModel(Base, UlidPrimaryKeyMixin):
__tablename__ = 'wallets'
user_id: Mapped[str] = mapped_column(
String(26),
ForeignKey('users.id', ondelete='RESTRICT'),
nullable=False,
index=True,
)
chain: Mapped[str] = mapped_column(String(16), nullable=False)
address: Mapped[str] = mapped_column(String(128), nullable=False, index=True)
derivation_path: Mapped[str] = mapped_column(String(64), nullable=False)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
server_default=func.now(),
)

View File

@@ -4,6 +4,7 @@ from src.infrastructure.database.repositories.user_repository import UserReposit
from src.infrastructure.database.repositories.legal_entity_repository import LegalEntityRepository
from src.infrastructure.database.repositories.organization_wallet_repository import OrganizationWalletRepository
from src.infrastructure.database.repositories.purchase_request_repository import PurchaseRequestRepository
from src.infrastructure.database.repositories.wallets_repository import WalletRepository
__all__ = [
'AdminUserRepository',
@@ -12,4 +13,5 @@ __all__ = [
'LegalEntityRepository',
'OrganizationWalletRepository',
'PurchaseRequestRepository',
'WalletRepository',
]

View File

@@ -45,6 +45,16 @@ class UserRepository(IUserRepository):
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,
@@ -104,9 +114,8 @@ class UserRepository(IUserRepository):
self._logger.exception(str(exception))
raise InternalServerException(message=f'Database error: {str(exception)}')
async def set_password(self, user_email: str, password_hash: str) -> UserEntity:
user = await self.get_user_by_email(user_email)
return await self._update_field(user.id, password_hash=password_hash)
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:
@@ -126,6 +135,41 @@ class UserRepository(IUserRepository):
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()}%'
@@ -192,3 +236,14 @@ class UserRepository(IUserRepository):
)
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())

View File

@@ -0,0 +1,43 @@
from __future__ import annotations
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
from src.application.abstractions.repositories import IWalletRepository
from src.application.contracts import ILogger
from src.application.domain.entities.user import WalletEntity
from src.application.domain.exceptions import ApplicationException
from src.infrastructure.database.models import WalletModel
class WalletRepository(IWalletRepository):
def __init__(self, session: AsyncSession, logger: ILogger):
self._session = session
self._logger = logger
def _to_entity(self, m: WalletModel) -> WalletEntity:
return WalletEntity(
id=m.id,
user_id=m.user_id,
chain=m.chain,
address=m.address,
derivation_path=m.derivation_path,
created_at=m.created_at,
)
async def list_by_user(self, user_id: str) -> list[WalletEntity]:
res = await self._session.execute(
select(WalletModel)
.where(WalletModel.user_id == user_id)
.order_by(WalletModel.chain)
)
return [self._to_entity(m) for m in res.scalars().all()]
async def exists_for_user(self, user_id: str) -> bool:
res = await self._session.execute(
select(WalletModel.id)
.where(WalletModel.user_id == user_id)
.limit(1)
)
return res.scalar_one_or_none() is not None

View File

@@ -8,6 +8,7 @@ from src.application.abstractions.repositories import (
IOrganizationWalletRepository,
IPurchaseRequestRepository,
IUserRepository,
IWalletRepository,
)
from src.application.contracts import ILogger
from src.application.domain.exceptions import RefreshConcurrentException
@@ -18,6 +19,7 @@ from src.infrastructure.database.repositories import (
OrganizationWalletRepository,
PurchaseRequestRepository,
UserRepository,
WalletRepository,
)
@@ -31,6 +33,7 @@ class UnitOfWork(IUnitOfWork):
self._legal_entity_repository: ILegalEntityRepository | None = None
self._organization_wallet_repository: IOrganizationWalletRepository | None = None
self._purchase_request_repository: IPurchaseRequestRepository | None = None
self._wallet_repository: IWalletRepository | None = None
self._logger: ILogger = logger
async def __aenter__(self):
@@ -84,6 +87,14 @@ class UnitOfWork(IUnitOfWork):
session=self._session, logger=self._logger
)
return self._organization_wallet_repository
@property
def wallet_repository(self) -> IWalletRepository:
if self._wallet_repository is None:
self._wallet_repository = WalletRepository(
session=self._session, logger=self._logger
)
return self._wallet_repository
@property
def purchase_request_repository(self) -> IPurchaseRequestRepository: