diff --git a/src/application/abstractions/repositories/i_purchase_request_repository.py b/src/application/abstractions/repositories/i_purchase_request_repository.py index a90c7f7..a99acbb 100644 --- a/src/application/abstractions/repositories/i_purchase_request_repository.py +++ b/src/application/abstractions/repositories/i_purchase_request_repository.py @@ -5,6 +5,10 @@ from src.application.domain.entities.organization import PurchaseRequestEntity class IPurchaseRequestRepository(ABC): + @abstractmethod + async def create(self, values: dict[str, Any]) -> PurchaseRequestEntity: + raise NotImplementedError + @abstractmethod async def get_by_id(self, request_id: str) -> PurchaseRequestEntity: raise NotImplementedError diff --git a/src/application/commands/__init__.py b/src/application/commands/__init__.py index 0907f29..27ce1ff 100644 --- a/src/application/commands/__init__.py +++ b/src/application/commands/__init__.py @@ -21,6 +21,7 @@ from src.application.commands.organization_wallet_commands import ( ListOrganizationWalletsCommand, ) from src.application.commands.purchase_request_commands import ( + CreatePurchaseRequestCommand, ListPurchaseRequestsCommand, GetPurchaseRequestCommand, UpdatePurchaseRequestCommand, @@ -40,6 +41,7 @@ __all__ = [ 'GetOrganizationCommand', 'SearchPartiesCommand', 'UpdateOrganizationCommand', + 'CreatePurchaseRequestCommand', 'ListPurchaseRequestsCommand', 'GetPurchaseRequestCommand', 'UpdatePurchaseRequestCommand', diff --git a/src/application/commands/purchase_request_commands.py b/src/application/commands/purchase_request_commands.py index 8f76769..f0450c6 100644 --- a/src/application/commands/purchase_request_commands.py +++ b/src/application/commands/purchase_request_commands.py @@ -54,6 +54,54 @@ class GetPurchaseRequestCommand: return await self._unit_of_work.purchase_request_repository.get_by_id(request_id) +class CreatePurchaseRequestCommand: + def __init__(self, unit_of_work: IUnitOfWork, logger: ILogger): + self._unit_of_work = unit_of_work + self._logger = logger + + @transactional + async def __call__( + self, + *, + organization_id: str, + usdt_amount: Decimal, + status: str = 'submitted', + rub_amount: Decimal | None = None, + exchange_rate: Decimal | None = None, + service_fee_percent: Decimal | None = None, + comment: str | None = None, + admin_comment: str | None = None, + target_wallet_chain: str | None = 'ETH', + target_wallet_address: str | None = None, + tx_hash: str | None = None, + assigned_to: str | None = None, + ) -> PurchaseRequestEntity: + if status not in VALID_STATUSES: + raise ApplicationException(status_code=400, message='Invalid status') + + await self._unit_of_work.legal_entity_repository.get_by_id(organization_id) + values: dict[str, Any] = { + 'organization_id': organization_id, + 'status': status, + 'usdt_amount': usdt_amount, + 'rub_amount': rub_amount, + 'exchange_rate': exchange_rate, + 'service_fee_percent': service_fee_percent, + 'comment': comment, + 'admin_comment': admin_comment, + 'target_wallet_chain': target_wallet_chain, + 'target_wallet_address': target_wallet_address, + 'tx_hash': tx_hash, + 'assigned_to': assigned_to, + } + if status == 'completed': + values['completed_at'] = datetime.now(timezone.utc) + + created = await self._unit_of_work.purchase_request_repository.create(values) + self._logger.info(f'Purchase request created id={created.id} org={organization_id}') + return created + + class UpdatePurchaseRequestStatusCommand: def __init__(self, unit_of_work: IUnitOfWork, logger: ILogger): self._unit_of_work = unit_of_work diff --git a/src/infrastructure/database/repositories/purchase_request_repository.py b/src/infrastructure/database/repositories/purchase_request_repository.py index 27d481c..aac8421 100644 --- a/src/infrastructure/database/repositories/purchase_request_repository.py +++ b/src/infrastructure/database/repositories/purchase_request_repository.py @@ -2,6 +2,7 @@ from __future__ import annotations from typing import Any +from ulid import ULID from sqlalchemy import func, select, update from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.exc import SQLAlchemyError @@ -45,6 +46,16 @@ class PurchaseRequestRepository(IPurchaseRequestRepository): stmt = stmt.where(PurchaseRequestModel.organization_id == organization_id) return stmt + async def create(self, values: dict[str, Any]) -> PurchaseRequestEntity: + model = PurchaseRequestModel(id=str(ULID()), **values) + 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, request_id: str) -> PurchaseRequestEntity: res = await self._session.execute(select(PurchaseRequestModel).where(PurchaseRequestModel.id == request_id)) m = res.scalar_one_or_none() diff --git a/src/presentation/dependencies/commands.py b/src/presentation/dependencies/commands.py index 5cd04be..c6c3058 100644 --- a/src/presentation/dependencies/commands.py +++ b/src/presentation/dependencies/commands.py @@ -7,6 +7,7 @@ from src.application.commands import ( AdminLoginCommand, AdminLogoutCommand, AdminJwtRefreshCommand, + CreatePurchaseRequestCommand, GetAdminMeCommand, CreateOrganizationCommand, CreateOrganizationWalletsCommand, @@ -173,6 +174,13 @@ def get_list_purchase_requests_command( return ListPurchaseRequestsCommand(uow, logger) +def get_create_purchase_request_command( + uow: IUnitOfWork = Depends(get_unit_of_work), + logger: ILogger = Depends(get_logger), +) -> CreatePurchaseRequestCommand: + return CreatePurchaseRequestCommand(uow, logger) + + def get_get_purchase_request_command( uow: IUnitOfWork = Depends(get_unit_of_work), logger: ILogger = Depends(get_logger), diff --git a/src/presentation/routing/purchase_requests.py b/src/presentation/routing/purchase_requests.py index 3d1d408..07b8ff3 100644 --- a/src/presentation/routing/purchase_requests.py +++ b/src/presentation/routing/purchase_requests.py @@ -1,6 +1,7 @@ from fastapi import APIRouter, Depends, Query, status from src.application.commands import ( + CreatePurchaseRequestCommand, GetPurchaseRequestCommand, ListPurchaseRequestsCommand, SetPurchaseRequestQuoteCommand, @@ -10,6 +11,7 @@ from src.application.commands import ( from src.application.domain.dto import AdminAuthContext from src.presentation.decorators.admin_auth import require_admin_access, require_admin_role from src.presentation.dependencies.commands import ( + get_create_purchase_request_command, get_get_purchase_request_command, get_list_purchase_requests_command, get_set_purchase_request_quote_command, @@ -18,6 +20,7 @@ from src.presentation.dependencies.commands import ( ) from src.presentation.schemas.mappers import purchase_request_to_response from src.presentation.schemas.organization import ( + CreatePurchaseRequestBody, PurchaseRequestListResponse, PurchaseRequestResponse, SetPurchaseRequestQuoteBody, @@ -49,6 +52,29 @@ async def list_purchase_requests( ) +@purchase_requests_router.post('', response_model=PurchaseRequestResponse, status_code=status.HTTP_201_CREATED) +async def create_purchase_request( + body: CreatePurchaseRequestBody, + auth: AdminAuthContext = Depends(require_admin_role('compliance', 'superadmin')), + command: CreatePurchaseRequestCommand = Depends(get_create_purchase_request_command), +): + item = await command( + organization_id=body.organization_id, + usdt_amount=body.usdt_amount, + status=body.status, + rub_amount=body.rub_amount, + exchange_rate=body.exchange_rate, + service_fee_percent=body.service_fee_percent, + comment=body.comment, + admin_comment=body.admin_comment, + target_wallet_chain=body.target_wallet_chain, + target_wallet_address=body.target_wallet_address, + tx_hash=body.tx_hash, + assigned_to=body.assigned_to, + ) + return purchase_request_to_response(item) + + @purchase_requests_router.get('/{request_id}', response_model=PurchaseRequestResponse) async def get_purchase_request( request_id: str, diff --git a/src/presentation/schemas/organization.py b/src/presentation/schemas/organization.py index dde9b63..c28e545 100644 --- a/src/presentation/schemas/organization.py +++ b/src/presentation/schemas/organization.py @@ -114,6 +114,21 @@ class DocumentResponse(BaseModel): download_url: str | None = None +class CreatePurchaseRequestBody(BaseModel): + organization_id: str + usdt_amount: Decimal = Field(gt=0) + status: str = 'submitted' + rub_amount: Decimal | None = Field(default=None, gt=0) + exchange_rate: Decimal | None = Field(default=None, gt=0) + service_fee_percent: Decimal | None = Field(default=None, ge=0) + comment: str | None = None + admin_comment: str | None = None + target_wallet_chain: str | None = Field(default='ETH', max_length=16) + target_wallet_address: str | None = Field(default=None, max_length=128) + tx_hash: str | None = Field(default=None, max_length=128) + assigned_to: str | None = None + + class UpdatePurchaseRequestStatusBody(BaseModel): status: str admin_comment: str | None = None