generated from vignesh/searchengine
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 41s
7.4 KiB
7.4 KiB
Cashfree Payments - Django Microservice Architecture Blueprint
1. Architecture Overview for Tradhox Payment Service
This blueprint outlines the recommended production architecture for integrating Cashfree Payment Gateway into the paymentservice Django app.
graph TD
A[Tradhox Customer Website / App] -->|1. Request Order| B[POST /api/payments/orders/create/]
B -->|2. Create Order via SDK| C[Cashfree API Gateway]
C -->|3. Return payment_session_id| B
B -->|4. Return session_id| A
A -->|5. Render Cashfree Checkout| D[Customer Completes Payment]
D -->|6. Direct S2S Webhook| E[POST /api/payments/webhooks/cashfree/]
E -->|7. Verify HMAC-SHA256| F[Update Database & Emit Events]
A -->|8. Redirect on Return| G[GET /api/payments/orders/status/]
2. Django Settings Configuration
Add the following to config/settings.py:
# Cashfree Payment Gateway Configuration
CASHFREE_APP_ID = os.environ.get('CASHFREE_APP_ID', 'TEST103848...')
CASHFREE_SECRET_KEY = os.environ.get('CASHFREE_SECRET_KEY', 'cfsk_ma_test_...')
CASHFREE_ENVIRONMENT = os.environ.get('CASHFREE_ENVIRONMENT', 'sandbox') # 'sandbox' or 'production'
CASHFREE_API_VERSION = os.environ.get('CASHFREE_API_VERSION', '2023-08-01')
if CASHFREE_ENVIRONMENT == 'production':
CASHFREE_BASE_URL = 'https://api.cashfree.com/pg'
else:
CASHFREE_BASE_URL = 'https://sandbox.cashfree.com/pg'
3. Core Cashfree Client Class
Implement payments/cashfree_client.py:
import hmac
import hashlib
import base64
import time
import requests
import logging
from django.conf import settings
logger = logging.getLogger(__name__)
class CashfreeClient:
def __init__(self):
self.app_id = settings.CASHFREE_APP_ID
self.secret_key = settings.CASHFREE_SECRET_KEY
self.base_url = settings.CASHFREE_BASE_URL
self.api_version = settings.CASHFREE_API_VERSION
def _get_headers(self, idempotency_key=None):
headers = {
'Content-Type': 'application/json',
'x-api-version': self.api_version,
'x-client-id': self.app_id,
'x-client-secret': self.secret_key,
}
if idempotency_key:
headers['x-idempotency-key'] = idempotency_key
return headers
def create_order(self, order_id, amount, currency, customer, return_url, notify_url=None, note=None):
url = f"{self.base_url}/orders"
payload = {
"order_id": str(order_id),
"order_amount": float(amount),
"order_currency": currency,
"customer_details": {
"customer_id": str(customer.get("id")),
"customer_name": customer.get("name", "Customer"),
"customer_email": customer.get("email"),
"customer_phone": str(customer.get("phone")),
},
"order_meta": {
"return_url": return_url,
"notify_url": notify_url or return_url
}
}
if note:
payload["order_note"] = note
response = requests.post(url, json=payload, headers=self._get_headers(), timeout=10)
return response.status_code, response.json()
def get_order(self, order_id):
url = f"{self.base_url}/orders/{order_id}"
response = requests.get(url, headers=self._get_headers(), timeout=10)
return response.status_code, response.json()
def create_refund(self, order_id, refund_id, amount, note="Refund", speed="STANDARD"):
url = f"{self.base_url}/orders/{order_id}/refunds"
payload = {
"refund_id": str(refund_id),
"refund_amount": float(amount),
"refund_note": note,
"refund_speed": speed
}
response = requests.post(url, json=payload, headers=self._get_headers(), timeout=10)
return response.status_code, response.json()
def verify_webhook_signature(self, raw_body: bytes, signature: str, timestamp: str) -> bool:
if not signature or not timestamp or not self.secret_key:
return False
try:
ts_val = int(timestamp)
if ts_val > 10000000000:
ts_val = ts_val // 1000
if abs(int(time.time()) - ts_val) > 300:
logger.warning("Webhook timestamp older than 5 minutes")
return False
except ValueError:
return False
payload = str(timestamp).encode('utf-8') + raw_body
computed_hmac = hmac.new(
key=self.secret_key.encode('utf-8'),
msg=payload,
digestmod=hashlib.sha256
).digest()
computed_sig = base64.b64encode(computed_hmac).decode('utf-8')
return hmac.compare_digest(computed_sig, signature)
4. Suggested Database Models
Add to payments/models.py:
from django.db import models
class PaymentOrder(models.Model):
STATUS_CHOICES = [
('ACTIVE', 'Active'),
('PAID', 'Paid'),
('EXPIRED', 'Expired'),
('TERMINATED', 'Terminated'),
]
order_id = models.CharField(max_length=64, unique=True, primary_key=True)
cf_order_id = models.CharField(max_length=64, blank=True, null=True)
amount = models.DecimalField(max_digits=12, decimal_places=2)
currency = models.CharField(max_length=3, default='INR')
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='ACTIVE')
customer_id = models.CharField(max_length=64)
customer_email = models.EmailField()
customer_phone = models.CharField(max_length=15)
payment_session_id = models.TextField(blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class PaymentTransaction(models.Model):
STATUS_CHOICES = [
('SUCCESS', 'Success'),
('FAILED', 'Failed'),
('PENDING', 'Pending'),
('USER_DROPPED', 'User Dropped'),
]
cf_payment_id = models.CharField(max_length=64, unique=True, primary_key=True)
order = models.ForeignKey(PaymentOrder, on_delete=models.CASCADE, related_name='transactions')
amount = models.DecimalField(max_digits=12, decimal_places=2)
status = models.CharField(max_length=20, choices=STATUS_CHOICES)
payment_method = models.CharField(max_length=50, blank=True, null=True)
bank_reference = models.CharField(max_length=100, blank=True, null=True)
payment_time = models.DateTimeField(blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
class PaymentRefund(models.Model):
STATUS_CHOICES = [
('SUCCESS', 'Success'),
('PENDING', 'Pending'),
('CANCELLED', 'Cancelled'),
('FAILED', 'Failed'),
]
refund_id = models.CharField(max_length=64, unique=True, primary_key=True)
cf_refund_id = models.CharField(max_length=64, blank=True, null=True)
order = models.ForeignKey(PaymentOrder, on_delete=models.CASCADE, related_name='refunds')
amount = models.DecimalField(max_digits=12, decimal_places=2)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='PENDING')
refund_arn = models.CharField(max_length=64, blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)