generated from vignesh/searchengine
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 36s
218 lines
8.1 KiB
Python
218 lines
8.1 KiB
Python
import logging
|
|
from typing import Optional, Dict, Any, Tuple
|
|
from django.utils import timezone
|
|
from .models import PaymentGatewayConfig, PaymentOrder, PaymentAttempt, PaymentRefund
|
|
from .gateways.base import BasePaymentGateway, GatewayOrderResponse, GatewayRefundResponse
|
|
from .gateways.cashfree import CashfreeGateway
|
|
from .gateways.razorpay import RazorpayGateway
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class PaymentEngine:
|
|
"""
|
|
Multi-Vendor Payment Orchestration Engine.
|
|
Handles smart gateway routing, circuit breaking, and automatic failover.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.gateway_registry = {
|
|
'CASHFREE': CashfreeGateway,
|
|
'RAZORPAY': RazorpayGateway,
|
|
}
|
|
|
|
|
|
def register_gateway_class(self, name: str, gateway_class):
|
|
"""Allows dynamic registration of gateway adapters (e.g. Razorpay)."""
|
|
self.gateway_registry[name.upper()] = gateway_class
|
|
|
|
def get_adapter(self, config: PaymentGatewayConfig, request=None, environment=None) -> Optional[BasePaymentGateway]:
|
|
gateway_class = self.gateway_registry.get(config.name.upper())
|
|
if gateway_class:
|
|
if config.name.upper() == 'CASHFREE':
|
|
return gateway_class(config=config, request=request, environment=environment)
|
|
return gateway_class(config=config)
|
|
logger.warning(f"No adapter registered for gateway: {config.name}")
|
|
return None
|
|
|
|
def get_candidate_gateways(self):
|
|
"""
|
|
Retrieves active gateways ordered by priority.
|
|
Prefers HEALTHY gateways over DEGRADED gateways.
|
|
Skips DOWN gateways unless all are DOWN.
|
|
"""
|
|
active_configs = list(PaymentGatewayConfig.objects.filter(is_active=True).order_by('priority'))
|
|
if not active_configs:
|
|
# Fallback: create default Cashfree sandbox config in DB
|
|
default_cf, _ = PaymentGatewayConfig.objects.get_or_create(
|
|
name='CASHFREE',
|
|
defaults={
|
|
'display_name': 'Cashfree Payments',
|
|
'is_active': True,
|
|
'priority': 1,
|
|
'environment': 'sandbox',
|
|
'health_status': 'HEALTHY'
|
|
}
|
|
)
|
|
active_configs = [default_cf]
|
|
|
|
healthy = [c for c in active_configs if c.health_status == 'HEALTHY']
|
|
degraded = [c for c in active_configs if c.health_status == 'DEGRADED']
|
|
down = [c for c in active_configs if c.health_status == 'DOWN']
|
|
|
|
# Prioritize healthy, then degraded, and lastly down as last resort
|
|
candidates = healthy + degraded
|
|
if not candidates:
|
|
candidates = down
|
|
return candidates
|
|
|
|
def initiate_payment_order(
|
|
self,
|
|
order_id: str,
|
|
amount: float,
|
|
currency: str,
|
|
customer_id: str,
|
|
customer_name: str,
|
|
customer_email: str,
|
|
customer_phone: str,
|
|
return_url: str,
|
|
notify_url: str,
|
|
order_note: Optional[str] = None,
|
|
metadata: Optional[Dict[str, Any]] = None,
|
|
request=None,
|
|
environment=None,
|
|
) -> Tuple[bool, PaymentOrder, Optional[PaymentAttempt], Optional[str]]:
|
|
"""
|
|
Creates an order with automatic failover across registered gateways.
|
|
Returns: (success, PaymentOrder, PaymentAttempt, error_message)
|
|
"""
|
|
order, _ = PaymentOrder.objects.update_or_create(
|
|
order_id=order_id,
|
|
defaults={
|
|
'amount': amount,
|
|
'currency': currency or 'INR',
|
|
'customer_id': customer_id,
|
|
'customer_name': customer_name or '',
|
|
'customer_email': customer_email,
|
|
'customer_phone': customer_phone or '',
|
|
'return_url': return_url,
|
|
'notify_url': notify_url,
|
|
'order_note': order_note,
|
|
'metadata': metadata or {},
|
|
'status': 'INITIATED',
|
|
}
|
|
)
|
|
|
|
candidates = self.get_candidate_gateways()
|
|
last_error = "No active gateways available"
|
|
|
|
for config in candidates:
|
|
adapter = self.get_adapter(config, request=request, environment=environment)
|
|
if not adapter:
|
|
continue
|
|
|
|
logger.info(f"Attempting order {order_id} via {adapter.gateway_name} (Priority {config.priority})")
|
|
|
|
|
|
# Execute order creation with gateway
|
|
resp: GatewayOrderResponse = adapter.create_order(
|
|
order_id=order_id,
|
|
amount=amount,
|
|
currency=currency,
|
|
customer_id=customer_id,
|
|
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=metadata,
|
|
)
|
|
|
|
if resp.success:
|
|
# Gateway succeeded!
|
|
config.record_success()
|
|
order.status = 'ACTIVE'
|
|
order.active_gateway = adapter.gateway_name
|
|
order.save(update_fields=['status', 'active_gateway', 'updated_at'])
|
|
|
|
attempt = PaymentAttempt.objects.create(
|
|
order=order,
|
|
gateway_name=adapter.gateway_name,
|
|
gateway_order_id=resp.gateway_order_id,
|
|
payment_session_id=resp.payment_session_id,
|
|
status='INITIATED',
|
|
raw_response=resp.raw_response
|
|
)
|
|
return True, order, attempt, None
|
|
|
|
else:
|
|
# Gateway failed: record failure and auto-failover to next candidate!
|
|
logger.warning(
|
|
f"Gateway {adapter.gateway_name} failed for order {order_id}: "
|
|
f"[{resp.error_code}] {resp.error_message}. Attempting automatic failover..."
|
|
)
|
|
config.record_failure()
|
|
PaymentAttempt.objects.create(
|
|
order=order,
|
|
gateway_name=adapter.gateway_name,
|
|
status='FAILED',
|
|
error_code=resp.error_code or 'GATEWAY_ERROR',
|
|
error_message=resp.error_message,
|
|
raw_response=resp.raw_response
|
|
)
|
|
last_error = f"{adapter.gateway_name} error: {resp.error_message}"
|
|
|
|
# If all candidates failed
|
|
order.status = 'FAILED'
|
|
order.save(update_fields=['status', 'updated_at'])
|
|
return False, order, None, last_error
|
|
|
|
def process_refund(
|
|
self,
|
|
order_id: str,
|
|
refund_id: str,
|
|
amount: float,
|
|
note: Optional[str] = None,
|
|
speed: str = "STANDARD"
|
|
) -> Tuple[bool, Optional[PaymentRefund], Optional[str]]:
|
|
"""Processes refund through the active gateway that captured the payment."""
|
|
try:
|
|
order = PaymentOrder.objects.get(order_id=order_id)
|
|
except PaymentOrder.DoesNotExist:
|
|
return False, None, f"Order {order_id} not found"
|
|
|
|
config = PaymentGatewayConfig.objects.filter(name=order.active_gateway).first()
|
|
adapter = self.get_adapter(config) if config else CashfreeGateway()
|
|
|
|
resp: GatewayRefundResponse = adapter.create_refund(
|
|
order_id=order_id,
|
|
refund_id=refund_id,
|
|
amount=amount,
|
|
note=note,
|
|
speed=speed
|
|
)
|
|
|
|
refund = PaymentRefund.objects.create(
|
|
refund_id=refund_id,
|
|
order=order,
|
|
gateway_name=adapter.gateway_name,
|
|
gateway_refund_id=resp.gateway_refund_id,
|
|
amount=amount,
|
|
status='SUCCESS' if resp.success else 'FAILED',
|
|
speed=speed,
|
|
refund_arn=resp.refund_arn,
|
|
reason=note,
|
|
raw_response=resp.raw_response
|
|
)
|
|
|
|
if resp.success:
|
|
order.status = 'REFUNDED'
|
|
order.save(update_fields=['status', 'updated_at'])
|
|
return True, refund, None
|
|
else:
|
|
return False, refund, resp.error_message
|
|
|
|
|
|
# Global singleton instance
|
|
payment_engine = PaymentEngine()
|