This commit is contained in:
2026-06-03 13:52:45 +03:00
commit f7309c4b4a
140 changed files with 7134 additions and 0 deletions

View 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',
]

View 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)

View 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)

View 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})"

View 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,
)

View 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

View 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(),
)

View 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)

View 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()))

View 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(),
)

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 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(),
)

View 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)

View 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)