feat: add tests command
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import Protocol, runtime_checkable
|
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
|
@runtime_checkable
|
||||||
@@ -17,3 +17,6 @@ class IUnitOfWork(Protocol):
|
|||||||
@property
|
@property
|
||||||
def payment_repository(self) -> IPaymentRepository: ...
|
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_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_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_PUBLIC_ID'] = str(public_id).strip()
|
||||||
data['ITPAY_API_SECRET'] = str(api_secret).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
|
return data
|
||||||
|
|
||||||
@property
|
@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 sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
||||||
from src.application.abstractions import IUnitOfWork
|
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.application.contracts import ILogger
|
||||||
from src.infrastructure.database.repositories.order_repository import OrderRepository
|
from src.infrastructure.database.repositories.order_repository import OrderRepository
|
||||||
from src.infrastructure.database.repositories.payment_repository import PaymentRepository
|
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._session: AsyncSession = None
|
||||||
self._order_repository: IOrderRepository | None = None
|
self._order_repository: IOrderRepository | None = None
|
||||||
self._payment_repository: IPaymentRepository | None = None
|
self._payment_repository: IPaymentRepository | None = None
|
||||||
# self._user_repository: IUserRepository = None
|
self._user_repository: IUserRepository | None = None
|
||||||
# self._session_repository: ISessionRepository = None
|
|
||||||
self._logger: ILogger = logger
|
self._logger: ILogger = logger
|
||||||
|
|
||||||
async def __aenter__(self):
|
async def __aenter__(self):
|
||||||
self._session = self.session_factory()
|
self._session = self.session_factory()
|
||||||
self._order_repository = None
|
self._order_repository = None
|
||||||
self._payment_repository = None
|
self._payment_repository = None
|
||||||
|
self._user_repository = None
|
||||||
return self
|
return self
|
||||||
|
|
||||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
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)
|
self._payment_repository = PaymentRepository(session=self._session, logger=self._logger)
|
||||||
return self._payment_repository
|
return self._payment_repository
|
||||||
|
|
||||||
# @property
|
|
||||||
# def user_repository(self) -> IUserRepository:
|
@property
|
||||||
# if self._user_repository is None:
|
def user_repository(self) -> IUserRepository:
|
||||||
# self._user_repository = UserRepository(session=self._session, logger=self._logger)
|
if self._user_repository is None:
|
||||||
# return self._user_repository
|
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
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
from src.application.abstractions import IUnitOfWork
|
from src.application.abstractions import IUnitOfWork
|
||||||
from src.application.commands import CreateOrderCommand,CreatePaymentCommand
|
from src.application.commands import CreateOrderCommand,CreatePaymentCommand,CreatePaymentCloudkassirCommand
|
||||||
from src.application.contracts import ICache,ILogger,IQueueMessanger
|
from src.application.contracts import ICache,ILogger,IQueueMessanger,IReceipt
|
||||||
from src.application.contracts.i_itpay_service import IItPayService
|
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.config import settings
|
||||||
from src.infrastructure.itpay.client import ItPayClient
|
from src.infrastructure.itpay.client import ItPayClient
|
||||||
from src.presentation.dependencies.cache import get_remote_cache
|
from src.presentation.dependencies.cache import get_remote_cache
|
||||||
@@ -39,3 +40,21 @@ def get_create_payment_command(
|
|||||||
queue_messanger: IQueueMessanger = Depends(get_rabbit),
|
queue_messanger: IQueueMessanger = Depends(get_rabbit),
|
||||||
) -> CreatePaymentCommand:
|
) -> 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 import APIRouter, Depends, Request
|
||||||
from fastapi.responses import ORJSONResponse
|
from fastapi.responses import ORJSONResponse
|
||||||
from src.application.commands import CreateOrderCommand
|
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.contracts import ILogger
|
||||||
from src.application.domain.dto import AuthContext
|
from src.application.domain.dto import AuthContext
|
||||||
from src.application.domain.enums import OrderStatus
|
from src.application.domain.enums import OrderStatus
|
||||||
from src.presentation.decorators import require_access_token, csrf_protect
|
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.dependencies.logger import get_logger
|
||||||
from src.presentation.schemas.order import CreateOrder
|
from src.presentation.schemas.order import CreateOrder
|
||||||
from src.presentation.schemas.itpay_payment_models import ItpayPaymentData
|
from src.presentation.schemas.itpay_payment_models import ItpayPaymentData
|
||||||
@@ -73,8 +73,8 @@ async def create_order(
|
|||||||
@order_router.post('/webhook/itpay')
|
@order_router.post('/webhook/itpay')
|
||||||
async def itpay_webhook(
|
async def itpay_webhook(
|
||||||
request: Request,
|
request: Request,
|
||||||
payment_command: CreatePaymentCommand = Depends(get_create_payment_command),
|
payment_command: CreatePaymentCloudkassirCommand = Depends(get_create_payment_cloudkassir_command),
|
||||||
logger: ILogger = Depends(get_logger)
|
logger: ILogger = Depends(get_logger),
|
||||||
) -> ORJSONResponse:
|
) -> ORJSONResponse:
|
||||||
raw = await request.body()
|
raw = await request.body()
|
||||||
ct = (request.headers.get('content-type') or '').lower()
|
ct = (request.headers.get('content-type') or '').lower()
|
||||||
|
|||||||
Reference in New Issue
Block a user