paymentservice/document/cashfree/11_django_integration_blueprint.md
vickytechkey befd83ad53
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 41s
docs: add Cashfree payment gateway documentation and integration guides
2026-10-18 21:11:24 +05:30

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)