diff --git a/.vscode/settings.json b/.vscode/settings.json index c659b65..adb3c07 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "files.associations": { - "*.html": "jinja-html" - } + "*.html": "django-html" + }, + "python.languageServer": "None" } diff --git a/src/donations/migrations/0002_alter_donationtier_visible.py b/src/donations/migrations/0002_alter_donationtier_visible.py new file mode 100644 index 0000000..81a3aa2 --- /dev/null +++ b/src/donations/migrations/0002_alter_donationtier_visible.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.1 on 2024-12-27 19:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("donations", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="donationtier", + name="visible", + field=models.BooleanField( + default=False, verbose_name="Show on Donations Page ?" + ), + ), + ] diff --git a/src/donations/models.py b/src/donations/models.py index 3353112..b57ada8 100644 --- a/src/donations/models.py +++ b/src/donations/models.py @@ -21,7 +21,7 @@ class DonationTier(models.Model): name = models.CharField(max_length=256) description = models.CharField(max_length=1024) amount = models.PositiveIntegerField(validators=[MaxValueValidator(10000)]) - visible = models.BooleanField(default=False) + visible = models.BooleanField(verbose_name="Show on Donations Page ?", default=False) def __str__(self): return self.name diff --git a/src/donations/views.py b/src/donations/views.py index 061eafa..6c5325f 100644 --- a/src/donations/views.py +++ b/src/donations/views.py @@ -4,7 +4,7 @@ from .forms import DonationForm from .models import DonationConfig, DonationTier, DonationReceived from temple_web.myconfig import PaymentGatewayConfig -from .upi_gateway import create_order, check_order_status +from utils.payment.upi_gateway import create_order, check_order_status from uuid import uuid4 from datetime import date from utils.adirect import adirect @@ -20,7 +20,7 @@ def donations(request): donation_config = DonationConfig.get_solo() - donation_tiers = DonationTier.objects.all() + donation_tiers = DonationTier.objects.filter(visible=True) context = {"donation_config": donation_config, "donation_tiers": donation_tiers} return render(request, "donations/donations.html", context=context) diff --git a/src/haps/admin.py b/src/haps/admin.py index 70dc045..55c471b 100644 --- a/src/haps/admin.py +++ b/src/haps/admin.py @@ -1,5 +1,11 @@ from django.contrib import admin -from .models import EventRegistration, Event +from .models import EventRegistration, Event, EventFormField + + +class EventFormFieldInline(admin.TabularInline): + model = EventFormField + extra = 1 + ordering = ['order'] @admin.register(Event) @@ -11,24 +17,144 @@ class EventAdmin(admin.ModelAdmin): "start_time", "accept_reg", "show_on_home", + "login_required", + "registration_fee", "event_page", ] - list_filter = ["show_on_home", "accept_reg"] - fields = [ - "name", - "description", - "cover_image", - "venue", - "start_time", - "end_time", - "accept_reg", - "show_on_home", - "content", + list_filter = ["show_on_home", "accept_reg", "login_required"] + fieldsets = [ + (None, { + 'fields': [ + "name", + "description", + "cover_image", + "venue", + "start_time", + "end_time", + ] + }), + ('Registration Settings', { + 'fields': [ + "accept_reg", + "login_required", + "registration_fee", + ], + 'description': 'Configure how users can register for this event' + }), + ('Display Settings', { + 'fields': [ + "show_on_home", + "content", + ] + }) ] + inlines = [EventFormFieldInline] @admin.register(EventRegistration) class EventRegistrationAdmin(admin.ModelAdmin): - search_fields = ["event", "user"] - list_display = ["event", "user_name", "user_whatsapp", "user", "user_profile_link"] - list_filter = ["event__name"] + search_fields = ["event__name", "user__email", "order_id", "client_txn_id"] + list_display = [ + "registration_number", + "event", + "user_name", + "user_whatsapp", + "datetime", + "amount", + "payment_status", + "order_id", + "payment_date_time", + ] + list_filter = ["event__name", "payment_status"] + readonly_fields = [ + "datetime", + "form_responses", + "payment_status", + "order_id", + "client_txn_id", + "payment_date_time", + "payment_data", + "get_payment_details" + ] + + fieldsets = [ + (None, { + 'fields': [ + "event", + "user", + "datetime", + "amount", + "form_responses", + ] + }), + ('Payment Information', { + 'fields': [ + "payment_status", + "order_id", + "client_txn_id", + "payment_date_time", + ], + 'classes': ['collapse'] + }), + ('Payment Diagnostic Data', { + 'fields': [ + "get_payment_details", + ], + 'classes': ['collapse'], + 'description': 'Detailed payment transaction data from UPI gateway' + }) + ] + + def has_change_permission(self, request, obj=None): + return False + + def has_delete_permission(self, request, obj=None): + return False + + def has_add_permission(self, request): + return False + + def get_queryset(self, request): + return super().get_queryset(request).select_related('event', 'user') + + def registration_number(self, obj): + return obj.registration_number + registration_number.short_description = "Registration No." + + def get_payment_details(self, obj): + if not obj.payment_data: + return "No payment data available" + + # Format payment data for display + details = [] + if obj.payment_data.get('customer_vpa'): + details.append(f"Customer UPI: {obj.payment_data['customer_vpa']}") + if obj.payment_data.get('upi_txn_id'): + details.append(f"UPI Transaction ID: {obj.payment_data['upi_txn_id']}") + if obj.payment_data.get('status'): + details.append(f"Status: {obj.payment_data['status']}") + if obj.payment_data.get('remark'): + details.append(f"Remark: {obj.payment_data['remark']}") + if obj.payment_data.get('txnAt'): + details.append(f"Transaction Time: {obj.payment_data['txnAt']}") + + # Merchant details + merchant = obj.payment_data.get('merchant', {}) + if merchant: + details.append("Merchant Details:") + if merchant.get('name'): + details.append(f" - Name: {merchant['name']}") + if merchant.get('upi_id'): + details.append(f" - UPI ID: {merchant['upi_id']}") + + # User defined fields + for i in range(1, 4): + udf = obj.payment_data.get(f'udf{i}') + if udf: + details.append(f"UDF{i}: {udf}") + + if obj.payment_data.get('createdAt'): + details.append(f"Created At: {obj.payment_data['createdAt']}") + + return "\n".join(details) + get_payment_details.short_description = "Payment Details" diff --git a/src/haps/migrations/0005_event_login_required_event_registration_fee_and_more.py b/src/haps/migrations/0005_event_login_required_event_registration_fee_and_more.py new file mode 100644 index 0000000..59a53cf --- /dev/null +++ b/src/haps/migrations/0005_event_login_required_event_registration_fee_and_more.py @@ -0,0 +1,109 @@ +# Generated by Django 4.2.1 on 2024-12-27 19:11 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("haps", "0004_event_content"), + ] + + operations = [ + migrations.AddField( + model_name="event", + name="login_required", + field=models.BooleanField( + default=True, verbose_name="Login required for registration?" + ), + ), + migrations.AddField( + model_name="event", + name="registration_fee", + field=models.PositiveIntegerField( + blank=True, help_text="Leave blank for free registration", null=True + ), + ), + migrations.AddField( + model_name="eventregistration", + name="amount", + field=models.PositiveIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name="eventregistration", + name="form_responses", + field=models.JSONField(default=dict), + ), + migrations.AddField( + model_name="eventregistration", + name="order_id", + field=models.CharField(blank=True, max_length=100, null=True), + ), + migrations.AddField( + model_name="eventregistration", + name="payment_status", + field=models.CharField( + choices=[ + ("pending", "Pending"), + ("success", "Success"), + ("failure", "Failure"), + ], + default="pending", + max_length=10, + ), + ), + migrations.AlterField( + model_name="eventregistration", + name="user", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.CreateModel( + name="EventFormField", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("field_label", models.CharField(max_length=100)), + ( + "field_type", + models.CharField( + choices=[ + ("text", "Text Input"), + ("number", "Number Input"), + ("email", "Email Input"), + ("textarea", "Text Area"), + ], + max_length=20, + ), + ), + ("required", models.BooleanField(default=True)), + ("order", models.PositiveIntegerField(default=0)), + ("help_text", models.CharField(blank=True, max_length=200)), + ( + "event", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="form_fields", + to="haps.event", + ), + ), + ], + options={ + "ordering": ["order"], + "unique_together": {("event", "field_label")}, + }, + ), + ] diff --git a/src/haps/migrations/0006_eventregistration_client_txn_id_and_more.py b/src/haps/migrations/0006_eventregistration_client_txn_id_and_more.py new file mode 100644 index 0000000..f3ae884 --- /dev/null +++ b/src/haps/migrations/0006_eventregistration_client_txn_id_and_more.py @@ -0,0 +1,32 @@ +# Generated by Django 4.2.1 on 2024-12-29 19:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("haps", "0005_event_login_required_event_registration_fee_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="eventregistration", + name="client_txn_id", + field=models.CharField( + blank=True, db_index=True, max_length=128, null=True, unique=True + ), + ), + migrations.AddField( + model_name="eventregistration", + name="payment_data", + field=models.JSONField( + default=dict, + help_text="\n Stores payment-related data from UPI gateway including:\n - customer_vpa: Customer's UPI ID\n - upi_txn_id: UPI transaction ID\n - status: Detailed payment status\n - remark: Payment remarks/failure reason\n - txnAt: Transaction timestamp\n - merchant: Merchant details (name, upi_id)\n - udf1, udf2, udf3: User defined fields\n - redirect_url: Payment redirect URL\n - createdAt: Order creation time\n ", + ), + ), + migrations.AddField( + model_name="eventregistration", + name="payment_date_time", + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/src/haps/models.py b/src/haps/models.py index 455c851..442f509 100644 --- a/src/haps/models.py +++ b/src/haps/models.py @@ -5,7 +5,6 @@ from utils.slugs import generate_unique_slug from ckeditor_uploader.fields import RichTextUploadingField from users.models import UserProfile -from django.utils.html import format_html User = get_user_model() @@ -21,6 +20,8 @@ class Event(models.Model): accept_reg = models.BooleanField(verbose_name="Accepting registrations ?") show_on_home = models.BooleanField(verbose_name="Show on Home Page ?") content = RichTextUploadingField(null=True, blank=True) + login_required = models.BooleanField(default=True, verbose_name="Login required for registration?") + registration_fee = models.PositiveIntegerField(null=True, blank=True, help_text="Leave blank for free registration") def __str__(self): return self.name + " (" + str(self.start_time) + ")" @@ -28,7 +29,7 @@ def __str__(self): def save(self, *args, **kwargs): if self.slug == "": self.slug = generate_unique_slug(self.name, Event) - super().save(args, kwargs) + super().save(*args, **kwargs) def get_absolute_url(self): return f"/events/{self.slug}" @@ -39,20 +40,90 @@ def event_page(self): ) +class EventFormField(models.Model): + FIELD_TYPES = [ + ('text', 'Text Input'), + ('number', 'Number Input'), + ('email', 'Email Input'), + ('textarea', 'Text Area'), + ] + + event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name='form_fields') + field_label = models.CharField(max_length=100) + field_type = models.CharField(max_length=20, choices=FIELD_TYPES) + required = models.BooleanField(default=True) + order = models.PositiveIntegerField(default=0) + help_text = models.CharField(max_length=200, blank=True) + + class Meta: + ordering = ['order'] + unique_together = ['event', 'field_label'] + + def __str__(self): + return f"{self.event.name} - {self.field_label}" + + class EventRegistration(models.Model): + PAYMENT_STATUS_CHOICES = [ + ('pending', 'Pending'), + ('success', 'Success'), + ('failure', 'Failure'), + ] + event = models.ForeignKey(Event, on_delete=models.CASCADE) - user = models.ForeignKey(User, on_delete=models.CASCADE) + user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True) datetime = models.DateTimeField(auto_now_add=True, null=True, blank=True) - # regno = models.CharField(unique=True) + form_responses = models.JSONField(default=dict) + amount = models.PositiveIntegerField(null=True, blank=True) + + # Payment related fields + payment_status = models.CharField( + max_length=10, + choices=PAYMENT_STATUS_CHOICES, + default='pending' + ) + order_id = models.CharField(max_length=100, blank=True, null=True) + client_txn_id = models.CharField(max_length=128, unique=True, db_index=True, null=True, blank=True) + payment_date_time = models.DateTimeField(null=True, blank=True) + + # Payment diagnostic data from UPI gateway + payment_data = models.JSONField(default=dict, help_text=""" + Stores payment-related data from UPI gateway including: + - customer_vpa: Customer's UPI ID + - upi_txn_id: UPI transaction ID + - status: Detailed payment status + - remark: Payment remarks/failure reason + - txnAt: Transaction timestamp + - merchant: Merchant details (name, upi_id) + - udf1, udf2, udf3: User defined fields + - redirect_url: Payment redirect URL + - createdAt: Order creation time + """) + + def __str__(self): + return f"Registration #{self.id} - {self.event.name}" + + @property + def registration_number(self): + """Returns a formatted registration number.""" + return f"#{self.id}" + def user_name(self): - return self.user.full_name() + if self.user: + return self.user.full_name() + return self.form_responses.get('name', 'Anonymous') def user_whatsapp(self): - return UserProfile.objects.get(user=self.user).whatsapp_number + if self.user: + return UserProfile.objects.get(user=self.user).whatsapp_number + return self.form_responses.get('whatsapp_number', '') def user_profile_link(self): - return self.user.profile_link() + if self.user: + return self.user.profile_link() + return None def __str__(self) -> str: - return self.user.__str__() + "%" + self.event.__str__() + user_str = str(self.user) if self.user else self.form_responses.get('name', 'Anonymous') + return f"{user_str} % {self.event}" \ No newline at end of file diff --git a/src/haps/templates/haps/item.html b/src/haps/templates/haps/item.html index e742453..18ad412 100644 --- a/src/haps/templates/haps/item.html +++ b/src/haps/templates/haps/item.html @@ -7,8 +7,14 @@
- image description + {% if hap.cover_image %} + {{hap.name}} cover image + {% else %} +
+ event +
+ {% endif %}
@@ -17,62 +23,141 @@ {{hap.name}} -

{{hap.description}}

-
-
- - event - + event {{hap.start_time}}
- - location_on - - - {{hap.venue}} - + location_on + {{hap.venue}} +
+ + {% if hap.registration_fee %} +
+ payments + Registration Fee: ₹{{hap.registration_fee}}
+ {% endif %}
- - Register Now - - - - - - - Contact us - + {% if hap.accept_reg %} + {% if hap.login_required and not user.is_authenticated %} + + Login to Register + + + + + {% else %} + + {% endif %} + {% endif %}
- - - - -
-
-
- {{hap.content | safe}} -
+ {% if hap.content %} +
+ {{hap.content|safe}}
+ {% endif %} +
+ + +{% endblock content %} \ No newline at end of file diff --git a/src/haps/templates/haps/register_failure.html b/src/haps/templates/haps/register_failure.html new file mode 100644 index 0000000..319222c --- /dev/null +++ b/src/haps/templates/haps/register_failure.html @@ -0,0 +1,137 @@ +{% extends "commons.html" %} + +{% block title %} {{ event.name }} - Registration Failed {% endblock title %} +{% load haps_extras %} + +{% block content %} +
+
+
+ +
+ +
+

Registration Failed!

+

Your registration for the event is incomplete due to payment failure.

+

Registration #{{registration.id}}

+ {% if remark %} +

{{remark}}

+ {% endif %} +
+ +
+ + + + + + + + + + + + + {% if registration.user %} + + + + + {% endif %} + + {% if not registration.user %} + + + + + {% endif %} + + + + + + + + + + + + {% if registration.datetime %} + + + + + {% endif %} + + + + + + + {% for field in event.form_fields.all %} + + + + + {% endfor %} + +
+ Event + + {{event.name}} +
+ Name + + {% if registration.user %} + {{registration.user.get_full_name|default:"Anonymous User"}} + {% else %} + {{registration.form_responses.name|default:"Anonymous User"}} + {% endif %} +
+ Email + + {{registration.user.email}} +
+ WhatsApp + + {{registration.form_responses.whatsapp_number}} +
+ Date + + {{event.start_time}} +
+ Venue + + {{event.venue}} +
+ Registration Time + + {{registration.datetime}} +
+ Registration Fee + + ₹{{event.registration_fee}} + + Payment Failed + +
+ {{field.field_label}} + + {{registration.form_responses|get_item:field.field_label}} +
+
+ + +
+
+{% endblock content %} diff --git a/src/haps/templates/haps/register_success.html b/src/haps/templates/haps/register_success.html index 2207e98..452f3f3 100644 --- a/src/haps/templates/haps/register_success.html +++ b/src/haps/templates/haps/register_success.html @@ -1,12 +1,12 @@ {% extends "commons.html" %} {% block title %} {{ event.name }} {% endblock title %} +{% load haps_extras %} {% block content %} - -
-
+
+
@@ -14,91 +14,135 @@

Registration Done!

-

Thank you for completing your registration for event

-

Looking forward to see you in the event!

- +

Thank you for completing your registration.

+

Registration #{{registration.id}}

+

Looking forward to seeing you at the event!

- -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {# show registration date time #} - -
- Event - - {{event.name}} -
- Name - - {{user.first_name}} {{user.last_name}} -
- Email - - {{user.email}} -
- Date - - {{event.start_time}} -
- Venue - - {{event.venue}} -
-
- - - +
+ + + + + + + + + + + + + {% if registration.user %} + + + + + {% endif %} + + {% if not registration.user %} + + + + + {% endif %} + + + + + + + + + + + + {% if registration.datetime %} + + + + + {% endif %} + + {% if event.registration_fee %} + + + + + {% endif %} + + {% for field in event.form_fields.all %} + + + + + {% endfor %} + +
+ Event + + {{event.name}} +
+ Name + + {% if registration.user %} + {{registration.user.get_full_name}} + {% else %} + {{registration.form_responses.name}} + {% endif %} +
+ Email + + {{registration.user.email}} +
+ WhatsApp + + {{registration.form_responses.whatsapp_number}} +
+ Date + + {{event.start_time}} +
+ Venue + + {{event.venue}} +
+ Registration Time + + {{registration.datetime}} +
+ Registration Fee + + ₹{{event.registration_fee}} + + {{registration.payment_status|title}} + +
+ {{field.field_label}} + + {{registration.form_responses|get_item:field.field_label}} +
-
- {% endblock content %} \ No newline at end of file diff --git a/src/haps/templatetags/__init__.py b/src/haps/templatetags/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/haps/templatetags/__init__.py @@ -0,0 +1 @@ + diff --git a/src/haps/templatetags/haps_extras.py b/src/haps/templatetags/haps_extras.py new file mode 100644 index 0000000..83bc6b3 --- /dev/null +++ b/src/haps/templatetags/haps_extras.py @@ -0,0 +1,8 @@ +from django import template + +register = template.Library() + +@register.filter +def get_item(dictionary, key): + """Get an item from a dictionary using template filter""" + return dictionary.get(key, '') diff --git a/src/haps/urls.py b/src/haps/urls.py index f0ddcec..d3d9c01 100644 --- a/src/haps/urls.py +++ b/src/haps/urls.py @@ -6,6 +6,11 @@ urlpatterns = [ path("", views.haps_list, name="events"), - path("", views.haps_item, name="event_item"), - path("/register", views.register_for_event, name="event_register"), + path("", views.haps_item, name="event_item"), + path("/register", views.register_for_event, name="register"), + path("registration//failure", views.register_failure, name="register_failure"), + path("registration//success", views.register_success, name="register_success"), + path("registration//pay", views.initiate_payment, name="initiate_payment"), + path("registration//retry", views.retry_payment, name="retry_payment"), + path("payment/callback", views.payment_callback, name="payment_callback"), ] diff --git a/src/haps/views.py b/src/haps/views.py index 2a22bbf..1ddb8bf 100644 --- a/src/haps/views.py +++ b/src/haps/views.py @@ -1,9 +1,15 @@ -from django.shortcuts import render -from django.http import HttpRequest, Http404 -from .models import Event, EventRegistration +from django.shortcuts import render, redirect +from django.http import HttpRequest, HttpResponse from django.contrib.auth.decorators import login_required +from django.contrib import messages +from .models import Event, EventRegistration import logging - +import json +from utils.payment.upi_gateway import create_order, check_order_status +from django.urls import reverse +from uuid import uuid4 +from datetime import date +from utils.logging import critical_logger log = logging.getLogger(__name__) @@ -23,10 +29,252 @@ def haps_item(request: HttpRequest, slug: str): @login_required(login_url="/users/register") def register_for_event(request: HttpRequest, slug: str): - event = Event.objects.get(slug=slug) - registration = EventRegistration.objects.get_or_create( - event=event, user=request.user + """Register for an event""" + try: + event = Event.objects.get(slug=slug) + except Event.DoesNotExist: + raise Http404("Event not found") + + if not event.accept_reg: + messages.error(request, "Registration is closed for this event.") + return redirect('haps:event_item', slug=slug) + + if event.login_required and not request.user.is_authenticated: + messages.error(request, "Please login to register for this event.") + return redirect('haps:event_item', slug=slug) + + if request.method == 'POST': + # Collect form responses + form_responses = {} + + # If not login required, collect basic info + if not event.login_required: + form_responses['name'] = request.POST.get('name') + form_responses['whatsapp_number'] = request.POST.get('whatsapp_number') + + # Collect custom field responses + for field in event.form_fields.all(): + field_id = f'field_{field.id}' + form_responses[field.field_label] = request.POST.get(field_id) + + # Create registration + registration = EventRegistration( + event=event, + user=request.user if request.user.is_authenticated else None, + form_responses=form_responses, + amount=event.registration_fee if event.registration_fee else None + ) + registration.save() + + # If payment required, redirect to payment page + if event.registration_fee: + return redirect('haps:initiate_payment', registration_id=registration.id) + + messages.success(request, "Successfully registered for the event!") + return render(request, "haps/register_success.html", + context={"registration": registration, "event": event}) + + # If GET request with registration modal, return to event page + return redirect('haps:event_item', slug=slug) + + +def register_failure(request: HttpRequest, registration_id: int): + """View for displaying registration failure page. + This is typically shown when payment fails.""" + + try: + registration = EventRegistration.objects.select_related('event', 'user').get(id=registration_id) + except EventRegistration.DoesNotExist: + raise Http404("Registration not found") + + # For testing, you can pass a remark through URL query parameter + remark = request.GET.get('remark', 'Payment was not completed') + + context = { + "registration": registration, + "event": registration.event, + "remark": remark + } + return render(request, "haps/register_failure.html", context=context) + + +def initiate_payment(request: HttpRequest, registration_id: int): + """Initiate payment for event registration""" + try: + registration = EventRegistration.objects.select_related('event', 'user').get(id=registration_id) + except EventRegistration.DoesNotExist: + raise Http404("Registration not found") + + # Validate amount + if not registration.amount: + log.error(f"Registration {registration.id} has no amount set") + messages.error(request, "Invalid registration amount") + return redirect('haps:register_failure', registration_id=registration.id) + + if registration.payment_status == 'success': + messages.info(request, "Payment already completed") + return redirect('haps:register_success', registration_id=registration.id) + + # Generate unique transaction ID if not exists + if not registration.client_txn_id: + registration.client_txn_id = f"TempleWebPay-event-{registration.id}-{uuid4().hex[:8]}" + registration.save(update_fields=['client_txn_id']) + + # Create callback URL + callback_url = request.build_absolute_uri( + reverse('haps:payment_callback') + ) + + # Get user details + name = None + if registration.user: + name = registration.user.get_full_name() + if not name: + name = registration.user.username + else: + name = registration.form_responses.get('name') + + if not name: + log.error(f"Registration {registration.id} has no customer name") + + # return redirect('haps:register_failure', registration_id=registration.id) + name = "Anonymous User" + + mobile = registration.form_responses.get('whatsapp_number', '') + email = registration.user.email if registration.user else '' + + try: + # Create payment order + status, api_resp = create_order( + client_txn_id=registration.client_txn_id, + redirect_url=callback_url, + amount=registration.amount, + product_info=f"Registration for {registration.event.name}", + customer_name=name, + # customer_email=email, + # customer_mobile=mobile + ) + + if not status: + log.error(f"Payment gateway error for registration {registration.id}: {api_resp.text}") + messages.error(request, "Payment gateway error. Please try again later.") + return redirect('haps:register_failure', + registration_id=registration.id, + remark=f"Payment gateway error: {api_resp.text}") + + # Save order ID and redirect to payment URL + registration.order_id = api_resp["order_id"] + registration.save(update_fields=['order_id']) + + return redirect(api_resp["payment_url"]) + + except Exception as e: + log.error(f"Payment initiation failed for registration {registration.id}: {str(e)}") + messages.error(request, "Failed to initiate payment. Please try again.") + return redirect('haps:register_failure', registration_id=registration.id) + + +def payment_callback(request: HttpRequest): + """Handle payment gateway callback""" + log.debug("Payment callback called") + client_txn_id = request.GET.get('client_txn_id') + if not client_txn_id: + messages.error(request, "Invalid payment callback") + return redirect('haps:events') + + try: + registration = EventRegistration.objects.get(client_txn_id=client_txn_id) + except EventRegistration.DoesNotExist: + messages.error(request, "Registration not found") + return redirect('haps:events') + + # Prevent duplicate processing + if registration.payment_status == 'success': + return redirect('haps:register_success', registration_id=registration.id) + + # Check payment status + try: + status_data = check_order_status(client_txn_id) + if not status_data: + raise ValueError("Empty response from payment gateway") + + # Store all payment diagnostic data + registration.payment_data = { + 'customer_vpa': status_data.get('customer_vpa'), + 'upi_txn_id': status_data.get('upi_txn_id'), + 'status': status_data.get('status'), + 'remark': status_data.get('remark'), + 'txnAt': status_data.get('txnAt'), + 'merchant': status_data.get('Merchant', {}), + 'udf1': status_data.get('udf1'), + 'udf2': status_data.get('udf2'), + 'udf3': status_data.get('udf3'), + 'redirect_url': status_data.get('redirect_url'), + 'createdAt': status_data.get('createdAt') + } + + # Update payment status + if status_data['status'] == 'success': + registration.payment_status = 'success' + registration.payment_date_time = status_data.get('txnAt') + registration.save(update_fields=['payment_status', 'payment_date_time', 'payment_data']) + messages.success(request, "Payment successful!") + return redirect('haps:register_success', registration_id=registration.id) + else: + registration.payment_status = 'failure' + registration.save(update_fields=['payment_status', 'payment_data']) + return redirect('haps:register_failure', + registration_id=registration.id, + remark=status_data.get('remark', 'Payment failed')) + + except Exception as e: + log.error(f"Payment verification failed for registration {registration.id}: {str(e)}") + registration.payment_data = {'error': str(e)} + registration.payment_status = 'failure' + registration.save(update_fields=['payment_status', 'payment_data']) + return redirect('haps:register_failure', + registration_id=registration.id, + remark="Payment verification failed") + + +def retry_payment(request: HttpRequest, registration_id: int): + """Retry failed payment""" + try: + registration = EventRegistration.objects.get(id=registration_id) + except EventRegistration.DoesNotExist: + raise Http404("Registration not found") + + if registration.payment_status == 'success': + messages.info(request, "Payment already completed") + return redirect('haps:register_success', registration_id=registration.id) + + # Log critical payment data before reset + critical_logger.critical( + f"Payment retry initiated for registration {registration.id}. " + f"Previous payment data: order_id={registration.order_id}, " + f"client_txn_id={registration.client_txn_id}, " + f"payment_status={registration.payment_status}, " + f"payment_data={registration.payment_data}" ) - context = {"event": event, "user": request.user, "reg": registration} + # Reset payment fields for retry + registration.order_id = None + registration.client_txn_id = None + registration.payment_status = 'pending' + registration.save(update_fields=['order_id', 'client_txn_id', 'payment_status']) + + return redirect('haps:initiate_payment', registration_id=registration.id) + + +def register_success(request: HttpRequest, registration_id: int): + """View for displaying registration success page.""" + try: + registration = EventRegistration.objects.select_related('event', 'user').get(id=registration_id) + except EventRegistration.DoesNotExist: + raise Http404("Registration not found") + + context = { + "registration": registration, + "event": registration.event, + } return render(request, "haps/register_success.html", context=context) diff --git a/src/static/styles.css b/src/static/styles.css index 8bfc291..2e4896c 100644 --- a/src/static/styles.css +++ b/src/static/styles.css @@ -1714,133 +1714,6 @@ input:checked + .toggle-bg { margin-bottom: 0; } -.format-lg { - font-size: 1.125rem; - line-height: 1.7777778; -} - -.format-lg :where(p):not(:where([class~="not-format"] *)) { - margin-top: 1.3333333em; - margin-bottom: 1.3333333em; -} - -.format-lg :where([class~="lead"]):not(:where([class~="not-format"] *)) { - font-size: 1.2222222em; - line-height: 1.4545455; - margin-top: 1.0909091em; - margin-bottom: 1.0909091em; -} - -.format-lg :where(blockquote):not(:where([class~="not-format"] *))::before { - margin-top: 1.6666667em; -} - -.format-lg :where(blockquote > p:first-child):not(:where([class~="not-format"] *)) { - margin-top: 0.5em; -} - -.format-lg :where(h1):not(:where([class~="not-format"] *)) { - font-size: 2.6666667em; - margin-top: 0; - margin-bottom: 0.8333333em; - line-height: 1; -} - -.format-lg :where(h2):not(:where([class~="not-format"] *)) { - font-size: 2em; - margin-top: 0; - margin-bottom: 0.6666667em; - line-height: 1.3333333; -} - -.format-lg :where(h3):not(:where([class~="not-format"] *)) { - font-size: 1.3333333em; - margin-top: 0; - margin-bottom: 0.6666667em; - line-height: 1.5; -} - -.format-lg :where(h4):not(:where([class~="not-format"] *)) { - margin-top: 0; - margin-bottom: 0.4444444em; - line-height: 1.5555556; -} - -.format-lg :where(img):not(:where([class~="not-format"] *)) { - margin-top: 1.7777778em; - margin-bottom: 1.7777778em; -} - -.format-lg :where(video):not(:where([class~="not-format"] *)) { - margin-top: 1.7777778em; - margin-bottom: 1.7777778em; -} - -.format-lg :where(figure):not(:where([class~="not-format"] *)) { - margin-top: 1.7777778em; - margin-bottom: 1.7777778em; -} - -.format-lg :where(figure > *):not(:where([class~="not-format"] *)) { - margin-top: 0; - margin-bottom: 0; -} - -.format-lg :where(figcaption):not(:where([class~="not-format"] *)) { - font-size: 0.8888889em; - line-height: 1.5; - margin-top: 1em; -} - -.format-lg :where(code):not(:where([class~="not-format"] *)) { - font-size: 0.8888889em; -} - -.format-lg :where(h2 code):not(:where([class~="not-format"] *)) { - font-size: 0.8666667em; -} - -.format-lg :where(h3 code):not(:where([class~="not-format"] *)) { - font-size: 0.875em; -} - -.format-lg :where(pre):not(:where([class~="not-format"] *)) { - font-size: 0.8888889em; - line-height: 1.75; - margin-top: 2em; - margin-bottom: 2em; - border-radius: 0.375rem; - padding-top: 1em; - padding-right: 1.5em; - padding-bottom: 1em; - padding-left: 1.5em; -} - -.format-lg :where(ol):not(:where([class~="not-format"] *)) { - margin-top: 1.3333333em; - margin-bottom: 1.3333333em; - padding-left: 1.5555556em; -} - -.format-lg :where(ul):not(:where([class~="not-format"] *)) { - margin-top: 1.3333333em; - margin-bottom: 1.3333333em; - padding-left: 1.5555556em; -} - -.format-lg :where(li):not(:where([class~="not-format"] *)) { - margin-top: 0.6666667em; - margin-bottom: 0.6666667em; -} - -.format-lg :where(ol > li):not(:where([class~="not-format"] *)) { - padding-left: 0.4444444em; -} - -.format-lg :where(ul > li):not(:where([class~="not-format"] *)) { - padding-left: 0.4444444em; -} - .format-lg :where(.format > ul > li p):not(:where([class~="not-format"] *)) { margin-top: 0.8888889em; margin-bottom: 0.8888889em; @@ -1862,58 +1735,6 @@ input:checked + .toggle-bg { margin-bottom: 1.3333333em; } -.format-lg :where(ul ul, ul ol, ol ul, ol ol):not(:where([class~="not-format"] *)) { - margin-top: 0.8888889em; - margin-bottom: 0.8888889em; -} - -.format-lg :where(hr):not(:where([class~="not-format"] *)) { - margin-top: 3.1111111em; - margin-bottom: 3.1111111em; -} - -.format-lg :where(hr + *):not(:where([class~="not-format"] *)) { - margin-top: 0; -} - -.format-lg :where(h2 + *):not(:where([class~="not-format"] *)) { - margin-top: 0; -} - -.format-lg :where(h3 + *):not(:where([class~="not-format"] *)) { - margin-top: 0; -} - -.format-lg :where(h4 + *):not(:where([class~="not-format"] *)) { - margin-top: 0; -} - -.format-lg :where(table):not(:where([class~="not-format"] *)) { - font-size: 0.8888889em; - line-height: 1.5; -} - -.format-lg :where(thead th):not(:where([class~="not-format"] *)) { - padding-right: 0.75em; - padding-bottom: 0.75em; - padding-left: 0.75em; -} - -.format-lg :where(thead th:last-child):not(:where([class~="not-format"] *)) { - padding-right: 0; -} - -.format-lg :where(tbody td, tfoot td):not(:where([class~="not-format"] *)) { - padding-top: 0.75em; - padding-right: 0.75em; - padding-bottom: 0.75em; - padding-left: 0.75em; -} - -.format-lg :where(tbody td:last-child, tfoot td:last-child):not(:where([class~="not-format"] *)) { - padding-right: 0; -} - .format-lg :where(.format > :first-child):not(:where([class~="not-format"] *)) { margin-top: 0; } @@ -2036,6 +1857,10 @@ input:checked + .toggle-bg { grid-column: span 12 / span 12; } +.col-span-2 { + grid-column: span 2 / span 2; +} + .m-4 { margin: 1rem; } @@ -2168,6 +1993,11 @@ input:checked + .toggle-bg { margin-inline-start: 0.5rem; } +.ms-auto { + -webkit-margin-start: auto; + margin-inline-start: auto; +} + .mt-1 { margin-top: 0.25rem; } @@ -2268,6 +2098,10 @@ input:checked + .toggle-bg { height: 1.5rem; } +.h-64 { + height: 16rem; +} + .h-7 { height: 1.75rem; } @@ -2360,6 +2194,10 @@ input:checked + .toggle-bg { width: 2rem; } +.w-96 { + width: 24rem; +} + .w-\[52rem\] { width: 52rem; } @@ -2695,6 +2533,11 @@ input:checked + .toggle-bg { border-bottom-right-radius: 0.5rem; } +.rounded-t { + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; +} + .rounded-t-lg { border-top-left-radius: 0.5rem; border-top-right-radius: 0.5rem; @@ -2827,6 +2670,10 @@ input:checked + .toggle-bg { background-color: rgb(17 24 39 / var(--tw-bg-opacity)); } +.bg-gray-900\/50 { + background-color: rgb(17 24 39 / 0.5); +} + .bg-green-100 { --tw-bg-opacity: 1; background-color: rgb(222 247 236 / var(--tw-bg-opacity)); @@ -2867,6 +2714,10 @@ input:checked + .toggle-bg { background-color: rgb(253 242 242 / var(--tw-bg-opacity)); } +.bg-transparent { + background-color: transparent; +} + .bg-white { --tw-bg-opacity: 1; background-color: rgb(255 255 255 / var(--tw-bg-opacity)); @@ -3103,6 +2954,11 @@ input:checked + .toggle-bg { line-height: 1; } +.text-6xl { + font-size: 3.75rem; + line-height: 1; +} + .text-7xl { font-size: 4.5rem; line-height: 1; @@ -3269,6 +3125,11 @@ input:checked + .toggle-bg { color: rgb(240 82 82 / var(--tw-text-opacity)); } +.text-red-600 { + --tw-text-opacity: 1; + color: rgb(224 36 36 / var(--tw-text-opacity)); +} + .text-red-800 { --tw-text-opacity: 1; color: rgb(155 28 28 / var(--tw-text-opacity)); @@ -3358,6 +3219,12 @@ input:checked + .toggle-bg { filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); } +.backdrop-blur-lg { + --tw-backdrop-blur: blur(16px); + -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); + backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); +} + .transition { transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; @@ -3876,14 +3743,14 @@ input:checked + .toggle-bg { background-color: rgb(243 244 246 / var(--tw-bg-opacity)); } -.hover\:bg-primary-100:hover { +.hover\:bg-gray-200:hover { --tw-bg-opacity: 1; - background-color: rgb(254 243 199 / var(--tw-bg-opacity)); + background-color: rgb(229 231 235 / var(--tw-bg-opacity)); } -.hover\:bg-primary-600:hover { +.hover\:bg-primary-100:hover { --tw-bg-opacity: 1; - background-color: rgb(217 119 6 / var(--tw-bg-opacity)); + background-color: rgb(254 243 199 / var(--tw-bg-opacity)); } .hover\:bg-primary-700:hover { @@ -3934,6 +3801,15 @@ input:checked + .toggle-bg { filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); } +.focus\:z-10:focus { + z-index: 10; +} + +.focus\:border-blue-500:focus { + --tw-border-opacity: 1; + border-color: rgb(63 131 248 / var(--tw-border-opacity)); +} + .focus\:border-primary-500:focus { --tw-border-opacity: 1; border-color: rgb(245 158 11 / var(--tw-border-opacity)); @@ -3966,9 +3842,9 @@ input:checked + .toggle-bg { --tw-ring-color: rgb(164 202 254 / var(--tw-ring-opacity)); } -.focus\:ring-gray-100:focus { +.focus\:ring-blue-500:focus { --tw-ring-opacity: 1; - --tw-ring-color: rgb(243 244 246 / var(--tw-ring-opacity)); + --tw-ring-color: rgb(63 131 248 / var(--tw-ring-opacity)); } .focus\:ring-gray-200:focus { @@ -4154,6 +4030,11 @@ input:checked + .toggle-bg { color: rgb(107 114 128 / var(--tw-text-opacity)); } +:is(.dark .dark\:text-gray-600) { + --tw-text-opacity: 1; + color: rgb(75 85 99 / var(--tw-text-opacity)); +} + :is(.dark .dark\:text-gray-800) { --tw-text-opacity: 1; color: rgb(31 41 55 / var(--tw-text-opacity)); @@ -4253,19 +4134,24 @@ input:checked + .toggle-bg { color: rgb(255 255 255 / var(--tw-text-opacity)); } +:is(.dark .dark\:focus\:border-blue-500:focus) { + --tw-border-opacity: 1; + border-color: rgb(63 131 248 / var(--tw-border-opacity)); +} + :is(.dark .dark\:focus\:border-primary-500:focus) { --tw-border-opacity: 1; border-color: rgb(245 158 11 / var(--tw-border-opacity)); } -:is(.dark .dark\:focus\:ring-gray-600:focus) { +:is(.dark .dark\:focus\:ring-blue-500:focus) { --tw-ring-opacity: 1; - --tw-ring-color: rgb(75 85 99 / var(--tw-ring-opacity)); + --tw-ring-color: rgb(63 131 248 / var(--tw-ring-opacity)); } -:is(.dark .dark\:focus\:ring-gray-800:focus) { +:is(.dark .dark\:focus\:ring-gray-600:focus) { --tw-ring-opacity: 1; - --tw-ring-color: rgb(31 41 55 / var(--tw-ring-opacity)); + --tw-ring-color: rgb(75 85 99 / var(--tw-ring-opacity)); } :is(.dark .dark\:focus\:ring-primary-500:focus) { @@ -4630,6 +4516,10 @@ input:checked + .toggle-bg { padding: 3rem; } + .md\:p-5 { + padding: 1.25rem; + } + .md\:px-12 { padding-left: 3rem; padding-right: 3rem; diff --git a/src/utils/logging.py b/src/utils/logging.py new file mode 100644 index 0000000..f745009 --- /dev/null +++ b/src/utils/logging.py @@ -0,0 +1,34 @@ +import logging +import os +from datetime import datetime +from pathlib import Path + +def setup_critical_logger(): + """Setup logger for critical events that need to be preserved""" + + # Create logs directory if it doesn't exist + logs_dir = Path(__file__).parent.parent.parent / 'logs' + logs_dir.mkdir(exist_ok=True) + + # Create critical logger + critical_logger = logging.getLogger('critical') + critical_logger.setLevel(logging.CRITICAL) + + # Create file handler + log_file = logs_dir / 'critical.log' + handler = logging.FileHandler(str(log_file)) + handler.setLevel(logging.CRITICAL) + + # Create formatter + formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + handler.setFormatter(formatter) + + # Add handler to logger + critical_logger.addHandler(handler) + + return critical_logger + +# Create the logger instance +critical_logger = setup_critical_logger() diff --git a/src/donations/upi_gateway.py b/src/utils/payment/upi_gateway.py similarity index 92% rename from src/donations/upi_gateway.py rename to src/utils/payment/upi_gateway.py index 345c3e3..cba53ed 100644 --- a/src/donations/upi_gateway.py +++ b/src/utils/payment/upi_gateway.py @@ -40,6 +40,8 @@ def create_order( """ if not customer_email: customer_email = "example@example.com" + if not customer_mobile: + customer_mobile = "9999999999" payload_dict = { "key": str(PaymentGatewayConfig.API_KEY), "client_txn_id": client_txn_id, @@ -55,19 +57,25 @@ def create_order( payload_json_str = json.dumps(payload_dict) + log.info(f"making payment request for {amount=} and {client_txn_id=}") + log.debug(payload_dict) + response = requests.request( "POST", PaymentGatewayConfig.CREATE_ORDER, headers=PaymentGatewayConfig.REQUEST_HEADERS, data=payload_json_str, ) - + log.debug(response.text) if response.status_code == 200: rj = response.json() if rj["status"] is True: + log.warning(rj) return True, rj["data"] else: log.warning("Failed to create order \n%s", response.text) + log.warning("rj[data]", rj["data"]) + log.warning("rj", rj) return False, response else: log.warning(