Skip to content
42 changes: 18 additions & 24 deletions backend/apps/cms/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,34 @@
# Generated by Django 5.2.5 on 2025-09-16 07:34
# Generated by Django 5.2.5 on 2025-11-24 15:52

from django.db import migrations, models


class Migration(migrations.Migration):

initial = True

dependencies = []
dependencies = [
]

operations = [
migrations.CreateModel(
name="CarouselItem",
name='CarouselItem',
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("carousel", models.CharField(max_length=255)),
("image", models.ImageField(upload_to="carousel")),
("alt_text", models.CharField(blank=True, max_length=255)),
("caption", models.CharField(blank=True, max_length=255)),
("link_url", models.URLField(blank=True)),
("is_active", models.BooleanField(default=True)),
("starts_at", models.DateTimeField(blank=True, null=True)),
("ends_at", models.DateTimeField(blank=True, null=True)),
("sort_order", models.PositiveIntegerField(db_index=True, default=0)),
("created", models.DateTimeField(auto_now_add=True)),
("updated", models.DateTimeField(auto_now=True)),
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('carousel', models.CharField(max_length=255)),
('image', models.ImageField(upload_to='carousel')),
('alt_text', models.CharField(blank=True, max_length=255)),
('caption', models.CharField(blank=True, max_length=255)),
('link_url', models.URLField(blank=True)),
('is_active', models.BooleanField(default=True)),
('starts_at', models.DateTimeField(blank=True, null=True)),
('ends_at', models.DateTimeField(blank=True, null=True)),
('sort_order', models.PositiveIntegerField(db_index=True, default=0)),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
],
options={
"ordering": ["sort_order", "created"],
'ordering': ['sort_order', 'created'],
},
),
]
23 changes: 22 additions & 1 deletion backend/apps/payments/admin.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
from django.contrib import admin
from .models import HDFCTransactionDetails, AcademicSubscription, PayeeHdfcTransaction, AcademicSubscriptionDetail

# Register your models here.
@admin.register(HDFCTransactionDetails)
class HDFCTransactionAdmin(admin.ModelAdmin):
list_display = ('order_id', 'transaction_id', 'order_status', 'amount', 'date_created')
search_fields = ('order_id', 'transaction_id', 'customer_email')


@admin.register(AcademicSubscription)
class AcademicSubscriptionAdmin(admin.ModelAdmin):
list_display = ('user', 'academic_id', 'amount', 'expiry_date')
search_fields = ('user__email', 'academic_id')


@admin.register(PayeeHdfcTransaction)
class PayeeHdfcTransactionAdmin(admin.ModelAdmin):
list_display = ('order_id', 'transaction_id', 'order_status', 'amount')


@admin.register(AcademicSubscriptionDetail)
class AcademicSubscriptionDetailAdmin(admin.ModelAdmin):
list_display = ('subscription', 'start_date', 'end_date', 'is_active', 'created_at')
search_fields = ('subscription__user__email', 'subscription__academic_id')
112 changes: 112 additions & 0 deletions backend/apps/payments/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Generated by Django 5.2.5 on 2025-12-05 17:50

import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

initial = True

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name='AcademicCenter',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('institution_name', models.CharField(max_length=255)),
('academic_code', models.CharField(max_length=50)),
('gst_number', models.CharField(blank=True, max_length=20, null=True)),
],
),
migrations.CreateModel(
name='HDFCTransactionDetails',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('transaction_id', models.CharField(max_length=100)),
('order_id', models.CharField(max_length=50)),
('requestId', models.CharField(blank=True, max_length=100, null=True)),
('amount', models.DecimalField(decimal_places=2, max_digits=10)),
('order_status', models.CharField(blank=True, max_length=50, null=True)),
('udf1', models.TextField(blank=True, null=True)),
('udf2', models.TextField(blank=True, null=True)),
('udf3', models.TextField(blank=True, null=True)),
('udf4', models.TextField(blank=True, null=True)),
('udf5', models.TextField(blank=True, null=True)),
('customer_email', models.EmailField(blank=True, max_length=254, null=True)),
<<<<<<< HEAD
<<<<<<< HEAD
=======
('customer_phone', models.CharField(blank=True, max_length=20, null=True)),
>>>>>>> 70ebc1e (AcademicSubscriptionDetail/payment_callback)
=======
('customer_phone', models.CharField(blank=True, max_length=20, null=True)),
>>>>>>> 70ebc1e (AcademicSubscriptionDetail/payment_callback)
('error_code', models.CharField(blank=True, max_length=50, null=True)),
('error_message', models.TextField(blank=True, null=True)),
('date_created', models.DateTimeField(auto_now_add=True)),
],
),
migrations.CreateModel(
<<<<<<< HEAD
<<<<<<< HEAD
name='PayeeHdfcTransaction',
fields=[
('hdfctransactiondetails_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='payments.hdfctransactiondetails')),
],
bases=('payments.hdfctransactiondetails',),
),
migrations.CreateModel(
=======
>>>>>>> 70ebc1e (AcademicSubscriptionDetail/payment_callback)
=======
>>>>>>> 70ebc1e (AcademicSubscriptionDetail/payment_callback)
name='AcademicSubscription',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('academic_id', models.CharField(blank=True, max_length=100, null=True)),
('phone', models.CharField(max_length=20)),
('amount', models.DecimalField(decimal_places=2, max_digits=10)),
('expiry_date', models.DateField()),
<<<<<<< HEAD
<<<<<<< HEAD
('transaction', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='payments.hdfctransactiondetails')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
=======
=======
>>>>>>> 70ebc1e (AcademicSubscriptionDetail/payment_callback)
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('transaction', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='payments.hdfctransactiondetails')),
],
),
migrations.CreateModel(
name='AcademicSubscriptionDetail',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('start_date', models.DateField(default=django.utils.timezone.now)),
('end_date', models.DateField(blank=True, null=True)),
('is_active', models.BooleanField(default=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('note', models.TextField(blank=True, null=True)),
('subscription', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='history', to='payments.academicsubscription')),
],
),
migrations.CreateModel(
name='PayeeHdfcTransaction',
fields=[
('hdfctransactiondetails_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='payments.hdfctransactiondetails')),
],
bases=('payments.hdfctransactiondetails',),
),
<<<<<<< HEAD
>>>>>>> 70ebc1e (AcademicSubscriptionDetail/payment_callback)
=======
>>>>>>> 70ebc1e (AcademicSubscriptionDetail/payment_callback)
]
69 changes: 68 additions & 1 deletion backend/apps/payments/models.py
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AcademicSubscriptionDetail model & its related logic is missing.

Original file line number Diff line number Diff line change
@@ -1,3 +1,70 @@
from django.db import models
from django.conf import settings
from django.utils import timezone

# Create your models here.
class HDFCTransactionDetails(models.Model):
transaction_id = models.CharField(max_length=100)
order_id = models.CharField(max_length=50)
requestId = models.CharField(max_length=100, null=True, blank=True)
amount = models.DecimalField(max_digits=10, decimal_places=2)
order_status = models.CharField(max_length=50, null=True, blank=True)
udf1 = models.TextField(null=True, blank=True)
udf2 = models.TextField(null=True, blank=True)
udf3 = models.TextField(null=True, blank=True)
udf4 = models.TextField(null=True, blank=True)
udf5 = models.TextField(null=True, blank=True)
customer_email = models.EmailField(null=True, blank=True)
customer_phone = models.CharField(max_length=20, null=True, blank=True)
error_code = models.CharField(max_length=50, null=True, blank=True)
error_message = models.TextField(null=True, blank=True)
date_created = models.DateTimeField(auto_now_add=True)

def __str__(self):
return f"HDFC Transaction {self.order_id}"


class AcademicSubscription(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
academic_id = models.CharField(max_length=100, null=True, blank=True) # Store academic center ID
transaction = models.ForeignKey(HDFCTransactionDetails, on_delete=models.CASCADE)
phone = models.CharField(max_length=20)
amount = models.DecimalField(max_digits=10, decimal_places=2)
expiry_date = models.DateField()

def __str__(self):
# use email if available (user.email may not exist for some custom user models)
try:
user_email = self.user.email
except Exception:
user_email = str(self.user)
return f"Subscription: {user_email} - {self.academic_id}"


class AcademicSubscriptionDetail(models.Model):
"""
History of subscription activations / changes.
Created so reviewers' comment about AcademicSubscriptionDetail is satisfied.
"""
subscription = models.ForeignKey(AcademicSubscription, on_delete=models.CASCADE, related_name='history')
start_date = models.DateField(default=timezone.now)
end_date = models.DateField(null=True, blank=True)
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
note = models.TextField(null=True, blank=True)

def __str__(self):
return f"SubscriptionDetail: {self.subscription} active={self.is_active} from={self.start_date}"


class PayeeHdfcTransaction(HDFCTransactionDetails):
"""Separate model for ILW/Payee-based payments."""
pass


class AcademicCenter(models.Model):
institution_name = models.CharField(max_length=255)
academic_code = models.CharField(max_length=50)
gst_number = models.CharField(max_length=20, null=True, blank=True)

def __str__(self):
return f"{self.institution_name} ({self.academic_code})"
12 changes: 12 additions & 0 deletions backend/apps/payments/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from rest_framework import serializers
from .models import HDFCTransactionDetails, AcademicSubscription

class HDFCTransactionSerializer(serializers.ModelSerializer):
class Meta:
model = HDFCTransactionDetails
fields = '__all__'

class AcademicSubscriptionSerializer(serializers.ModelSerializer):
class Meta:
model = AcademicSubscription
fields = '__all__'
12 changes: 11 additions & 1 deletion backend/apps/payments/tests.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
from django.test import TestCase
#from django.test import TestCase

# Create your tests here.
from rest_framework.test import APITestCase
from django.urls import reverse

class PaymentTests(APITestCase):

def test_create_session(self):
url = reverse('create-academic-session')
data = {"email": "test@example.com", "academic_ids": [1], "amount": 1000}
response = self.client.post(url, data, format='json')
self.assertIn(response.status_code, [200, 400])
11 changes: 11 additions & 0 deletions backend/apps/payments/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from django.urls import path
from . import views

urlpatterns = [
path('academic/session/', views.create_academic_payment_session, name='create-academic-session'),
path('academic/status/<str:order_id>/', views.check_payment_status, name='check-academic-status'),
path('transaction/<str:transaction_id>/', views.get_transaction_details, name='get-transaction-details'),
path('callback-handler/', views.callback_handler, name='callback-handler'),
path('payment/callback/', views.payment_callback, name='payment-callback'),
path('academic/session/', views.create_academic_payment_session, name='create-academic-session'),
]
Empty file.
68 changes: 68 additions & 0 deletions backend/apps/payments/utils/hdfc_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import hashlib
import time
import base64
import hmac
import urllib.parse
import json
import requests as http_requests
from django.conf import settings
from ..models import HDFCTransactionDetails

def generate_hashed_order_id(email):
data = f"{email}{int(time.time())}"
return hashlib.sha256(data.encode()).hexdigest()[:15].upper()

def get_request_headers(email):
encoded_api_key = base64.b64encode(f"{settings.HDFC_API_KEY}:".encode()).decode()
return {
"Authorization": f"Basic {encoded_api_key}",
"Content-Type": "application/json",
"x-merchantid": settings.MERCHANT_ID,
"x-customerid": email
}

def verify_hmac_signature(params):
key = settings.RESPONSE_KEY.encode()
data = params.copy()
signature_algorithm = data.pop('signature_algorithm', None)
signature = data.pop('signature', [None])[0]
if not signature:
return False

encoded_params = {
urllib.parse.quote_plus(str(k)): urllib.parse.quote_plus(str(v))
for k, v in data.items()
}
encoded_string = '&'.join(f"{k}={encoded_params[k]}" for k in sorted(encoded_params))
p_encoded_string = urllib.parse.quote_plus(encoded_string)
dig = hmac.new(key, msg=p_encoded_string.encode(), digestmod=hashlib.sha256).digest()
computed_sign = base64.b64encode(dig).decode()
return computed_sign == signature

def poll_payment_status(order_id, email, sub_amount, model=HDFCTransactionDetails):
url = f"{settings.ORDER_STATUS_URL}{order_id}"
headers = get_request_headers(email)
attempt = 0
while attempt < settings.HDFC_POLL_MAX_RETRIES:
try:
response = http_requests.get(url, headers=headers)
data = response.json()
if response.status_code == 200 and "status" in data:
payment_status = data.get('status', '')
if payment_status == 'CHARGED':
obj = model.objects.get(order_id=order_id)
obj.order_status = 'CHARGED'
obj.save()
return {"status": "CHARGED"}
elif payment_status in ["AUTHENTICATION_FAILED", "AUTHORIZATION_FAILED"]:
return {"status": "FAILED"}
except Exception as e:
return {"status": "ERROR", "message": str(e)}
time.sleep(settings.HDFC_POLL_INTERVAL)
attempt += 1
return {"status": "TIMEOUT"}


def create_hdfc_session(payload, headers):
return http_requests.post(settings.HDFC_API_URL, json=payload, headers=headers)

Loading