feat: add create request

This commit is contained in:
2026-06-10 18:54:30 +03:00
parent dc05528405
commit 5e085ae67e
7 changed files with 114 additions and 0 deletions

View File

@@ -5,6 +5,10 @@ from src.application.domain.entities.organization import PurchaseRequestEntity
class IPurchaseRequestRepository(ABC): class IPurchaseRequestRepository(ABC):
@abstractmethod
async def create(self, values: dict[str, Any]) -> PurchaseRequestEntity:
raise NotImplementedError
@abstractmethod @abstractmethod
async def get_by_id(self, request_id: str) -> PurchaseRequestEntity: async def get_by_id(self, request_id: str) -> PurchaseRequestEntity:
raise NotImplementedError raise NotImplementedError

View File

@@ -21,6 +21,7 @@ from src.application.commands.organization_wallet_commands import (
ListOrganizationWalletsCommand, ListOrganizationWalletsCommand,
) )
from src.application.commands.purchase_request_commands import ( from src.application.commands.purchase_request_commands import (
CreatePurchaseRequestCommand,
ListPurchaseRequestsCommand, ListPurchaseRequestsCommand,
GetPurchaseRequestCommand, GetPurchaseRequestCommand,
UpdatePurchaseRequestCommand, UpdatePurchaseRequestCommand,
@@ -40,6 +41,7 @@ __all__ = [
'GetOrganizationCommand', 'GetOrganizationCommand',
'SearchPartiesCommand', 'SearchPartiesCommand',
'UpdateOrganizationCommand', 'UpdateOrganizationCommand',
'CreatePurchaseRequestCommand',
'ListPurchaseRequestsCommand', 'ListPurchaseRequestsCommand',
'GetPurchaseRequestCommand', 'GetPurchaseRequestCommand',
'UpdatePurchaseRequestCommand', 'UpdatePurchaseRequestCommand',

View File

@@ -54,6 +54,54 @@ class GetPurchaseRequestCommand:
return await self._unit_of_work.purchase_request_repository.get_by_id(request_id) 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: class UpdatePurchaseRequestStatusCommand:
def __init__(self, unit_of_work: IUnitOfWork, logger: ILogger): def __init__(self, unit_of_work: IUnitOfWork, logger: ILogger):
self._unit_of_work = unit_of_work self._unit_of_work = unit_of_work

View File

@@ -2,6 +2,7 @@ from __future__ import annotations
from typing import Any from typing import Any
from ulid import ULID
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 SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
@@ -45,6 +46,16 @@ class PurchaseRequestRepository(IPurchaseRequestRepository):
stmt = stmt.where(PurchaseRequestModel.organization_id == organization_id) stmt = stmt.where(PurchaseRequestModel.organization_id == organization_id)
return stmt 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: async def get_by_id(self, request_id: str) -> PurchaseRequestEntity:
res = await self._session.execute(select(PurchaseRequestModel).where(PurchaseRequestModel.id == request_id)) res = await self._session.execute(select(PurchaseRequestModel).where(PurchaseRequestModel.id == request_id))
m = res.scalar_one_or_none() m = res.scalar_one_or_none()

View File

@@ -7,6 +7,7 @@ from src.application.commands import (
AdminLoginCommand, AdminLoginCommand,
AdminLogoutCommand, AdminLogoutCommand,
AdminJwtRefreshCommand, AdminJwtRefreshCommand,
CreatePurchaseRequestCommand,
GetAdminMeCommand, GetAdminMeCommand,
CreateOrganizationCommand, CreateOrganizationCommand,
CreateOrganizationWalletsCommand, CreateOrganizationWalletsCommand,
@@ -173,6 +174,13 @@ def get_list_purchase_requests_command(
return ListPurchaseRequestsCommand(uow, logger) 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( def get_get_purchase_request_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),

View File

@@ -1,6 +1,7 @@
from fastapi import APIRouter, Depends, Query, status from fastapi import APIRouter, Depends, Query, status
from src.application.commands import ( from src.application.commands import (
CreatePurchaseRequestCommand,
GetPurchaseRequestCommand, GetPurchaseRequestCommand,
ListPurchaseRequestsCommand, ListPurchaseRequestsCommand,
SetPurchaseRequestQuoteCommand, SetPurchaseRequestQuoteCommand,
@@ -10,6 +11,7 @@ from src.application.commands import (
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_create_purchase_request_command,
get_get_purchase_request_command, get_get_purchase_request_command,
get_list_purchase_requests_command, get_list_purchase_requests_command,
get_set_purchase_request_quote_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.mappers import purchase_request_to_response
from src.presentation.schemas.organization import ( from src.presentation.schemas.organization import (
CreatePurchaseRequestBody,
PurchaseRequestListResponse, PurchaseRequestListResponse,
PurchaseRequestResponse, PurchaseRequestResponse,
SetPurchaseRequestQuoteBody, 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) @purchase_requests_router.get('/{request_id}', response_model=PurchaseRequestResponse)
async def get_purchase_request( async def get_purchase_request(
request_id: str, request_id: str,

View File

@@ -114,6 +114,21 @@ class DocumentResponse(BaseModel):
download_url: str | None = None 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): class UpdatePurchaseRequestStatusBody(BaseModel):
status: str status: str
admin_comment: str | None = None admin_comment: str | None = None