feat: update for b2b

This commit is contained in:
2026-06-02 23:46:57 +03:00
parent 41d0fe8aa7
commit f6ffe68e6a
15 changed files with 201 additions and 6 deletions

View File

@@ -1,6 +1,6 @@
from __future__ import annotations
from typing import Protocol, runtime_checkable
from src.application.abstractions.repositories import IUserRepository, ISessionRepository
from src.application.abstractions.repositories import IUserRepository, ISessionRepository, ILegalEntityRepository
@runtime_checkable
@@ -17,3 +17,6 @@ class IUnitOfWork(Protocol):
@property
def session_repository(self) -> ISessionRepository: ...
@property
def legal_entity_repository(self) -> ILegalEntityRepository: ...

View File

@@ -1,2 +1,3 @@
from src.application.abstractions.repositories.i_user_repository import IUserRepository
from src.application.abstractions.repositories.i_session_repository import ISessionRepository
from src.application.abstractions.repositories.i_session_repository import ISessionRepository
from src.application.abstractions.repositories.i_legal_entity_repository import ILegalEntityRepository

View File

@@ -0,0 +1,9 @@
from abc import ABC, abstractmethod
from src.application.domain.entities.legal_entity import LegalEntityEntity
class ILegalEntityRepository(ABC):
@abstractmethod
async def get_by_user_id(self, user_id: str) -> LegalEntityEntity | None:
raise NotImplementedError

View File

@@ -1,6 +1,7 @@
from src.application.abstractions import IUnitOfWork
from src.application.contracts import ILogger, ICache
from src.application.domain.entities import UserEntity
from src.application.domain.enums.account_type import AccountType
from src.infrastructure.database.decorators import transactional
@@ -13,5 +14,7 @@ class GetMeCommand:
@transactional
async def __call__(self, user_id: str) -> UserEntity:
user = await self._unit_of_work.user_repository.get_user_by_id(user_id=user_id)
if user.account_type == AccountType.LEGAL_ENTITY.value:
user.legal_entity = await self._unit_of_work.legal_entity_repository.get_by_user_id(user_id)
self._logger.info(f'User ID: {user.id}')
return user

View File

@@ -0,0 +1,24 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
from typing import Any
@dataclass(slots=True)
class LegalEntityEntity:
id: str
user_id: str
name: str
inn: str
status: str
short_name: str | None = None
ogrn: str | None = None
kpp: str | None = None
legal_address: str | None = None
actual_address: str | None = None
bank_details: dict[str, Any] | None = None
contact_person: str | None = None
contact_phone: str | None = None
kyc_verified: bool = True
kyc_verified_at: datetime | None = None

View File

@@ -2,6 +2,8 @@ from __future__ import annotations
from dataclasses import dataclass
from datetime import date, datetime
from src.application.domain.entities.legal_entity import LegalEntityEntity
@dataclass(slots=True)
class UserEntity:
@@ -28,3 +30,6 @@ class UserEntity:
created_at: datetime | None = None
updated_at: datetime | None = None
kyc_verified_at: datetime | None = None
account_type: str = 'individual'
legal_entity: LegalEntityEntity | None = None

View File

@@ -0,0 +1,6 @@
from enum import StrEnum
class AccountType(StrEnum):
INDIVIDUAL = 'individual'
LEGAL_ENTITY = 'legal_entity'

View File

@@ -1,6 +1,7 @@
from src.infrastructure.database.models.base import Base
from src.infrastructure.database.models.user import UserModel
from src.infrastructure.database.models.legal_entity import LegalEntityModel
from src.infrastructure.database.models.sessions import Session
__all__ = ['Base', 'UserModel', 'Session']
__all__ = ['Base', 'UserModel', 'LegalEntityModel', 'Session']

View File

@@ -0,0 +1,32 @@
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), nullable=True)

View File

@@ -27,3 +27,7 @@ class UserModel(Base, UlidPrimaryKeyMixin, AuditTimestampsMixin, SoftDeleteMixin
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)
account_type: Mapped[str] = mapped_column(String(20), nullable=False, server_default='individual', default='individual')
provisioned_by: Mapped[str | None] = mapped_column(String(26), nullable=True)
provisioned_at: Mapped[DateTime | None] = mapped_column(DateTime(timezone=True), nullable=True)

View File

@@ -1,2 +1,3 @@
from src.infrastructure.database.repositories.user_repository import UserRepository
from src.infrastructure.database.repositories.session_repository import SessionRepository
from src.infrastructure.database.repositories.session_repository import SessionRepository
from src.infrastructure.database.repositories.legal_entity_repository import LegalEntityRepository

View File

@@ -0,0 +1,49 @@
from __future__ import annotations
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.exc import SQLAlchemyError
from src.application.abstractions.repositories.i_legal_entity_repository import ILegalEntityRepository
from src.application.contracts import ILogger
from src.application.domain.entities.legal_entity import LegalEntityEntity
from src.application.domain.exceptions import InternalException
from src.infrastructure.database.models.legal_entity import LegalEntityModel
class LegalEntityRepository(ILegalEntityRepository):
def __init__(self, session: AsyncSession, logger: ILogger):
self._session = session
self._logger = logger
@staticmethod
def _to_entity(model: LegalEntityModel) -> LegalEntityEntity:
return LegalEntityEntity(
id=model.id,
user_id=model.user_id,
name=model.name,
inn=model.inn,
status=model.status,
short_name=model.short_name,
ogrn=model.ogrn,
kpp=model.kpp,
legal_address=model.legal_address,
actual_address=model.actual_address,
bank_details=model.bank_details,
contact_person=model.contact_person,
contact_phone=model.contact_phone,
kyc_verified=model.kyc_verified,
kyc_verified_at=model.kyc_verified_at,
)
async def get_by_user_id(self, user_id: str) -> LegalEntityEntity | None:
try:
stmt = select(LegalEntityModel).where(LegalEntityModel.user_id == user_id)
result = await self._session.execute(stmt)
model = result.scalar_one_or_none()
if model is None:
return None
return self._to_entity(model)
except SQLAlchemyError as exc:
self._logger.exception(str(exc))
raise InternalException(message=f'Database error: {exc}') from exc

View File

@@ -50,6 +50,7 @@ class UserRepository(IUserRepository):
is_deleted=user.is_deleted,
created_at=user.created_at,
updated_at=user.updated_at,
account_type=user.account_type,
)
async def get_user_by_id(self, user_id: str) -> UserEntity:

View File

@@ -1,8 +1,8 @@
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from src.application.abstractions import IUnitOfWork
from src.application.abstractions.repositories import IUserRepository, ISessionRepository
from src.application.abstractions.repositories import IUserRepository, ISessionRepository, ILegalEntityRepository
from src.application.contracts import ILogger
from src.infrastructure.database.repositories import UserRepository, SessionRepository
from src.infrastructure.database.repositories import UserRepository, SessionRepository, LegalEntityRepository
@@ -12,12 +12,14 @@ class UnitOfWork(IUnitOfWork):
self._session: AsyncSession = None
self._user_repository: IUserRepository = None
self._session_repository: ISessionRepository = None
self._legal_entity_repository: ILegalEntityRepository = None
self._logger: ILogger = logger
async def __aenter__(self):
self._logger.debug('UnitOfWork enter')
self._user_repository = None
self._session_repository = None
self._legal_entity_repository = None
self._session = self.session_factory()
return self
@@ -44,3 +46,9 @@ class UnitOfWork(IUnitOfWork):
if self._session_repository is None:
self._session_repository = SessionRepository(session=self._session, logger=self._logger)
return self._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

View File

@@ -5,6 +5,45 @@ from datetime import date, datetime
from pydantic import BaseModel, ConfigDict, Field
from src.application.domain.entities import UserEntity
from src.application.domain.entities.legal_entity import LegalEntityEntity
class LegalEntityPublicResponse(BaseModel):
model_config = ConfigDict(from_attributes=False)
id: str
name: str
inn: str
status: str
short_name: str | None = None
ogrn: str | None = None
kpp: str | None = None
legal_address: str | None = None
actual_address: str | None = None
bank_details: dict | None = None
contact_person: str | None = None
contact_phone: str | None = None
kyc_verified: bool | None = None
kyc_verified_at: datetime | None = None
@classmethod
def from_entity(cls, entity: LegalEntityEntity) -> LegalEntityPublicResponse:
return cls(
id=entity.id,
name=entity.name,
inn=entity.inn,
status=entity.status,
short_name=entity.short_name,
ogrn=entity.ogrn,
kpp=entity.kpp,
legal_address=entity.legal_address,
actual_address=entity.actual_address,
bank_details=entity.bank_details,
contact_person=entity.contact_person,
contact_phone=entity.contact_phone,
kyc_verified=entity.kyc_verified,
kyc_verified_at=entity.kyc_verified_at,
)
class MeUserPublicResponse(BaseModel):
@@ -27,9 +66,16 @@ class MeUserPublicResponse(BaseModel):
created_at: datetime | None = Field(None, description='Время создания записи')
updated_at: datetime | None = Field(None, description='Время последнего обновления')
kyc_verified_at: datetime | None = Field(None, description='Время подтверждения KYC')
account_type: str | None = Field(None, description='individual | legal_entity')
legal_entity: LegalEntityPublicResponse | None = Field(None, description='Профиль юрлица')
@classmethod
def from_user(cls, user: UserEntity) -> MeUserPublicResponse:
legal_entity = (
LegalEntityPublicResponse.from_entity(user.legal_entity)
if user.legal_entity is not None
else None
)
return cls(
id=user.id,
email=user.email,
@@ -48,6 +94,8 @@ class MeUserPublicResponse(BaseModel):
created_at=user.created_at,
updated_at=user.updated_at,
kyc_verified_at=user.kyc_verified_at,
account_type=user.account_type,
legal_entity=legal_entity,
)