feat: add withdraw

This commit is contained in:
2026-06-21 14:16:20 +03:00
parent 8f60637e8d
commit 56841b83ce
32 changed files with 1291 additions and 44 deletions

View File

@@ -1,7 +1,8 @@
from src.infrastructure.database.models.base import Base
from src.infrastructure.database.models.order import Order
from src.infrastructure.database.models.payment import Payment
from src.infrastructure.database.models.sbp_withdrawal import SbpWithdrawal
from src.infrastructure.database.models.user import UserModel
__all__ = ['Base','Order','Payment','UserModel']
__all__ = ['Base','Order','Payment','SbpWithdrawal','UserModel']

View File

@@ -0,0 +1,40 @@
from __future__ import annotations
from datetime import datetime
from decimal import Decimal
from sqlalchemy import DateTime,Enum as SAEnum,ForeignKey,Numeric,String,Text
from sqlalchemy.orm import Mapped,mapped_column
from src.application.domain.enums import SbpWithdrawalStatus
from src.infrastructure.database.models.base import Base
from src.infrastructure.database.models.mixins import AuditTimestampsMixin,UlidPrimaryKeyMixin
class SbpWithdrawal(Base,UlidPrimaryKeyMixin,AuditTimestampsMixin):
__tablename__='sbp_withdrawals'
user_id: Mapped[str]=mapped_column(
String(26),
ForeignKey('users.id',ondelete='RESTRICT'),
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)
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)
service_fee_rate: Mapped[Decimal]=mapped_column(Numeric(8,4),nullable=False)
service_fee_usdt: Mapped[Decimal]=mapped_column(Numeric(38,2),nullable=False)
mozen_order_id: Mapped[str | None]=mapped_column(String(128),nullable=True,index=True)
trace_id: Mapped[str]=mapped_column(String(64),nullable=False,index=True)
status: Mapped[SbpWithdrawalStatus]=mapped_column(
SAEnum(SbpWithdrawalStatus,name='sbp_withdrawal_status_enum',values_callable=lambda x:[e.value for e in x]),
nullable=False,
index=True,
default=SbpWithdrawalStatus.CREATED,
)
provider_error: Mapped[str | None]=mapped_column(Text,nullable=True)
usdt_received_at: Mapped[datetime | None]=mapped_column(DateTime(timezone=True),nullable=True)
payout_created_at: Mapped[datetime | None]=mapped_column(DateTime(timezone=True),nullable=True)
payout_completed_at: Mapped[datetime | None]=mapped_column(DateTime(timezone=True),nullable=True)

View File

@@ -0,0 +1,139 @@
from __future__ import annotations
from dataclasses import replace
from datetime import datetime,timezone
from sqlalchemy import desc,select
from sqlalchemy.ext.asyncio import AsyncSession
from src.application.abstractions.repositories import ISbpWithdrawalRepository
from src.application.contracts import ILogger
from src.application.domain.entities import SbpWithdrawalEntity
from src.application.domain.enums import SbpWithdrawalStatus
from src.infrastructure.database.models.sbp_withdrawal import SbpWithdrawal
class SbpWithdrawalRepository(ISbpWithdrawalRepository):
def __init__(self,session: AsyncSession,logger: ILogger):
self._session=session
self._logger=logger
@staticmethod
def _to_entity(model: SbpWithdrawal) -> SbpWithdrawalEntity:
return SbpWithdrawalEntity(
id=model.id,
created_at=model.created_at,
updated_at=model.updated_at,
user_id=model.user_id,
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,
service_fee_rate=model.service_fee_rate,
service_fee_usdt=model.service_fee_usdt,
mozen_order_id=model.mozen_order_id,
trace_id=model.trace_id,
status=model.status,
provider_error=model.provider_error,
usdt_received_at=model.usdt_received_at,
payout_created_at=model.payout_created_at,
payout_completed_at=model.payout_completed_at,
)
async def create(self,withdrawal: SbpWithdrawalEntity) -> SbpWithdrawalEntity:
values={
'user_id':withdrawal.user_id,
'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,
'service_fee_rate':withdrawal.service_fee_rate,
'service_fee_usdt':withdrawal.service_fee_usdt,
'mozen_order_id':withdrawal.mozen_order_id,
'trace_id':withdrawal.trace_id,
'status':withdrawal.status or SbpWithdrawalStatus.CREATED,
'provider_error':withdrawal.provider_error,
'usdt_received_at':withdrawal.usdt_received_at,
'payout_created_at':withdrawal.payout_created_at,
'payout_completed_at':withdrawal.payout_completed_at,
}
if withdrawal.id is not None:
values['id']=withdrawal.id
model=SbpWithdrawal(**values)
self._session.add(model)
await self._session.flush()
return replace(withdrawal,id=model.id,created_at=model.created_at,updated_at=model.updated_at)
async def get_by_id(self,withdraw_id: str) -> SbpWithdrawalEntity | None:
stmt=select(SbpWithdrawal).where(SbpWithdrawal.id==withdraw_id)
model=await self._session.scalar(stmt)
if model is None:
return None
return self._to_entity(model)
async def get_by_id_for_user(self,*,withdraw_id: str,user_id: str) -> SbpWithdrawalEntity | None:
stmt=select(SbpWithdrawal).where(SbpWithdrawal.id==withdraw_id,SbpWithdrawal.user_id==user_id)
model=await self._session.scalar(stmt)
if model is None:
return None
return self._to_entity(model)
async def list_by_user_id(self,*,user_id: str,limit: int,offset: int) -> list[SbpWithdrawalEntity]:
stmt=(
select(SbpWithdrawal)
.where(SbpWithdrawal.user_id==user_id)
.order_by(desc(SbpWithdrawal.created_at))
.limit(limit)
.offset(offset)
)
result=await self._session.scalars(stmt)
return [self._to_entity(model) for model in result.all()]
async def update_status(
self,
*,
withdraw_id: str,
status: SbpWithdrawalStatus,
provider_error: str | None = None,
) -> None:
model=await self._session.get(SbpWithdrawal,withdraw_id)
if model is None:
return
model.status=status
model.provider_error=provider_error
if status==SbpWithdrawalStatus.USDT_RECEIVED:
model.usdt_received_at=datetime.now(timezone.utc)
if status==SbpWithdrawalStatus.PAYOUT_PROCESSING:
model.payout_created_at=datetime.now(timezone.utc)
if status==SbpWithdrawalStatus.PAYOUT_COMPLETED:
model.payout_completed_at=datetime.now(timezone.utc)
await self._session.flush()
async def update_payout_created(
self,
*,
withdraw_id: str,
mozen_order_id: str | None,
status: SbpWithdrawalStatus,
provider_error: str | None,
) -> None:
model=await self._session.get(SbpWithdrawal,withdraw_id)
if model is None:
return
model.mozen_order_id=mozen_order_id
model.status=status
model.provider_error=provider_error
model.payout_created_at=datetime.now(timezone.utc)
if status==SbpWithdrawalStatus.PAYOUT_COMPLETED:
model.payout_completed_at=datetime.now(timezone.utc)
await self._session.flush()

View File

@@ -1,9 +1,10 @@
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from src.application.abstractions import IUnitOfWork
from src.application.abstractions.repositories import IOrderRepository,IPaymentRepository,IUserRepository
from src.application.abstractions.repositories import IOrderRepository,IPaymentRepository,ISbpWithdrawalRepository,IUserRepository
from src.application.contracts import ILogger
from src.infrastructure.database.repositories.order_repository import OrderRepository
from src.infrastructure.database.repositories.payment_repository import PaymentRepository
from src.infrastructure.database.repositories.sbp_withdrawal_repository import SbpWithdrawalRepository
from src.infrastructure.database.repositories.user_repository import UserRepository
@@ -14,6 +15,7 @@ class UnitOfWork(IUnitOfWork):
self._session: AsyncSession = None
self._order_repository: IOrderRepository | None = None
self._payment_repository: IPaymentRepository | None = None
self._sbp_withdrawal_repository: ISbpWithdrawalRepository | None = None
self._user_repository: IUserRepository | None = None
self._logger: ILogger = logger
@@ -21,6 +23,7 @@ class UnitOfWork(IUnitOfWork):
self._session = self.session_factory()
self._order_repository = None
self._payment_repository = None
self._sbp_withdrawal_repository = None
self._user_repository = None
return self
@@ -49,6 +52,13 @@ class UnitOfWork(IUnitOfWork):
return self._payment_repository
@property
def sbp_withdrawal_repository(self) -> ISbpWithdrawalRepository:
if self._sbp_withdrawal_repository is None:
self._sbp_withdrawal_repository = SbpWithdrawalRepository(session=self._session,logger=self._logger)
return self._sbp_withdrawal_repository
@property
def user_repository(self) -> IUserRepository:
if self._user_repository is None: