feat: update le docs logic
This commit is contained in:
@@ -4,14 +4,14 @@ version = "0.1.0"
|
|||||||
description = "Admin service for legal entities and B2B operations"
|
description = "Admin service for legal entities and B2B operations"
|
||||||
requires-python = "==3.12.*"
|
requires-python = "==3.12.*"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"acryl-datahub>=1.5.0.19",
|
"acryl-datahub==1.5.0.19",
|
||||||
"acryl-sqlglot>=25.25.2.dev9",
|
"acryl-sqlglot==25.25.2.dev9",
|
||||||
"aiobotocore>=2.21.0",
|
"aiobotocore==2.21.0",
|
||||||
"apscheduler==3.11.2",
|
"apscheduler==3.11.2",
|
||||||
"asyncpg==0.31.0",
|
"asyncpg==0.31.0",
|
||||||
"bcrypt==5.0.0",
|
"bcrypt==5.0.0",
|
||||||
"bip-utils>=2.9.3",
|
"bip-utils==2.9.3",
|
||||||
"cryptography>=44.0.0",
|
"cryptography==44.0.0",
|
||||||
"dotenv==0.9.9",
|
"dotenv==0.9.9",
|
||||||
"fastapi==0.128.7",
|
"fastapi==0.128.7",
|
||||||
"granian==2.6.1",
|
"granian==2.6.1",
|
||||||
@@ -19,7 +19,7 @@ dependencies = [
|
|||||||
"orjson==3.11.7",
|
"orjson==3.11.7",
|
||||||
"pydantic-settings==2.12.0",
|
"pydantic-settings==2.12.0",
|
||||||
"python-jose==3.5.0",
|
"python-jose==3.5.0",
|
||||||
"python-multipart>=0.0.20",
|
"python-multipart==0.0.20",
|
||||||
"python-ulid==3.1.0",
|
"python-ulid==3.1.0",
|
||||||
"redis==7.2.0",
|
"redis==7.2.0",
|
||||||
"sqlalchemy==2.0.46",
|
"sqlalchemy==2.0.46",
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from src.application.abstractions.repositories import (
|
|||||||
IAdminSessionRepository,
|
IAdminSessionRepository,
|
||||||
IAdminUserRepository,
|
IAdminUserRepository,
|
||||||
ILegalEntityRepository,
|
ILegalEntityRepository,
|
||||||
IOrganizationDocumentRepository,
|
|
||||||
IOrganizationWalletRepository,
|
IOrganizationWalletRepository,
|
||||||
IPurchaseRequestRepository,
|
IPurchaseRequestRepository,
|
||||||
IUserRepository,
|
IUserRepository,
|
||||||
@@ -36,8 +35,5 @@ class IUnitOfWork(Protocol):
|
|||||||
@property
|
@property
|
||||||
def organization_wallet_repository(self) -> IOrganizationWalletRepository: ...
|
def organization_wallet_repository(self) -> IOrganizationWalletRepository: ...
|
||||||
|
|
||||||
@property
|
|
||||||
def organization_document_repository(self) -> IOrganizationDocumentRepository: ...
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def purchase_request_repository(self) -> IPurchaseRequestRepository: ...
|
def purchase_request_repository(self) -> IPurchaseRequestRepository: ...
|
||||||
|
|||||||
@@ -3,5 +3,4 @@ from src.application.abstractions.repositories.i_admin_user_repository import IA
|
|||||||
from src.application.abstractions.repositories.i_admin_session_repository import IAdminSessionRepository
|
from src.application.abstractions.repositories.i_admin_session_repository import IAdminSessionRepository
|
||||||
from src.application.abstractions.repositories.i_legal_entity_repository import ILegalEntityRepository
|
from src.application.abstractions.repositories.i_legal_entity_repository import ILegalEntityRepository
|
||||||
from src.application.abstractions.repositories.i_organization_wallet_repository import IOrganizationWalletRepository
|
from src.application.abstractions.repositories.i_organization_wallet_repository import IOrganizationWalletRepository
|
||||||
from src.application.abstractions.repositories.i_organization_document_repository import IOrganizationDocumentRepository
|
|
||||||
from src.application.abstractions.repositories.i_purchase_request_repository import IPurchaseRequestRepository
|
from src.application.abstractions.repositories.i_purchase_request_repository import IPurchaseRequestRepository
|
||||||
|
|||||||
@@ -48,6 +48,14 @@ class ILegalEntityRepository(ABC):
|
|||||||
async def set_encrypted_mnemonic(self, organization_id: str, encrypted_mnemonic: str) -> None:
|
async def set_encrypted_mnemonic(self, organization_id: str, encrypted_mnemonic: str) -> None:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def get_document_s3_key(self, organization_id: str, document_type: str) -> str | None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def set_document_s3_key(self, organization_id: str, document_type: str, s3_key: str) -> LegalEntityEntity:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def count_all(self) -> int:
|
async def count_all(self) -> int:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
from abc import ABC, abstractmethod
|
|
||||||
|
|
||||||
from src.application.domain.entities.organization import OrganizationDocumentEntity
|
|
||||||
|
|
||||||
|
|
||||||
class IOrganizationDocumentRepository(ABC):
|
|
||||||
@abstractmethod
|
|
||||||
async def create(self, document: OrganizationDocumentEntity) -> OrganizationDocumentEntity:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
async def get_by_id(self, document_id: str) -> OrganizationDocumentEntity:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
async def list_by_organization(self, organization_id: str) -> list[OrganizationDocumentEntity]:
|
|
||||||
raise NotImplementedError
|
|
||||||
@@ -4,7 +4,7 @@ from src.application.commands.admin_jwt_refresh import AdminJwtRefreshCommand
|
|||||||
from src.application.commands.get_admin_me import GetAdminMeCommand
|
from src.application.commands.get_admin_me import GetAdminMeCommand
|
||||||
from src.application.commands.create_organization import CreateOrganizationCommand
|
from src.application.commands.create_organization import CreateOrganizationCommand
|
||||||
from src.application.commands.create_organization_wallets import CreateOrganizationWalletsCommand
|
from src.application.commands.create_organization_wallets import CreateOrganizationWalletsCommand
|
||||||
from src.application.commands.upload_organization_document import UploadOrganizationDocumentCommand
|
from src.application.commands.put_organization_document import PutOrganizationDocumentCommand
|
||||||
from src.application.commands.organization_commands import (
|
from src.application.commands.organization_commands import (
|
||||||
ListOrganizationsCommand,
|
ListOrganizationsCommand,
|
||||||
GetOrganizationCommand,
|
GetOrganizationCommand,
|
||||||
@@ -33,7 +33,7 @@ __all__ = [
|
|||||||
'GetAdminMeCommand',
|
'GetAdminMeCommand',
|
||||||
'CreateOrganizationCommand',
|
'CreateOrganizationCommand',
|
||||||
'CreateOrganizationWalletsCommand',
|
'CreateOrganizationWalletsCommand',
|
||||||
'UploadOrganizationDocumentCommand',
|
'PutOrganizationDocumentCommand',
|
||||||
'ListOrganizationsCommand',
|
'ListOrganizationsCommand',
|
||||||
'GetOrganizationCommand',
|
'GetOrganizationCommand',
|
||||||
'UpdateOrganizationCommand',
|
'UpdateOrganizationCommand',
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from src.application.abstractions import IUnitOfWork
|
from src.application.abstractions import IUnitOfWork
|
||||||
from src.application.contracts import ILogger
|
from src.application.contracts import ILogger
|
||||||
from src.application.domain.entities.organization import OrganizationDocumentEntity
|
from src.application.domain.entities.organization import OrganizationDocumentSlot
|
||||||
from src.application.domain.exceptions import ApplicationException
|
from src.application.domain.exceptions import ApplicationException
|
||||||
|
from src.application.domain.organization_documents import ORGANIZATION_DOCUMENT_TYPES
|
||||||
from src.infrastructure.database.decorators import transactional
|
from src.infrastructure.database.decorators import transactional
|
||||||
|
from src.infrastructure.storage.s3_documents_service import S3DocumentsService
|
||||||
|
|
||||||
|
|
||||||
class ListOrganizationDocumentsCommand:
|
class ListOrganizationDocumentsCommand:
|
||||||
@@ -13,9 +15,20 @@ class ListOrganizationDocumentsCommand:
|
|||||||
self._logger = logger
|
self._logger = logger
|
||||||
|
|
||||||
@transactional
|
@transactional
|
||||||
async def __call__(self, organization_id: str) -> list[OrganizationDocumentEntity]:
|
async def __call__(self, organization_id: str) -> list[OrganizationDocumentSlot]:
|
||||||
await self._unit_of_work.legal_entity_repository.get_by_id(organization_id)
|
org = await self._unit_of_work.legal_entity_repository.get_by_id(organization_id)
|
||||||
return await self._unit_of_work.organization_document_repository.list_by_organization(organization_id)
|
slots: list[OrganizationDocumentSlot] = []
|
||||||
|
for document_type in ORGANIZATION_DOCUMENT_TYPES:
|
||||||
|
s3_key = org.document_s3_keys.get(document_type)
|
||||||
|
slots.append(
|
||||||
|
OrganizationDocumentSlot(
|
||||||
|
organization_id=organization_id,
|
||||||
|
document_type=document_type,
|
||||||
|
s3_key=s3_key,
|
||||||
|
file_name=S3DocumentsService.file_name_from_key(s3_key) if s3_key else None,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return slots
|
||||||
|
|
||||||
|
|
||||||
class GetOrganizationDocumentCommand:
|
class GetOrganizationDocumentCommand:
|
||||||
@@ -24,8 +37,18 @@ class GetOrganizationDocumentCommand:
|
|||||||
self._logger = logger
|
self._logger = logger
|
||||||
|
|
||||||
@transactional
|
@transactional
|
||||||
async def __call__(self, organization_id: str, document_id: str) -> OrganizationDocumentEntity:
|
async def __call__(self, organization_id: str, document_type: str) -> OrganizationDocumentSlot:
|
||||||
doc = await self._unit_of_work.organization_document_repository.get_by_id(document_id)
|
if document_type not in ORGANIZATION_DOCUMENT_TYPES:
|
||||||
if doc.organization_id != organization_id:
|
raise ApplicationException(status_code=400, message='Invalid document type')
|
||||||
raise ApplicationException(status_code=404, message='Document not found')
|
|
||||||
return doc
|
org = await self._unit_of_work.legal_entity_repository.get_by_id(organization_id)
|
||||||
|
s3_key = org.document_s3_keys.get(document_type)
|
||||||
|
if not s3_key:
|
||||||
|
raise ApplicationException(status_code=404, message='Document not uploaded')
|
||||||
|
|
||||||
|
return OrganizationDocumentSlot(
|
||||||
|
organization_id=organization_id,
|
||||||
|
document_type=document_type,
|
||||||
|
s3_key=s3_key,
|
||||||
|
file_name=S3DocumentsService.file_name_from_key(s3_key),
|
||||||
|
)
|
||||||
|
|||||||
73
src/application/commands/put_organization_document.py
Normal file
73
src/application/commands/put_organization_document.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from src.application.abstractions import IUnitOfWork
|
||||||
|
from src.application.contracts import ILogger
|
||||||
|
from src.application.domain.entities.organization import OrganizationDocumentSlot
|
||||||
|
from src.application.domain.exceptions import ApplicationException
|
||||||
|
from src.application.domain.organization_documents import ORGANIZATION_DOCUMENT_TYPES
|
||||||
|
from src.infrastructure.database.decorators import transactional
|
||||||
|
from src.infrastructure.storage.s3_documents_service import S3DocumentsService
|
||||||
|
|
||||||
|
|
||||||
|
class PutOrganizationDocumentCommand:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
unit_of_work: IUnitOfWork,
|
||||||
|
s3_service: S3DocumentsService,
|
||||||
|
logger: ILogger,
|
||||||
|
):
|
||||||
|
self._unit_of_work = unit_of_work
|
||||||
|
self._s3 = s3_service
|
||||||
|
self._logger = logger
|
||||||
|
|
||||||
|
@transactional
|
||||||
|
async def __call__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
organization_id: str,
|
||||||
|
document_type: str,
|
||||||
|
file_name: str,
|
||||||
|
content_type: str,
|
||||||
|
body: bytes,
|
||||||
|
) -> OrganizationDocumentSlot:
|
||||||
|
if document_type not in ORGANIZATION_DOCUMENT_TYPES:
|
||||||
|
raise ApplicationException(status_code=400, message='Invalid document type')
|
||||||
|
|
||||||
|
if not body:
|
||||||
|
raise ApplicationException(status_code=400, message='File is empty')
|
||||||
|
|
||||||
|
await self._unit_of_work.legal_entity_repository.get_by_id(organization_id)
|
||||||
|
old_s3_key = await self._unit_of_work.legal_entity_repository.get_document_s3_key(
|
||||||
|
organization_id,
|
||||||
|
document_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
new_s3_key = self._s3.build_object_key(organization_id, document_type, file_name)
|
||||||
|
await self._s3.upload_bytes(key=new_s3_key, body=body, content_type=content_type)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await self._unit_of_work.legal_entity_repository.set_document_s3_key(
|
||||||
|
organization_id,
|
||||||
|
document_type,
|
||||||
|
new_s3_key,
|
||||||
|
)
|
||||||
|
await self._unit_of_work.commit()
|
||||||
|
except Exception:
|
||||||
|
await self._unit_of_work.rollback()
|
||||||
|
await self._s3.delete_object(key=new_s3_key)
|
||||||
|
raise
|
||||||
|
|
||||||
|
if old_s3_key and old_s3_key != new_s3_key:
|
||||||
|
await self._s3.delete_object(key=old_s3_key)
|
||||||
|
|
||||||
|
self._logger.info(
|
||||||
|
f'Document uploaded org={organization_id} type={document_type} key={new_s3_key}'
|
||||||
|
)
|
||||||
|
return OrganizationDocumentSlot(
|
||||||
|
organization_id=organization_id,
|
||||||
|
document_type=document_type,
|
||||||
|
s3_key=new_s3_key,
|
||||||
|
file_name=self._s3.file_name_from_key(new_s3_key),
|
||||||
|
content_type=content_type,
|
||||||
|
file_size_bytes=len(body),
|
||||||
|
)
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from ulid import ULID
|
|
||||||
|
|
||||||
from src.application.abstractions import IUnitOfWork
|
|
||||||
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.decorators import transactional
|
|
||||||
from src.infrastructure.storage.s3_documents_service import S3DocumentsService
|
|
||||||
|
|
||||||
|
|
||||||
ALLOWED_DOCUMENT_TYPES = frozenset({
|
|
||||||
'charter', 'inn_certificate', 'ogrn_certificate',
|
|
||||||
'bank_details', 'kyc_representative', 'power_of_attorney', 'other',
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
class UploadOrganizationDocumentCommand:
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
unit_of_work: IUnitOfWork,
|
|
||||||
s3_service: S3DocumentsService,
|
|
||||||
logger: ILogger,
|
|
||||||
):
|
|
||||||
self._unit_of_work = unit_of_work
|
|
||||||
self._s3 = s3_service
|
|
||||||
self._logger = logger
|
|
||||||
|
|
||||||
@transactional
|
|
||||||
async def __call__(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
organization_id: str,
|
|
||||||
admin_user_id: str,
|
|
||||||
document_type: str,
|
|
||||||
file_name: str,
|
|
||||||
content_type: str,
|
|
||||||
body: bytes,
|
|
||||||
) -> OrganizationDocumentEntity:
|
|
||||||
if document_type not in ALLOWED_DOCUMENT_TYPES:
|
|
||||||
raise ApplicationException(status_code=400, message='Invalid document type')
|
|
||||||
|
|
||||||
await self._unit_of_work.legal_entity_repository.get_by_id(organization_id)
|
|
||||||
|
|
||||||
document_id = str(ULID())
|
|
||||||
s3_key = self._s3.build_object_key(organization_id, document_id, file_name)
|
|
||||||
await self._s3.upload_bytes(key=s3_key, body=body, content_type=content_type)
|
|
||||||
|
|
||||||
entity = OrganizationDocumentEntity(
|
|
||||||
id=document_id,
|
|
||||||
organization_id=organization_id,
|
|
||||||
document_type=document_type,
|
|
||||||
file_name=file_name,
|
|
||||||
s3_key=s3_key,
|
|
||||||
content_type=content_type,
|
|
||||||
file_size_bytes=len(body),
|
|
||||||
uploaded_by=admin_user_id,
|
|
||||||
)
|
|
||||||
saved = await self._unit_of_work.organization_document_repository.create(entity)
|
|
||||||
self._logger.info(f'Document uploaded id={saved.id} org={organization_id}')
|
|
||||||
return saved
|
|
||||||
@@ -24,6 +24,7 @@ class LegalEntityEntity:
|
|||||||
kyc_verified: bool
|
kyc_verified: bool
|
||||||
kyc_verified_at: datetime | None
|
kyc_verified_at: datetime | None
|
||||||
encrypted_mnemonic: str | None
|
encrypted_mnemonic: str | None
|
||||||
|
document_s3_keys: dict[str, str | None]
|
||||||
created_by: str | None
|
created_by: str | None
|
||||||
created_at: datetime | None = None
|
created_at: datetime | None = None
|
||||||
updated_at: datetime | None = None
|
updated_at: datetime | None = None
|
||||||
@@ -46,16 +47,13 @@ class CreateOrganizationWalletsResult:
|
|||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class OrganizationDocumentEntity:
|
class OrganizationDocumentSlot:
|
||||||
id: str
|
|
||||||
organization_id: str
|
organization_id: str
|
||||||
document_type: str
|
document_type: str
|
||||||
file_name: str
|
s3_key: str | None
|
||||||
s3_key: str
|
file_name: str | None = None
|
||||||
content_type: str
|
content_type: str | None = None
|
||||||
file_size_bytes: int
|
file_size_bytes: int | None = None
|
||||||
uploaded_by: str | None
|
|
||||||
created_at: datetime | None = None
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
35
src/application/domain/organization_documents.py
Normal file
35
src/application/domain/organization_documents.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
ORGANIZATION_DOCUMENT_TYPES: tuple[str, ...] = (
|
||||||
|
'charter',
|
||||||
|
'inn_certificate',
|
||||||
|
'ogrn_certificate',
|
||||||
|
'bank_details',
|
||||||
|
'kyc_representative',
|
||||||
|
'power_of_attorney',
|
||||||
|
'other',
|
||||||
|
)
|
||||||
|
|
||||||
|
DOCUMENT_TYPE_TO_COLUMN: dict[str, str] = {
|
||||||
|
'charter': 'charter_s3_key',
|
||||||
|
'inn_certificate': 'inn_certificate_s3_key',
|
||||||
|
'ogrn_certificate': 'ogrn_certificate_s3_key',
|
||||||
|
'bank_details': 'bank_details_document_s3_key',
|
||||||
|
'kyc_representative': 'kyc_representative_s3_key',
|
||||||
|
'power_of_attorney': 'power_of_attorney_s3_key',
|
||||||
|
'other': 'other_document_s3_key',
|
||||||
|
}
|
||||||
|
|
||||||
|
DOCUMENT_TYPE_URL_SLUGS: dict[str, str] = {
|
||||||
|
'charter': 'charter',
|
||||||
|
'inn_certificate': 'inn-certificate',
|
||||||
|
'ogrn_certificate': 'ogrn-certificate',
|
||||||
|
'bank_details': 'bank-details',
|
||||||
|
'kyc_representative': 'kyc-representative',
|
||||||
|
'power_of_attorney': 'power-of-attorney',
|
||||||
|
'other': 'other',
|
||||||
|
}
|
||||||
|
|
||||||
|
URL_SLUG_TO_DOCUMENT_TYPE: dict[str, str] = {
|
||||||
|
slug: document_type for document_type, slug in DOCUMENT_TYPE_URL_SLUGS.items()
|
||||||
|
}
|
||||||
@@ -4,7 +4,6 @@ from src.infrastructure.database.models.admin_user import AdminUserModel
|
|||||||
from src.infrastructure.database.models.admin_session import AdminSessionModel
|
from src.infrastructure.database.models.admin_session import AdminSessionModel
|
||||||
from src.infrastructure.database.models.legal_entity import LegalEntityModel
|
from src.infrastructure.database.models.legal_entity import LegalEntityModel
|
||||||
from src.infrastructure.database.models.organization_wallet import OrganizationWalletModel
|
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
|
from src.infrastructure.database.models.purchase_request import PurchaseRequestModel
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@@ -14,6 +13,5 @@ __all__ = [
|
|||||||
'AdminSessionModel',
|
'AdminSessionModel',
|
||||||
'LegalEntityModel',
|
'LegalEntityModel',
|
||||||
'OrganizationWalletModel',
|
'OrganizationWalletModel',
|
||||||
'OrganizationDocumentModel',
|
|
||||||
'PurchaseRequestModel',
|
'PurchaseRequestModel',
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -35,6 +35,13 @@ class LegalEntityModel(Base, UlidPrimaryKeyMixin, AuditTimestampsMixin):
|
|||||||
kyc_verified: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='true', default=True)
|
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)
|
kyc_verified_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||||
encrypted_mnemonic: Mapped[str | None] = mapped_column(Text, nullable=True)
|
encrypted_mnemonic: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
|
charter_s3_key: Mapped[str | None] = mapped_column(String(1024), nullable=True)
|
||||||
|
inn_certificate_s3_key: Mapped[str | None] = mapped_column(String(1024), nullable=True)
|
||||||
|
ogrn_certificate_s3_key: Mapped[str | None] = mapped_column(String(1024), nullable=True)
|
||||||
|
bank_details_document_s3_key: Mapped[str | None] = mapped_column(String(1024), nullable=True)
|
||||||
|
kyc_representative_s3_key: Mapped[str | None] = mapped_column(String(1024), nullable=True)
|
||||||
|
power_of_attorney_s3_key: Mapped[str | None] = mapped_column(String(1024), nullable=True)
|
||||||
|
other_document_s3_key: Mapped[str | None] = mapped_column(String(1024), nullable=True)
|
||||||
created_by: Mapped[str | None] = mapped_column(
|
created_by: Mapped[str | None] = mapped_column(
|
||||||
String(26),
|
String(26),
|
||||||
ForeignKey('admin_users.id'),
|
ForeignKey('admin_users.id'),
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
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(),
|
|
||||||
)
|
|
||||||
@@ -3,7 +3,6 @@ from src.infrastructure.database.repositories.admin_session_repository import Ad
|
|||||||
from src.infrastructure.database.repositories.user_repository import UserRepository
|
from src.infrastructure.database.repositories.user_repository import UserRepository
|
||||||
from src.infrastructure.database.repositories.legal_entity_repository import LegalEntityRepository
|
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_wallet_repository import OrganizationWalletRepository
|
||||||
from src.infrastructure.database.repositories.organization_document_repository import OrganizationDocumentRepository
|
|
||||||
from src.infrastructure.database.repositories.purchase_request_repository import PurchaseRequestRepository
|
from src.infrastructure.database.repositories.purchase_request_repository import PurchaseRequestRepository
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@@ -12,6 +11,5 @@ __all__ = [
|
|||||||
'UserRepository',
|
'UserRepository',
|
||||||
'LegalEntityRepository',
|
'LegalEntityRepository',
|
||||||
'OrganizationWalletRepository',
|
'OrganizationWalletRepository',
|
||||||
'OrganizationDocumentRepository',
|
|
||||||
'PurchaseRequestRepository',
|
'PurchaseRequestRepository',
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from fastapi import status
|
|
||||||
from sqlalchemy import func, select, update
|
from sqlalchemy import func, select, update
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
|
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
|
||||||
@@ -11,6 +10,7 @@ from src.application.abstractions.repositories import ILegalEntityRepository
|
|||||||
from src.application.contracts import ILogger
|
from src.application.contracts import ILogger
|
||||||
from src.application.domain.entities.organization import LegalEntityEntity
|
from src.application.domain.entities.organization import LegalEntityEntity
|
||||||
from src.application.domain.exceptions import ApplicationException
|
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
|
||||||
|
|
||||||
|
|
||||||
@@ -19,6 +19,12 @@ class LegalEntityRepository(ILegalEntityRepository):
|
|||||||
self._session = session
|
self._session = session
|
||||||
self._logger = logger
|
self._logger = logger
|
||||||
|
|
||||||
|
def _document_s3_keys_from_model(self, m: LegalEntityModel) -> dict[str, str | None]:
|
||||||
|
return {
|
||||||
|
document_type: getattr(m, DOCUMENT_TYPE_TO_COLUMN[document_type])
|
||||||
|
for document_type in ORGANIZATION_DOCUMENT_TYPES
|
||||||
|
}
|
||||||
|
|
||||||
def _to_entity(self, m: LegalEntityModel) -> LegalEntityEntity:
|
def _to_entity(self, m: LegalEntityModel) -> LegalEntityEntity:
|
||||||
return LegalEntityEntity(
|
return LegalEntityEntity(
|
||||||
id=m.id,
|
id=m.id,
|
||||||
@@ -37,6 +43,7 @@ class LegalEntityRepository(ILegalEntityRepository):
|
|||||||
kyc_verified=m.kyc_verified,
|
kyc_verified=m.kyc_verified,
|
||||||
kyc_verified_at=m.kyc_verified_at,
|
kyc_verified_at=m.kyc_verified_at,
|
||||||
encrypted_mnemonic=m.encrypted_mnemonic,
|
encrypted_mnemonic=m.encrypted_mnemonic,
|
||||||
|
document_s3_keys=self._document_s3_keys_from_model(m),
|
||||||
created_by=m.created_by,
|
created_by=m.created_by,
|
||||||
created_at=m.created_at,
|
created_at=m.created_at,
|
||||||
updated_at=m.updated_at,
|
updated_at=m.updated_at,
|
||||||
@@ -121,3 +128,19 @@ class LegalEntityRepository(ILegalEntityRepository):
|
|||||||
.values(encrypted_mnemonic=encrypted_mnemonic)
|
.values(encrypted_mnemonic=encrypted_mnemonic)
|
||||||
)
|
)
|
||||||
await self._session.flush()
|
await self._session.flush()
|
||||||
|
|
||||||
|
async def get_document_s3_key(self, organization_id: str, document_type: str) -> str | None:
|
||||||
|
org = await self.get_by_id(organization_id)
|
||||||
|
return org.document_s3_keys.get(document_type)
|
||||||
|
|
||||||
|
async def set_document_s3_key(self, organization_id: str, document_type: str, s3_key: str) -> LegalEntityEntity:
|
||||||
|
column = DOCUMENT_TYPE_TO_COLUMN.get(document_type)
|
||||||
|
if column is None:
|
||||||
|
raise ApplicationException(status_code=400, message='Invalid document type')
|
||||||
|
await self._session.execute(
|
||||||
|
update(LegalEntityModel)
|
||||||
|
.where(LegalEntityModel.id == organization_id)
|
||||||
|
.values(**{column: s3_key})
|
||||||
|
)
|
||||||
|
await self._session.flush()
|
||||||
|
return await self.get_by_id(organization_id)
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
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()]
|
|
||||||
@@ -5,7 +5,6 @@ from src.application.abstractions.repositories import (
|
|||||||
IAdminSessionRepository,
|
IAdminSessionRepository,
|
||||||
IAdminUserRepository,
|
IAdminUserRepository,
|
||||||
ILegalEntityRepository,
|
ILegalEntityRepository,
|
||||||
IOrganizationDocumentRepository,
|
|
||||||
IOrganizationWalletRepository,
|
IOrganizationWalletRepository,
|
||||||
IPurchaseRequestRepository,
|
IPurchaseRequestRepository,
|
||||||
IUserRepository,
|
IUserRepository,
|
||||||
@@ -16,7 +15,6 @@ from src.infrastructure.database.repositories import (
|
|||||||
AdminSessionRepository,
|
AdminSessionRepository,
|
||||||
AdminUserRepository,
|
AdminUserRepository,
|
||||||
LegalEntityRepository,
|
LegalEntityRepository,
|
||||||
OrganizationDocumentRepository,
|
|
||||||
OrganizationWalletRepository,
|
OrganizationWalletRepository,
|
||||||
PurchaseRequestRepository,
|
PurchaseRequestRepository,
|
||||||
UserRepository,
|
UserRepository,
|
||||||
@@ -32,7 +30,6 @@ class UnitOfWork(IUnitOfWork):
|
|||||||
self._admin_session_repository: IAdminSessionRepository | None = None
|
self._admin_session_repository: IAdminSessionRepository | None = None
|
||||||
self._legal_entity_repository: ILegalEntityRepository | None = None
|
self._legal_entity_repository: ILegalEntityRepository | None = None
|
||||||
self._organization_wallet_repository: IOrganizationWalletRepository | None = None
|
self._organization_wallet_repository: IOrganizationWalletRepository | None = None
|
||||||
self._organization_document_repository: IOrganizationDocumentRepository | None = None
|
|
||||||
self._purchase_request_repository: IPurchaseRequestRepository | None = None
|
self._purchase_request_repository: IPurchaseRequestRepository | None = None
|
||||||
self._logger: ILogger = logger
|
self._logger: ILogger = logger
|
||||||
|
|
||||||
@@ -88,14 +85,6 @@ class UnitOfWork(IUnitOfWork):
|
|||||||
)
|
)
|
||||||
return self._organization_wallet_repository
|
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
|
@property
|
||||||
def purchase_request_repository(self) -> IPurchaseRequestRepository:
|
def purchase_request_repository(self) -> IPurchaseRequestRepository:
|
||||||
if self._purchase_request_repository is None:
|
if self._purchase_request_repository is None:
|
||||||
|
|||||||
@@ -23,9 +23,13 @@ class S3DocumentsService:
|
|||||||
self._key_prefix = key_prefix.strip('/')
|
self._key_prefix = key_prefix.strip('/')
|
||||||
self._presigned_ttl_seconds = presigned_ttl_seconds
|
self._presigned_ttl_seconds = presigned_ttl_seconds
|
||||||
|
|
||||||
def build_object_key(self, organization_id: str, document_id: str, file_name: str) -> str:
|
@staticmethod
|
||||||
|
def file_name_from_key(key: str) -> str:
|
||||||
|
return key.rsplit('/', 1)[-1]
|
||||||
|
|
||||||
|
def build_object_key(self, organization_id: str, document_type: str, file_name: str) -> str:
|
||||||
safe_name = file_name.replace('/', '_').replace('\\', '_')
|
safe_name = file_name.replace('/', '_').replace('\\', '_')
|
||||||
return f'{self._key_prefix}/{organization_id}/{document_id}/{safe_name}'
|
return f'{self._key_prefix}/{organization_id}/{document_type}/{safe_name}'
|
||||||
|
|
||||||
def _client_kwargs(self) -> dict[str, object]:
|
def _client_kwargs(self) -> dict[str, object]:
|
||||||
kw: dict[str, object] = {'region_name': self._region}
|
kw: dict[str, object] = {'region_name': self._region}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ from src.application.commands import (
|
|||||||
SetPurchaseRequestQuoteCommand,
|
SetPurchaseRequestQuoteCommand,
|
||||||
UpdateOrganizationCommand,
|
UpdateOrganizationCommand,
|
||||||
UpdatePurchaseRequestStatusCommand,
|
UpdatePurchaseRequestStatusCommand,
|
||||||
UploadOrganizationDocumentCommand,
|
PutOrganizationDocumentCommand,
|
||||||
)
|
)
|
||||||
from src.application.contracts import IHashService, IJwtService, ILogger
|
from src.application.contracts import IHashService, IJwtService, ILogger
|
||||||
from src.infrastructure.config import settings
|
from src.infrastructure.config import settings
|
||||||
@@ -115,11 +115,11 @@ def get_get_organization_secret_keys_command(
|
|||||||
return GetOrganizationSecretKeysCommand(uow, logger)
|
return GetOrganizationSecretKeysCommand(uow, logger)
|
||||||
|
|
||||||
|
|
||||||
def get_upload_organization_document_command(
|
def get_put_organization_document_command(
|
||||||
uow: IUnitOfWork = Depends(get_unit_of_work),
|
uow: IUnitOfWork = Depends(get_unit_of_work),
|
||||||
logger: ILogger = Depends(get_logger),
|
logger: ILogger = Depends(get_logger),
|
||||||
) -> UploadOrganizationDocumentCommand:
|
) -> PutOrganizationDocumentCommand:
|
||||||
return UploadOrganizationDocumentCommand(uow, get_s3_documents_service(), logger)
|
return PutOrganizationDocumentCommand(uow, get_s3_documents_service(), logger)
|
||||||
|
|
||||||
|
|
||||||
def get_list_organizations_command(
|
def get_list_organizations_command(
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
from fastapi import APIRouter, Depends, File, Form, UploadFile, status
|
from fastapi import APIRouter, Depends, File, UploadFile
|
||||||
|
|
||||||
from src.application.commands import (
|
from src.application.commands import (
|
||||||
GetOrganizationDocumentCommand,
|
GetOrganizationDocumentCommand,
|
||||||
ListOrganizationDocumentsCommand,
|
ListOrganizationDocumentsCommand,
|
||||||
UploadOrganizationDocumentCommand,
|
PutOrganizationDocumentCommand,
|
||||||
)
|
)
|
||||||
from src.application.domain.dto import AdminAuthContext
|
from src.application.domain.dto import AdminAuthContext
|
||||||
from src.presentation.decorators.admin_auth import require_admin_access, require_admin_role
|
from src.presentation.decorators.admin_auth import require_admin_access, require_admin_role
|
||||||
from src.presentation.dependencies.commands import (
|
from src.presentation.dependencies.commands import (
|
||||||
get_get_organization_document_command,
|
get_get_organization_document_command,
|
||||||
get_list_organization_documents_command,
|
get_list_organization_documents_command,
|
||||||
|
get_put_organization_document_command,
|
||||||
get_s3_documents_service,
|
get_s3_documents_service,
|
||||||
get_upload_organization_document_command,
|
|
||||||
)
|
)
|
||||||
from src.infrastructure.storage.s3_documents_service import S3DocumentsService
|
from src.infrastructure.storage.s3_documents_service import S3DocumentsService
|
||||||
from src.presentation.schemas.mappers import document_to_response
|
from src.presentation.schemas.mappers import document_to_response
|
||||||
@@ -20,6 +20,12 @@ from src.presentation.schemas.organization import DocumentResponse
|
|||||||
documents_router = APIRouter(prefix='/organizations/{organization_id}/documents', tags=['documents'])
|
documents_router = APIRouter(prefix='/organizations/{organization_id}/documents', tags=['documents'])
|
||||||
|
|
||||||
|
|
||||||
|
async def _document_download_url(s3: S3DocumentsService, s3_key: str | None) -> str | None:
|
||||||
|
if not s3_key:
|
||||||
|
return None
|
||||||
|
return await s3.generate_presigned_download_url(key=s3_key)
|
||||||
|
|
||||||
|
|
||||||
@documents_router.get('', response_model=list[DocumentResponse])
|
@documents_router.get('', response_model=list[DocumentResponse])
|
||||||
async def list_documents(
|
async def list_documents(
|
||||||
organization_id: str,
|
organization_id: str,
|
||||||
@@ -30,41 +36,226 @@ async def list_documents(
|
|||||||
docs = await command(organization_id)
|
docs = await command(organization_id)
|
||||||
result: list[DocumentResponse] = []
|
result: list[DocumentResponse] = []
|
||||||
for doc in docs:
|
for doc in docs:
|
||||||
url = await s3.generate_presigned_download_url(key=doc.s3_key)
|
url = await _document_download_url(s3, doc.s3_key)
|
||||||
result.append(document_to_response(doc, download_url=url))
|
result.append(document_to_response(doc, download_url=url))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@documents_router.post('', response_model=DocumentResponse, status_code=status.HTTP_201_CREATED)
|
async def _put_document(
|
||||||
async def upload_document(
|
*,
|
||||||
organization_id: str,
|
organization_id: str,
|
||||||
document_type: str = Form(...),
|
document_type: str,
|
||||||
file: UploadFile = File(...),
|
file: UploadFile,
|
||||||
auth: AdminAuthContext = Depends(require_admin_role('compliance', 'superadmin')),
|
command: PutOrganizationDocumentCommand,
|
||||||
command: UploadOrganizationDocumentCommand = Depends(get_upload_organization_document_command),
|
s3: S3DocumentsService,
|
||||||
s3: S3DocumentsService = Depends(get_s3_documents_service),
|
) -> DocumentResponse:
|
||||||
):
|
|
||||||
body = await file.read()
|
body = await file.read()
|
||||||
saved = await command(
|
saved = await command(
|
||||||
organization_id=organization_id,
|
organization_id=organization_id,
|
||||||
admin_user_id=auth.admin_user_id,
|
|
||||||
document_type=document_type,
|
document_type=document_type,
|
||||||
file_name=file.filename or 'document',
|
file_name=file.filename or 'document',
|
||||||
content_type=file.content_type or 'application/octet-stream',
|
content_type=file.content_type or 'application/octet-stream',
|
||||||
body=body,
|
body=body,
|
||||||
)
|
)
|
||||||
url = await s3.generate_presigned_download_url(key=saved.s3_key)
|
url = await _document_download_url(s3, saved.s3_key)
|
||||||
return document_to_response(saved, download_url=url)
|
return document_to_response(saved, download_url=url)
|
||||||
|
|
||||||
|
|
||||||
@documents_router.get('/{document_id}', response_model=DocumentResponse)
|
@documents_router.put('/charter', response_model=DocumentResponse)
|
||||||
async def get_document(
|
async def put_charter_document(
|
||||||
|
organization_id: str,
|
||||||
|
file: UploadFile = File(...),
|
||||||
|
auth: AdminAuthContext = Depends(require_admin_role('compliance', 'superadmin')),
|
||||||
|
command: PutOrganizationDocumentCommand = Depends(get_put_organization_document_command),
|
||||||
|
s3: S3DocumentsService = Depends(get_s3_documents_service),
|
||||||
|
):
|
||||||
|
return await _put_document(
|
||||||
|
organization_id=organization_id,
|
||||||
|
document_type='charter',
|
||||||
|
file=file,
|
||||||
|
command=command,
|
||||||
|
s3=s3,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@documents_router.put('/inn-certificate', response_model=DocumentResponse)
|
||||||
|
async def put_inn_certificate_document(
|
||||||
|
organization_id: str,
|
||||||
|
file: UploadFile = File(...),
|
||||||
|
auth: AdminAuthContext = Depends(require_admin_role('compliance', 'superadmin')),
|
||||||
|
command: PutOrganizationDocumentCommand = Depends(get_put_organization_document_command),
|
||||||
|
s3: S3DocumentsService = Depends(get_s3_documents_service),
|
||||||
|
):
|
||||||
|
return await _put_document(
|
||||||
|
organization_id=organization_id,
|
||||||
|
document_type='inn_certificate',
|
||||||
|
file=file,
|
||||||
|
command=command,
|
||||||
|
s3=s3,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@documents_router.put('/ogrn-certificate', response_model=DocumentResponse)
|
||||||
|
async def put_ogrn_certificate_document(
|
||||||
|
organization_id: str,
|
||||||
|
file: UploadFile = File(...),
|
||||||
|
auth: AdminAuthContext = Depends(require_admin_role('compliance', 'superadmin')),
|
||||||
|
command: PutOrganizationDocumentCommand = Depends(get_put_organization_document_command),
|
||||||
|
s3: S3DocumentsService = Depends(get_s3_documents_service),
|
||||||
|
):
|
||||||
|
return await _put_document(
|
||||||
|
organization_id=organization_id,
|
||||||
|
document_type='ogrn_certificate',
|
||||||
|
file=file,
|
||||||
|
command=command,
|
||||||
|
s3=s3,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@documents_router.put('/bank-details', response_model=DocumentResponse)
|
||||||
|
async def put_bank_details_document(
|
||||||
|
organization_id: str,
|
||||||
|
file: UploadFile = File(...),
|
||||||
|
auth: AdminAuthContext = Depends(require_admin_role('compliance', 'superadmin')),
|
||||||
|
command: PutOrganizationDocumentCommand = Depends(get_put_organization_document_command),
|
||||||
|
s3: S3DocumentsService = Depends(get_s3_documents_service),
|
||||||
|
):
|
||||||
|
return await _put_document(
|
||||||
|
organization_id=organization_id,
|
||||||
|
document_type='bank_details',
|
||||||
|
file=file,
|
||||||
|
command=command,
|
||||||
|
s3=s3,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@documents_router.put('/kyc-representative', response_model=DocumentResponse)
|
||||||
|
async def put_kyc_representative_document(
|
||||||
|
organization_id: str,
|
||||||
|
file: UploadFile = File(...),
|
||||||
|
auth: AdminAuthContext = Depends(require_admin_role('compliance', 'superadmin')),
|
||||||
|
command: PutOrganizationDocumentCommand = Depends(get_put_organization_document_command),
|
||||||
|
s3: S3DocumentsService = Depends(get_s3_documents_service),
|
||||||
|
):
|
||||||
|
return await _put_document(
|
||||||
|
organization_id=organization_id,
|
||||||
|
document_type='kyc_representative',
|
||||||
|
file=file,
|
||||||
|
command=command,
|
||||||
|
s3=s3,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@documents_router.put('/power-of-attorney', response_model=DocumentResponse)
|
||||||
|
async def put_power_of_attorney_document(
|
||||||
|
organization_id: str,
|
||||||
|
file: UploadFile = File(...),
|
||||||
|
auth: AdminAuthContext = Depends(require_admin_role('compliance', 'superadmin')),
|
||||||
|
command: PutOrganizationDocumentCommand = Depends(get_put_organization_document_command),
|
||||||
|
s3: S3DocumentsService = Depends(get_s3_documents_service),
|
||||||
|
):
|
||||||
|
return await _put_document(
|
||||||
|
organization_id=organization_id,
|
||||||
|
document_type='power_of_attorney',
|
||||||
|
file=file,
|
||||||
|
command=command,
|
||||||
|
s3=s3,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@documents_router.put('/other', response_model=DocumentResponse)
|
||||||
|
async def put_other_document(
|
||||||
|
organization_id: str,
|
||||||
|
file: UploadFile = File(...),
|
||||||
|
auth: AdminAuthContext = Depends(require_admin_role('compliance', 'superadmin')),
|
||||||
|
command: PutOrganizationDocumentCommand = Depends(get_put_organization_document_command),
|
||||||
|
s3: S3DocumentsService = Depends(get_s3_documents_service),
|
||||||
|
):
|
||||||
|
return await _put_document(
|
||||||
|
organization_id=organization_id,
|
||||||
|
document_type='other',
|
||||||
|
file=file,
|
||||||
|
command=command,
|
||||||
|
s3=s3,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@documents_router.get('/charter', response_model=DocumentResponse)
|
||||||
|
async def get_charter_document(
|
||||||
organization_id: str,
|
organization_id: str,
|
||||||
document_id: str,
|
|
||||||
auth: AdminAuthContext = Depends(require_admin_access),
|
auth: AdminAuthContext = Depends(require_admin_access),
|
||||||
command: GetOrganizationDocumentCommand = Depends(get_get_organization_document_command),
|
command: GetOrganizationDocumentCommand = Depends(get_get_organization_document_command),
|
||||||
s3: S3DocumentsService = Depends(get_s3_documents_service),
|
s3: S3DocumentsService = Depends(get_s3_documents_service),
|
||||||
):
|
):
|
||||||
doc = await command(organization_id, document_id)
|
return await _get_document(organization_id, 'charter', command, s3)
|
||||||
url = await s3.generate_presigned_download_url(key=doc.s3_key)
|
|
||||||
|
|
||||||
|
@documents_router.get('/inn-certificate', response_model=DocumentResponse)
|
||||||
|
async def get_inn_certificate_document(
|
||||||
|
organization_id: str,
|
||||||
|
auth: AdminAuthContext = Depends(require_admin_access),
|
||||||
|
command: GetOrganizationDocumentCommand = Depends(get_get_organization_document_command),
|
||||||
|
s3: S3DocumentsService = Depends(get_s3_documents_service),
|
||||||
|
):
|
||||||
|
return await _get_document(organization_id, 'inn_certificate', command, s3)
|
||||||
|
|
||||||
|
|
||||||
|
@documents_router.get('/ogrn-certificate', response_model=DocumentResponse)
|
||||||
|
async def get_ogrn_certificate_document(
|
||||||
|
organization_id: str,
|
||||||
|
auth: AdminAuthContext = Depends(require_admin_access),
|
||||||
|
command: GetOrganizationDocumentCommand = Depends(get_get_organization_document_command),
|
||||||
|
s3: S3DocumentsService = Depends(get_s3_documents_service),
|
||||||
|
):
|
||||||
|
return await _get_document(organization_id, 'ogrn_certificate', command, s3)
|
||||||
|
|
||||||
|
|
||||||
|
@documents_router.get('/bank-details', response_model=DocumentResponse)
|
||||||
|
async def get_bank_details_document(
|
||||||
|
organization_id: str,
|
||||||
|
auth: AdminAuthContext = Depends(require_admin_access),
|
||||||
|
command: GetOrganizationDocumentCommand = Depends(get_get_organization_document_command),
|
||||||
|
s3: S3DocumentsService = Depends(get_s3_documents_service),
|
||||||
|
):
|
||||||
|
return await _get_document(organization_id, 'bank_details', command, s3)
|
||||||
|
|
||||||
|
|
||||||
|
@documents_router.get('/kyc-representative', response_model=DocumentResponse)
|
||||||
|
async def get_kyc_representative_document(
|
||||||
|
organization_id: str,
|
||||||
|
auth: AdminAuthContext = Depends(require_admin_access),
|
||||||
|
command: GetOrganizationDocumentCommand = Depends(get_get_organization_document_command),
|
||||||
|
s3: S3DocumentsService = Depends(get_s3_documents_service),
|
||||||
|
):
|
||||||
|
return await _get_document(organization_id, 'kyc_representative', command, s3)
|
||||||
|
|
||||||
|
|
||||||
|
@documents_router.get('/power-of-attorney', response_model=DocumentResponse)
|
||||||
|
async def get_power_of_attorney_document(
|
||||||
|
organization_id: str,
|
||||||
|
auth: AdminAuthContext = Depends(require_admin_access),
|
||||||
|
command: GetOrganizationDocumentCommand = Depends(get_get_organization_document_command),
|
||||||
|
s3: S3DocumentsService = Depends(get_s3_documents_service),
|
||||||
|
):
|
||||||
|
return await _get_document(organization_id, 'power_of_attorney', command, s3)
|
||||||
|
|
||||||
|
|
||||||
|
@documents_router.get('/other', response_model=DocumentResponse)
|
||||||
|
async def get_other_document(
|
||||||
|
organization_id: str,
|
||||||
|
auth: AdminAuthContext = Depends(require_admin_access),
|
||||||
|
command: GetOrganizationDocumentCommand = Depends(get_get_organization_document_command),
|
||||||
|
s3: S3DocumentsService = Depends(get_s3_documents_service),
|
||||||
|
):
|
||||||
|
return await _get_document(organization_id, 'other', command, s3)
|
||||||
|
|
||||||
|
|
||||||
|
async def _get_document(
|
||||||
|
organization_id: str,
|
||||||
|
document_type: str,
|
||||||
|
command: GetOrganizationDocumentCommand,
|
||||||
|
s3: S3DocumentsService,
|
||||||
|
) -> DocumentResponse:
|
||||||
|
doc = await command(organization_id, document_type)
|
||||||
|
url = await _document_download_url(s3, doc.s3_key)
|
||||||
return document_to_response(doc, download_url=url)
|
return document_to_response(doc, download_url=url)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from src.application.domain.entities.organization import (
|
from src.application.domain.entities.organization import (
|
||||||
LegalEntityEntity,
|
LegalEntityEntity,
|
||||||
OrganizationDocumentEntity,
|
OrganizationDocumentSlot,
|
||||||
OrganizationWalletEntity,
|
OrganizationWalletEntity,
|
||||||
PurchaseRequestEntity,
|
PurchaseRequestEntity,
|
||||||
)
|
)
|
||||||
@@ -78,19 +78,17 @@ def secret_key_to_response(
|
|||||||
|
|
||||||
|
|
||||||
def document_to_response(
|
def document_to_response(
|
||||||
entity: OrganizationDocumentEntity,
|
entity: OrganizationDocumentSlot,
|
||||||
*,
|
*,
|
||||||
download_url: str | None = None,
|
download_url: str | None = None,
|
||||||
) -> DocumentResponse:
|
) -> DocumentResponse:
|
||||||
return DocumentResponse(
|
return DocumentResponse(
|
||||||
id=entity.id,
|
|
||||||
organization_id=entity.organization_id,
|
organization_id=entity.organization_id,
|
||||||
document_type=entity.document_type,
|
document_type=entity.document_type,
|
||||||
|
s3_key=entity.s3_key,
|
||||||
file_name=entity.file_name,
|
file_name=entity.file_name,
|
||||||
content_type=entity.content_type,
|
content_type=entity.content_type,
|
||||||
file_size_bytes=entity.file_size_bytes,
|
file_size_bytes=entity.file_size_bytes,
|
||||||
uploaded_by=entity.uploaded_by,
|
|
||||||
created_at=entity.created_at.isoformat() if entity.created_at else None,
|
|
||||||
download_url=download_url,
|
download_url=download_url,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -87,14 +87,12 @@ class SecretKeyResponse(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class DocumentResponse(BaseModel):
|
class DocumentResponse(BaseModel):
|
||||||
id: str
|
|
||||||
organization_id: str
|
organization_id: str
|
||||||
document_type: str
|
document_type: str
|
||||||
file_name: str
|
s3_key: str | None
|
||||||
content_type: str
|
file_name: str | None
|
||||||
file_size_bytes: int
|
content_type: str | None = None
|
||||||
uploaded_by: str | None
|
file_size_bytes: int | None = None
|
||||||
created_at: str | None
|
|
||||||
download_url: str | None = None
|
download_url: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user