generated from vignesh/searchengine
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 36s
683 lines
27 KiB
Python
683 lines
27 KiB
Python
import json
|
|
import logging
|
|
from django.db import models
|
|
from django.conf import settings
|
|
from django.views.decorators.csrf import csrf_exempt
|
|
from django.utils.decorators import method_decorator
|
|
from django.utils import timezone
|
|
from rest_framework import status, permissions
|
|
from rest_framework.views import APIView
|
|
from rest_framework.response import Response
|
|
|
|
from .models import PaymentGatewayConfig, PaymentOrder, PaymentAttempt, PaymentRefund, WebhookLog
|
|
from .serializers import (
|
|
CreateOrderRequestSerializer,
|
|
PaymentOrderSerializer,
|
|
CreateRefundRequestSerializer,
|
|
PaymentRefundSerializer,
|
|
PaymentGatewayConfigSerializer,
|
|
)
|
|
from .engine import payment_engine
|
|
from .gateways.cashfree import CashfreeGateway
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class CreatePaymentOrderView(APIView):
|
|
"""
|
|
POST /api/payments/orders/create/
|
|
Creates a new payment order and returns a checkout session.
|
|
Automatically handles smart routing and fallback if primary gateway is degraded.
|
|
"""
|
|
permission_classes = [permissions.AllowAny]
|
|
|
|
def post(self, request):
|
|
serializer = CreateOrderRequestSerializer(data=request.data)
|
|
if not serializer.is_valid():
|
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
data = serializer.validated_data
|
|
requested_env = request.data.get('environment')
|
|
success, order, attempt, error_msg = payment_engine.initiate_payment_order(
|
|
order_id=data['order_id'],
|
|
amount=float(data['amount']),
|
|
currency=data.get('currency', 'INR'),
|
|
customer_id=data.get('customer_id') or f"cust_{data['order_id'][-8:]}",
|
|
customer_name=data.get('customer_name', ''),
|
|
customer_email=data.get('customer_email', ''),
|
|
customer_phone=data.get('customer_phone', ''),
|
|
return_url=data.get('return_url', ''),
|
|
notify_url=data.get('notify_url', ''),
|
|
order_note=data.get('order_note', ''),
|
|
metadata=data.get('metadata', {}),
|
|
request=request,
|
|
environment=requested_env,
|
|
)
|
|
|
|
if not success:
|
|
return Response({
|
|
"success": False,
|
|
"order_id": data['order_id'],
|
|
"error": error_msg or "Failed to initiate payment on all configured gateways",
|
|
"status": order.status
|
|
}, status=status.HTTP_502_BAD_GATEWAY)
|
|
|
|
# Build checkout URLs depending on gateway
|
|
checkout_url = None
|
|
if attempt and attempt.raw_response:
|
|
checkout_url = attempt.raw_response.get("payment_session_id")
|
|
if checkout_url and attempt.gateway_name == 'CASHFREE':
|
|
origin = request.META.get('HTTP_ORIGIN', '').lower()
|
|
referer = request.META.get('HTTP_REFERER', '').lower()
|
|
host = request.META.get('HTTP_HOST', '').lower()
|
|
prod_domains = getattr(settings, 'TRADHOX_PRODUCTION_DOMAINS', ['beta.tradhox.com', 'tradhox.com'])
|
|
is_prod = (requested_env == 'production') or any(d in origin or d in referer or d in host for d in prod_domains)
|
|
base = 'https://payments.cashfree.com/order/#' if is_prod else 'https://payments-test.cashfree.com/order/#'
|
|
checkout_url = f"{base}{attempt.payment_session_id}"
|
|
|
|
return Response({
|
|
"success": True,
|
|
"order_id": order.order_id,
|
|
"amount": float(order.amount),
|
|
"currency": order.currency,
|
|
"active_gateway": order.active_gateway,
|
|
"gateway_name": order.active_gateway,
|
|
"gateway_order_id": attempt.gateway_order_id if attempt else None,
|
|
"payment_session_id": attempt.payment_session_id if attempt else None,
|
|
"checkout_url": checkout_url,
|
|
"status": order.status,
|
|
"created_at": order.created_at
|
|
}, status=status.HTTP_201_CREATED)
|
|
|
|
|
|
class PaymentOrderDetailView(APIView):
|
|
"""
|
|
GET /api/payments/orders/<order_id>/
|
|
Fetch status and attempt history for an order.
|
|
"""
|
|
permission_classes = [permissions.AllowAny]
|
|
|
|
def get(self, request, order_id):
|
|
try:
|
|
order = PaymentOrder.objects.prefetch_related('attempts', 'refunds').get(order_id=order_id)
|
|
except PaymentOrder.DoesNotExist:
|
|
return Response({"error": f"Order {order_id} not found"}, status=status.HTTP_404_NOT_FOUND)
|
|
|
|
serializer = PaymentOrderSerializer(order)
|
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
|
|
|
|
@method_decorator(csrf_exempt, name='dispatch')
|
|
class CashfreeWebhookView(APIView):
|
|
"""
|
|
POST /api/payments/webhooks/cashfree/
|
|
Receives and cryptographically verifies webhooks from Cashfree.
|
|
Updates order and attempt status upon payment confirmation.
|
|
"""
|
|
permission_classes = [permissions.AllowAny]
|
|
|
|
def post(self, request):
|
|
raw_body = request.body
|
|
headers = {k.lower(): v for k, v in request.headers.items()}
|
|
|
|
# Audit log incoming webhook
|
|
try:
|
|
parsed_body = json.loads(raw_body.decode('utf-8')) if raw_body else {}
|
|
except Exception:
|
|
parsed_body = {}
|
|
|
|
event_type = parsed_body.get('type', 'UNKNOWN')
|
|
webhook_log = WebhookLog.objects.create(
|
|
gateway_name='CASHFREE',
|
|
event_type=event_type,
|
|
payload=parsed_body
|
|
)
|
|
|
|
config = PaymentGatewayConfig.objects.filter(name='CASHFREE').first()
|
|
gateway = CashfreeGateway(config=config)
|
|
|
|
# Cryptographic Verification
|
|
verify_result = gateway.verify_webhook(raw_body=raw_body, headers=headers)
|
|
webhook_log.signature_valid = verify_result.is_valid
|
|
|
|
if not verify_result.is_valid:
|
|
webhook_log.error_message = verify_result.error_message
|
|
webhook_log.save(update_fields=['signature_valid', 'error_message'])
|
|
logger.warning(f"Cashfree Webhook signature invalid: {verify_result.error_message}")
|
|
return Response({"error": verify_result.error_message}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
order_id = verify_result.order_id
|
|
if not order_id:
|
|
webhook_log.error_message = "No order_id in webhook"
|
|
webhook_log.save(update_fields=['error_message'])
|
|
return Response({"status": "ignored", "reason": "No order_id in payload"}, status=status.HTTP_200_OK)
|
|
|
|
logger.info(f"Processing verified Cashfree webhook for order {order_id}: {verify_result.event_type}")
|
|
|
|
try:
|
|
order = PaymentOrder.objects.get(order_id=order_id)
|
|
|
|
# Find or create latest attempt for this gateway
|
|
attempt = PaymentAttempt.objects.filter(order=order, gateway_name='CASHFREE').first()
|
|
if not attempt:
|
|
attempt = PaymentAttempt.objects.create(
|
|
order=order,
|
|
gateway_name='CASHFREE',
|
|
gateway_order_id=verify_result.gateway_payment_id,
|
|
)
|
|
|
|
# Update attempt details
|
|
attempt.gateway_payment_id = verify_result.gateway_payment_id
|
|
attempt.status = verify_result.payment_status or attempt.status
|
|
attempt.payment_method = verify_result.payment_method or attempt.payment_method
|
|
attempt.bank_reference = verify_result.bank_reference or attempt.bank_reference
|
|
attempt.raw_response = verify_result.raw_payload
|
|
attempt.save()
|
|
|
|
# Update Order status
|
|
if verify_result.payment_status == 'SUCCESS':
|
|
order.status = 'PAID'
|
|
order.save(update_fields=['status', 'updated_at'])
|
|
elif verify_result.payment_status == 'FAILED' and order.status != 'PAID':
|
|
order.status = 'FAILED'
|
|
order.save(update_fields=['status', 'updated_at'])
|
|
|
|
webhook_log.processed = True
|
|
webhook_log.save(update_fields=['signature_valid', 'processed'])
|
|
|
|
return Response({"status": "acknowledged", "order_id": order_id}, status=status.HTTP_200_OK)
|
|
|
|
except PaymentOrder.DoesNotExist:
|
|
logger.error(f"Order {order_id} not found in database for webhook")
|
|
webhook_log.error_message = f"Order {order_id} not found"
|
|
webhook_log.save(update_fields=['error_message'])
|
|
return Response({"status": "acknowledged", "warning": "Order not found in DB"}, status=status.HTTP_200_OK)
|
|
|
|
|
|
class CreateRefundView(APIView):
|
|
"""
|
|
POST /api/payments/orders/<order_id>/refund/
|
|
Initiate a refund for a previously captured order.
|
|
"""
|
|
permission_classes = [permissions.AllowAny]
|
|
|
|
def post(self, request, order_id):
|
|
serializer = CreateRefundRequestSerializer(data=request.data)
|
|
if not serializer.is_valid():
|
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
data = serializer.validated_data
|
|
success, refund, error_msg = payment_engine.process_refund(
|
|
order_id=order_id,
|
|
refund_id=data['refund_id'],
|
|
amount=float(data['amount']),
|
|
note=data.get('reason', 'Customer Refund'),
|
|
speed=data.get('speed', 'STANDARD')
|
|
)
|
|
|
|
if not success:
|
|
return Response({
|
|
"success": False,
|
|
"error": error_msg or "Refund processing failed"
|
|
}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
return Response(PaymentRefundSerializer(refund).data, status=status.HTTP_201_CREATED)
|
|
|
|
|
|
class GatewayHealthView(APIView):
|
|
"""
|
|
GET /api/payments/engine/health/
|
|
Live health and circuit status check across all configured payment gateways.
|
|
"""
|
|
permission_classes = [permissions.AllowAny]
|
|
|
|
def get(self, request):
|
|
configs = PaymentGatewayConfig.objects.all()
|
|
results = []
|
|
|
|
for config in configs:
|
|
adapter = payment_engine.get_adapter(config)
|
|
is_healthy = adapter.health_check() if adapter else False
|
|
config.last_health_check = timezone.now()
|
|
|
|
if is_healthy:
|
|
config.record_success()
|
|
else:
|
|
config.record_failure()
|
|
|
|
results.append({
|
|
"name": config.name,
|
|
"display_name": config.display_name,
|
|
"priority": config.priority,
|
|
"is_active": config.is_active,
|
|
"health_status": config.health_status,
|
|
"consecutive_failures": config.consecutive_failures,
|
|
"environment": config.environment,
|
|
"last_health_check": config.last_health_check
|
|
})
|
|
|
|
return Response({
|
|
"service": "Tradhox Multi-Vendor Payment Engine",
|
|
"timestamp": timezone.now(),
|
|
"gateways": results
|
|
}, status=status.HTTP_200_OK)
|
|
|
|
|
|
class PortalStatsView(APIView):
|
|
"""
|
|
GET /api/payments/engine/stats/
|
|
Returns dashboard metrics, recent orders, and webhook logs for the portal.
|
|
"""
|
|
permission_classes = [permissions.AllowAny]
|
|
|
|
def get(self, request):
|
|
total_orders = PaymentOrder.objects.count()
|
|
paid_orders = PaymentOrder.objects.filter(status='PAID').count()
|
|
total_volume = PaymentOrder.objects.filter(status='PAID').aggregate(total=models.Sum('amount'))['total'] or 0.0
|
|
total_refunds = PaymentRefund.objects.count()
|
|
|
|
recent_orders = PaymentOrder.objects.prefetch_related('attempts').order_by('-created_at')[:15]
|
|
recent_webhooks = WebhookLog.objects.order_by('-received_at')[:10]
|
|
gateways = PaymentGatewayConfig.objects.all().order_by('priority')
|
|
|
|
return Response({
|
|
"metrics": {
|
|
"total_orders": total_orders,
|
|
"paid_orders": paid_orders,
|
|
"total_volume": float(total_volume),
|
|
"total_refunds": total_refunds,
|
|
"success_rate": round((paid_orders / total_orders * 100), 1) if total_orders > 0 else 100.0,
|
|
},
|
|
"gateways": PaymentGatewayConfigSerializer(gateways, many=True).data,
|
|
"recent_orders": PaymentOrderSerializer(recent_orders, many=True).data,
|
|
"recent_webhooks": [
|
|
{
|
|
"id": w.id,
|
|
"gateway": w.gateway_name,
|
|
"event_type": w.event_type,
|
|
"signature_valid": w.signature_valid,
|
|
"processed": w.processed,
|
|
"received_at": w.received_at,
|
|
"payload": w.payload,
|
|
"error_message": w.error_message
|
|
}
|
|
for w in recent_webhooks
|
|
]
|
|
}, status=status.HTTP_200_OK)
|
|
|
|
|
|
class GatewayToggleView(APIView):
|
|
"""
|
|
POST /api/payments/engine/gateway-toggle/
|
|
Allows manual simulation of gateway status (HEALTHY, DEGRADED, DOWN, or priority change)
|
|
for testing automatic failover live.
|
|
"""
|
|
permission_classes = [permissions.AllowAny]
|
|
|
|
def post(self, request):
|
|
gateway_name = request.data.get('name', 'CASHFREE').upper()
|
|
target_status = request.data.get('health_status')
|
|
is_active = request.data.get('is_active')
|
|
|
|
try:
|
|
config = PaymentGatewayConfig.objects.get(name=gateway_name)
|
|
except PaymentGatewayConfig.DoesNotExist:
|
|
return Response({"error": f"Gateway {gateway_name} not found"}, status=status.HTTP_404_NOT_FOUND)
|
|
|
|
if target_status:
|
|
config.health_status = target_status
|
|
if target_status == 'HEALTHY':
|
|
config.consecutive_failures = 0
|
|
if is_active is not None:
|
|
config.is_active = bool(is_active)
|
|
|
|
config.save()
|
|
return Response({
|
|
"message": f"Gateway {config.name} updated successfully",
|
|
"gateway": PaymentGatewayConfigSerializer(config).data
|
|
}, status=status.HTTP_200_OK)
|
|
|
|
|
|
class SimulateWebhookView(APIView):
|
|
"""
|
|
POST /api/payments/engine/simulate-webhook/
|
|
Generates a cryptographically signed test webhook and invokes the internal webhook processor.
|
|
"""
|
|
permission_classes = [permissions.AllowAny]
|
|
|
|
def post(self, request):
|
|
order_id = request.data.get('order_id')
|
|
event_type = request.data.get('event_type', 'PAYMENT_SUCCESS_WEBHOOK')
|
|
|
|
try:
|
|
order = PaymentOrder.objects.get(order_id=order_id)
|
|
except PaymentOrder.DoesNotExist:
|
|
return Response({"error": f"Order {order_id} not found"}, status=status.HTTP_404_NOT_FOUND)
|
|
|
|
config = PaymentGatewayConfig.objects.filter(name='CASHFREE').first()
|
|
secret_key = config.secret_key if config and config.secret_key else getattr(settings, 'CASHFREE_SECRET_KEY', '')
|
|
|
|
# Build realistic Cashfree webhook payload
|
|
payload = {
|
|
"data": {
|
|
"order": {
|
|
"order_id": order.order_id,
|
|
"order_amount": float(order.amount),
|
|
"order_currency": order.currency,
|
|
"order_tags": order.metadata
|
|
},
|
|
"payment": {
|
|
"cf_payment_id": f"cf_sim_{int(timezone.now().timestamp())}",
|
|
"payment_status": "SUCCESS" if "SUCCESS" in event_type else "FAILED",
|
|
"payment_amount": float(order.amount),
|
|
"payment_currency": order.currency,
|
|
"payment_message": "Simulation: Transaction Completed",
|
|
"payment_time": timezone.now().isoformat(),
|
|
"payment_completion_time": timezone.now().isoformat(),
|
|
"bank_reference": f"SIM_UTR_{int(timezone.now().timestamp())}",
|
|
"payment_group": "upi"
|
|
},
|
|
"customer_details": {
|
|
"customer_id": order.customer_id,
|
|
"customer_name": order.customer_name,
|
|
"customer_email": order.customer_email,
|
|
"customer_phone": order.customer_phone
|
|
}
|
|
},
|
|
"event_time": timezone.now().isoformat(),
|
|
"type": event_type
|
|
}
|
|
|
|
raw_body = json.dumps(payload).encode('utf-8')
|
|
timestamp = str(int(timezone.now().timestamp()))
|
|
to_sign = timestamp.encode('utf-8') + raw_body
|
|
|
|
import hmac, hashlib, base64
|
|
computed_hmac = hmac.new(
|
|
key=secret_key.encode('utf-8'),
|
|
msg=to_sign,
|
|
digestmod=hashlib.sha256
|
|
).digest()
|
|
signature = base64.b64encode(computed_hmac).decode('utf-8')
|
|
|
|
# Trigger internal webhook view directly
|
|
from django.test import RequestFactory
|
|
factory = RequestFactory()
|
|
sim_request = factory.post(
|
|
'/api/payments/webhooks/cashfree/',
|
|
data=raw_body,
|
|
content_type='application/json',
|
|
HTTP_X_WEBHOOK_SIGNATURE=signature,
|
|
HTTP_X_WEBHOOK_TIMESTAMP=timestamp
|
|
)
|
|
webhook_view = CashfreeWebhookView.as_view()
|
|
response = webhook_view(sim_request)
|
|
|
|
order.refresh_from_db()
|
|
return Response({
|
|
"message": "Webhook simulated and dispatched successfully",
|
|
"webhook_response_status": response.status_code,
|
|
"order_id": order.order_id,
|
|
"order_status": order.status,
|
|
"simulated_signature": signature,
|
|
"timestamp": timestamp,
|
|
"payload": payload
|
|
}, status=status.HTTP_200_OK)
|
|
|
|
|
|
from django.views.generic import TemplateView
|
|
|
|
class PortalView(TemplateView):
|
|
"""Renders the interactive Payment Engine Test & Operations Portal."""
|
|
template_name = "payments/portal.html"
|
|
|
|
|
|
class CashfreeDirectPaySessionView(APIView):
|
|
"""
|
|
POST /api/payments/cashfree/pay/
|
|
Directly invokes Cashfree /orders/sessions for UPI Dynamic QR or Collect.
|
|
"""
|
|
permission_classes = [permissions.AllowAny]
|
|
|
|
def post(self, request):
|
|
session_id = request.data.get('payment_session_id')
|
|
channel = request.data.get('channel', 'qrcode') # 'qrcode' or 'collect'
|
|
upi_id = request.data.get('upi_id', 'success@upi')
|
|
|
|
if not session_id:
|
|
return Response({"error": "payment_session_id is required"}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
config = PaymentGatewayConfig.objects.filter(name='CASHFREE').first()
|
|
gateway = CashfreeGateway(config=config, request=request, environment=request.data.get('environment'))
|
|
|
|
url = f"{gateway.base_url}/orders/sessions"
|
|
method_payload = {"channel": channel}
|
|
if channel == 'collect':
|
|
method_payload["upi_id"] = upi_id
|
|
|
|
payload = {
|
|
"payment_session_id": session_id,
|
|
"payment_method": {
|
|
"upi": method_payload
|
|
}
|
|
}
|
|
|
|
import requests
|
|
try:
|
|
r = requests.post(url, json=payload, headers=gateway._get_headers(), timeout=12)
|
|
data = r.json() if r.content else {}
|
|
return Response(data, status=r.status_code)
|
|
except Exception as e:
|
|
return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
|
|
|
|
class CashfreeDirectPaymentsListView(APIView):
|
|
"""
|
|
GET /api/payments/cashfree/orders/<order_id>/payments/
|
|
Directly queries Cashfree API for all payment attempts associated with an order.
|
|
"""
|
|
permission_classes = [permissions.AllowAny]
|
|
|
|
def get(self, request, order_id):
|
|
config = PaymentGatewayConfig.objects.filter(name='CASHFREE').first()
|
|
gateway = CashfreeGateway(config=config, request=request, environment=request.query_params.get('environment'))
|
|
|
|
url = f"{gateway.base_url}/orders/{order_id}/payments"
|
|
import requests
|
|
try:
|
|
r = requests.get(url, headers=gateway._get_headers(), timeout=10)
|
|
data = r.json() if r.content else []
|
|
return Response(data, status=r.status_code)
|
|
except Exception as e:
|
|
return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
|
|
|
|
class CashfreeDirectSettlementsView(APIView):
|
|
"""
|
|
GET /api/payments/cashfree/orders/<order_id>/settlements/
|
|
Directly queries Cashfree API for settlement details of an order.
|
|
"""
|
|
permission_classes = [permissions.AllowAny]
|
|
|
|
def get(self, request, order_id):
|
|
config = PaymentGatewayConfig.objects.filter(name='CASHFREE').first()
|
|
gateway = CashfreeGateway(config=config, request=request, environment=request.query_params.get('environment'))
|
|
|
|
url = f"{gateway.base_url}/orders/{order_id}/settlements"
|
|
import requests
|
|
try:
|
|
r = requests.get(url, headers=gateway._get_headers(), timeout=10)
|
|
data = r.json() if r.content else {}
|
|
return Response(data, status=r.status_code)
|
|
except Exception as e:
|
|
return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
|
|
|
|
class CashfreeDirectCreateOrderView(APIView):
|
|
"""
|
|
POST /api/payments/cashfree/orders/create/
|
|
Creates an order directly against Cashfree, bypassing orchestrator routing.
|
|
Records the order and attempt in the database for tracking and refunds.
|
|
"""
|
|
permission_classes = [permissions.AllowAny]
|
|
|
|
def post(self, request):
|
|
order_id = request.data.get('order_id')
|
|
amount = request.data.get('amount')
|
|
currency = request.data.get('currency', 'INR')
|
|
customer_name = request.data.get('customer_name', 'Cashfree Tester')
|
|
customer_email = request.data.get('customer_email', 'tester@tradhox.com')
|
|
customer_phone = request.data.get('customer_phone', '9876543210')
|
|
order_note = request.data.get('order_note', 'Direct Cashfree Order')
|
|
return_url = request.data.get('return_url')
|
|
notify_url = request.data.get('notify_url')
|
|
|
|
if not order_id or not amount:
|
|
return Response({"error": "order_id and amount are required"}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
config = PaymentGatewayConfig.objects.filter(name='CASHFREE').first()
|
|
gateway = CashfreeGateway(config=config, request=request, environment=request.data.get('environment'))
|
|
|
|
# Call Cashfree API directly
|
|
resp = gateway.create_order(
|
|
order_id=order_id,
|
|
amount=float(amount),
|
|
currency=currency,
|
|
customer_id=f"cust_{order_id[-6:]}",
|
|
customer_name=customer_name,
|
|
customer_email=customer_email,
|
|
customer_phone=customer_phone,
|
|
return_url=return_url,
|
|
notify_url=notify_url,
|
|
order_note=order_note,
|
|
metadata={"engine": "CASHFREE_DIRECT", "env": gateway.environment}
|
|
)
|
|
|
|
if not resp.success:
|
|
return Response({
|
|
"success": False,
|
|
"error": resp.error_message or "Failed to create order on Cashfree",
|
|
"raw_response": resp.raw_response
|
|
}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
# Save to DB for historical explorer and refunds
|
|
order, _ = PaymentOrder.objects.update_or_create(
|
|
order_id=order_id,
|
|
defaults={
|
|
"amount": float(amount),
|
|
"currency": currency,
|
|
"customer_id": f"cust_{order_id[-6:]}",
|
|
"customer_name": customer_name,
|
|
"customer_email": customer_email,
|
|
"customer_phone": customer_phone,
|
|
"status": "PENDING",
|
|
"active_gateway": "CASHFREE",
|
|
"order_note": order_note,
|
|
"metadata": {"direct_engine": True}
|
|
}
|
|
)
|
|
|
|
PaymentAttempt.objects.create(
|
|
order=order,
|
|
gateway_name="CASHFREE",
|
|
gateway_order_id=resp.gateway_order_id,
|
|
status="PENDING",
|
|
raw_response=resp.raw_response
|
|
)
|
|
|
|
return Response({
|
|
"success": True,
|
|
"order_id": order_id,
|
|
"gateway_name": "CASHFREE",
|
|
"gateway_order_id": resp.gateway_order_id,
|
|
"payment_session_id": resp.payment_session_id,
|
|
"checkout_url": resp.checkout_url,
|
|
"raw_response": resp.raw_response
|
|
}, status=status.HTTP_201_CREATED)
|
|
|
|
|
|
class CashfreeDirectRawOrderView(APIView):
|
|
"""
|
|
GET /api/payments/cashfree/orders/<order_id>/raw/
|
|
Queries Cashfree live server for raw order object.
|
|
"""
|
|
permission_classes = [permissions.AllowAny]
|
|
|
|
def get(self, request, order_id):
|
|
config = PaymentGatewayConfig.objects.filter(name='CASHFREE').first()
|
|
gateway = CashfreeGateway(config=config, request=request, environment=request.query_params.get('environment'))
|
|
raw_order = gateway.get_order(order_id)
|
|
return Response(raw_order, status=status.HTTP_200_OK)
|
|
|
|
|
|
class CashfreeQuickSessionView(APIView):
|
|
"""
|
|
POST /api/payments/cashfree/quick-session/
|
|
Automatically generates a live Cashfree order and returns the payment_session_id.
|
|
Requires zero manual parameters, enabling 1-click dynamic session creation.
|
|
"""
|
|
permission_classes = [permissions.AllowAny]
|
|
|
|
def post(self, request):
|
|
amount = float(request.data.get('amount') or 100.00)
|
|
rand_id = f"CF-UPI-{int(timezone.now().timestamp())}"
|
|
config = PaymentGatewayConfig.objects.filter(name='CASHFREE').first()
|
|
gateway = CashfreeGateway(config=config, request=request, environment=request.data.get('environment'))
|
|
|
|
resp = gateway.create_order(
|
|
order_id=rand_id,
|
|
amount=amount,
|
|
currency="INR",
|
|
customer_id=f"cust_{rand_id[-6:]}",
|
|
customer_name="Tradhox UPI Tester",
|
|
customer_email="vignesh@tradhox.com",
|
|
customer_phone="9876543210",
|
|
return_url=f"https://betapayments.tradhox.com/portal/?order_id={rand_id}",
|
|
notify_url="https://betapayments.tradhox.com/api/payments/webhooks/cashfree/",
|
|
order_note="Quick UPI Session Auto-Generated",
|
|
metadata={"source": "upi_studio_auto_gen", "env": gateway.environment}
|
|
)
|
|
|
|
if not resp.success:
|
|
return Response({
|
|
"success": False,
|
|
"error": resp.error_message or "Failed to auto-generate session on Cashfree"
|
|
}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
# Record in DB
|
|
order, _ = PaymentOrder.objects.update_or_create(
|
|
order_id=rand_id,
|
|
defaults={
|
|
"amount": amount,
|
|
"currency": "INR",
|
|
"customer_id": f"cust_{rand_id[-6:]}",
|
|
"customer_name": "Tradhox UPI Tester",
|
|
"customer_email": "vignesh@tradhox.com",
|
|
"customer_phone": "9876543210",
|
|
"status": "PENDING",
|
|
"active_gateway": "CASHFREE",
|
|
"order_note": "Quick UPI Session Auto-Generated",
|
|
"metadata": {"auto_generated_session": True}
|
|
}
|
|
)
|
|
|
|
PaymentAttempt.objects.create(
|
|
order=order,
|
|
gateway_name="CASHFREE",
|
|
gateway_order_id=resp.gateway_order_id,
|
|
payment_session_id=resp.payment_session_id,
|
|
status="PENDING",
|
|
raw_response=resp.raw_response
|
|
)
|
|
|
|
return Response({
|
|
"success": True,
|
|
"order_id": rand_id,
|
|
"payment_session_id": resp.payment_session_id,
|
|
"amount": amount,
|
|
"checkout_url": resp.checkout_url
|
|
}, status=status.HTTP_201_CREATED)
|
|
|
|
|
|
|
|
|