feat: add search

This commit is contained in:
2026-06-09 20:24:25 +03:00
parent cc75f61e76
commit f9dc59729c
11 changed files with 278 additions and 15 deletions

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
from typing import Any
from sqlalchemy import func, select, update
from sqlalchemy import func, or_, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
@@ -11,7 +11,7 @@ from src.application.contracts import ILogger
from src.application.domain.entities.organization import LegalEntityEntity
from src.application.domain.exceptions import ApplicationException
from src.application.domain.organization_documents import DOCUMENT_TYPE_TO_COLUMN, ORGANIZATION_DOCUMENT_TYPES
from src.infrastructure.database.models import LegalEntityModel
from src.infrastructure.database.models import LegalEntityModel, UserModel
class LegalEntityRepository(ILegalEntityRepository):
@@ -49,6 +49,37 @@ class LegalEntityRepository(ILegalEntityRepository):
updated_at=m.updated_at,
)
def _search_filter(self, search: str | None):
if not search or not search.strip():
return None
pattern = f'%{search.strip()}%'
full_name = func.concat_ws(
' ',
UserModel.last_name,
UserModel.first_name,
UserModel.middle_name,
)
return or_(
LegalEntityModel.id.ilike(pattern),
LegalEntityModel.name.ilike(pattern),
LegalEntityModel.short_name.ilike(pattern),
LegalEntityModel.inn.ilike(pattern),
LegalEntityModel.ogrn.ilike(pattern),
LegalEntityModel.kpp.ilike(pattern),
LegalEntityModel.legal_address.ilike(pattern),
LegalEntityModel.actual_address.ilike(pattern),
LegalEntityModel.contact_person.ilike(pattern),
LegalEntityModel.contact_phone.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),
full_name.ilike(pattern),
)
async def create(
self,
*,
@@ -102,14 +133,37 @@ class LegalEntityRepository(ILegalEntityRepository):
raise ApplicationException(status_code=404, message='Organization not found')
return self._to_entity(m)
async def list_all(self, *, limit: int, offset: int) -> list[LegalEntityEntity]:
res = await self._session.execute(
select(LegalEntityModel).order_by(LegalEntityModel.created_at.desc()).limit(limit).offset(offset)
async def list_all(
self,
*,
limit: int,
offset: int,
search: str | None = None,
) -> list[LegalEntityEntity]:
stmt = (
select(LegalEntityModel)
.join(UserModel, UserModel.id == LegalEntityModel.user_id)
.where(UserModel.is_deleted.is_(False))
.order_by(LegalEntityModel.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()]
async def count_all(self) -> int:
res = await self._session.execute(select(func.count()).select_from(LegalEntityModel))
async def count_all(self, *, search: str | None = None) -> int:
stmt = (
select(func.count(LegalEntityModel.id))
.join(UserModel, UserModel.id == LegalEntityModel.user_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())
async def update(self, organization_id: str, *, values: dict[str, Any]) -> LegalEntityEntity:

View File

@@ -3,13 +3,14 @@ from __future__ import annotations
from datetime import datetime
from fastapi import status
from sqlalchemy import select
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.organization import PartySearchEntity
from src.application.domain.enums.account_type import AccountType
from src.application.domain.exceptions import ApplicationException
from src.infrastructure.database.models import UserModel
@@ -92,3 +93,69 @@ 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 _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())