feat: add mpre endpoints
This commit is contained in:
@@ -1,20 +1,155 @@
|
||||
import asyncio
|
||||
from decimal import Decimal
|
||||
from urllib.parse import parse_qs
|
||||
import orjson
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi import APIRouter,Depends,Query,Request,WebSocket,WebSocketDisconnect
|
||||
from fastapi.responses import ORJSONResponse
|
||||
from src.application.commands import CreateOrderCommand
|
||||
from src.application.commands import CreatePaymentCommand
|
||||
from src.application.contracts import ILogger
|
||||
from src.application.domain.dto import AuthContext
|
||||
from fastapi.security.utils import get_authorization_scheme_param
|
||||
from ulid import ULID
|
||||
from src.application.commands import CreateOrderCommand,CreatePaymentCommand,GetOrderCommand,GetOrderStatusCommand,GetPaymentConfigCommand,GetPaymentQuoteCommand,ListOrdersCommand,ListPaymentsCommand
|
||||
from src.application.contracts import IJwtService,ILogger
|
||||
from src.application.domain.dto import AccessTokenPayload,AuthContext
|
||||
from src.application.domain.entities import OrderEntity,PaymentEntity
|
||||
from src.application.domain.enums import OrderStatus
|
||||
from src.application.domain.exceptions import ConflictException
|
||||
from src.application.domain.exceptions import ApplicationException,ConflictException
|
||||
from src.application.services import PaymentQuote
|
||||
from src.infrastructure.context_vars import trace_id_var
|
||||
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_command,get_list_orders_command,get_list_payments_command,get_order_command,get_order_status_command,get_payment_config_command,get_payment_quote_command
|
||||
from src.presentation.dependencies.logger import get_logger
|
||||
from src.presentation.schemas.order import CreateOrder,CreateOrderResponse,ErrorResponse,OrderPaymentResponse
|
||||
from src.presentation.dependencies.security import get_jwt_service
|
||||
from src.presentation.schemas.order import CreateOrder,CreateOrderResponse,ErrorResponse,OrderDetailResponse,OrderPaymentResponse,OrdersResponse,OrderStatusResponse,OrderWithPaymentResponse,PaymentConfigResponse,PaymentQuoteResponse,PaymentResponse,PaymentsResponse
|
||||
from src.presentation.schemas.itpay_payment_models import ItpayPaymentData
|
||||
|
||||
order_router = APIRouter(prefix='/order', tags=['orders'])
|
||||
orders_router = APIRouter(tags=['orders'])
|
||||
payment_router = APIRouter(prefix='/payment', tags=['payments'])
|
||||
payments_router = APIRouter(tags=['payments'])
|
||||
|
||||
|
||||
def _payment_config_response(quote: PaymentQuote) -> PaymentConfigResponse:
|
||||
return PaymentConfigResponse(
|
||||
status_code=200,
|
||||
usdt_exchange_rate=str(quote.usdt_exchange_rate),
|
||||
gas_fee=str(quote.gas_fee),
|
||||
service_fee_rate=str(quote.service_fee_rate),
|
||||
one_usdt_service_fee=str(quote.service_fee),
|
||||
one_usdt_total_price=str(quote.total_price),
|
||||
created_at=quote.created_at.isoformat(),
|
||||
)
|
||||
|
||||
|
||||
def _payment_quote_response(quote: PaymentQuote) -> PaymentQuoteResponse:
|
||||
return PaymentQuoteResponse(
|
||||
status_code=200,
|
||||
usdt_amount=str(quote.usdt_amount),
|
||||
usdt_exchange_rate=str(quote.usdt_exchange_rate),
|
||||
gas_fee=str(quote.gas_fee),
|
||||
service_fee=str(quote.service_fee),
|
||||
total_price=str(quote.total_price),
|
||||
service_fee_rate=str(quote.service_fee_rate),
|
||||
created_at=quote.created_at.isoformat(),
|
||||
)
|
||||
|
||||
|
||||
def _order_response(o: OrderEntity) -> OrderPaymentResponse:
|
||||
return OrderPaymentResponse(
|
||||
id=o.id,
|
||||
created_at=o.created_at.isoformat() if o.created_at is not None else None,
|
||||
updated_at=o.updated_at.isoformat() if o.updated_at is not None else None,
|
||||
user_id=o.user_id,
|
||||
usdt_amount=str(o.usdt_amount) if o.usdt_amount is not None else None,
|
||||
usdt_exchange_rate=str(o.usdt_exchange_rate) if o.usdt_exchange_rate is not None else None,
|
||||
gas_fee=str(o.gas_fee) if o.gas_fee is not None else None,
|
||||
total_price=str(o.total_price) if o.total_price is not None else None,
|
||||
service_fee=str(o.service_fee) if o.service_fee is not None else None,
|
||||
status=o.status,
|
||||
client_payment_id=o.client_payment_id,
|
||||
itpay_payment_qr_url_desktop=o.itpay_payment_qr_url_desktop,
|
||||
itpay_payment_qr_url_android=o.itpay_payment_qr_url_android,
|
||||
itpay_payment_qr_url_ios=o.itpay_payment_qr_url_ios,
|
||||
itpay_payment_qr_image_desktop=o.itpay_payment_qr_image_desktop,
|
||||
itpay_payment_qr_image_android=o.itpay_payment_qr_image_android,
|
||||
itpay_payment_qr_image_ios=o.itpay_payment_qr_image_ios,
|
||||
itpay_id=o.itpay_id,
|
||||
itpay_qr_id=o.itpay_qr_id,
|
||||
itpay_amount=str(o.itpay_amount) if o.itpay_amount is not None else None,
|
||||
itpay_created_at=o.itpay_created_at.isoformat() if o.itpay_created_at is not None else None,
|
||||
)
|
||||
|
||||
|
||||
def _payment_response(payment: PaymentEntity) -> PaymentResponse:
|
||||
return PaymentResponse(
|
||||
id=payment.id,
|
||||
created_at=payment.created_at.isoformat() if payment.created_at is not None else None,
|
||||
updated_at=payment.updated_at.isoformat() if payment.updated_at is not None else None,
|
||||
user_id=payment.user_id,
|
||||
order_id=payment.order_id,
|
||||
status=payment.status,
|
||||
receipt_cloudekassir_id=payment.receipt_cloudekassir_id,
|
||||
receipt_cloudekassir_link=payment.receipt_cloudekassir_link,
|
||||
itpay_payment_id=payment.itpay_payment_id,
|
||||
itpay_paid_amount=str(payment.itpay_paid_amount) if payment.itpay_paid_amount is not None else None,
|
||||
transaction_id=payment.transaction_id,
|
||||
web3_transaction_hash=payment.web3_transaction_hash,
|
||||
paid_at=payment.paid_at.isoformat() if payment.paid_at is not None else None,
|
||||
expired_date=payment.expired_date.isoformat() if payment.expired_date is not None else None,
|
||||
)
|
||||
|
||||
|
||||
def _order_detail_response(order: OrderEntity, payment: PaymentEntity | None) -> OrderDetailResponse:
|
||||
return OrderDetailResponse(
|
||||
status_code=200,
|
||||
order=_order_response(order),
|
||||
payment=_payment_response(payment) if payment is not None else None,
|
||||
)
|
||||
|
||||
|
||||
def _order_with_payment_response(order: OrderEntity, payment: PaymentEntity | None) -> OrderWithPaymentResponse:
|
||||
return OrderWithPaymentResponse(
|
||||
order=_order_response(order),
|
||||
payment=_payment_response(payment) if payment is not None else None,
|
||||
)
|
||||
|
||||
|
||||
def _order_status_response(order: OrderEntity, payment: PaymentEntity | None) -> OrderStatusResponse:
|
||||
return OrderStatusResponse(
|
||||
status_code=200,
|
||||
order_id=str(order.id),
|
||||
order_status=order.status,
|
||||
payment_status=payment.status if payment is not None else None,
|
||||
receipt_cloudekassir_link=payment.receipt_cloudekassir_link if payment is not None else None,
|
||||
web3_transaction_hash=payment.web3_transaction_hash if payment is not None else None,
|
||||
updated_at=payment.updated_at.isoformat() if payment is not None and payment.updated_at is not None else order.updated_at.isoformat() if order.updated_at is not None else None,
|
||||
)
|
||||
|
||||
|
||||
def _extract_websocket_access_token(websocket: WebSocket) -> str | None:
|
||||
token = websocket.cookies.get('access_token')
|
||||
if token:
|
||||
return token
|
||||
auth = websocket.headers.get('Authorization')
|
||||
if auth:
|
||||
scheme,param = get_authorization_scheme_param(auth)
|
||||
if scheme.lower() == 'bearer' and param:
|
||||
return param
|
||||
query_token = websocket.query_params.get('access_token') or websocket.query_params.get('token')
|
||||
if query_token:
|
||||
return str(query_token)
|
||||
return None
|
||||
|
||||
|
||||
async def _websocket_auth_context(websocket: WebSocket, jwt_service: IJwtService) -> AuthContext | None:
|
||||
token = _extract_websocket_access_token(websocket)
|
||||
if not token:
|
||||
return None
|
||||
try:
|
||||
payload: AccessTokenPayload = await jwt_service.decode_access_token(token)
|
||||
except ApplicationException:
|
||||
return None
|
||||
if payload.type != 'access':
|
||||
return None
|
||||
return AuthContext(user_id=payload.sub, sid=payload.sid, token=payload)
|
||||
|
||||
|
||||
@order_router.post(
|
||||
@@ -59,33 +194,113 @@ async def create_order(
|
||||
raise ConflictException(message='Payment provider rejected order')
|
||||
content = CreateOrderResponse(
|
||||
status_code=201,
|
||||
order=OrderPaymentResponse(
|
||||
id=o.id,
|
||||
created_at=o.created_at.isoformat() if o.created_at is not None else None,
|
||||
updated_at=o.updated_at.isoformat() if o.updated_at is not None else None,
|
||||
user_id=o.user_id,
|
||||
usdt_amount=str(o.usdt_amount) if o.usdt_amount is not None else None,
|
||||
usdt_exchange_rate=str(o.usdt_exchange_rate) if o.usdt_exchange_rate is not None else None,
|
||||
gas_fee=str(o.gas_fee) if o.gas_fee is not None else None,
|
||||
total_price=str(o.total_price) if o.total_price is not None else None,
|
||||
service_fee=str(o.service_fee) if o.service_fee is not None else None,
|
||||
status=o.status,
|
||||
client_payment_id=o.client_payment_id,
|
||||
itpay_payment_qr_url_desktop=o.itpay_payment_qr_url_desktop,
|
||||
itpay_payment_qr_url_android=o.itpay_payment_qr_url_android,
|
||||
itpay_payment_qr_url_ios=o.itpay_payment_qr_url_ios,
|
||||
itpay_payment_qr_image_desktop=o.itpay_payment_qr_image_desktop,
|
||||
itpay_payment_qr_image_android=o.itpay_payment_qr_image_android,
|
||||
itpay_payment_qr_image_ios=o.itpay_payment_qr_image_ios,
|
||||
itpay_id=o.itpay_id,
|
||||
itpay_qr_id=o.itpay_qr_id,
|
||||
itpay_amount=str(o.itpay_amount) if o.itpay_amount is not None else None,
|
||||
itpay_created_at=o.itpay_created_at.isoformat() if o.itpay_created_at is not None else None,
|
||||
),
|
||||
order=_order_response(o),
|
||||
)
|
||||
return content
|
||||
|
||||
|
||||
@payment_router.get('/config', response_model=PaymentConfigResponse)
|
||||
async def payment_config(
|
||||
command: GetPaymentConfigCommand = Depends(get_payment_config_command),
|
||||
) -> PaymentConfigResponse:
|
||||
quote = await command()
|
||||
return _payment_config_response(quote)
|
||||
|
||||
|
||||
@payment_router.get('/quote', response_model=PaymentQuoteResponse)
|
||||
async def payment_quote(
|
||||
usdt_amount: Decimal = Query(gt=0, decimal_places=2, max_digits=20),
|
||||
command: GetPaymentQuoteCommand = Depends(get_payment_quote_command),
|
||||
) -> PaymentQuoteResponse:
|
||||
quote = await command(usdt_amount)
|
||||
return _payment_quote_response(quote)
|
||||
|
||||
|
||||
@orders_router.get('/orders', response_model=OrdersResponse)
|
||||
async def list_orders(
|
||||
limit: int = Query(default=20, ge=1, le=100),
|
||||
offset: int = Query(default=0, ge=0),
|
||||
auth: AuthContext = Depends(require_access_token),
|
||||
command: ListOrdersCommand = Depends(get_list_orders_command),
|
||||
) -> OrdersResponse:
|
||||
orders = await command(user_id=auth.user_id,limit=limit,offset=offset)
|
||||
items = [_order_with_payment_response(item.order,item.payment) for item in orders]
|
||||
return OrdersResponse(status_code=200,orders=items,limit=limit,offset=offset)
|
||||
|
||||
|
||||
@payments_router.get('/payments', response_model=PaymentsResponse)
|
||||
async def list_payments(
|
||||
limit: int = Query(default=20, ge=1, le=100),
|
||||
offset: int = Query(default=0, ge=0),
|
||||
auth: AuthContext = Depends(require_access_token),
|
||||
command: ListPaymentsCommand = Depends(get_list_payments_command),
|
||||
) -> PaymentsResponse:
|
||||
payments = await command(user_id=auth.user_id,limit=limit,offset=offset)
|
||||
return PaymentsResponse(
|
||||
status_code=200,
|
||||
payments=[_payment_response(payment) for payment in payments],
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
|
||||
|
||||
@order_router.get('/{order_id}/status', response_model=OrderStatusResponse)
|
||||
async def order_status(
|
||||
order_id: str,
|
||||
auth: AuthContext = Depends(require_access_token),
|
||||
command: GetOrderStatusCommand = Depends(get_order_status_command),
|
||||
) -> OrderStatusResponse:
|
||||
result = await command(order_id=order_id,user_id=auth.user_id)
|
||||
return _order_status_response(result.order,result.payment)
|
||||
|
||||
|
||||
@order_router.websocket('/{order_id}/events')
|
||||
async def order_events(
|
||||
websocket: WebSocket,
|
||||
order_id: str,
|
||||
command: GetOrderStatusCommand = Depends(get_order_status_command),
|
||||
jwt_service: IJwtService = Depends(get_jwt_service),
|
||||
logger: ILogger = Depends(get_logger),
|
||||
) -> None:
|
||||
trace_id = websocket.headers.get('X-Trace-ID') or websocket.headers.get('X-Request-ID') or str(ULID())
|
||||
token = trace_id_var.set(trace_id)
|
||||
try:
|
||||
auth = await _websocket_auth_context(websocket, jwt_service)
|
||||
if auth is None:
|
||||
await websocket.close(code=1008)
|
||||
return
|
||||
await websocket.accept()
|
||||
logger.info({'event':'order_events_connected','order_id':order_id,'user_id':auth.user_id})
|
||||
last_payload: dict | None = None
|
||||
while True:
|
||||
try:
|
||||
result = await command(order_id=order_id,user_id=auth.user_id)
|
||||
except ApplicationException as exception:
|
||||
await websocket.send_json({'event':'order_events_error','detail':exception.message,'status_code':exception.status_code})
|
||||
await websocket.close(code=1008)
|
||||
return
|
||||
status_payload = _order_status_response(result.order,result.payment).model_dump(mode='json')
|
||||
payload = {'event':'order_status','data':status_payload}
|
||||
if payload != last_payload:
|
||||
await websocket.send_text(orjson.dumps(payload).decode())
|
||||
last_payload = payload
|
||||
await asyncio.sleep(2)
|
||||
except WebSocketDisconnect:
|
||||
logger.info({'event':'order_events_disconnected','order_id':order_id})
|
||||
finally:
|
||||
trace_id_var.reset(token)
|
||||
|
||||
|
||||
@order_router.get('/{order_id}', response_model=OrderDetailResponse)
|
||||
async def order_detail(
|
||||
order_id: str,
|
||||
auth: AuthContext = Depends(require_access_token),
|
||||
command: GetOrderCommand = Depends(get_order_command),
|
||||
) -> OrderDetailResponse:
|
||||
result = await command(order_id=order_id,user_id=auth.user_id)
|
||||
return _order_detail_response(result.order,result.payment)
|
||||
|
||||
|
||||
@order_router.post('/webhook/itpay')
|
||||
async def itpay_webhook(
|
||||
request: Request,
|
||||
|
||||
Reference in New Issue
Block a user