feat: add tests command
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
from typing import Protocol, runtime_checkable
|
||||
from src.application.abstractions.repositories import IOrderRepository,IPaymentRepository
|
||||
from src.application.abstractions.repositories import IOrderRepository,IPaymentRepository,IUserRepository
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
@@ -17,3 +17,6 @@ class IUnitOfWork(Protocol):
|
||||
@property
|
||||
def payment_repository(self) -> IPaymentRepository: ...
|
||||
|
||||
@property
|
||||
def user_repository(self) -> IUserRepository: ...
|
||||
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
from src.application.abstractions.repositories.i_order_repository import IOrderRepository
|
||||
from src.application.abstractions.repositories.i_payment_repository import IPaymentRepository
|
||||
from src.application.abstractions.repositories.i_payment_repository import IPaymentRepository
|
||||
from src.application.abstractions.repositories.i_user_repository import IUserRepository
|
||||
@@ -0,0 +1,9 @@
|
||||
from abc import ABC,abstractmethod
|
||||
|
||||
from src.application.domain.entities.user import UserEntity
|
||||
|
||||
|
||||
class IUserRepository(ABC):
|
||||
@abstractmethod
|
||||
async def get(self,user_id:str) -> UserEntity|None:
|
||||
raise NotImplementedError
|
||||
@@ -1,2 +1,3 @@
|
||||
from src.application.commands.create_order_command import CreateOrderCommand
|
||||
from src.application.commands.create_payment_command import CreatePaymentCommand
|
||||
from src.application.commands.create_payment_command import CreatePaymentCommand
|
||||
from src.application.commands.create_payment_cloudkassir_command import CreatePaymentCloudkassirCommand
|
||||
@@ -0,0 +1,70 @@
|
||||
from __future__ import annotations
|
||||
from decimal import Decimal
|
||||
from ulid import ULID
|
||||
from src.application.abstractions import IUnitOfWork
|
||||
from src.application.contracts import IReceipt
|
||||
from src.application.domain.exceptions import ApplicationException
|
||||
from src.infrastructure.database.decorators import transactional
|
||||
from src.presentation.schemas.itpay_payment_models import ItpayPaymentData
|
||||
|
||||
|
||||
class CreatePaymentCloudkassirCommand:
|
||||
def __init__(self, *, unit_of_work: IUnitOfWork, receipt: IReceipt):
|
||||
self._unit_of_work = unit_of_work
|
||||
self._receipt = receipt
|
||||
|
||||
|
||||
@transactional
|
||||
async def __call__(self, payment: ItpayPaymentData) -> None:
|
||||
if str(payment.status).strip().lower() != 'completed':
|
||||
return
|
||||
metadata = payment.metadata or {}
|
||||
order_id = str(metadata.get('order_id') or '')
|
||||
user_id = str(metadata.get('user_id') or '')
|
||||
if not order_id:
|
||||
raise ApplicationException(status_code=400, message='Itpay webhook metadata missing order_id')
|
||||
if not user_id:
|
||||
raise ApplicationException(status_code=400, message='Itpay webhook metadata missing user_id')
|
||||
await self._unit_of_work.payment_repository.create_completed(
|
||||
user_id=user_id,
|
||||
order_id=order_id,
|
||||
itpay_payment_id=str(payment.id),
|
||||
itpay_paid_amount=str(payment.amount) if payment.amount is not None else None,
|
||||
transaction_id=str(payment.transaction_id) if payment.transaction_id is not None else None,
|
||||
paid_at=str(payment.paid) if payment.paid is not None else None,
|
||||
expired_date=str(payment.expired_date) if payment.expired_date is not None else None,
|
||||
)
|
||||
user = await self._unit_of_work.user_repository.get(user_id)
|
||||
if user is None:
|
||||
raise ApplicationException(status_code=404, message='User not found')
|
||||
email = str(user.email or '').strip()
|
||||
if not email:
|
||||
raise ApplicationException(status_code=400, message='User email missing')
|
||||
phone_raw = (user.phone or '').strip()
|
||||
phone = phone_raw if phone_raw else None
|
||||
total_amount: Decimal | None = None
|
||||
if payment.amount is not None:
|
||||
total_amount = Decimal(str(payment.amount)).quantize(Decimal('0.01'))
|
||||
if total_amount is None:
|
||||
raw_amt = metadata.get('amount')
|
||||
if raw_amt is not None:
|
||||
total_amount = Decimal(str(raw_amt)).quantize(Decimal('0.01'))
|
||||
if total_amount is None:
|
||||
raise ApplicationException(status_code=400, message='Itpay webhook payment amount missing for receipt')
|
||||
raw_sf = metadata.get('service_fee')
|
||||
if raw_sf is None:
|
||||
raise ApplicationException(status_code=400, message='Itpay webhook metadata missing service_fee for receipt')
|
||||
service_fee = Decimal(str(raw_sf)).quantize(Decimal('0.01'))
|
||||
principal_amount = (total_amount - service_fee).quantize(Decimal('0.01'))
|
||||
if principal_amount < 0:
|
||||
raise ApplicationException(status_code=400, message='Invalid receipt amounts: principal negative')
|
||||
await self._receipt.create_receipt(
|
||||
order_id=order_id,
|
||||
user_id=user_id,
|
||||
email=email,
|
||||
total_amount=total_amount,
|
||||
principal_amount=principal_amount,
|
||||
service_fee=service_fee,
|
||||
phone=phone,
|
||||
request_id=str(ULID()),
|
||||
)
|
||||
@@ -270,6 +270,30 @@ class Settings(BaseSettings):
|
||||
data['ITPAY_PUBLIC_ID'] = str(public_id).strip()
|
||||
data['ITPAY_API_SECRET'] = str(api_secret).strip()
|
||||
|
||||
ck_public = data.get('CLOUD_KASSIR_PUBLIC_ID') or os.getenv('CLOUD_KASSIR_PUBLIC_ID')
|
||||
ck_secret = data.get('CLOUD_KASSIR_API_SECRET') or os.getenv('CLOUD_KASSIR_API_SECRET')
|
||||
if ck_public is not None and str(ck_public).strip() and ck_secret is not None and str(ck_secret).strip():
|
||||
data['CLOUD_KASSIR_PUBLIC_ID'] = str(ck_public).strip()
|
||||
data['CLOUD_KASSIR_API_SECRET'] = str(ck_secret).strip()
|
||||
else:
|
||||
cloudkassir = read_secret_optional('cloudkassir')
|
||||
if cloudkassir:
|
||||
ck_ci = {str(k).lower(): v for k, v in cloudkassir.items()}
|
||||
public_id_ck = ck_ci.get('public_id')
|
||||
api_secret_ck = ck_ci.get('api_secret')
|
||||
if api_secret_ck is None:
|
||||
api_secret_ck = ck_ci.get('secret')
|
||||
if public_id_ck is not None and str(public_id_ck).strip():
|
||||
data['CLOUD_KASSIR_PUBLIC_ID'] = str(public_id_ck).strip()
|
||||
if api_secret_ck is not None and str(api_secret_ck).strip():
|
||||
data['CLOUD_KASSIR_API_SECRET'] = str(api_secret_ck).strip()
|
||||
inn_ck = ck_ci.get('inn')
|
||||
if inn_ck is not None and str(inn_ck).strip():
|
||||
data['CLOUD_KASSIR_INN'] = str(inn_ck).strip()
|
||||
base_ck = ck_ci.get('api_base_url')
|
||||
if base_ck is not None and str(base_ck).strip():
|
||||
data['CLOUD_KASSIR_API_BASE_URL'] = str(base_ck).strip()
|
||||
|
||||
return data
|
||||
|
||||
@property
|
||||
|
||||
50
src/infrastructure/database/repositories/user_repository.py
Normal file
50
src/infrastructure/database/repositories/user_repository.py
Normal file
@@ -0,0 +1,50 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from src.application.abstractions.repositories.i_user_repository import IUserRepository
|
||||
from src.application.contracts import ILogger
|
||||
from src.application.domain.entities.user import UserEntity
|
||||
from src.infrastructure.database.models.user import UserModel
|
||||
|
||||
|
||||
class UserRepository(IUserRepository):
|
||||
def __init__(self,session:AsyncSession,logger:ILogger):
|
||||
self._session=session
|
||||
self._logger=logger
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _to_entity(model:UserModel) -> UserEntity:
|
||||
return UserEntity(
|
||||
id=model.id,
|
||||
email=model.email,
|
||||
password_hash=model.password_hash,
|
||||
first_name=model.first_name,
|
||||
middle_name=model.middle_name,
|
||||
last_name=model.last_name,
|
||||
birth_date=model.birth_date,
|
||||
crypto_wallet=model.crypto_wallet,
|
||||
phone=model.phone,
|
||||
bik=model.bik,
|
||||
account_number=model.account_number,
|
||||
card_number=model.card_number,
|
||||
inn=model.inn,
|
||||
kyc_verified=model.kyc_verified,
|
||||
is_deleted=model.is_deleted,
|
||||
created_at=model.created_at,
|
||||
updated_at=model.updated_at,
|
||||
kyc_verified_at=model.kyc_verified_at,
|
||||
)
|
||||
|
||||
|
||||
async def get(self,user_id:str) -> UserEntity|None:
|
||||
stmt=(
|
||||
select(UserModel)
|
||||
.where(UserModel.id==user_id)
|
||||
.where(UserModel.is_deleted.is_(False))
|
||||
)
|
||||
model=await self._session.scalar(stmt)
|
||||
if model is None:
|
||||
return None
|
||||
return self._to_entity(model)
|
||||
@@ -1,10 +1,10 @@
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
||||
from src.application.abstractions import IUnitOfWork
|
||||
from src.application.abstractions.repositories import IOrderRepository,IPaymentRepository
|
||||
from src.application.abstractions.repositories import IOrderRepository,IPaymentRepository,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 import UserRepository, SessionRepository
|
||||
from src.infrastructure.database.repositories.user_repository import UserRepository
|
||||
|
||||
|
||||
|
||||
@@ -14,14 +14,14 @@ class UnitOfWork(IUnitOfWork):
|
||||
self._session: AsyncSession = None
|
||||
self._order_repository: IOrderRepository | None = None
|
||||
self._payment_repository: IPaymentRepository | None = None
|
||||
# self._user_repository: IUserRepository = None
|
||||
# self._session_repository: ISessionRepository = None
|
||||
self._user_repository: IUserRepository | None = None
|
||||
self._logger: ILogger = logger
|
||||
|
||||
async def __aenter__(self):
|
||||
self._session = self.session_factory()
|
||||
self._order_repository = None
|
||||
self._payment_repository = None
|
||||
self._user_repository = None
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
@@ -48,14 +48,9 @@ class UnitOfWork(IUnitOfWork):
|
||||
self._payment_repository = PaymentRepository(session=self._session, logger=self._logger)
|
||||
return self._payment_repository
|
||||
|
||||
# @property
|
||||
# def user_repository(self) -> IUserRepository:
|
||||
# if self._user_repository is None:
|
||||
# self._user_repository = UserRepository(session=self._session, logger=self._logger)
|
||||
# return self._user_repository
|
||||
#
|
||||
# @property
|
||||
# def session_repository(self) -> ISessionRepository:
|
||||
# if self._session_repository is None:
|
||||
# self._session_repository = SessionRepository(session=self._session, logger=self._logger)
|
||||
# return self._session_repository
|
||||
|
||||
@property
|
||||
def user_repository(self) -> IUserRepository:
|
||||
if self._user_repository is None:
|
||||
self._user_repository = UserRepository(session=self._session, logger=self._logger)
|
||||
return self._user_repository
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from __future__ import annotations
|
||||
from fastapi import Depends
|
||||
from src.application.abstractions import IUnitOfWork
|
||||
from src.application.commands import CreateOrderCommand,CreatePaymentCommand
|
||||
from src.application.contracts import ICache,ILogger,IQueueMessanger
|
||||
from src.application.commands import CreateOrderCommand,CreatePaymentCommand,CreatePaymentCloudkassirCommand
|
||||
from src.application.contracts import ICache,ILogger,IQueueMessanger,IReceipt
|
||||
from src.application.contracts.i_itpay_service import IItPayService
|
||||
from src.infrastructure.cloud_kassir import ClaudeKassirClient
|
||||
from src.infrastructure.config import settings
|
||||
from src.infrastructure.itpay.client import ItPayClient
|
||||
from src.presentation.dependencies.cache import get_remote_cache
|
||||
@@ -38,4 +39,22 @@ def get_create_payment_command(
|
||||
unit_of_work: IUnitOfWork = Depends(get_unit_of_work),
|
||||
queue_messanger: IQueueMessanger = Depends(get_rabbit),
|
||||
) -> CreatePaymentCommand:
|
||||
return CreatePaymentCommand(unit_of_work=unit_of_work,logger=logger,queue_messanger=queue_messanger)
|
||||
return CreatePaymentCommand(unit_of_work=unit_of_work,logger=logger,queue_messanger=queue_messanger)
|
||||
|
||||
|
||||
def get_cloud_kassir_receipt() -> IReceipt:
|
||||
return ClaudeKassirClient(
|
||||
public_id=settings.CLOUD_KASSIR_PUBLIC_ID,
|
||||
api_secret=settings.CLOUD_KASSIR_API_SECRET,
|
||||
inn=settings.CLOUD_KASSIR_INN,
|
||||
api_base_url=settings.CLOUD_KASSIR_API_BASE_URL,
|
||||
success_url=settings.CLOUD_KASSIR_SUCCESS_URL,
|
||||
fail_url=settings.CLOUD_KASSIR_FAIL_URL,
|
||||
)
|
||||
|
||||
|
||||
def get_create_payment_cloudkassir_command(
|
||||
unit_of_work: IUnitOfWork = Depends(get_unit_of_work),
|
||||
receipt: IReceipt = Depends(get_cloud_kassir_receipt),
|
||||
) -> CreatePaymentCloudkassirCommand:
|
||||
return CreatePaymentCloudkassirCommand(unit_of_work=unit_of_work,receipt=receipt)
|
||||
@@ -3,12 +3,12 @@ import orjson
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi.responses import ORJSONResponse
|
||||
from src.application.commands import CreateOrderCommand
|
||||
from src.application.commands import CreatePaymentCommand
|
||||
from src.application.commands import CreatePaymentCloudkassirCommand
|
||||
from src.application.contracts import ILogger
|
||||
from src.application.domain.dto import AuthContext
|
||||
from src.application.domain.enums import OrderStatus
|
||||
from src.presentation.decorators import require_access_token, csrf_protect
|
||||
from src.presentation.dependencies.commands import get_create_order_command, get_create_payment_command
|
||||
from src.presentation.dependencies.commands import get_create_order_command, get_create_payment_cloudkassir_command
|
||||
from src.presentation.dependencies.logger import get_logger
|
||||
from src.presentation.schemas.order import CreateOrder
|
||||
from src.presentation.schemas.itpay_payment_models import ItpayPaymentData
|
||||
@@ -73,8 +73,8 @@ async def create_order(
|
||||
@order_router.post('/webhook/itpay')
|
||||
async def itpay_webhook(
|
||||
request: Request,
|
||||
payment_command: CreatePaymentCommand = Depends(get_create_payment_command),
|
||||
logger: ILogger = Depends(get_logger)
|
||||
payment_command: CreatePaymentCloudkassirCommand = Depends(get_create_payment_cloudkassir_command),
|
||||
logger: ILogger = Depends(get_logger),
|
||||
) -> ORJSONResponse:
|
||||
raw = await request.body()
|
||||
ct = (request.headers.get('content-type') or '').lower()
|
||||
|
||||
Reference in New Issue
Block a user