init
This commit is contained in:
1
src/infrastructure/database/__init__.py
Normal file
1
src/infrastructure/database/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from src.infrastructure.database.unit_of_work import UnitOfWork
|
||||
22
src/infrastructure/database/context.py
Normal file
22
src/infrastructure/database/context.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from sqlalchemy.ext.asyncio import async_sessionmaker
|
||||
from sqlalchemy.ext.asyncio.engine import create_async_engine
|
||||
from sqlalchemy.ext.asyncio.session import AsyncSession
|
||||
from typing import AsyncGenerator
|
||||
from src.infrastructure.config import settings
|
||||
|
||||
|
||||
engine = create_async_engine(
|
||||
settings.DATABASE_URL,
|
||||
pool_size=settings.DATABASE_POOL_SIZE,
|
||||
max_overflow=settings.DATABASE_MAX_OVERFLOW,
|
||||
pool_timeout=settings.DATABASE_POOL_TIMEOUT,
|
||||
pool_recycle=settings.DATABASE_POOL_RECYCLE,
|
||||
echo=settings.DATABASE_ECHO
|
||||
)
|
||||
|
||||
async_session_maker = async_sessionmaker(engine, expire_on_commit=False, class_=AsyncSession)
|
||||
|
||||
|
||||
async def get_session() -> AsyncGenerator[AsyncSession, None]:
|
||||
async with async_session_maker() as session:
|
||||
yield session
|
||||
1
src/infrastructure/database/decorators/__init__.py
Normal file
1
src/infrastructure/database/decorators/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from src.infrastructure.database.decorators.transactional import transactional
|
||||
15
src/infrastructure/database/decorators/transactional.py
Normal file
15
src/infrastructure/database/decorators/transactional.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from __future__ import annotations
|
||||
from functools import wraps
|
||||
from typing import Callable, Awaitable, TypeVar, ParamSpec
|
||||
|
||||
|
||||
P = ParamSpec("P")
|
||||
R = TypeVar("R")
|
||||
|
||||
|
||||
def transactional(method: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[R]]:
|
||||
@wraps(method)
|
||||
async def wrapper(self, *args: P.args, **kwargs: P.kwargs) -> R:
|
||||
async with self._unit_of_work:
|
||||
return await method(self, *args, **kwargs)
|
||||
return wrapper
|
||||
19
src/infrastructure/database/models/__init__.py
Normal file
19
src/infrastructure/database/models/__init__.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from src.infrastructure.database.models.base import Base
|
||||
from src.infrastructure.database.models.user import UserModel
|
||||
from src.infrastructure.database.models.admin_user import AdminUserModel
|
||||
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.organization_document import OrganizationDocumentModel
|
||||
from src.infrastructure.database.models.purchase_request import PurchaseRequestModel
|
||||
|
||||
__all__ = [
|
||||
'Base',
|
||||
'UserModel',
|
||||
'AdminUserModel',
|
||||
'AdminSessionModel',
|
||||
'LegalEntityModel',
|
||||
'OrganizationWalletModel',
|
||||
'OrganizationDocumentModel',
|
||||
'PurchaseRequestModel',
|
||||
]
|
||||
43
src/infrastructure/database/models/admin_session.py
Normal file
43
src/infrastructure/database/models/admin_session.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, Index, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from ulid import ULID
|
||||
|
||||
from src.infrastructure.database.models.base import Base
|
||||
from src.infrastructure.database.models.mixins import AuditTimestampsMixin, UlidPrimaryKeyMixin
|
||||
|
||||
|
||||
class AdminSessionModel(Base, UlidPrimaryKeyMixin, AuditTimestampsMixin):
|
||||
__tablename__ = 'admin_sessions'
|
||||
|
||||
sid: Mapped[str] = mapped_column(
|
||||
String(26),
|
||||
unique=True,
|
||||
index=True,
|
||||
nullable=False,
|
||||
default=lambda: str(ULID()),
|
||||
)
|
||||
admin_user_id: Mapped[str] = mapped_column(
|
||||
String(26),
|
||||
ForeignKey('admin_users.id', ondelete='CASCADE'),
|
||||
index=True,
|
||||
nullable=False,
|
||||
)
|
||||
device_id: Mapped[str] = mapped_column(String(26), nullable=False, index=True)
|
||||
user_agent: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
||||
first_ip: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||||
last_ip: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||||
last_seen_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
nullable=False,
|
||||
default=lambda: datetime.now(timezone.utc),
|
||||
)
|
||||
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
refresh_jti_hash: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||
refresh_expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
|
||||
Index('ux_admin_sessions_user_device', AdminSessionModel.admin_user_id, AdminSessionModel.device_id, unique=True)
|
||||
21
src/infrastructure/database/models/admin_user.py
Normal file
21
src/infrastructure/database/models/admin_user.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import Boolean, DateTime, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from src.infrastructure.database.models.base import Base
|
||||
from src.infrastructure.database.models.mixins import AuditTimestampsMixin, UlidPrimaryKeyMixin
|
||||
|
||||
|
||||
class AdminUserModel(Base, UlidPrimaryKeyMixin, AuditTimestampsMixin):
|
||||
__tablename__ = 'admin_users'
|
||||
|
||||
email: Mapped[str] = mapped_column(String(255), nullable=False, unique=True, index=True)
|
||||
password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
first_name: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||||
last_name: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||||
role: Mapped[str] = mapped_column(String(32), nullable=False, server_default='operator', default='operator')
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='true', default=True)
|
||||
last_login_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
19
src/infrastructure/database/models/base.py
Normal file
19
src/infrastructure/database/models/base.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from sqlalchemy.ext.asyncio import AsyncAttrs
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
|
||||
|
||||
class Base(AsyncAttrs, DeclarativeBase):
|
||||
__abstract__ = True
|
||||
|
||||
def __repr__(self) -> str:
|
||||
class_name = self.__class__.__name__
|
||||
attributes = ', '.join(f"{col.name}={getattr(self, col.name, None)!r}"
|
||||
for col in self.__table__.columns)
|
||||
return f"<{class_name}({attributes})>"
|
||||
|
||||
def __str__(self) -> str:
|
||||
class_name = self.__class__.__name__
|
||||
attributes = ', '.join(f"{col.name}={getattr(self, col.name)}"
|
||||
for col in self.__table__.columns
|
||||
if getattr(self, col.name) is not None)
|
||||
return f"{class_name}({attributes})"
|
||||
42
src/infrastructure/database/models/legal_entity.py
Normal file
42
src/infrastructure/database/models/legal_entity.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import Boolean, DateTime, ForeignKey, String, Text
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from src.infrastructure.database.models.base import Base
|
||||
from src.infrastructure.database.models.mixins import AuditTimestampsMixin, UlidPrimaryKeyMixin
|
||||
|
||||
|
||||
class LegalEntityModel(Base, UlidPrimaryKeyMixin, AuditTimestampsMixin):
|
||||
__tablename__ = 'legal_entities'
|
||||
|
||||
user_id: Mapped[str] = mapped_column(
|
||||
String(26),
|
||||
ForeignKey('users.id', ondelete='RESTRICT'),
|
||||
nullable=False,
|
||||
unique=True,
|
||||
index=True,
|
||||
)
|
||||
name: Mapped[str] = mapped_column(String(512), nullable=False)
|
||||
short_name: Mapped[str | None] = mapped_column(String(256), nullable=True)
|
||||
inn: Mapped[str] = mapped_column(String(12), nullable=False, index=True)
|
||||
ogrn: Mapped[str | None] = mapped_column(String(15), nullable=True)
|
||||
kpp: Mapped[str | None] = mapped_column(String(9), nullable=True)
|
||||
legal_address: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
actual_address: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
bank_details: Mapped[dict[str, Any] | None] = mapped_column(JSONB, nullable=True)
|
||||
contact_person: Mapped[str | None] = mapped_column(String(256), nullable=True)
|
||||
contact_phone: Mapped[str | None] = mapped_column(String(16), nullable=True)
|
||||
status: Mapped[str] = mapped_column(String(32), nullable=False, server_default='active', default='active')
|
||||
kyc_verified: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='true', default=True)
|
||||
kyc_verified_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
encrypted_mnemonic: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
created_by: Mapped[str | None] = mapped_column(
|
||||
String(26),
|
||||
ForeignKey('admin_users.id'),
|
||||
nullable=True,
|
||||
)
|
||||
3
src/infrastructure/database/models/mixins/__init__.py
Normal file
3
src/infrastructure/database/models/mixins/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from src.infrastructure.database.models.mixins.audit import AuditTimestampsMixin
|
||||
from src.infrastructure.database.models.mixins.ulid import UlidPrimaryKeyMixin
|
||||
from src.infrastructure.database.models.mixins.soft_delete import SoftDeleteMixin
|
||||
16
src/infrastructure/database/models/mixins/audit.py
Normal file
16
src/infrastructure/database/models/mixins/audit.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from sqlalchemy import DateTime, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
|
||||
class AuditTimestampsMixin:
|
||||
created_at: Mapped[DateTime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
nullable=False,
|
||||
server_default=func.now(),
|
||||
)
|
||||
updated_at: Mapped[DateTime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
nullable=False,
|
||||
server_default=func.now(),
|
||||
onupdate=func.now(),
|
||||
)
|
||||
6
src/infrastructure/database/models/mixins/soft_delete.py
Normal file
6
src/infrastructure/database/models/mixins/soft_delete.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from sqlalchemy import Boolean
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
|
||||
class SoftDeleteMixin:
|
||||
is_deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='false', default=False)
|
||||
8
src/infrastructure/database/models/mixins/ulid.py
Normal file
8
src/infrastructure/database/models/mixins/ulid.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from sqlalchemy import String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from ulid import ULID
|
||||
|
||||
|
||||
class UlidPrimaryKeyMixin:
|
||||
|
||||
id: Mapped[str] = mapped_column(String(26), primary_key=True, default=lambda: str(ULID()))
|
||||
35
src/infrastructure/database/models/organization_document.py
Normal file
35
src/infrastructure/database/models/organization_document.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import BigInteger, 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 OrganizationDocumentModel(Base, UlidPrimaryKeyMixin):
|
||||
__tablename__ = 'organization_documents'
|
||||
|
||||
organization_id: Mapped[str] = mapped_column(
|
||||
String(26),
|
||||
ForeignKey('legal_entities.id', ondelete='RESTRICT'),
|
||||
nullable=False,
|
||||
index=True,
|
||||
)
|
||||
document_type: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
file_name: Mapped[str] = mapped_column(String(512), nullable=False)
|
||||
s3_key: Mapped[str] = mapped_column(String(1024), nullable=False)
|
||||
content_type: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||
file_size_bytes: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
||||
uploaded_by: Mapped[str | None] = mapped_column(
|
||||
String(26),
|
||||
ForeignKey('admin_users.id'),
|
||||
nullable=True,
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
nullable=False,
|
||||
server_default=func.now(),
|
||||
)
|
||||
28
src/infrastructure/database/models/organization_wallet.py
Normal file
28
src/infrastructure/database/models/organization_wallet.py
Normal 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 OrganizationWalletModel(Base, UlidPrimaryKeyMixin):
|
||||
__tablename__ = 'organization_wallets'
|
||||
|
||||
organization_id: Mapped[str] = mapped_column(
|
||||
String(26),
|
||||
ForeignKey('legal_entities.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(),
|
||||
)
|
||||
37
src/infrastructure/database/models/purchase_request.py
Normal file
37
src/infrastructure/database/models/purchase_request.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, Numeric, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from src.infrastructure.database.models.base import Base
|
||||
from src.infrastructure.database.models.mixins import AuditTimestampsMixin, UlidPrimaryKeyMixin
|
||||
|
||||
|
||||
class PurchaseRequestModel(Base, UlidPrimaryKeyMixin, AuditTimestampsMixin):
|
||||
__tablename__ = 'purchase_requests'
|
||||
|
||||
organization_id: Mapped[str] = mapped_column(
|
||||
String(26),
|
||||
ForeignKey('legal_entities.id', ondelete='RESTRICT'),
|
||||
nullable=False,
|
||||
index=True,
|
||||
)
|
||||
status: Mapped[str] = mapped_column(String(32), nullable=False, server_default='submitted', default='submitted')
|
||||
usdt_amount: Mapped[Decimal] = mapped_column(Numeric(18, 8), nullable=False)
|
||||
rub_amount: Mapped[Decimal | None] = mapped_column(Numeric(18, 2), nullable=True)
|
||||
exchange_rate: Mapped[Decimal | None] = mapped_column(Numeric(18, 8), nullable=True)
|
||||
service_fee_percent: Mapped[Decimal | None] = mapped_column(Numeric(5, 2), nullable=True)
|
||||
comment: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
admin_comment: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
target_wallet_chain: Mapped[str | None] = mapped_column(String(16), nullable=True, server_default='ETH')
|
||||
target_wallet_address: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||||
tx_hash: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||||
assigned_to: Mapped[str | None] = mapped_column(
|
||||
String(26),
|
||||
ForeignKey('admin_users.id'),
|
||||
nullable=True,
|
||||
)
|
||||
completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
30
src/infrastructure/database/models/user.py
Normal file
30
src/infrastructure/database/models/user.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlalchemy import Boolean, Date, String, DateTime, Text
|
||||
from sqlalchemy.orm import Mapped,mapped_column
|
||||
from src.infrastructure.database.models.base import Base
|
||||
from src.infrastructure.database.models.mixins import UlidPrimaryKeyMixin,AuditTimestampsMixin,SoftDeleteMixin
|
||||
|
||||
|
||||
class UserModel(Base, UlidPrimaryKeyMixin, AuditTimestampsMixin, SoftDeleteMixin):
|
||||
__tablename__ = 'users'
|
||||
|
||||
email: Mapped[str] = mapped_column(String(255), nullable=False, unique=True, index=True)
|
||||
password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
|
||||
last_name: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||||
first_name: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||||
middle_name: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||||
birth_date: Mapped[Date | None] = mapped_column(Date, nullable=True)
|
||||
|
||||
encrypted_mnemonic: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
phone: Mapped[str | None] = mapped_column(String(16), nullable=True)
|
||||
|
||||
passport_data: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||
inn: Mapped[str | None] = mapped_column(String(12), nullable=True)
|
||||
erc20: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||
|
||||
avatar_link: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
|
||||
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)
|
||||
17
src/infrastructure/database/repositories/__init__.py
Normal file
17
src/infrastructure/database/repositories/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from src.infrastructure.database.repositories.admin_user_repository import AdminUserRepository
|
||||
from src.infrastructure.database.repositories.admin_session_repository import AdminSessionRepository
|
||||
from src.infrastructure.database.repositories.user_repository import UserRepository
|
||||
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.organization_document_repository import OrganizationDocumentRepository
|
||||
from src.infrastructure.database.repositories.purchase_request_repository import PurchaseRequestRepository
|
||||
|
||||
__all__ = [
|
||||
'AdminUserRepository',
|
||||
'AdminSessionRepository',
|
||||
'UserRepository',
|
||||
'LegalEntityRepository',
|
||||
'OrganizationWalletRepository',
|
||||
'OrganizationDocumentRepository',
|
||||
'PurchaseRequestRepository',
|
||||
]
|
||||
@@ -0,0 +1,120 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.application.abstractions.repositories import IAdminSessionRepository
|
||||
from src.application.contracts import ILogger
|
||||
from src.application.domain.entities.admin_session import AdminSessionEntity
|
||||
from src.infrastructure.database.models import AdminSessionModel
|
||||
|
||||
|
||||
class AdminSessionRepository(IAdminSessionRepository):
|
||||
def __init__(self, session: AsyncSession, logger: ILogger):
|
||||
self._session = session
|
||||
self._logger = logger
|
||||
|
||||
def _to_entity(self, m: AdminSessionModel) -> AdminSessionEntity:
|
||||
return AdminSessionEntity(
|
||||
sid=m.sid,
|
||||
admin_user_id=m.admin_user_id,
|
||||
device_id=m.device_id,
|
||||
revoked_at=m.revoked_at,
|
||||
last_seen_at=m.last_seen_at,
|
||||
refresh_jti_hash=m.refresh_jti_hash,
|
||||
refresh_expires_at=m.refresh_expires_at,
|
||||
user_agent=m.user_agent,
|
||||
first_ip=m.first_ip,
|
||||
last_ip=m.last_ip,
|
||||
)
|
||||
|
||||
async def get_by_sid(self, sid: str) -> Optional[AdminSessionEntity]:
|
||||
res = await self._session.execute(select(AdminSessionModel).where(AdminSessionModel.sid == sid))
|
||||
m = res.scalar_one_or_none()
|
||||
return self._to_entity(m) if m else None
|
||||
|
||||
async def upsert_by_device(
|
||||
self,
|
||||
*,
|
||||
admin_user_id: str,
|
||||
device_id: str,
|
||||
sid: str,
|
||||
refresh_jti_hash: str,
|
||||
refresh_expires_at: datetime,
|
||||
user_agent: str | None,
|
||||
ip: str | None,
|
||||
now: datetime,
|
||||
) -> AdminSessionEntity:
|
||||
res = await self._session.execute(
|
||||
select(AdminSessionModel).where(
|
||||
AdminSessionModel.admin_user_id == admin_user_id,
|
||||
AdminSessionModel.device_id == device_id,
|
||||
)
|
||||
)
|
||||
m = res.scalar_one_or_none()
|
||||
if m is None:
|
||||
m = AdminSessionModel(
|
||||
sid=sid,
|
||||
admin_user_id=admin_user_id,
|
||||
device_id=device_id,
|
||||
revoked_at=None,
|
||||
last_seen_at=now,
|
||||
refresh_jti_hash=refresh_jti_hash,
|
||||
refresh_expires_at=refresh_expires_at,
|
||||
user_agent=user_agent,
|
||||
first_ip=ip,
|
||||
last_ip=ip,
|
||||
)
|
||||
self._session.add(m)
|
||||
else:
|
||||
m.sid = sid
|
||||
m.revoked_at = None
|
||||
m.last_seen_at = now
|
||||
m.refresh_jti_hash = refresh_jti_hash
|
||||
m.refresh_expires_at = refresh_expires_at
|
||||
m.user_agent = user_agent
|
||||
m.last_ip = ip
|
||||
await self._session.flush()
|
||||
return self._to_entity(m)
|
||||
|
||||
async def revoke_by_sid(self, sid: str, now: datetime) -> None:
|
||||
await self._session.execute(
|
||||
update(AdminSessionModel)
|
||||
.where(AdminSessionModel.sid == sid, AdminSessionModel.revoked_at.is_(None))
|
||||
.values(revoked_at=now)
|
||||
)
|
||||
await self._session.flush()
|
||||
|
||||
async def rotate_refresh_if_match(
|
||||
self,
|
||||
*,
|
||||
sid: str,
|
||||
old_jti_hash: str,
|
||||
new_jti_hash: str,
|
||||
new_refresh_expires_at: datetime,
|
||||
now: datetime,
|
||||
ip: str | None,
|
||||
user_agent: str | None,
|
||||
) -> bool:
|
||||
values = {
|
||||
'refresh_jti_hash': new_jti_hash,
|
||||
'refresh_expires_at': new_refresh_expires_at,
|
||||
'last_seen_at': now,
|
||||
'user_agent': user_agent,
|
||||
}
|
||||
if ip is not None:
|
||||
values['last_ip'] = ip
|
||||
res = await self._session.execute(
|
||||
update(AdminSessionModel)
|
||||
.where(
|
||||
AdminSessionModel.sid == sid,
|
||||
AdminSessionModel.revoked_at.is_(None),
|
||||
AdminSessionModel.refresh_jti_hash == old_jti_hash,
|
||||
)
|
||||
.values(**values)
|
||||
)
|
||||
await self._session.flush()
|
||||
return (res.rowcount or 0) > 0
|
||||
@@ -0,0 +1,70 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import status
|
||||
from sqlalchemy import func, select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
|
||||
|
||||
from src.application.abstractions.repositories import IAdminUserRepository
|
||||
from src.application.contracts import ILogger
|
||||
from src.application.domain.entities.admin_user import AdminUserEntity
|
||||
from src.application.domain.exceptions import ApplicationException
|
||||
from src.infrastructure.database.models import AdminUserModel
|
||||
|
||||
|
||||
class AdminUserRepository(IAdminUserRepository):
|
||||
def __init__(self, session: AsyncSession, logger: ILogger):
|
||||
self._session = session
|
||||
self._logger = logger
|
||||
|
||||
def _to_entity(self, m: AdminUserModel) -> AdminUserEntity:
|
||||
return AdminUserEntity(
|
||||
id=m.id,
|
||||
email=m.email,
|
||||
password_hash=m.password_hash,
|
||||
first_name=m.first_name,
|
||||
last_name=m.last_name,
|
||||
role=m.role,
|
||||
is_active=m.is_active,
|
||||
last_login_at=m.last_login_at,
|
||||
created_at=m.created_at,
|
||||
updated_at=m.updated_at,
|
||||
)
|
||||
|
||||
async def get_by_email(self, email: str) -> AdminUserEntity:
|
||||
try:
|
||||
stmt = select(AdminUserModel).where(func.lower(AdminUserModel.email) == email.lower())
|
||||
result = await self._session.execute(stmt)
|
||||
user = result.scalar_one_or_none()
|
||||
if user is None:
|
||||
raise ApplicationException(status_code=status.HTTP_404_NOT_FOUND, message='Admin 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 get_by_id(self, admin_user_id: str) -> AdminUserEntity:
|
||||
try:
|
||||
stmt = select(AdminUserModel).where(AdminUserModel.id == admin_user_id)
|
||||
result = await self._session.execute(stmt)
|
||||
user = result.scalar_one_or_none()
|
||||
if user is None:
|
||||
raise ApplicationException(status_code=status.HTTP_404_NOT_FOUND, message='Admin 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 update_last_login(self, admin_user_id: str, *, last_login_at: datetime) -> None:
|
||||
await self._session.execute(
|
||||
update(AdminUserModel)
|
||||
.where(AdminUserModel.id == admin_user_id)
|
||||
.values(last_login_at=last_login_at)
|
||||
)
|
||||
await self._session.flush()
|
||||
@@ -0,0 +1,123 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import status
|
||||
from sqlalchemy import func, select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
|
||||
|
||||
from src.application.abstractions.repositories import ILegalEntityRepository
|
||||
from src.application.contracts import ILogger
|
||||
from src.application.domain.entities.organization import LegalEntityEntity
|
||||
from src.application.domain.exceptions import ApplicationException
|
||||
from src.infrastructure.database.models import LegalEntityModel
|
||||
|
||||
|
||||
class LegalEntityRepository(ILegalEntityRepository):
|
||||
def __init__(self, session: AsyncSession, logger: ILogger):
|
||||
self._session = session
|
||||
self._logger = logger
|
||||
|
||||
def _to_entity(self, m: LegalEntityModel) -> LegalEntityEntity:
|
||||
return LegalEntityEntity(
|
||||
id=m.id,
|
||||
user_id=m.user_id,
|
||||
name=m.name,
|
||||
short_name=m.short_name,
|
||||
inn=m.inn,
|
||||
ogrn=m.ogrn,
|
||||
kpp=m.kpp,
|
||||
legal_address=m.legal_address,
|
||||
actual_address=m.actual_address,
|
||||
bank_details=m.bank_details,
|
||||
contact_person=m.contact_person,
|
||||
contact_phone=m.contact_phone,
|
||||
status=m.status,
|
||||
kyc_verified=m.kyc_verified,
|
||||
kyc_verified_at=m.kyc_verified_at,
|
||||
encrypted_mnemonic=m.encrypted_mnemonic,
|
||||
created_by=m.created_by,
|
||||
created_at=m.created_at,
|
||||
updated_at=m.updated_at,
|
||||
)
|
||||
|
||||
async def create(
|
||||
self,
|
||||
*,
|
||||
user_id: str,
|
||||
name: str,
|
||||
short_name: str | None,
|
||||
inn: str,
|
||||
ogrn: str | None,
|
||||
kpp: str | None,
|
||||
legal_address: str | None,
|
||||
actual_address: str | None,
|
||||
bank_details: dict[str, Any] | None,
|
||||
contact_person: str | None,
|
||||
contact_phone: str | None,
|
||||
status: str,
|
||||
kyc_verified: bool,
|
||||
kyc_verified_at,
|
||||
created_by: str | None,
|
||||
) -> LegalEntityEntity:
|
||||
entity = LegalEntityModel(
|
||||
user_id=user_id,
|
||||
name=name,
|
||||
short_name=short_name,
|
||||
inn=inn,
|
||||
ogrn=ogrn,
|
||||
kpp=kpp,
|
||||
legal_address=legal_address,
|
||||
actual_address=actual_address,
|
||||
bank_details=bank_details,
|
||||
contact_person=contact_person,
|
||||
contact_phone=contact_phone,
|
||||
status=status,
|
||||
kyc_verified=kyc_verified,
|
||||
kyc_verified_at=kyc_verified_at,
|
||||
created_by=created_by,
|
||||
)
|
||||
self._session.add(entity)
|
||||
try:
|
||||
await self._session.flush()
|
||||
return self._to_entity(entity)
|
||||
except IntegrityError:
|
||||
raise ApplicationException(status_code=409, message='Organization with this INN or user already exists')
|
||||
except SQLAlchemyError as exc:
|
||||
self._logger.exception(str(exc))
|
||||
raise ApplicationException(status_code=500, message='Database error')
|
||||
|
||||
async def get_by_id(self, organization_id: str) -> LegalEntityEntity:
|
||||
res = await self._session.execute(select(LegalEntityModel).where(LegalEntityModel.id == organization_id))
|
||||
m = res.scalar_one_or_none()
|
||||
if m is None:
|
||||
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)
|
||||
)
|
||||
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))
|
||||
return int(res.scalar_one())
|
||||
|
||||
async def update(self, organization_id: str, *, values: dict[str, Any]) -> LegalEntityEntity:
|
||||
if not values:
|
||||
return await self.get_by_id(organization_id)
|
||||
await self._session.execute(
|
||||
update(LegalEntityModel).where(LegalEntityModel.id == organization_id).values(**values)
|
||||
)
|
||||
await self._session.flush()
|
||||
return await self.get_by_id(organization_id)
|
||||
|
||||
async def set_encrypted_mnemonic(self, organization_id: str, encrypted_mnemonic: str) -> None:
|
||||
await self._session.execute(
|
||||
update(LegalEntityModel)
|
||||
.where(LegalEntityModel.id == organization_id)
|
||||
.values(encrypted_mnemonic=encrypted_mnemonic)
|
||||
)
|
||||
await self._session.flush()
|
||||
@@ -0,0 +1,66 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
from src.application.abstractions.repositories import IOrganizationDocumentRepository
|
||||
from src.application.contracts import ILogger
|
||||
from src.application.domain.entities.organization import OrganizationDocumentEntity
|
||||
from src.application.domain.exceptions import ApplicationException
|
||||
from src.infrastructure.database.models import OrganizationDocumentModel
|
||||
|
||||
|
||||
class OrganizationDocumentRepository(IOrganizationDocumentRepository):
|
||||
def __init__(self, session: AsyncSession, logger: ILogger):
|
||||
self._session = session
|
||||
self._logger = logger
|
||||
|
||||
def _to_entity(self, m: OrganizationDocumentModel) -> OrganizationDocumentEntity:
|
||||
return OrganizationDocumentEntity(
|
||||
id=m.id,
|
||||
organization_id=m.organization_id,
|
||||
document_type=m.document_type,
|
||||
file_name=m.file_name,
|
||||
s3_key=m.s3_key,
|
||||
content_type=m.content_type,
|
||||
file_size_bytes=m.file_size_bytes,
|
||||
uploaded_by=m.uploaded_by,
|
||||
created_at=m.created_at,
|
||||
)
|
||||
|
||||
async def create(self, document: OrganizationDocumentEntity) -> OrganizationDocumentEntity:
|
||||
model = OrganizationDocumentModel(
|
||||
id=document.id,
|
||||
organization_id=document.organization_id,
|
||||
document_type=document.document_type,
|
||||
file_name=document.file_name,
|
||||
s3_key=document.s3_key,
|
||||
content_type=document.content_type,
|
||||
file_size_bytes=document.file_size_bytes,
|
||||
uploaded_by=document.uploaded_by,
|
||||
)
|
||||
self._session.add(model)
|
||||
try:
|
||||
await self._session.flush()
|
||||
return self._to_entity(model)
|
||||
except SQLAlchemyError as exc:
|
||||
self._logger.exception(str(exc))
|
||||
raise ApplicationException(status_code=500, message='Database error')
|
||||
|
||||
async def get_by_id(self, document_id: str) -> OrganizationDocumentEntity:
|
||||
res = await self._session.execute(
|
||||
select(OrganizationDocumentModel).where(OrganizationDocumentModel.id == document_id)
|
||||
)
|
||||
m = res.scalar_one_or_none()
|
||||
if m is None:
|
||||
raise ApplicationException(status_code=404, message='Document not found')
|
||||
return self._to_entity(m)
|
||||
|
||||
async def list_by_organization(self, organization_id: str) -> list[OrganizationDocumentEntity]:
|
||||
res = await self._session.execute(
|
||||
select(OrganizationDocumentModel)
|
||||
.where(OrganizationDocumentModel.organization_id == organization_id)
|
||||
.order_by(OrganizationDocumentModel.created_at.desc())
|
||||
)
|
||||
return [self._to_entity(m) for m in res.scalars().all()]
|
||||
@@ -0,0 +1,64 @@
|
||||
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 IOrganizationWalletRepository
|
||||
from src.application.contracts import ILogger
|
||||
from src.application.domain.entities.organization import OrganizationWalletEntity
|
||||
from src.application.domain.exceptions import ApplicationException
|
||||
from src.infrastructure.database.models import OrganizationWalletModel
|
||||
|
||||
|
||||
class OrganizationWalletRepository(IOrganizationWalletRepository):
|
||||
def __init__(self, session: AsyncSession, logger: ILogger):
|
||||
self._session = session
|
||||
self._logger = logger
|
||||
|
||||
def _to_entity(self, m: OrganizationWalletModel) -> OrganizationWalletEntity:
|
||||
return OrganizationWalletEntity(
|
||||
id=m.id,
|
||||
organization_id=m.organization_id,
|
||||
chain=m.chain,
|
||||
address=m.address,
|
||||
derivation_path=m.derivation_path,
|
||||
created_at=m.created_at,
|
||||
)
|
||||
|
||||
async def create_many(self, wallets: list[OrganizationWalletEntity]) -> list[OrganizationWalletEntity]:
|
||||
models = [
|
||||
OrganizationWalletModel(
|
||||
id=w.id,
|
||||
organization_id=w.organization_id,
|
||||
chain=w.chain,
|
||||
address=w.address,
|
||||
derivation_path=w.derivation_path,
|
||||
)
|
||||
for w in wallets
|
||||
]
|
||||
self._session.add_all(models)
|
||||
try:
|
||||
await self._session.flush()
|
||||
return [self._to_entity(m) for m in models]
|
||||
except IntegrityError:
|
||||
raise ApplicationException(status_code=409, message='Wallets already exist for organization')
|
||||
except SQLAlchemyError as exc:
|
||||
self._logger.exception(str(exc))
|
||||
raise ApplicationException(status_code=500, message='Database error')
|
||||
|
||||
async def list_by_organization(self, organization_id: str) -> list[OrganizationWalletEntity]:
|
||||
res = await self._session.execute(
|
||||
select(OrganizationWalletModel)
|
||||
.where(OrganizationWalletModel.organization_id == organization_id)
|
||||
.order_by(OrganizationWalletModel.chain)
|
||||
)
|
||||
return [self._to_entity(m) for m in res.scalars().all()]
|
||||
|
||||
async def exists_for_organization(self, organization_id: str) -> bool:
|
||||
res = await self._session.execute(
|
||||
select(OrganizationWalletModel.id)
|
||||
.where(OrganizationWalletModel.organization_id == organization_id)
|
||||
.limit(1)
|
||||
)
|
||||
return res.scalar_one_or_none() is not None
|
||||
@@ -0,0 +1,81 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import func, select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
from src.application.abstractions.repositories import IPurchaseRequestRepository
|
||||
from src.application.contracts import ILogger
|
||||
from src.application.domain.entities.organization import PurchaseRequestEntity
|
||||
from src.application.domain.exceptions import ApplicationException
|
||||
from src.infrastructure.database.models import PurchaseRequestModel
|
||||
|
||||
|
||||
class PurchaseRequestRepository(IPurchaseRequestRepository):
|
||||
def __init__(self, session: AsyncSession, logger: ILogger):
|
||||
self._session = session
|
||||
self._logger = logger
|
||||
|
||||
def _to_entity(self, m: PurchaseRequestModel) -> PurchaseRequestEntity:
|
||||
return PurchaseRequestEntity(
|
||||
id=m.id,
|
||||
organization_id=m.organization_id,
|
||||
status=m.status,
|
||||
usdt_amount=m.usdt_amount,
|
||||
rub_amount=m.rub_amount,
|
||||
exchange_rate=m.exchange_rate,
|
||||
service_fee_percent=m.service_fee_percent,
|
||||
comment=m.comment,
|
||||
admin_comment=m.admin_comment,
|
||||
target_wallet_chain=m.target_wallet_chain,
|
||||
target_wallet_address=m.target_wallet_address,
|
||||
tx_hash=m.tx_hash,
|
||||
assigned_to=m.assigned_to,
|
||||
created_at=m.created_at,
|
||||
updated_at=m.updated_at,
|
||||
completed_at=m.completed_at,
|
||||
)
|
||||
|
||||
def _apply_filters(self, stmt, *, status: str | None, organization_id: str | None):
|
||||
if status:
|
||||
stmt = stmt.where(PurchaseRequestModel.status == status)
|
||||
if organization_id:
|
||||
stmt = stmt.where(PurchaseRequestModel.organization_id == organization_id)
|
||||
return stmt
|
||||
|
||||
async def get_by_id(self, request_id: str) -> PurchaseRequestEntity:
|
||||
res = await self._session.execute(select(PurchaseRequestModel).where(PurchaseRequestModel.id == request_id))
|
||||
m = res.scalar_one_or_none()
|
||||
if m is None:
|
||||
raise ApplicationException(status_code=404, message='Purchase request not found')
|
||||
return self._to_entity(m)
|
||||
|
||||
async def list_all(
|
||||
self,
|
||||
*,
|
||||
status: str | None,
|
||||
organization_id: str | None,
|
||||
limit: int,
|
||||
offset: int,
|
||||
) -> list[PurchaseRequestEntity]:
|
||||
stmt = select(PurchaseRequestModel).order_by(PurchaseRequestModel.created_at.desc())
|
||||
stmt = self._apply_filters(stmt, status=status, organization_id=organization_id)
|
||||
res = await self._session.execute(stmt.limit(limit).offset(offset))
|
||||
return [self._to_entity(m) for m in res.scalars().all()]
|
||||
|
||||
async def count_all(self, *, status: str | None, organization_id: str | None) -> int:
|
||||
stmt = select(func.count()).select_from(PurchaseRequestModel)
|
||||
stmt = self._apply_filters(stmt, status=status, organization_id=organization_id)
|
||||
res = await self._session.execute(stmt)
|
||||
return int(res.scalar_one())
|
||||
|
||||
async def update(self, request_id: str, *, values: dict[str, Any]) -> PurchaseRequestEntity:
|
||||
if not values:
|
||||
return await self.get_by_id(request_id)
|
||||
await self._session.execute(
|
||||
update(PurchaseRequestModel).where(PurchaseRequestModel.id == request_id).values(**values)
|
||||
)
|
||||
await self._session.flush()
|
||||
return await self.get_by_id(request_id)
|
||||
94
src/infrastructure/database/repositories/user_repository.py
Normal file
94
src/infrastructure/database/repositories/user_repository.py
Normal file
@@ -0,0 +1,94 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import status
|
||||
from sqlalchemy import 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.enums.account_type import AccountType
|
||||
from src.application.domain.exceptions import 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 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_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
|
||||
105
src/infrastructure/database/unit_of_work.py
Normal file
105
src/infrastructure/database/unit_of_work.py
Normal file
@@ -0,0 +1,105 @@
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
||||
|
||||
from src.application.abstractions import IUnitOfWork
|
||||
from src.application.abstractions.repositories import (
|
||||
IAdminSessionRepository,
|
||||
IAdminUserRepository,
|
||||
ILegalEntityRepository,
|
||||
IOrganizationDocumentRepository,
|
||||
IOrganizationWalletRepository,
|
||||
IPurchaseRequestRepository,
|
||||
IUserRepository,
|
||||
)
|
||||
from src.application.contracts import ILogger
|
||||
from src.application.domain.exceptions import RefreshConcurrentException
|
||||
from src.infrastructure.database.repositories import (
|
||||
AdminSessionRepository,
|
||||
AdminUserRepository,
|
||||
LegalEntityRepository,
|
||||
OrganizationDocumentRepository,
|
||||
OrganizationWalletRepository,
|
||||
PurchaseRequestRepository,
|
||||
UserRepository,
|
||||
)
|
||||
|
||||
|
||||
class UnitOfWork(IUnitOfWork):
|
||||
def __init__(self, session_factory: async_sessionmaker[AsyncSession], logger: ILogger):
|
||||
self.session_factory = session_factory
|
||||
self._session: AsyncSession | None = None
|
||||
self._user_repository: IUserRepository | None = None
|
||||
self._admin_user_repository: IAdminUserRepository | None = None
|
||||
self._admin_session_repository: IAdminSessionRepository | None = None
|
||||
self._legal_entity_repository: ILegalEntityRepository | None = None
|
||||
self._organization_wallet_repository: IOrganizationWalletRepository | None = None
|
||||
self._organization_document_repository: IOrganizationDocumentRepository | None = None
|
||||
self._purchase_request_repository: IPurchaseRequestRepository | None = None
|
||||
self._logger: ILogger = logger
|
||||
|
||||
async def __aenter__(self):
|
||||
self._session = self.session_factory()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
if exc_type:
|
||||
if not isinstance(exc_val, RefreshConcurrentException):
|
||||
self._logger.error(str(exc_val))
|
||||
await self._session.rollback()
|
||||
else:
|
||||
await self._session.flush()
|
||||
await self._session.commit()
|
||||
await self._session.close()
|
||||
|
||||
async def commit(self) -> None:
|
||||
await self._session.commit()
|
||||
|
||||
async def rollback(self) -> None:
|
||||
await self._session.rollback()
|
||||
|
||||
@property
|
||||
def user_repository(self) -> IUserRepository:
|
||||
if self._user_repository is None:
|
||||
self._user_repository = UserRepository(session=self._session, logger=self._logger)
|
||||
return self._user_repository
|
||||
|
||||
@property
|
||||
def admin_user_repository(self) -> IAdminUserRepository:
|
||||
if self._admin_user_repository is None:
|
||||
self._admin_user_repository = AdminUserRepository(session=self._session, logger=self._logger)
|
||||
return self._admin_user_repository
|
||||
|
||||
@property
|
||||
def admin_session_repository(self) -> IAdminSessionRepository:
|
||||
if self._admin_session_repository is None:
|
||||
self._admin_session_repository = AdminSessionRepository(session=self._session, logger=self._logger)
|
||||
return self._admin_session_repository
|
||||
|
||||
@property
|
||||
def legal_entity_repository(self) -> ILegalEntityRepository:
|
||||
if self._legal_entity_repository is None:
|
||||
self._legal_entity_repository = LegalEntityRepository(session=self._session, logger=self._logger)
|
||||
return self._legal_entity_repository
|
||||
|
||||
@property
|
||||
def organization_wallet_repository(self) -> IOrganizationWalletRepository:
|
||||
if self._organization_wallet_repository is None:
|
||||
self._organization_wallet_repository = OrganizationWalletRepository(
|
||||
session=self._session, logger=self._logger
|
||||
)
|
||||
return self._organization_wallet_repository
|
||||
|
||||
@property
|
||||
def organization_document_repository(self) -> IOrganizationDocumentRepository:
|
||||
if self._organization_document_repository is None:
|
||||
self._organization_document_repository = OrganizationDocumentRepository(
|
||||
session=self._session, logger=self._logger
|
||||
)
|
||||
return self._organization_document_repository
|
||||
|
||||
@property
|
||||
def purchase_request_repository(self) -> IPurchaseRequestRepository:
|
||||
if self._purchase_request_repository is None:
|
||||
self._purchase_request_repository = PurchaseRequestRepository(
|
||||
session=self._session, logger=self._logger
|
||||
)
|
||||
return self._purchase_request_repository
|
||||
Reference in New Issue
Block a user