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 decimal import Decimal
from typing import Any
import re
from ulid import ULID
from src.application.abstractions import IUnitOfWork
from src.application.contracts import IQueueMessanger,ILogger,IMozenSbpService,SbpBank
@@ -48,14 +49,30 @@ class CreateSbpWithdrawalCommand:
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
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)
if user is None:
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:
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()
withdraw_id=str(ULID())
bank_name=await self._bank_name(bank_id)
@@ -66,7 +83,6 @@ class CreateSbpWithdrawalCommand:
bank_id=bank_id,
bank_name=bank_name,
phone=phone,
wallet_address=wallet_address,
usdt_amount=quote.usdt_amount,
rub_amount=quote.rub_amount,
usdt_exchange_rate=quote.usdt_exchange_rate,
@@ -83,6 +99,8 @@ class CreateSbpWithdrawalCommand:
'usdt_amount':str(quote.usdt_amount),
'trace_id':trace_id,
'wallet_address':wallet_address,
'sender_wallet_address':sender_wallet_address,
'receiver_wallet_address':wallet_address,
'withdraw_id':withdraw_id,
'message_id':message_id,
}

View File

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

View File

@@ -103,7 +103,7 @@ class Settings(BaseSettings):
MOZEN_MERCHANT_ID: str = ''
MOZEN_PARK_ID: str = ''
MOZEN_PARK_TOKEN: str = ''
SBP_WITHDRAWAL_WALLET_ADDRESS: str = ''
CRYPTO_USDT_CONTRACT_ADDRESS: str = ''
CLOUD_KASSIR_PUBLIC_ID: str = ''
CLOUD_KASSIR_API_SECRET: str = ''
@@ -240,8 +240,13 @@ class Settings(BaseSettings):
extgw_set('merchant_id', 'MOZEN_MERCHANT_ID')
extgw_set('park_id', 'MOZEN_PARK_ID')
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')
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_name: Mapped[str]=mapped_column(String(255),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)
rub_amount: 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_name=model.bank_name,
phone=model.phone,
wallet_address=model.wallet_address,
usdt_amount=model.usdt_amount,
rub_amount=model.rub_amount,
usdt_exchange_rate=model.usdt_exchange_rate,
@@ -48,7 +47,6 @@ class SbpWithdrawalRepository(ISbpWithdrawalRepository):
'bank_id':withdrawal.bank_id,
'bank_name':withdrawal.bank_name,
'phone':withdrawal.phone,
'wallet_address':withdrawal.wallet_address,
'usdt_amount':withdrawal.usdt_amount,
'rub_amount':withdrawal.rub_amount,
'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
crypto_transfer_router=RabbitRouter(settings.RABBIT_URL)
crypto_transfer_router=RabbitRouter(settings.RABBIT_URL,schema_url=None)
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
sbp_withdrawal_messaging_router=RabbitRouter(settings.RABBIT_URL)
sbp_withdrawal_messaging_router=RabbitRouter(settings.RABBIT_URL,schema_url=None)
class SbpWithdrawalWalletEventMessage(BaseModel):

View File

@@ -119,7 +119,6 @@ def _withdrawal_operation_response(withdrawal: SbpWithdrawalEntity) -> SbpWithdr
bank_id=withdrawal.bank_id,
bank_name=withdrawal.bank_name,
phone=withdrawal.phone,
wallet_address=withdrawal.wallet_address,
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,
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_name=withdrawal.bank_name,
phone=withdrawal.phone,
wallet_address=withdrawal.wallet_address,
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,
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(
user_id=auth.user_id,
bank_id=payload.bank_id,
phone=payload.phone,
usdt_amount=payload.usdt_amount,
)
return CreateSbpWithdrawalResponse(status_code=201,withdrawal=_withdrawal_response(withdrawal))

View File

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

View File

@@ -15,7 +15,6 @@ class SbpBanksResponse(BaseModel):
class CreateSbpWithdrawal(BaseModel):
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)
@@ -27,7 +26,6 @@ class SbpWithdrawalResponse(BaseModel):
bank_id: str | None = None
bank_name: str | None = None
phone: str | None = None
wallet_address: str | None = None
usdt_amount: str | None = None
rub_amount: str | None = None
usdt_exchange_rate: str | None = None