feat: add withdraw
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
from src.application.commands.create_order_command import CreateOrderCommand
|
||||
from src.application.commands.create_payment_command import CreatePaymentCommand
|
||||
from src.application.commands.create_crypto_transfer_completed_command import CreateCryptoTransferCompletedCommand
|
||||
from src.application.commands.payment_read_commands import GetOrderCommand,GetOrderStatusCommand,GetPaymentCommand,GetPaymentConfigCommand,GetPaymentQuoteCommand,GetPaymentQuoteFromRubCommand,ListOrdersCommand,ListPaymentsCommand,OrderPaymentResult
|
||||
from src.application.commands.payment_read_commands import ClientOperationResult,GetOrderCommand,GetOrderStatusCommand,GetPaymentCommand,GetPaymentConfigCommand,GetPaymentQuoteCommand,GetPaymentQuoteFromRubCommand,ListClientOperationsCommand,ListOrdersCommand,ListPaymentsCommand,OrderPaymentResult
|
||||
from src.application.commands.sbp_withdrawal_commands import CreateSbpWithdrawalCommand,GetSbpBanksCommand,GetSbpWithdrawalCommand,HandleSbpWithdrawalWalletEventCommand
|
||||
@@ -1,9 +1,11 @@
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime,timezone
|
||||
from decimal import Decimal
|
||||
from typing import Literal
|
||||
from src.application.abstractions import IUnitOfWork
|
||||
from src.application.contracts import ILogger
|
||||
from src.application.domain.entities import OrderEntity,PaymentEntity
|
||||
from src.application.domain.entities import OrderEntity,PaymentEntity,SbpWithdrawalEntity
|
||||
from src.application.domain.exceptions import NotFoundException
|
||||
from src.application.services import PaymentQuote,PaymentQuoteService
|
||||
from src.infrastructure.database.decorators import transactional
|
||||
@@ -15,6 +17,14 @@ class OrderPaymentResult:
|
||||
payment: PaymentEntity | None
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ClientOperationResult:
|
||||
type: Literal['payment','withdrawal']
|
||||
created_at: datetime
|
||||
payment: PaymentEntity | None = None
|
||||
withdrawal: SbpWithdrawalEntity | None = None
|
||||
|
||||
|
||||
class GetPaymentConfigCommand:
|
||||
def __init__(self, *, payment_quote_service: PaymentQuoteService, logger: ILogger):
|
||||
self._payment_quote_service = payment_quote_service
|
||||
@@ -91,6 +101,39 @@ class ListPaymentsCommand:
|
||||
return payments
|
||||
|
||||
|
||||
class ListClientOperationsCommand:
|
||||
def __init__(self, *, unit_of_work: IUnitOfWork, logger: ILogger):
|
||||
self._unit_of_work = unit_of_work
|
||||
self._logger = logger
|
||||
|
||||
|
||||
@transactional
|
||||
async def __call__(self, *, user_id: str, limit: int, offset: int) -> list[ClientOperationResult]:
|
||||
fetch_limit=limit+offset
|
||||
payments=await self._unit_of_work.payment_repository.list_by_user_id(
|
||||
user_id=user_id,
|
||||
limit=fetch_limit,
|
||||
offset=0,
|
||||
)
|
||||
withdrawals=await self._unit_of_work.sbp_withdrawal_repository.list_by_user_id(
|
||||
user_id=user_id,
|
||||
limit=fetch_limit,
|
||||
offset=0,
|
||||
)
|
||||
min_dt=datetime.min.replace(tzinfo=timezone.utc)
|
||||
items=[
|
||||
ClientOperationResult(type='payment',created_at=payment.created_at or min_dt,payment=payment)
|
||||
for payment in payments
|
||||
]
|
||||
items.extend(
|
||||
ClientOperationResult(type='withdrawal',created_at=withdrawal.created_at or min_dt,withdrawal=withdrawal)
|
||||
for withdrawal in withdrawals
|
||||
)
|
||||
items.sort(key=lambda item:item.created_at,reverse=True)
|
||||
self._logger.info({'event':'client_operations_list_requested','user_id':user_id,'limit':limit,'offset':offset})
|
||||
return items[offset:offset+limit]
|
||||
|
||||
|
||||
class GetPaymentCommand:
|
||||
def __init__(self, *, unit_of_work: IUnitOfWork, logger: ILogger):
|
||||
self._unit_of_work = unit_of_work
|
||||
|
||||
254
src/application/commands/sbp_withdrawal_commands.py
Normal file
254
src/application/commands/sbp_withdrawal_commands.py
Normal file
@@ -0,0 +1,254 @@
|
||||
from __future__ import annotations
|
||||
from datetime import datetime,timezone
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
from ulid import ULID
|
||||
from src.application.abstractions import IUnitOfWork
|
||||
from src.application.contracts import IQueueMessanger,ILogger,IMozenSbpService,SbpBank
|
||||
from src.application.domain.entities import SbpWithdrawalEntity
|
||||
from src.application.domain.enums import SbpWithdrawalStatus
|
||||
from src.application.domain.exceptions import BadRequestException,NotFoundException,PaymentProviderException,ServiceUnavailableException
|
||||
from src.application.services import SbpWithdrawalQuoteService
|
||||
from src.infrastructure.config import settings
|
||||
from src.infrastructure.database.decorators import transactional
|
||||
|
||||
|
||||
class GetSbpBanksCommand:
|
||||
def __init__(self,*,mozen_sbp_service: IMozenSbpService,logger: ILogger):
|
||||
self._mozen_sbp_service=mozen_sbp_service
|
||||
self._logger=logger
|
||||
|
||||
|
||||
async def __call__(self) -> list[SbpBank]:
|
||||
return await self._mozen_sbp_service.get_banks(self._logger.get_trace_id())
|
||||
|
||||
|
||||
class CreateSbpWithdrawalCommand:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
unit_of_work: IUnitOfWork,
|
||||
logger: ILogger,
|
||||
queue_messanger: IQueueMessanger,
|
||||
mozen_sbp_service: IMozenSbpService,
|
||||
quote_service: SbpWithdrawalQuoteService,
|
||||
):
|
||||
self._unit_of_work=unit_of_work
|
||||
self._logger=logger
|
||||
self._queue_messanger=queue_messanger
|
||||
self._mozen_sbp_service=mozen_sbp_service
|
||||
self._quote_service=quote_service
|
||||
|
||||
|
||||
async def _bank_name(self,bank_id: str) -> str:
|
||||
banks=await self._mozen_sbp_service.get_banks(self._logger.get_trace_id())
|
||||
for bank in banks:
|
||||
if bank.id==bank_id:
|
||||
return bank.name
|
||||
raise BadRequestException(message='SBP bank not found')
|
||||
|
||||
|
||||
@transactional
|
||||
async def __call__(self,*,user_id: str,bank_id: str,phone: 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()
|
||||
if not wallet_address:
|
||||
raise ServiceUnavailableException(message='SBP withdrawal wallet address unavailable')
|
||||
trace_id=self._logger.get_trace_id()
|
||||
withdraw_id=str(ULID())
|
||||
bank_name=await self._bank_name(bank_id)
|
||||
quote=await self._quote_service.get_quote(usdt_amount)
|
||||
withdrawal=SbpWithdrawalEntity(
|
||||
id=withdraw_id,
|
||||
user_id=user_id,
|
||||
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,
|
||||
service_fee_rate=quote.service_fee_rate,
|
||||
service_fee_usdt=quote.service_fee_usdt,
|
||||
trace_id=trace_id,
|
||||
status=SbpWithdrawalStatus.WAITING_USDT,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
)
|
||||
saved=await self._unit_of_work.sbp_withdrawal_repository.create(withdrawal)
|
||||
message_id=str(ULID())
|
||||
message: dict[str,Any]={
|
||||
'user_id':user_id,
|
||||
'usdt_amount':str(quote.usdt_amount),
|
||||
'trace_id':trace_id,
|
||||
'wallet_address':wallet_address,
|
||||
'withdraw_id':withdraw_id,
|
||||
'message_id':message_id,
|
||||
}
|
||||
await self._queue_messanger.publish_to_queue(
|
||||
queue=settings.RABBIT_SBP_WITHDRAWAL_USDT_REQUESTED_QUEUE,
|
||||
message=message,
|
||||
message_id=message_id,
|
||||
correlation_id=trace_id,
|
||||
headers={'trace_id':trace_id},
|
||||
)
|
||||
self._logger.info({
|
||||
'event':'sbp_withdrawal_usdt_request_published',
|
||||
'withdraw_id':withdraw_id,
|
||||
'user_id':user_id,
|
||||
'message_id':message_id,
|
||||
})
|
||||
return saved
|
||||
|
||||
|
||||
class GetSbpWithdrawalCommand:
|
||||
def __init__(self,*,unit_of_work: IUnitOfWork,logger: ILogger):
|
||||
self._unit_of_work=unit_of_work
|
||||
self._logger=logger
|
||||
|
||||
|
||||
@transactional
|
||||
async def __call__(self,*,withdraw_id: str,user_id: str) -> SbpWithdrawalEntity:
|
||||
withdrawal=await self._unit_of_work.sbp_withdrawal_repository.get_by_id_for_user(
|
||||
withdraw_id=withdraw_id,
|
||||
user_id=user_id,
|
||||
)
|
||||
if withdrawal is None:
|
||||
raise NotFoundException(message='SBP withdrawal not found')
|
||||
return withdrawal
|
||||
|
||||
|
||||
class HandleSbpWithdrawalWalletEventCommand:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
unit_of_work: IUnitOfWork,
|
||||
logger: ILogger,
|
||||
mozen_sbp_service: IMozenSbpService,
|
||||
):
|
||||
self._unit_of_work=unit_of_work
|
||||
self._logger=logger
|
||||
self._mozen_sbp_service=mozen_sbp_service
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _event_error(
|
||||
*,
|
||||
event_type: str,
|
||||
received_usdt_amount: Decimal | None,
|
||||
tx_hash: str | None,
|
||||
error: str | None,
|
||||
) -> str | None:
|
||||
parts=(
|
||||
f'event_type={event_type}',
|
||||
f'received_usdt_amount={received_usdt_amount}' if received_usdt_amount is not None else '',
|
||||
f'tx_hash={tx_hash}' if tx_hash else '',
|
||||
f'error={error}' if error else '',
|
||||
)
|
||||
value=';'.join(part for part in parts if part)
|
||||
return value or None
|
||||
|
||||
|
||||
@transactional
|
||||
async def __call__(
|
||||
self,
|
||||
*,
|
||||
withdraw_id: str,
|
||||
user_id: str,
|
||||
event_type: str,
|
||||
received_usdt_amount: Decimal | None = None,
|
||||
tx_hash: str | None = None,
|
||||
error: str | None = None,
|
||||
) -> None:
|
||||
withdrawal=await self._unit_of_work.sbp_withdrawal_repository.get_by_id(withdraw_id)
|
||||
if withdrawal is None:
|
||||
raise NotFoundException(message='SBP withdrawal not found')
|
||||
if withdrawal.user_id!=user_id:
|
||||
raise BadRequestException(message='SBP withdrawal user mismatch')
|
||||
normalized_event_type=event_type.strip().lower()
|
||||
if withdrawal.status in (
|
||||
SbpWithdrawalStatus.PAYOUT_PROCESSING,
|
||||
SbpWithdrawalStatus.PAYOUT_COMPLETED,
|
||||
SbpWithdrawalStatus.PAYOUT_FAILED,
|
||||
SbpWithdrawalStatus.CANCELLED,
|
||||
SbpWithdrawalStatus.EXPIRED,
|
||||
):
|
||||
self._logger.info({
|
||||
'event':'sbp_withdrawal_wallet_event_ignored',
|
||||
'withdraw_id':withdraw_id,
|
||||
'user_id':user_id,
|
||||
'wallet_event_type':normalized_event_type,
|
||||
'status':withdrawal.status.value if withdrawal.status is not None else None,
|
||||
})
|
||||
return
|
||||
provider_error=self._event_error(
|
||||
event_type=normalized_event_type,
|
||||
received_usdt_amount=received_usdt_amount,
|
||||
tx_hash=tx_hash,
|
||||
error=error,
|
||||
)
|
||||
if normalized_event_type in ('usdt_underpaid','underpaid'):
|
||||
await self._unit_of_work.sbp_withdrawal_repository.update_status(
|
||||
withdraw_id=withdraw_id,
|
||||
status=SbpWithdrawalStatus.USDT_UNDERPAID,
|
||||
provider_error=provider_error,
|
||||
)
|
||||
return
|
||||
if normalized_event_type in ('expired','usdt_expired'):
|
||||
await self._unit_of_work.sbp_withdrawal_repository.update_status(
|
||||
withdraw_id=withdraw_id,
|
||||
status=SbpWithdrawalStatus.EXPIRED,
|
||||
provider_error=provider_error,
|
||||
)
|
||||
return
|
||||
if normalized_event_type in ('failed','wallet_error','watch_failed','error'):
|
||||
await self._unit_of_work.sbp_withdrawal_repository.update_status(
|
||||
withdraw_id=withdraw_id,
|
||||
status=SbpWithdrawalStatus.WALLET_ERROR,
|
||||
provider_error=provider_error,
|
||||
)
|
||||
return
|
||||
if normalized_event_type not in ('usdt_received','received','confirmed'):
|
||||
raise BadRequestException(message='Unknown SBP withdrawal wallet event type')
|
||||
await self._unit_of_work.sbp_withdrawal_repository.update_status(
|
||||
withdraw_id=withdraw_id,
|
||||
status=SbpWithdrawalStatus.USDT_RECEIVED,
|
||||
provider_error=provider_error,
|
||||
)
|
||||
try:
|
||||
result=await self._mozen_sbp_service.make_withdrawal(
|
||||
user_id=user_id,
|
||||
withdraw_id=withdraw_id,
|
||||
phone=str(withdrawal.phone or ''),
|
||||
bank_id=str(withdrawal.bank_id or ''),
|
||||
# amount=Decimal(str(withdrawal.rub_amount or '0')),
|
||||
amount=Decimal('1'),
|
||||
trace_id=str(withdrawal.trace_id or self._logger.get_trace_id()),
|
||||
)
|
||||
except PaymentProviderException as exception:
|
||||
await self._unit_of_work.sbp_withdrawal_repository.update_status(
|
||||
withdraw_id=withdraw_id,
|
||||
status=SbpWithdrawalStatus.PAYOUT_FAILED,
|
||||
provider_error=exception.message,
|
||||
)
|
||||
return
|
||||
status=SbpWithdrawalStatus.PAYOUT_PROCESSING
|
||||
provider_error=result.error
|
||||
if result.error:
|
||||
status=SbpWithdrawalStatus.PAYOUT_FAILED
|
||||
elif result.status.strip().lower() in ('completed','success','succeeded','paid'):
|
||||
status=SbpWithdrawalStatus.PAYOUT_COMPLETED
|
||||
await self._unit_of_work.sbp_withdrawal_repository.update_payout_created(
|
||||
withdraw_id=withdraw_id,
|
||||
mozen_order_id=result.mozen_order_id,
|
||||
status=status,
|
||||
provider_error=provider_error,
|
||||
)
|
||||
self._logger.info({
|
||||
'event':'sbp_withdrawal_payout_created',
|
||||
'withdraw_id':withdraw_id,
|
||||
'user_id':user_id,
|
||||
'mozen_order_id':result.mozen_order_id,
|
||||
'status':status.value,
|
||||
})
|
||||
Reference in New Issue
Block a user