diff --git a/backend/apps/cms/migrations/0001_initial.py b/backend/apps/cms/migrations/0001_initial.py index d6aa258..ede0ade 100644 --- a/backend/apps/cms/migrations/0001_initial.py +++ b/backend/apps/cms/migrations/0001_initial.py @@ -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'], }, ), ] diff --git a/backend/apps/payments/admin.py b/backend/apps/payments/admin.py index 8c38f3f..69eb98c 100644 --- a/backend/apps/payments/admin.py +++ b/backend/apps/payments/admin.py @@ -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') diff --git a/backend/apps/payments/migrations/0001_initial.py b/backend/apps/payments/migrations/0001_initial.py new file mode 100644 index 0000000..67c0483 --- /dev/null +++ b/backend/apps/payments/migrations/0001_initial.py @@ -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) + ] diff --git a/backend/apps/payments/models.py b/backend/apps/payments/models.py index 71a8362..7b33094 100644 --- a/backend/apps/payments/models.py +++ b/backend/apps/payments/models.py @@ -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})" diff --git a/backend/apps/payments/serializers.py b/backend/apps/payments/serializers.py new file mode 100644 index 0000000..dd670fb --- /dev/null +++ b/backend/apps/payments/serializers.py @@ -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__' \ No newline at end of file diff --git a/backend/apps/payments/tests.py b/backend/apps/payments/tests.py index 7ce503c..6b52fdb 100644 --- a/backend/apps/payments/tests.py +++ b/backend/apps/payments/tests.py @@ -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]) \ No newline at end of file diff --git a/backend/apps/payments/urls.py b/backend/apps/payments/urls.py new file mode 100644 index 0000000..73f534b --- /dev/null +++ b/backend/apps/payments/urls.py @@ -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//', views.check_payment_status, name='check-academic-status'), + path('transaction//', 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'), +] \ No newline at end of file diff --git a/backend/apps/payments/utils/__init__.py b/backend/apps/payments/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/apps/payments/utils/hdfc_utils.py b/backend/apps/payments/utils/hdfc_utils.py new file mode 100644 index 0000000..f4e339d --- /dev/null +++ b/backend/apps/payments/utils/hdfc_utils.py @@ -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) + diff --git a/backend/apps/payments/views.py b/backend/apps/payments/views.py index 91ea44a..b90daf0 100644 --- a/backend/apps/payments/views.py +++ b/backend/apps/payments/views.py @@ -1,3 +1,279 @@ -from django.shortcuts import render +from rest_framework.decorators import api_view, permission_classes +from rest_framework.response import Response +from rest_framework import status +from django.conf import settings +from django.http import JsonResponse +from .models import AcademicSubscription, HDFCTransactionDetails, PayeeHdfcTransaction, AcademicCenter, AcademicSubscriptionDetail +from .serializers import HDFCTransactionSerializer +from .utils.hdfc_utils import generate_hashed_order_id, get_request_headers, poll_payment_status +from decimal import Decimal +import requests +from django.shortcuts import redirect +from django.utils import timezone +from django.contrib.auth import get_user_model -# Create your views here. +User = get_user_model() + +@api_view(['POST']) +def create_academic_payment_session(request): + """Creates payment session for academic subscription""" + data = request.data + email = data.get('email') + academic_ids = data.get('academic_ids', []) + amount = data.get('amount') + gst_json = data.get('gst_json', '') # GST data as JSON string + + payload = { + "order_id": generate_hashed_order_id(email), + "amount": str(amount), + "customer_id": email, + "customer_email": email, + "customer_phone": data.get("phone"), + "payment_page_client_id": "your_client_id_here", + "action": "paymentPage", + "return_url": request.build_absolute_uri("/api/payments/callback-handler/"), + "description": "Complete Academic Subscription Payment...", + "udf3": data.get('name'), + "udf4": data.get('state'), + "udf5": gst_json, # GST data for HDFC + } + + #AcademicCenter lookup removed - module 'events' not available + (AcademicSubscriptionDetail/payment_callback) + values = AcademicCenter.objects.filter(id__in=academic_ids).values('institution_name', 'academic_code') + payload["udf1"] = ' ** '.join([v['institution_name'] for v in values])[:90] + payload["udf2"] = ' ** '.join([v['academic_code'] for v in values]) + + headers = get_request_headers(email) + try: + response = requests.post(settings.HDFC_API_URL, json=payload, headers=headers) + response_data = response.json() + if response.status_code == 200: + transaction_id = response_data.get("id") + request_id = response_data.get("requestId") + + transaction = HDFCTransactionDetails.objects.create( + transaction_id=transaction_id, + order_id=response_data.get("order_id"), + requestId=request_id, + amount=amount, + customer_email=email, + customer_phone=data.get("phone"), + udf3=data.get('name'), + udf4=data.get('state'), + udf5=gst_json, # Store GST data in database + ) + + return Response({ + "payment_link": response_data.get("payment_links", {}).get("web"), + "transaction_id": transaction_id + }) + else: + return Response(response_data, status=status.HTTP_400_BAD_REQUEST) + except Exception as e: + return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +@api_view(['GET']) +def check_payment_status(request, order_id): + """Polls the payment gateway for order status""" + email = request.query_params.get("email") + amount = request.query_params.get("amount") + result = poll_payment_status(order_id, email, Decimal(amount)) + return Response(result) + + +@api_view(['GET']) +def get_transaction_details(request, transaction_id): + """Fetch transaction details by transaction_id""" + try: + transaction = HDFCTransactionDetails.objects.get(transaction_id=transaction_id) + serializer = HDFCTransactionSerializer(transaction) + return Response(serializer.data) + except HDFCTransactionDetails.DoesNotExist: + return Response( + {"error": "Transaction not found"}, + status=status.HTTP_404_NOT_FOUND + ) + except Exception as e: + return Response( + {"error": str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + +@api_view(['POST']) +def payment_callback(request): + """ + Endpoint called by HDFC (or client) to post payment result. + - Verifies HMAC/signature when present + - Updates HDFCTransactionDetails record + - On success (CHARGED), attempt to create AcademicSubscription & AcademicSubscriptionDetail + """ + raw = request.data or {} + # Prepare params in the form expected by verify_hmac_signature: + # the util expects values as lists (signature access uses [0]), so convert single values -> list + params_for_verify = {k: (v if isinstance(v, list) else [v]) for k, v in raw.items()} + + # signature verification (if signature present in payload). If verification fails, we still log but mark transaction. + signature_ok = True + try: + if 'signature' in params_for_verify: + signature_ok = verify_hmac_signature(params_for_verify) + except Exception: + signature_ok = False + + # Extract identifying fields - HDFC may send transaction id, order id or requestId + transaction_id = raw.get('transaction_id') or raw.get('id') or raw.get('txnId') or raw.get('txn_id') or raw.get('transactionId') + order_id = raw.get('order_id') or raw.get('orderId') or raw.get('orderid') + request_id = raw.get('requestId') or raw.get('request_id') + + amount = raw.get('amount') or raw.get('txn_amount') or raw.get('amount_paid') or raw.get('txAmount') + status_field = raw.get('status') or raw.get('order_status') or raw.get('orderStatus') or raw.get('statusCode') + + # Try to find existing transaction record by order_id or transaction_id or requestId + transaction = None + try: + if order_id: + transaction = HDFCTransactionDetails.objects.filter(order_id=order_id).first() + if not transaction and transaction_id: + transaction = HDFCTransactionDetails.objects.filter(transaction_id=transaction_id).first() + if not transaction and request_id: + transaction = HDFCTransactionDetails.objects.filter(requestId=request_id).first() + except Exception as e: + # unexpected DB issue; return 500 + return Response({"error": "db_error", "message": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # If no transaction found, create a minimal record (so we have a DB row to inspect) + if not transaction: + try: + transaction = HDFCTransactionDetails.objects.create( + transaction_id=transaction_id or generate_hashed_order_id("unknown"), + order_id=order_id or f"ORDER_{timezone.now().timestamp()}", + requestId=request_id, + amount=Decimal(amount) if amount else Decimal("0.00"), + order_status=status_field or ("UNKNOWN" if not signature_ok else "PENDING"), + customer_email=raw.get('customer_email') or raw.get('email'), + customer_phone=raw.get('customer_phone') or raw.get('phone'), + udf1=raw.get('udf1'), + udf2=raw.get('udf2'), + udf3=raw.get('udf3'), + udf4=raw.get('udf4'), + udf5=raw.get('udf5'), + error_message=None if signature_ok else "signature_verification_failed", + ) + except Exception as e: + return Response({"error": "create_transaction_failed", "message": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # Update transaction details from payload where present + try: + if amount: + # safe cast + try: + transaction.amount = Decimal(str(amount)) + except Exception: + pass + if status_field: + transaction.order_status = status_field + if raw.get('error_code'): + transaction.error_code = raw.get('error_code') + if raw.get('error_message'): + transaction.error_message = raw.get('error_message') + # update customer fields + transaction.customer_email = raw.get('customer_email') or raw.get('email') or transaction.customer_email + transaction.customer_phone = raw.get('customer_phone') or raw.get('phone') or transaction.customer_phone + # store requestId if provided + if request_id: + transaction.requestId = request_id + transaction.save() + except Exception as e: + return Response({"error": "update_failed", "message": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # If payment successful, attempt to create subscription record(s) + # Common HDFC success state name used in your utils: 'CHARGED' + success_states = {"CHARGED", "SUCCESS", "COMPLETED", "OK"} + if str(transaction.order_status).upper() in success_states: + # parse academic id(s) from udf2 or udf1 or custom field. Adjust as per your payload contract + academic_id = None + # udf2 in create_academic_payment_session was academic_code joined + if transaction.udf2: + academic_id = transaction.udf2.split(' ** ')[0] if isinstance(transaction.udf2, str) else transaction.udf2 + + user = None + email = transaction.customer_email + if email: + try: + user = User.objects.filter(email__iexact=email).first() + except Exception: + user = None + + # Only create a subscription if we can find a user (db integrity requires user) + if user: + try: + # expiry: default 1 year from now if not provided in any payload + expiry_date = None + if raw.get('expiry_date'): + expiry_date = raw.get('expiry_date') + else: + expiry_date = (timezone.now().date().replace(day=1) + timezone.timedelta(days=365)) + + # Create or update subscription; simplistic approach: always create a new subscription row + subscription = AcademicSubscription.objects.create( + user=user, + academic_id=academic_id, + transaction=transaction, + phone=transaction.customer_phone or "", + amount=transaction.amount or Decimal('0.00'), + expiry_date=expiry_date + ) + + # Add a subscription detail / history record + AcademicSubscriptionDetail.objects.create( + subscription=subscription, + start_date=timezone.now().date(), + end_date=subscription.expiry_date, + is_active=True, + note=f"Created from HDFC callback. txn={transaction.transaction_id}" + ) + except Exception as e: + # don't fail the whole callback if subscription creation fails; log the error in transaction + transaction.error_message = (transaction.error_message or "") + f" | subscription_creation_failed: {str(e)}" + transaction.save() + + # Successful receipt acknowledgement for gateway + return Response({"status": "received", "verified": signature_ok}) + + +@api_view(['GET', 'POST']) +def callback_handler(request): + """Handle HDFC callback and redirect to frontend payment status page""" + # Try to get transaction ID from various sources + transaction_id = ( + request.GET.get('id') or + request.GET.get('transaction_id') or + request.GET.get('txnId') or + request.GET.get('requestId') + ) + + # Also try order_id which HDFC might send + order_id = ( + request.GET.get('order_id') or + request.GET.get('orderId') or + request.POST.get('order_id') or + request.POST.get('orderId') + ) + + # If we have order_id, look up the transaction + if order_id and not transaction_id: + try: + transaction = HDFCTransactionDetails.objects.get(order_id=order_id) + transaction_id = transaction.transaction_id + except HDFCTransactionDetails.DoesNotExist: + pass + + if not transaction_id: + # Fallback: redirect to subscription page with error + return redirect("http://localhost:5173/subscription?error=payment_callback_missing_id") + + # Redirect to frontend payment status page + frontend_url = f"http://localhost:5173/payment-status/{transaction_id}" + return redirect(frontend_url) diff --git a/backend/apps/spoken/migrations/0001_initial.py b/backend/apps/spoken/migrations/0001_initial.py new file mode 100644 index 0000000..ac3a167 --- /dev/null +++ b/backend/apps/spoken/migrations/0001_initial.py @@ -0,0 +1,143 @@ +# Generated by Django 5.2.5 on 2025-11-20 07:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='CreationDomain', + fields=[ + ('id', models.IntegerField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=255, unique=True)), + ('created', models.DateTimeField()), + ('updated', models.DateTimeField()), + ('show_on_homepage', models.IntegerField()), + ('icon', models.ImageField(blank=True, null=True, upload_to='domain_icons/')), + ('description', models.TextField()), + ('is_active', models.BooleanField()), + ], + options={ + 'db_table': 'creation_domain', + 'managed': False, + }, + ), + migrations.CreateModel( + name='CreationFosscategory', + fields=[ + ('id', models.IntegerField(primary_key=True, serialize=False)), + ('foss', models.CharField(max_length=255, unique=True)), + ('description', models.TextField()), + ('status', models.IntegerField()), + ('user_id', models.IntegerField(db_column='user_id')), + ('created', models.DateTimeField()), + ('updated', models.DateTimeField()), + ('is_learners_allowed', models.IntegerField()), + ('show_on_homepage', models.PositiveSmallIntegerField()), + ('is_translation_allowed', models.IntegerField()), + ('available_for_nasscom', models.IntegerField()), + ('available_for_jio', models.IntegerField()), + ('csc_dca_programme', models.IntegerField()), + ('credits', models.PositiveSmallIntegerField()), + ('is_fossee', models.IntegerField()), + ('icon', models.ImageField(blank=True, null=True, upload_to='foss_icons/')), + ], + options={ + 'db_table': 'creation_fosscategory', + 'managed': False, + }, + ), + migrations.CreateModel( + name='CreationFosscategoryDomain', + fields=[ + ('id', models.IntegerField(primary_key=True, serialize=False)), + ('is_primary', models.IntegerField()), + ], + options={ + 'db_table': 'creation_fosscategorydomain', + 'managed': False, + }, + ), + migrations.CreateModel( + name='CreationLanguage', + fields=[ + ('id', models.IntegerField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=255, unique=True)), + ('code', models.CharField(max_length=10)), + ('user', models.IntegerField()), + ('created', models.DateTimeField()), + ('updated', models.DateTimeField()), + ], + options={ + 'db_table': 'creation_language', + 'managed': False, + }, + ), + migrations.CreateModel( + name='CreationLevel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('level', models.CharField(max_length=255)), + ('code', models.CharField(max_length=10)), + ], + options={ + 'db_table': 'creation_level', + 'managed': False, + }, + ), + migrations.CreateModel( + name='CreationTutorialdetail', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('tutorial', models.CharField(max_length=255)), + ('order', models.IntegerField()), + ('user_id', models.IntegerField(db_column='user_id')), + ('created', models.DateTimeField()), + ('updated', models.DateTimeField()), + ], + options={ + 'db_table': 'creation_tutorialdetail', + 'managed': False, + }, + ), + migrations.CreateModel( + name='CreationTutorialresource', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('common_content_id', models.IntegerField(db_column='common_content_id')), + ('outline', models.TextField()), + ('outline_user_id', models.IntegerField(db_column='outline_user_id')), + ('outline_status', models.PositiveSmallIntegerField()), + ('script', models.CharField(max_length=255)), + ('script_user_id', models.IntegerField(db_column='script_user_id')), + ('script_status', models.PositiveSmallIntegerField()), + ('timed_script', models.CharField(max_length=255)), + ('video', models.CharField(max_length=255)), + ('video_id', models.CharField(blank=True, max_length=255, null=True)), + ('playlist_item_id', models.CharField(blank=True, max_length=255, null=True)), + ('video_thumbnail_time', models.TimeField()), + ('video_user_id', models.IntegerField(db_column='video_user_id')), + ('video_status', models.PositiveSmallIntegerField()), + ('status', models.PositiveSmallIntegerField()), + ('version', models.PositiveSmallIntegerField()), + ('hit_count', models.PositiveIntegerField()), + ('created', models.DateTimeField()), + ('updated', models.DateTimeField()), + ('publish_at', models.DateTimeField(blank=True, null=True)), + ('assignment_status', models.PositiveSmallIntegerField()), + ('extension_status', models.PositiveIntegerField()), + ('submissiondate', models.DateTimeField()), + ('is_unrestricted', models.IntegerField()), + ], + options={ + 'db_table': 'creation_tutorialresource', + 'managed': False, + }, + ), + ] diff --git a/backend/config/settings.py b/backend/config/settings.py index 3b4adb1..2e479a6 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -9,7 +9,7 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/5.2/ref/settings/ """ - +import os from pathlib import Path # Build paths inside the project like this: BASE_DIR / 'subdir'. diff --git a/backend/config/settings/base.py b/backend/config/settings/base.py index f6610e3..4bb0619 100644 --- a/backend/config/settings/base.py +++ b/backend/config/settings/base.py @@ -30,6 +30,7 @@ "apps.spoken", "apps.cms", "apps.users", + "apps.payments", ] MIDDLEWARE = [ @@ -151,4 +152,15 @@ # 'django.contrib.auth.hashers.SHA1PasswordHasher', 'django.contrib.auth.hashers.MD5PasswordHasher', # 'django.contrib.auth.hashers.CryptPasswordHasher', -] \ No newline at end of file +] + + +# HDFC settings +MERCHANT_ID = os.getenv("MERCHANT_ID") +CLIENT_ID = os.getenv("CLIENT_ID") +RESPONSE_KEY = os.getenv("RESPONSE_KEY") +HDFC_API_URL = os.getenv("HDFC_API_URL") +HDFC_API_KEY = os.getenv("HDFC_API_KEY") +ORDER_STATUS_URL = os.getenv("ORDER_STATUS_URL") +HDFC_POLL_MAX_RETRIES = int(os.getenv("HDFC_POLL_MAX_RETRIES", "5")) +HDFC_POLL_INTERVAL = int(os.getenv("HDFC_POLL_INTERVAL", "2")) \ No newline at end of file diff --git a/backend/config/urls.py b/backend/config/urls.py index 662e06c..75fe68b 100644 --- a/backend/config/urls.py +++ b/backend/config/urls.py @@ -24,6 +24,7 @@ path("api/auth/", include("apps.users.urls")), path("api/", include("apps.core.urls")), path("api/", include("apps.spoken.urls")), + path("api/payments/", include("apps.payments.urls")), ] diff --git a/backend/python_script.py b/backend/python_script.py new file mode 100644 index 0000000..bf0e753 --- /dev/null +++ b/backend/python_script.py @@ -0,0 +1,95 @@ +import requests +import csv +from datetime import datetime +import time + +INPUT_FILE = "foss-lang.csv" +OUTPUT_FILE = "request_results.csv" + +# Limit number of output rows (you can change this!) +counter = 50 + +# Spoken Tutorial search URL +BASE_URL = "https://beta.spoken-tutorial.org/tutorial-search/" + + +def make_request(url, params): + timestamp = datetime.now().isoformat() + try: + res = requests.get(url, params=params, timeout=10) + elapsed = res.elapsed.total_seconds() + status = res.status_code + return elapsed, status, timestamp + except: + return None, None, timestamp + + +def main(): + with open(INPUT_FILE, "r") as infile, open(OUTPUT_FILE, "w", newline="") as outfile: + reader = csv.DictReader(infile) + writer = csv.writer(outfile) + + # CSV header + writer.writerow([ + "foss", + "language", + "url", + "elapsed1", + "elapsed2", + "status_code", + "timestamp" + ]) + + count = 0 + + for row in reader: + + if count >= counter: + break + + foss = row["foss"].strip() + language = row["language"].strip() + + # Replace spaces with "+" for URL + foss_url = foss.replace(" ", "+") + + # Build URL correctly + final_url = ( + f"{BASE_URL}?search_foss={foss_url}" + f"&search_language={language}" + ) + + params = { + "search_foss": foss, + "search_language": language + } + + # 1st request + elapsed1, status1, ts1 = make_request(BASE_URL, params) + + time.sleep(1) + + # 2nd request + elapsed2, status2, ts2 = make_request(BASE_URL, params) + + final_status = status2 if status2 is not None else status1 + final_timestamp = ts2 + + writer.writerow([ + foss, + language, + final_url, + elapsed1, + elapsed2, + final_status, + final_timestamp + ]) + + print(f"Done → {foss}-{language}: {elapsed1}s, {elapsed2}s") + count += 1 + + print("\nCSV created:", OUTPUT_FILE) + + +if __name__ == "__main__": + main() diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7c8e0be..8bd3add 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,18 +1,14 @@ import { Routes, Route } from "react-router-dom"; +import ResponsiveAppBar from "./components/homepage/ResponsiveAppBar"; import MegaMenu from "./components/homepage/AppBar"; // import FeatureTiles from "./components/homepage/HomeComponents"; import HomePage from "./pages/home/Homepage"; import DomainPage from "./pages/public/DomainsPage"; import CoursePage from "./pages/public/CoursePage"; import TutorialSearch from "./pages/public/TutorialSearch"; +import PaymentStatus from "./pages/public/PaymentStatus"; import SubscriptionPage from "./pages/public/SubscriptionPage"; -import LoginPage from "./features/auth/pages/LoginPage"; -import DashboardLayout from "./features/dashboard/pages/DashboardLayout"; -import PublicLayout from "./pages/public/PublicLayout"; -import Dashboard from "./features/dashboard/pages/Dashboard"; -import TrainingPlanner from "./features/training/pages/TrainingPlanner"; -import TrainingAttendance from "./features/training/pages/TrainingAttendance"; export default function App(){ @@ -20,28 +16,16 @@ export default function App(){ return ( <> {/* */} - {/* */} + {/* */} - + {/* Define the routes */} - {/* Public routes */} - }> - } /> - } /> - } /> - } /> - } /> - } /> - - - {/* Dashboard routes */} - }> - } /> - } /> - } /> - {/* } /> */} - - + } /> + } /> + } /> + } /> + } /> + } /> {/* catch-all for 404 */} Page Not Found} /> diff --git a/frontend/src/pages/public/PaymentStatus.tsx b/frontend/src/pages/public/PaymentStatus.tsx new file mode 100644 index 0000000..a2613bf --- /dev/null +++ b/frontend/src/pages/public/PaymentStatus.tsx @@ -0,0 +1,414 @@ +import { Box, Container, Card, CardHeader, CardContent, Typography, List, ListItem, ListItemText, Stack, Button, CircularProgress } from "@mui/material"; +import { useState, useEffect } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import CheckCircleIcon from "@mui/icons-material/CheckCircle"; +import CancelIcon from "@mui/icons-material/Cancel"; +import AccessTimeIcon from "@mui/icons-material/AccessTime"; +import WarningIcon from "@mui/icons-material/Warning"; +import { BRAND } from "../../theme"; + +interface PaymentData { + transaction_id: string; + order_id: string; + udf3: string; + customer_email: string; + customer_phone: string; + amount: string; + udf1: string; + udf2: string; + udf4: string; + udf5: string; + date_created: string; + order_status: string; +} + +interface GSTInfo { + institute_id: string; + want_gst: string; + gst_number: string; + gst_name: string; +} + +type PaymentStatus = "CHARGED" | "FAILED" | "PENDING" | "TIMEOUT" | "ERROR"; + +export default function PaymentStatus() { + const navigate = useNavigate(); + const { transactionId } = useParams<{ transactionId: string }>(); + const [status, setStatus] = useState("PENDING"); + const [paymentData, setPaymentData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + if (!transactionId) { + setError("No transaction ID provided"); + setLoading(false); + return; + } + + const fetchTransactionDetails = async () => { + try { + setLoading(true); + const response = await fetch( + `http://localhost:8000/api/payments/transaction/${transactionId}/` + ); + + if (!response.ok) { + throw new Error("Failed to fetch transaction details"); + } + + const data = await response.json(); + setPaymentData(data); + setError(null); // Clear error when data loads successfully + + // Determine status based on order_status + if (data.order_status === "CHARGED") { + setStatus("CHARGED"); + } else if (data.order_status === "FAILED" || data.order_status === "NO_TRANSACTION") { + setStatus("FAILED"); + } else if (data.order_status === "PENDING" || data.order_status === "PENDING_VBV" || data.order_status === "AUTHORIZING") { + setStatus("PENDING"); + } else if (!data.order_status || data.order_status === "" || data.order_status === null) { + // If no status yet, assume CHARGED (transaction was created successfully) + setStatus("CHARGED"); + } else { + setStatus("PENDING"); // Default to pending for unknown statuses + } + } catch (err) { + console.error("Error fetching transaction:", err); + setError(err instanceof Error ? err.message : "An error occurred"); + setStatus("ERROR"); + } finally { + setLoading(false); + } + }; + + fetchTransactionDetails(); + }, [transactionId]); + + const parseGSTData = (): GSTInfo[] => { + if (!paymentData?.udf5) return []; + try { + return JSON.parse(paymentData.udf5); + } catch (error) { + console.error("Error parsing GST data:", error); + return []; + } + }; + + return ( + + + {loading && ( + + + + )} + + {error && !loading && ( + + {error} + + )} + + {!loading && paymentData && ( + + + + Transaction Details + + + } + sx={{ + backgroundColor: BRAND.lightBgHighlight, + borderBottom: `1px solid ${BRAND.borderColor}`, + "& .MuiCardHeader-title": { + display: "flex", + alignItems: "center", + gap: 1, + }, + }} + /> + + + {/* Status Message */} + + {status === "CHARGED" && ( + + + + The payment was successful! + + + )} + + {status === "FAILED" && ( + + + + Payment failed. Please contact Training Manager. + + + )} + + {status === "PENDING" && ( + + + + Your transaction is being processed. Please do not close this window while we retrieve the latest details. + + + )} + + {status === "TIMEOUT" && ( + + + + We are still processing your payment. Don't worry! You will receive an email confirmation once the transaction is complete. + + + )} + + {status === "ERROR" && ( + + + + Error checking payment status. Please contact Training Manager. + + + )} + + + {/* Payment Details */} + + + + + + + + + + + + + + + + + + + + {paymentData.udf1 && ( + + + + )} + {paymentData.udf2 && ( + + + + )} + + + + + + + {paymentData.udf5 && ( + <> + + + GST Invoice Information + + + {parseGSTData().map((gstItem, index) => ( + + + Institute {index + 1} + + + + + {gstItem.want_gst === 'yes' && ( + <> + + + + + + + + )} + + ))} + + )} + + + {/* Action Buttons */} + + {status === "FAILED" && ( + + )} + {status === "CHARGED" && ( + + )} + + + + )} + + + ); +} diff --git a/frontend/src/pages/public/SubscriptionPage.tsx b/frontend/src/pages/public/SubscriptionPage.tsx index 6042a63..67ac376 100644 --- a/frontend/src/pages/public/SubscriptionPage.tsx +++ b/frontend/src/pages/public/SubscriptionPage.tsx @@ -30,7 +30,6 @@ import PaymentIcon from '@mui/icons-material/Payment'; import PersonIcon from '@mui/icons-material/Person'; import EmailIcon from '@mui/icons-material/Email'; import PhoneIcon from '@mui/icons-material/Phone'; -import LocationOnIcon from '@mui/icons-material/LocationOn'; import BuildingIcon from '@mui/icons-material/Business'; import CurrencyRupeeIcon from '@mui/icons-material/CurrencyRupee'; import HistoryIcon from '@mui/icons-material/History'; @@ -39,6 +38,7 @@ interface AcademicCenter { id: number; academic_code: string; institution_name: string; + amount?: number; } interface GSTFieldData { @@ -58,6 +58,67 @@ interface Transaction { order_status: string; } +// Dummy data for states +const DUMMY_STATES = [ + { id: 1, name: 'Andhra Pradesh' }, + { id: 2, name: 'Arunachal Pradesh' }, + { id: 3, name: 'Assam' }, + { id: 4, name: 'Bihar' }, + { id: 5, name: 'Chhattisgarh' }, + { id: 6, name: 'Goa' }, + { id: 7, name: 'Gujarat' }, + { id: 8, name: 'Haryana' }, + { id: 9, name: 'Himachal Pradesh' }, + { id: 10, name: 'Jharkhand' }, + { id: 11, name: 'Karnataka' }, + { id: 12, name: 'Kerala' }, + { id: 13, name: 'Madhya Pradesh' }, + { id: 14, name: 'Maharashtra' }, + { id: 15, name: 'Manipur' }, + { id: 16, name: 'Meghalaya' }, + { id: 17, name: 'Mizoram' }, + { id: 18, name: 'Nagaland' }, + { id: 19, name: 'Odisha' }, + { id: 20, name: 'Punjab' }, + { id: 21, name: 'Rajasthan' }, + { id: 22, name: 'Sikkim' }, + { id: 23, name: 'Tamil Nadu' }, + { id: 24, name: 'Telangana' }, + { id: 25, name: 'Tripura' }, + { id: 26, name: 'Uttar Pradesh' }, + { id: 27, name: 'Uttarakhand' }, + { id: 28, name: 'West Bengal' }, +]; + +// Dummy data for academic centers per state with pre-populated amounts (15-20k range) +const DUMMY_ACADEMIC_CENTERS: { [key: string]: AcademicCenter[] } = { + '1': [ + { id: 101, academic_code: 'AP001', institution_name: 'Andhra University, Visakhapatnam', amount: 15000 }, + { id: 102, academic_code: 'AP002', institution_name: 'Sri Venkateswara University, Tirupati', amount: 16500 }, + { id: 103, academic_code: 'AP003', institution_name: 'Osmania University, Hyderabad', amount: 17500 }, + ], + '2': [ + { id: 201, academic_code: 'AR001', institution_name: 'North Eastern University, Itanagar', amount: 15500 }, + { id: 202, academic_code: 'AR002', institution_name: 'Delhi Skill University, Arunachal Campus', amount: 18000 }, + ], + '3': [ + { id: 301, academic_code: 'AS001', institution_name: 'Gauhati University, Guwahati', amount: 16000 }, + { id: 302, academic_code: 'AS002', institution_name: 'Dibrugarh University, Dibrugarh', amount: 17000 }, + { id: 303, academic_code: 'AS003', institution_name: 'Indian Institute of Technology Guwahati', amount: 19500 }, + ], + '14': [ + { id: 1401, academic_code: 'MH001', institution_name: 'University of Mumbai, Mumbai', amount: 18500 }, + { id: 1402, academic_code: 'MH002', institution_name: 'Indian Institute of Technology Bombay', amount: 20000 }, + { id: 1403, academic_code: 'MH003', institution_name: 'Pune University, Pune', amount: 17000 }, + { id: 1404, academic_code: 'MH004', institution_name: 'NMIMS University, Mumbai', amount: 19000 }, + ], + '26': [ + { id: 2601, academic_code: 'UP001', institution_name: 'University of Lucknow, Lucknow', amount: 15500 }, + { id: 2602, academic_code: 'UP002', institution_name: 'Indian Institute of Technology BHU, Varanasi', amount: 19500 }, + { id: 2603, academic_code: 'UP003', institution_name: 'Aligarh Muslim University, Aligarh', amount: 16500 }, + ], +}; + const SubscriptionPage: React.FC = () => { const theme = useTheme(); const [formData, setFormData] = useState({ @@ -69,22 +130,14 @@ const SubscriptionPage: React.FC = () => { const [selectedInstitutes, setSelectedInstitutes] = useState([]); const [academicCenters, setAcademicCenters] = useState([]); - const [amount, setAmount] = useState(0); + const [amount, setAmount] = useState(''); const [gstFields, setGSTFields] = useState({}); const [errors, setErrors] = useState<{ [key: string]: string }>({}); const [loadingCenters, setLoadingCenters] = useState(false); - const [subscriptionAmount] = useState(5000); // Example amount const [userTransactions] = useState([]); - const [isAuthenticated] = useState(false); // This should come from your auth context + const [isAuthenticated] = useState(false); const [submitLoading, setSubmitLoading] = useState(false); - const states = [ - { id: 1, name: 'Andhra Pradesh' }, - { id: 2, name: 'Arunachal Pradesh' }, - { id: 3, name: 'Assam' }, - // Add more states as needed - ]; - useEffect(() => { if (formData.state) { fetchAcademicCenters(formData.state); @@ -94,10 +147,9 @@ const SubscriptionPage: React.FC = () => { const fetchAcademicCenters = async (stateId: string) => { setLoadingCenters(true); try { - // Replace with your actual API endpoint - const response = await fetch(`/api/academic-centers/?stateId=${stateId}`); - const data = await response.json(); - setAcademicCenters(data || []); + const dummyData = DUMMY_ACADEMIC_CENTERS[stateId] || []; + await new Promise(resolve => setTimeout(resolve, 300)); + setAcademicCenters(dummyData); } catch (error) { console.error('Error fetching academic centers:', error); setAcademicCenters([]); @@ -107,7 +159,13 @@ const SubscriptionPage: React.FC = () => { const handleFormChange = (e: React.ChangeEvent) => { const { name, value } = e.target as HTMLInputElement; - setFormData((prev) => ({ ...prev, [name]: value })); + + if (name === 'amount') { + setAmount(value); + } else { + setFormData((prev) => ({ ...prev, [name]: value })); + } + if (errors[name]) { setErrors((prev) => ({ ...prev, [name]: '' })); } @@ -116,9 +174,20 @@ const SubscriptionPage: React.FC = () => { const handleInstituteChange = (e: React.ChangeEvent<{ name?: string; value: unknown }>) => { const newSelected = e.target.value as number[]; setSelectedInstitutes(newSelected); - setAmount(newSelected.length * subscriptionAmount); - // Initialize GST fields for each selected institute + // Calculate total amount from selected institutes + let totalAmount = 0; + newSelected.forEach((id) => { + const institute = academicCenters.find((c) => c.id === id); + if (institute && institute.amount) { + totalAmount += institute.amount; + } + }); + + // Auto-populate amount field + setAmount(totalAmount > 0 ? totalAmount.toString() : ''); + + // Initialize GST fields for selected institutes const newGSTFields: GSTFieldData = {}; newSelected.forEach((id) => { if (!gstFields[id]) { @@ -169,7 +238,10 @@ const SubscriptionPage: React.FC = () => { newErrors.institute = 'Please select at least one institute'; } - // Validate GST fields + if (!amount || parseFloat(amount) <= 0) { + newErrors.amount = 'Payment amount must be greater than 0'; + } + selectedInstitutes.forEach((id) => { const gst = gstFields[id]; if (gst && gst.wantGST === 'yes') { @@ -195,19 +267,28 @@ const SubscriptionPage: React.FC = () => { setSubmitLoading(true); try { - // Prepare payment data + const stateName = DUMMY_STATES.find(s => s.id === parseInt(formData.state))?.name || formData.state; + + // Prepare GST data as JSON string for udf5 + const gstDataForPayment = Object.entries(gstFields).map(([instituteId, gstInfo]) => ({ + institute_id: instituteId, + want_gst: gstInfo.wantGST, + gst_number: gstInfo.gstNumber, + gst_name: gstInfo.gstName, + })); + const paymentData = { name: formData.name, email: formData.email, phone: formData.phone, - state: formData.state, - institutes: selectedInstitutes, - amount, + state: stateName, + academic_ids: selectedInstitutes, + amount: parseFloat(amount), gst_data: gstFields, + gst_json: JSON.stringify(gstDataForPayment), // For HDFC API udf5 field }; - // Send to your payment API - const response = await fetch('/api/subscription/create-payment/', { + const response = await fetch('http://localhost:8000/api/payments/academic/session/', { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -215,17 +296,21 @@ const SubscriptionPage: React.FC = () => { body: JSON.stringify(paymentData), }); + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || 'Failed to create payment session'); + } + const data = await response.json(); - if (data.success) { - // Handle successful payment initiation - alert('Payment initiated successfully'); - // Redirect to payment gateway or handle response + + if (data.payment_link) { + window.location.href = data.payment_link; } else { - alert('Error: ' + (data.error || 'Unknown error')); + alert('Payment link not received. Please try again.'); } } catch (error) { console.error('Error submitting form:', error); - alert('An error occurred. Please try again.'); + alert(`Error: ${error instanceof Error ? error.message : 'An error occurred. Please try again.'}`); } setSubmitLoading(false); }; @@ -317,10 +402,9 @@ const SubscriptionPage: React.FC = () => { value={formData.state} onChange={handleFormChange as any} label="State" - startAdornment={} > -- Select State -- - {states.map((state) => ( + {DUMMY_STATES.map((state) => ( {state.name} @@ -366,10 +450,16 @@ const SubscriptionPage: React.FC = () => { , }}