feat: add withdraw

This commit is contained in:
2026-06-21 15:00:59 +03:00
parent 56841b83ce
commit 39979f9976
12 changed files with 39 additions and 19 deletions

View File

@@ -0,0 +1,7 @@
BEGIN;
ALTER TABLE sbp_withdrawals
DROP COLUMN IF EXISTS wallet_address,
DROP COLUMN IF EXISTS sender_wallet_address;
COMMIT;

View File

@@ -2,6 +2,7 @@ from __future__ import annotations
from datetime import datetime,timezone from datetime import datetime,timezone
from decimal import Decimal from decimal import Decimal
from typing import Any from typing import Any
import re
from ulid import ULID from ulid import ULID
from src.application.abstractions import IUnitOfWork from src.application.abstractions import IUnitOfWork
from src.application.contracts import IQueueMessanger,ILogger,IMozenSbpService,SbpBank from src.application.contracts import IQueueMessanger,ILogger,IMozenSbpService,SbpBank
@@ -48,14 +49,30 @@ class CreateSbpWithdrawalCommand:
raise BadRequestException(message='SBP bank not found') raise BadRequestException(message='SBP bank not found')
@staticmethod
def _normalize_russian_phone(phone: str | None) -> str:
digits=re.sub(r'\D','',phone or '')
if len(digits)!=11:
raise BadRequestException(message='Russian phone number is required for SBP withdrawal')
if digits.startswith('8'):
digits=f'7{digits[1:]}'
if not digits.startswith('7'):
raise BadRequestException(message='Russian phone number is required for SBP withdrawal')
return f'+{digits}'
@transactional @transactional
async def __call__(self,*,user_id: str,bank_id: str,phone: str,usdt_amount: Decimal) -> SbpWithdrawalEntity: async def __call__(self,*,user_id: str,bank_id: str,usdt_amount: Decimal) -> SbpWithdrawalEntity:
user=await self._unit_of_work.user_repository.get(user_id) user=await self._unit_of_work.user_repository.get(user_id)
if user is None: if user is None:
raise NotFoundException(message='User not found') raise NotFoundException(message='User not found')
wallet_address=str(settings.SBP_WITHDRAWAL_WALLET_ADDRESS or '').strip() phone=self._normalize_russian_phone(user.phone)
wallet_address=str(settings.CRYPTO_USDT_CONTRACT_ADDRESS or '').strip()
if not wallet_address: if not wallet_address:
raise ServiceUnavailableException(message='SBP withdrawal wallet address unavailable') raise ServiceUnavailableException(message='USDT contract address unavailable')
sender_wallet_address=str(user.erc20 or '').strip()
if not sender_wallet_address:
raise BadRequestException(message='User ERC20 wallet address is required for SBP withdrawal')
trace_id=self._logger.get_trace_id() trace_id=self._logger.get_trace_id()
withdraw_id=str(ULID()) withdraw_id=str(ULID())
bank_name=await self._bank_name(bank_id) bank_name=await self._bank_name(bank_id)
@@ -66,7 +83,6 @@ class CreateSbpWithdrawalCommand:
bank_id=bank_id, bank_id=bank_id,
bank_name=bank_name, bank_name=bank_name,
phone=phone, phone=phone,
wallet_address=wallet_address,
usdt_amount=quote.usdt_amount, usdt_amount=quote.usdt_amount,
rub_amount=quote.rub_amount, rub_amount=quote.rub_amount,
usdt_exchange_rate=quote.usdt_exchange_rate, usdt_exchange_rate=quote.usdt_exchange_rate,
@@ -83,6 +99,8 @@ class CreateSbpWithdrawalCommand:
'usdt_amount':str(quote.usdt_amount), 'usdt_amount':str(quote.usdt_amount),
'trace_id':trace_id, 'trace_id':trace_id,
'wallet_address':wallet_address, 'wallet_address':wallet_address,
'sender_wallet_address':sender_wallet_address,
'receiver_wallet_address':wallet_address,
'withdraw_id':withdraw_id, 'withdraw_id':withdraw_id,
'message_id':message_id, 'message_id':message_id,
} }

View File

@@ -15,7 +15,6 @@ class SbpWithdrawalEntity:
bank_id: str | None = None bank_id: str | None = None
bank_name: str | None = None bank_name: str | None = None
phone: str | None = None phone: str | None = None
wallet_address: str | None = None
usdt_amount: Decimal | None = None usdt_amount: Decimal | None = None
rub_amount: Decimal | None = None rub_amount: Decimal | None = None
usdt_exchange_rate: Decimal | None = None usdt_exchange_rate: Decimal | None = None

View File

@@ -103,7 +103,7 @@ class Settings(BaseSettings):
MOZEN_MERCHANT_ID: str = '' MOZEN_MERCHANT_ID: str = ''
MOZEN_PARK_ID: str = '' MOZEN_PARK_ID: str = ''
MOZEN_PARK_TOKEN: str = '' MOZEN_PARK_TOKEN: str = ''
SBP_WITHDRAWAL_WALLET_ADDRESS: str = '' CRYPTO_USDT_CONTRACT_ADDRESS: str = ''
CLOUD_KASSIR_PUBLIC_ID: str = '' CLOUD_KASSIR_PUBLIC_ID: str = ''
CLOUD_KASSIR_API_SECRET: str = '' CLOUD_KASSIR_API_SECRET: str = ''
@@ -240,8 +240,13 @@ class Settings(BaseSettings):
extgw_set('merchant_id', 'MOZEN_MERCHANT_ID') extgw_set('merchant_id', 'MOZEN_MERCHANT_ID')
extgw_set('park_id', 'MOZEN_PARK_ID') extgw_set('park_id', 'MOZEN_PARK_ID')
extgw_set('park_token', 'MOZEN_PARK_TOKEN') extgw_set('park_token', 'MOZEN_PARK_TOKEN')
extgw_set('wallet_address', 'SBP_WITHDRAWAL_WALLET_ADDRESS')
extgw_set('usdt_wallet_address', 'SBP_WITHDRAWAL_WALLET_ADDRESS') crypto = read_secret_optional('crypto')
if crypto:
crypto_ci = {str(k).lower(): v for k, v in crypto.items()}
usdt_contract_address = crypto_ci.get('usdt_contract_address')
if usdt_contract_address is not None and str(usdt_contract_address).strip():
data['CRYPTO_USDT_CONTRACT_ADDRESS'] = str(usdt_contract_address).strip()
keydb = read_secret('keydb') keydb = read_secret('keydb')
k_ci = {str(k).lower(): v for k, v in keydb.items()} k_ci = {str(k).lower(): v for k, v in keydb.items()}

View File

@@ -20,7 +20,6 @@ class SbpWithdrawal(Base,UlidPrimaryKeyMixin,AuditTimestampsMixin):
bank_id: Mapped[str]=mapped_column(String(32),nullable=False,index=True) bank_id: Mapped[str]=mapped_column(String(32),nullable=False,index=True)
bank_name: Mapped[str]=mapped_column(String(255),nullable=False) bank_name: Mapped[str]=mapped_column(String(255),nullable=False)
phone: Mapped[str]=mapped_column(String(32),nullable=False) phone: Mapped[str]=mapped_column(String(32),nullable=False)
wallet_address: Mapped[str]=mapped_column(String(255),nullable=False)
usdt_amount: Mapped[Decimal]=mapped_column(Numeric(38,2),nullable=False) usdt_amount: Mapped[Decimal]=mapped_column(Numeric(38,2),nullable=False)
rub_amount: Mapped[Decimal]=mapped_column(Numeric(38,2),nullable=False) rub_amount: Mapped[Decimal]=mapped_column(Numeric(38,2),nullable=False)
usdt_exchange_rate: Mapped[Decimal]=mapped_column(Numeric(38,2),nullable=False) usdt_exchange_rate: Mapped[Decimal]=mapped_column(Numeric(38,2),nullable=False)

View File

@@ -26,7 +26,6 @@ class SbpWithdrawalRepository(ISbpWithdrawalRepository):
bank_id=model.bank_id, bank_id=model.bank_id,
bank_name=model.bank_name, bank_name=model.bank_name,
phone=model.phone, phone=model.phone,
wallet_address=model.wallet_address,
usdt_amount=model.usdt_amount, usdt_amount=model.usdt_amount,
rub_amount=model.rub_amount, rub_amount=model.rub_amount,
usdt_exchange_rate=model.usdt_exchange_rate, usdt_exchange_rate=model.usdt_exchange_rate,
@@ -48,7 +47,6 @@ class SbpWithdrawalRepository(ISbpWithdrawalRepository):
'bank_id':withdrawal.bank_id, 'bank_id':withdrawal.bank_id,
'bank_name':withdrawal.bank_name, 'bank_name':withdrawal.bank_name,
'phone':withdrawal.phone, 'phone':withdrawal.phone,
'wallet_address':withdrawal.wallet_address,
'usdt_amount':withdrawal.usdt_amount, 'usdt_amount':withdrawal.usdt_amount,
'rub_amount':withdrawal.rub_amount, 'rub_amount':withdrawal.rub_amount,
'usdt_exchange_rate':withdrawal.usdt_exchange_rate, 'usdt_exchange_rate':withdrawal.usdt_exchange_rate,

View File

@@ -10,7 +10,7 @@ from src.presentation.dependencies.commands import get_crypto_transfer_completed
from src.presentation.dependencies.logger import get_logger from src.presentation.dependencies.logger import get_logger
crypto_transfer_router=RabbitRouter(settings.RABBIT_URL) crypto_transfer_router=RabbitRouter(settings.RABBIT_URL,schema_url=None)
class CryptoTransferCompletedMessage(BaseModel): class CryptoTransferCompletedMessage(BaseModel):

View File

@@ -11,7 +11,7 @@ from src.presentation.dependencies.commands import get_sbp_withdrawal_wallet_eve
from src.presentation.dependencies.logger import get_logger from src.presentation.dependencies.logger import get_logger
sbp_withdrawal_messaging_router=RabbitRouter(settings.RABBIT_URL) sbp_withdrawal_messaging_router=RabbitRouter(settings.RABBIT_URL,schema_url=None)
class SbpWithdrawalWalletEventMessage(BaseModel): class SbpWithdrawalWalletEventMessage(BaseModel):

View File

@@ -119,7 +119,6 @@ def _withdrawal_operation_response(withdrawal: SbpWithdrawalEntity) -> SbpWithdr
bank_id=withdrawal.bank_id, bank_id=withdrawal.bank_id,
bank_name=withdrawal.bank_name, bank_name=withdrawal.bank_name,
phone=withdrawal.phone, phone=withdrawal.phone,
wallet_address=withdrawal.wallet_address,
usdt_amount=str(withdrawal.usdt_amount) if withdrawal.usdt_amount is not None else None, usdt_amount=str(withdrawal.usdt_amount) if withdrawal.usdt_amount is not None else None,
rub_amount=str(withdrawal.rub_amount) if withdrawal.rub_amount is not None else None, rub_amount=str(withdrawal.rub_amount) if withdrawal.rub_amount is not None else None,
usdt_exchange_rate=str(withdrawal.usdt_exchange_rate) if withdrawal.usdt_exchange_rate is not None else None, usdt_exchange_rate=str(withdrawal.usdt_exchange_rate) if withdrawal.usdt_exchange_rate is not None else None,

View File

@@ -34,7 +34,6 @@ def _withdrawal_response(withdrawal: SbpWithdrawalEntity) -> SbpWithdrawalRespon
bank_id=withdrawal.bank_id, bank_id=withdrawal.bank_id,
bank_name=withdrawal.bank_name, bank_name=withdrawal.bank_name,
phone=withdrawal.phone, phone=withdrawal.phone,
wallet_address=withdrawal.wallet_address,
usdt_amount=str(withdrawal.usdt_amount) if withdrawal.usdt_amount is not None else None, usdt_amount=str(withdrawal.usdt_amount) if withdrawal.usdt_amount is not None else None,
rub_amount=str(withdrawal.rub_amount) if withdrawal.rub_amount is not None else None, rub_amount=str(withdrawal.rub_amount) if withdrawal.rub_amount is not None else None,
usdt_exchange_rate=str(withdrawal.usdt_exchange_rate) if withdrawal.usdt_exchange_rate is not None else None, usdt_exchange_rate=str(withdrawal.usdt_exchange_rate) if withdrawal.usdt_exchange_rate is not None else None,
@@ -84,7 +83,6 @@ async def create_sbp_withdrawal(
withdrawal=await command( withdrawal=await command(
user_id=auth.user_id, user_id=auth.user_id,
bank_id=payload.bank_id, bank_id=payload.bank_id,
phone=payload.phone,
usdt_amount=payload.usdt_amount, usdt_amount=payload.usdt_amount,
) )
return CreateSbpWithdrawalResponse(status_code=201,withdrawal=_withdrawal_response(withdrawal)) return CreateSbpWithdrawalResponse(status_code=201,withdrawal=_withdrawal_response(withdrawal))

View File

@@ -59,7 +59,6 @@ class SbpWithdrawalOperationResponse(BaseModel):
bank_id: str | None = None bank_id: str | None = None
bank_name: str | None = None bank_name: str | None = None
phone: str | None = None phone: str | None = None
wallet_address: str | None = None
usdt_amount: str | None = None usdt_amount: str | None = None
rub_amount: str | None = None rub_amount: str | None = None
usdt_exchange_rate: str | None = None usdt_exchange_rate: str | None = None

View File

@@ -15,7 +15,6 @@ class SbpBanksResponse(BaseModel):
class CreateSbpWithdrawal(BaseModel): class CreateSbpWithdrawal(BaseModel):
bank_id: str = Field(min_length=1,max_length=32) bank_id: str = Field(min_length=1,max_length=32)
phone: str = Field(min_length=5,max_length=32)
usdt_amount: Decimal = Field(ge=Decimal('100'),le=Decimal('5000'),decimal_places=2,max_digits=20) usdt_amount: Decimal = Field(ge=Decimal('100'),le=Decimal('5000'),decimal_places=2,max_digits=20)
@@ -27,7 +26,6 @@ class SbpWithdrawalResponse(BaseModel):
bank_id: str | None = None bank_id: str | None = None
bank_name: str | None = None bank_name: str | None = None
phone: str | None = None phone: str | None = None
wallet_address: str | None = None
usdt_amount: str | None = None usdt_amount: str | None = None
rub_amount: str | None = None rub_amount: str | None = None
usdt_exchange_rate: str | None = None usdt_exchange_rate: str | None = None