Skip to content

Commit 50302f4

Browse files
authoredDec 29, 2024··
Merge pull request #2 from aahnik/feat/event-registrations
Dynamic event registrations with custom form fields and payment options
2 parents 40fa042 + aa136db commit 50302f4

18 files changed

+1167
-349
lines changed
 

‎.vscode/settings.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"files.associations": {
3-
"*.html": "jinja-html"
4-
}
3+
"*.html": "django-html"
4+
},
5+
"python.languageServer": "None"
56
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Generated by Django 4.2.1 on 2024-12-27 19:11
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("donations", "0001_initial"),
9+
]
10+
11+
operations = [
12+
migrations.AlterField(
13+
model_name="donationtier",
14+
name="visible",
15+
field=models.BooleanField(
16+
default=False, verbose_name="Show on Donations Page ?"
17+
),
18+
),
19+
]

‎src/donations/models.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class DonationTier(models.Model):
2121
name = models.CharField(max_length=256)
2222
description = models.CharField(max_length=1024)
2323
amount = models.PositiveIntegerField(validators=[MaxValueValidator(10000)])
24-
visible = models.BooleanField(default=False)
24+
visible = models.BooleanField(verbose_name="Show on Donations Page ?", default=False)
2525

2626
def __str__(self):
2727
return self.name

‎src/donations/views.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from .forms import DonationForm
55
from .models import DonationConfig, DonationTier, DonationReceived
66
from temple_web.myconfig import PaymentGatewayConfig
7-
from .upi_gateway import create_order, check_order_status
7+
from utils.payment.upi_gateway import create_order, check_order_status
88
from uuid import uuid4
99
from datetime import date
1010
from utils.adirect import adirect
@@ -20,7 +20,7 @@
2020

2121
def donations(request):
2222
donation_config = DonationConfig.get_solo()
23-
donation_tiers = DonationTier.objects.all()
23+
donation_tiers = DonationTier.objects.filter(visible=True)
2424
context = {"donation_config": donation_config, "donation_tiers": donation_tiers}
2525
return render(request, "donations/donations.html", context=context)
2626

‎src/haps/admin.py

+141-15
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
from django.contrib import admin
2-
from .models import EventRegistration, Event
2+
from .models import EventRegistration, Event, EventFormField
3+
4+
5+
class EventFormFieldInline(admin.TabularInline):
6+
model = EventFormField
7+
extra = 1
8+
ordering = ['order']
39

410

511
@admin.register(Event)
@@ -11,24 +17,144 @@ class EventAdmin(admin.ModelAdmin):
1117
"start_time",
1218
"accept_reg",
1319
"show_on_home",
20+
"login_required",
21+
"registration_fee",
1422
"event_page",
1523
]
16-
list_filter = ["show_on_home", "accept_reg"]
17-
fields = [
18-
"name",
19-
"description",
20-
"cover_image",
21-
"venue",
22-
"start_time",
23-
"end_time",
24-
"accept_reg",
25-
"show_on_home",
26-
"content",
24+
list_filter = ["show_on_home", "accept_reg", "login_required"]
25+
fieldsets = [
26+
(None, {
27+
'fields': [
28+
"name",
29+
"description",
30+
"cover_image",
31+
"venue",
32+
"start_time",
33+
"end_time",
34+
]
35+
}),
36+
('Registration Settings', {
37+
'fields': [
38+
"accept_reg",
39+
"login_required",
40+
"registration_fee",
41+
],
42+
'description': 'Configure how users can register for this event'
43+
}),
44+
('Display Settings', {
45+
'fields': [
46+
"show_on_home",
47+
"content",
48+
]
49+
})
2750
]
51+
inlines = [EventFormFieldInline]
2852

2953

3054
@admin.register(EventRegistration)
3155
class EventRegistrationAdmin(admin.ModelAdmin):
32-
search_fields = ["event", "user"]
33-
list_display = ["event", "user_name", "user_whatsapp", "user", "user_profile_link"]
34-
list_filter = ["event__name"]
56+
search_fields = ["event__name", "user__email", "order_id", "client_txn_id"]
57+
list_display = [
58+
"registration_number",
59+
"event",
60+
"user_name",
61+
"user_whatsapp",
62+
"datetime",
63+
"amount",
64+
"payment_status",
65+
"order_id",
66+
"payment_date_time",
67+
]
68+
list_filter = ["event__name", "payment_status"]
69+
readonly_fields = [
70+
"datetime",
71+
"form_responses",
72+
"payment_status",
73+
"order_id",
74+
"client_txn_id",
75+
"payment_date_time",
76+
"payment_data",
77+
"get_payment_details"
78+
]
79+
80+
fieldsets = [
81+
(None, {
82+
'fields': [
83+
"event",
84+
"user",
85+
"datetime",
86+
"amount",
87+
"form_responses",
88+
]
89+
}),
90+
('Payment Information', {
91+
'fields': [
92+
"payment_status",
93+
"order_id",
94+
"client_txn_id",
95+
"payment_date_time",
96+
],
97+
'classes': ['collapse']
98+
}),
99+
('Payment Diagnostic Data', {
100+
'fields': [
101+
"get_payment_details",
102+
],
103+
'classes': ['collapse'],
104+
'description': 'Detailed payment transaction data from UPI gateway'
105+
})
106+
]
107+
108+
def has_change_permission(self, request, obj=None):
109+
return False
110+
111+
def has_delete_permission(self, request, obj=None):
112+
return False
113+
114+
def has_add_permission(self, request):
115+
return False
116+
117+
def get_queryset(self, request):
118+
return super().get_queryset(request).select_related('event', 'user')
119+
120+
def registration_number(self, obj):
121+
return obj.registration_number
122+
registration_number.short_description = "Registration No."
123+
124+
def get_payment_details(self, obj):
125+
if not obj.payment_data:
126+
return "No payment data available"
127+
128+
# Format payment data for display
129+
details = []
130+
if obj.payment_data.get('customer_vpa'):
131+
details.append(f"Customer UPI: {obj.payment_data['customer_vpa']}")
132+
if obj.payment_data.get('upi_txn_id'):
133+
details.append(f"UPI Transaction ID: {obj.payment_data['upi_txn_id']}")
134+
if obj.payment_data.get('status'):
135+
details.append(f"Status: {obj.payment_data['status']}")
136+
if obj.payment_data.get('remark'):
137+
details.append(f"Remark: {obj.payment_data['remark']}")
138+
if obj.payment_data.get('txnAt'):
139+
details.append(f"Transaction Time: {obj.payment_data['txnAt']}")
140+
141+
# Merchant details
142+
merchant = obj.payment_data.get('merchant', {})
143+
if merchant:
144+
details.append("Merchant Details:")
145+
if merchant.get('name'):
146+
details.append(f" - Name: {merchant['name']}")
147+
if merchant.get('upi_id'):
148+
details.append(f" - UPI ID: {merchant['upi_id']}")
149+
150+
# User defined fields
151+
for i in range(1, 4):
152+
udf = obj.payment_data.get(f'udf{i}')
153+
if udf:
154+
details.append(f"UDF{i}: {udf}")
155+
156+
if obj.payment_data.get('createdAt'):
157+
details.append(f"Created At: {obj.payment_data['createdAt']}")
158+
159+
return "\n".join(details)
160+
get_payment_details.short_description = "Payment Details"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# Generated by Django 4.2.1 on 2024-12-27 19:11
2+
3+
from django.conf import settings
4+
from django.db import migrations, models
5+
import django.db.models.deletion
6+
7+
8+
class Migration(migrations.Migration):
9+
dependencies = [
10+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
11+
("haps", "0004_event_content"),
12+
]
13+
14+
operations = [
15+
migrations.AddField(
16+
model_name="event",
17+
name="login_required",
18+
field=models.BooleanField(
19+
default=True, verbose_name="Login required for registration?"
20+
),
21+
),
22+
migrations.AddField(
23+
model_name="event",
24+
name="registration_fee",
25+
field=models.PositiveIntegerField(
26+
blank=True, help_text="Leave blank for free registration", null=True
27+
),
28+
),
29+
migrations.AddField(
30+
model_name="eventregistration",
31+
name="amount",
32+
field=models.PositiveIntegerField(blank=True, null=True),
33+
),
34+
migrations.AddField(
35+
model_name="eventregistration",
36+
name="form_responses",
37+
field=models.JSONField(default=dict),
38+
),
39+
migrations.AddField(
40+
model_name="eventregistration",
41+
name="order_id",
42+
field=models.CharField(blank=True, max_length=100, null=True),
43+
),
44+
migrations.AddField(
45+
model_name="eventregistration",
46+
name="payment_status",
47+
field=models.CharField(
48+
choices=[
49+
("pending", "Pending"),
50+
("success", "Success"),
51+
("failure", "Failure"),
52+
],
53+
default="pending",
54+
max_length=10,
55+
),
56+
),
57+
migrations.AlterField(
58+
model_name="eventregistration",
59+
name="user",
60+
field=models.ForeignKey(
61+
blank=True,
62+
null=True,
63+
on_delete=django.db.models.deletion.CASCADE,
64+
to=settings.AUTH_USER_MODEL,
65+
),
66+
),
67+
migrations.CreateModel(
68+
name="EventFormField",
69+
fields=[
70+
(
71+
"id",
72+
models.BigAutoField(
73+
auto_created=True,
74+
primary_key=True,
75+
serialize=False,
76+
verbose_name="ID",
77+
),
78+
),
79+
("field_label", models.CharField(max_length=100)),
80+
(
81+
"field_type",
82+
models.CharField(
83+
choices=[
84+
("text", "Text Input"),
85+
("number", "Number Input"),
86+
("email", "Email Input"),
87+
("textarea", "Text Area"),
88+
],
89+
max_length=20,
90+
),
91+
),
92+
("required", models.BooleanField(default=True)),
93+
("order", models.PositiveIntegerField(default=0)),
94+
("help_text", models.CharField(blank=True, max_length=200)),
95+
(
96+
"event",
97+
models.ForeignKey(
98+
on_delete=django.db.models.deletion.CASCADE,
99+
related_name="form_fields",
100+
to="haps.event",
101+
),
102+
),
103+
],
104+
options={
105+
"ordering": ["order"],
106+
"unique_together": {("event", "field_label")},
107+
},
108+
),
109+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Generated by Django 4.2.1 on 2024-12-29 19:34
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("haps", "0005_event_login_required_event_registration_fee_and_more"),
9+
]
10+
11+
operations = [
12+
migrations.AddField(
13+
model_name="eventregistration",
14+
name="client_txn_id",
15+
field=models.CharField(
16+
blank=True, db_index=True, max_length=128, null=True, unique=True
17+
),
18+
),
19+
migrations.AddField(
20+
model_name="eventregistration",
21+
name="payment_data",
22+
field=models.JSONField(
23+
default=dict,
24+
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 ",
25+
),
26+
),
27+
migrations.AddField(
28+
model_name="eventregistration",
29+
name="payment_date_time",
30+
field=models.DateTimeField(blank=True, null=True),
31+
),
32+
]

‎src/haps/models.py

+79-8
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from utils.slugs import generate_unique_slug
66
from ckeditor_uploader.fields import RichTextUploadingField
77
from users.models import UserProfile
8-
from django.utils.html import format_html
98

109
User = get_user_model()
1110

@@ -21,14 +20,16 @@ class Event(models.Model):
2120
accept_reg = models.BooleanField(verbose_name="Accepting registrations ?")
2221
show_on_home = models.BooleanField(verbose_name="Show on Home Page ?")
2322
content = RichTextUploadingField(null=True, blank=True)
23+
login_required = models.BooleanField(default=True, verbose_name="Login required for registration?")
24+
registration_fee = models.PositiveIntegerField(null=True, blank=True, help_text="Leave blank for free registration")
2425

2526
def __str__(self):
2627
return self.name + " (" + str(self.start_time) + ")"
2728

2829
def save(self, *args, **kwargs):
2930
if self.slug == "":
3031
self.slug = generate_unique_slug(self.name, Event)
31-
super().save(args, kwargs)
32+
super().save(*args, **kwargs)
3233

3334
def get_absolute_url(self):
3435
return f"/events/{self.slug}"
@@ -39,20 +40,90 @@ def event_page(self):
3940
)
4041

4142

43+
class EventFormField(models.Model):
44+
FIELD_TYPES = [
45+
('text', 'Text Input'),
46+
('number', 'Number Input'),
47+
('email', 'Email Input'),
48+
('textarea', 'Text Area'),
49+
]
50+
51+
event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name='form_fields')
52+
field_label = models.CharField(max_length=100)
53+
field_type = models.CharField(max_length=20, choices=FIELD_TYPES)
54+
required = models.BooleanField(default=True)
55+
order = models.PositiveIntegerField(default=0)
56+
help_text = models.CharField(max_length=200, blank=True)
57+
58+
class Meta:
59+
ordering = ['order']
60+
unique_together = ['event', 'field_label']
61+
62+
def __str__(self):
63+
return f"{self.event.name} - {self.field_label}"
64+
65+
4266
class EventRegistration(models.Model):
67+
PAYMENT_STATUS_CHOICES = [
68+
('pending', 'Pending'),
69+
('success', 'Success'),
70+
('failure', 'Failure'),
71+
]
72+
4373
event = models.ForeignKey(Event, on_delete=models.CASCADE)
44-
user = models.ForeignKey(User, on_delete=models.CASCADE)
74+
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True)
4575
datetime = models.DateTimeField(auto_now_add=True, null=True, blank=True)
46-
# regno = models.CharField(unique=True)
76+
form_responses = models.JSONField(default=dict)
77+
amount = models.PositiveIntegerField(null=True, blank=True)
78+
79+
# Payment related fields
80+
payment_status = models.CharField(
81+
max_length=10,
82+
choices=PAYMENT_STATUS_CHOICES,
83+
default='pending'
84+
)
85+
order_id = models.CharField(max_length=100, blank=True, null=True)
86+
client_txn_id = models.CharField(max_length=128, unique=True, db_index=True, null=True, blank=True)
87+
payment_date_time = models.DateTimeField(null=True, blank=True)
88+
89+
# Payment diagnostic data from UPI gateway
90+
payment_data = models.JSONField(default=dict, help_text="""
91+
Stores payment-related data from UPI gateway including:
92+
- customer_vpa: Customer's UPI ID
93+
- upi_txn_id: UPI transaction ID
94+
- status: Detailed payment status
95+
- remark: Payment remarks/failure reason
96+
- txnAt: Transaction timestamp
97+
- merchant: Merchant details (name, upi_id)
98+
- udf1, udf2, udf3: User defined fields
99+
- redirect_url: Payment redirect URL
100+
- createdAt: Order creation time
101+
""")
102+
103+
def __str__(self):
104+
return f"Registration #{self.id} - {self.event.name}"
105+
106+
@property
107+
def registration_number(self):
108+
"""Returns a formatted registration number."""
109+
return f"#{self.id}"
110+
47111

48112
def user_name(self):
49-
return self.user.full_name()
113+
if self.user:
114+
return self.user.full_name()
115+
return self.form_responses.get('name', 'Anonymous')
50116

51117
def user_whatsapp(self):
52-
return UserProfile.objects.get(user=self.user).whatsapp_number
118+
if self.user:
119+
return UserProfile.objects.get(user=self.user).whatsapp_number
120+
return self.form_responses.get('whatsapp_number', '')
53121

54122
def user_profile_link(self):
55-
return self.user.profile_link()
123+
if self.user:
124+
return self.user.profile_link()
125+
return None
56126

57127
def __str__(self) -> str:
58-
return self.user.__str__() + "%" + self.event.__str__()
128+
user_str = str(self.user) if self.user else self.form_responses.get('name', 'Anonymous')
129+
return f"{user_str} % {self.event}"

‎src/haps/templates/haps/item.html

+125-40
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,14 @@
77
<section class="bg-white dark:bg-gray-900">
88
<div class="max-w-3xl flex px-4 py-8 mx-auto justify-around flex-wrap">
99
<div>
10-
<img class="my-4 h-auto md:max-w-2xl sm:max-w-xl rounded-lg shadow-xl dark:shadow-gray-800"
11-
src="{{hap.cover_image.url}}" alt="image description">
10+
{% if hap.cover_image %}
11+
<img class="my-4 h-auto md:max-w-2xl sm:max-w-xl rounded-lg shadow-xl dark:shadow-gray-800"
12+
src="{{hap.cover_image.url}}" alt="{{hap.name}} cover image">
13+
{% else %}
14+
<div class="my-4 h-64 w-96 rounded-lg shadow-xl dark:shadow-gray-800 bg-gray-100 dark:bg-gray-800 flex items-center justify-center">
15+
<span class="material-symbols-outlined text-6xl text-gray-400 dark:text-gray-600">event</span>
16+
</div>
17+
{% endif %}
1218
</div>
1319

1420
<div class="my-4 place-self-center text-center">
@@ -17,62 +23,141 @@
1723
{{hap.name}}
1824
</h1>
1925

20-
2126
<p class="mt-4 max-w-2xl mb-2 font-light text-gray-500 lg:mb-8 md:text-lg lg:text-xl dark:text-gray-400">
2227
{{hap.description}}
2328
</p>
2429

25-
2630
<div class="md:text-lg lg:text-xl mb-8 text-gray-500 dark:text-gray-400 align-middle">
27-
2831
<div class="align-middle">
29-
<span class="material-symbols-outlined">
30-
event
31-
</span>
32+
<span class="material-symbols-outlined">event</span>
3233
<span> {{hap.start_time}}</span>
3334
</div>
3435

3536
<div class="align-middle">
36-
<span class="material-symbols-outlined">
37-
location_on
38-
</span>
39-
<span class="">
40-
{{hap.venue}}
41-
</span>
37+
<span class="material-symbols-outlined">location_on</span>
38+
<span class="">{{hap.venue}}</span>
39+
</div>
40+
41+
{% if hap.registration_fee %}
42+
<div class="align-middle mt-2">
43+
<span class="material-symbols-outlined">payments</span>
44+
<span class="">Registration Fee: ₹{{hap.registration_fee}}</span>
4245
</div>
46+
{% endif %}
4347
</div>
4448

45-
<a href="{{hap.slug}}/register"
46-
class="inline-flex items-center justify-center px-5 py-3 mr-3 text-base font-medium text-center text-white rounded-lg bg-primary-700 hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 dark:focus:ring-primary-900">
47-
Register Now
48-
<svg class="w-5 h-5 ml-2 -mr-1" fill="currentColor" viewBox="0 0 20 20"
49-
xmlns="http://www.w3.org/2000/svg">
50-
<path fill-rule="evenodd"
51-
d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z"
52-
clip-rule="evenodd">
53-
</path>
54-
</svg>
55-
</a>
56-
<a href="#"
57-
class="inline-flex items-center justify-center px-5 py-3 text-base font-medium text-center text-gray-900 border border-gray-300 rounded-lg hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 dark:text-white dark:border-gray-700 dark:hover:bg-gray-700 dark:focus:ring-gray-800">
58-
Contact us
59-
</a>
49+
{% if hap.accept_reg %}
50+
{% if hap.login_required and not user.is_authenticated %}
51+
<a href="{% url 'users:register' %}?next={{ request.path }}"
52+
class="inline-flex items-center justify-center px-5 py-3 mr-3 text-base font-medium text-center text-white rounded-lg bg-primary-700 hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 dark:focus:ring-primary-900">
53+
Login to Register
54+
<svg class="w-5 h-5 ml-2 -mr-1" fill="currentColor" viewBox="0 0 20 20"
55+
xmlns="http://www.w3.org/2000/svg">
56+
<path fill-rule="evenodd"
57+
d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z"
58+
clip-rule="evenodd"></path>
59+
</svg>
60+
</a>
61+
{% else %}
62+
<button data-modal-target="registration-modal" data-modal-toggle="registration-modal"
63+
class="inline-flex items-center justify-center px-5 py-3 mr-3 text-base font-medium text-center text-white rounded-lg bg-primary-700 hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 dark:focus:ring-primary-900">
64+
Register Now
65+
<svg class="w-5 h-5 ml-2 -mr-1" fill="currentColor" viewBox="0 0 20 20"
66+
xmlns="http://www.w3.org/2000/svg">
67+
<path fill-rule="evenodd"
68+
d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z"
69+
clip-rule="evenodd"></path>
70+
</svg>
71+
</button>
72+
{% endif %}
73+
{% endif %}
6074
</div>
61-
62-
63-
64-
65-
6675
</div>
6776

68-
<div class="px-4 py-8 w-full flex justify-center">
69-
<div class="format format-lg w-full">
70-
{{hap.content | safe}}
71-
</div>
77+
{% if hap.content %}
78+
<div class="max-w-3xl mx-auto px-4 py-8">
79+
{{hap.content|safe}}
7280
</div>
81+
{% endif %}
82+
</section>
7383

84+
<!-- Registration Modal -->
85+
<div id="registration-modal" tabindex="-1" aria-hidden="true"
86+
class="hidden fixed top-0 right-0 left-0 z-50 w-full h-full flex items-center justify-center bg-gray-900/50 backdrop-blur-lg">
87+
<div class="relative w-full max-w-md p-4">
88+
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
89+
<div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600">
90+
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
91+
Register for {{hap.name}}
92+
</h3>
93+
<button type="button"
94+
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white"
95+
data-modal-toggle="registration-modal">
96+
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
97+
viewBox="0 0 14 14">
98+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
99+
d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
100+
</svg>
101+
<span class="sr-only">Close modal</span>
102+
</button>
103+
</div>
74104

105+
<form class="p-4 md:p-5" method="post" action="{{hap.slug}}/register">
106+
{% csrf_token %}
107+
<div class="grid gap-4 mb-4 grid-cols-2">
108+
{% if not hap.login_required %}
109+
<div class="col-span-2">
110+
<label for="name" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Full Name</label>
111+
<input type="text" name="name" id="name" required
112+
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
113+
placeholder="Your full name">
114+
</div>
115+
<div class="col-span-2">
116+
<label for="whatsapp_number" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">WhatsApp Number</label>
117+
<input type="tel" name="whatsapp_number" id="whatsapp_number" required pattern="[0-9]{10}"
118+
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
119+
placeholder="10 digit number">
120+
</div>
121+
{% endif %}
122+
123+
{% for field in hap.form_fields.all %}
124+
<div class="col-span-2">
125+
<label for="field_{{field.id}}" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
126+
{{field.field_label}}
127+
{% if field.help_text %}
128+
<span class="text-xs text-gray-500">({{field.help_text}})</span>
129+
{% endif %}
130+
</label>
131+
132+
{% if field.field_type == 'textarea' %}
133+
<textarea id="field_{{field.id}}" name="field_{{field.id}}" rows="4"
134+
class="block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
135+
{% if field.required %}required{% endif %}></textarea>
136+
{% else %}
137+
<input type="{{field.field_type}}" name="field_{{field.id}}" id="field_{{field.id}}"
138+
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
139+
{% if field.required %}required{% endif %}>
140+
{% endif %}
141+
</div>
142+
{% endfor %}
143+
</div>
75144

76-
</section>
145+
{% if hap.registration_fee %}
146+
<div class="mb-4">
147+
<p class="text-sm text-gray-500 dark:text-gray-400">
148+
A registration fee of ₹{{hap.registration_fee}} will be collected after form submission.
149+
</p>
150+
</div>
151+
{% endif %}
77152

78-
{% endblock %}
153+
<div class="flex justify-end">
154+
<button type="submit"
155+
class="text-white inline-flex items-center bg-primary-700 hover:bg-primary-800 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">
156+
Submit
157+
</button>
158+
</div>
159+
</form>
160+
</div>
161+
</div>
162+
</div>
163+
{% endblock content %}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
{% extends "commons.html" %}
2+
3+
{% block title %} {{ event.name }} - Registration Failed {% endblock title %}
4+
{% load haps_extras %}
5+
6+
{% block content %}
7+
<div class="bg-gray-100 min-h-screen py-8">
8+
<div class="bg-white p-6 md:mx-auto max-w-2xl rounded-lg shadow">
9+
<div class="text-red-600 w-16 h-16 mx-auto my-6">
10+
<i class="fa-regular fa-circle-xmark fa-3x"></i>
11+
</div>
12+
13+
<div class="text-center">
14+
<h3 class="md:text-2xl text-base text-gray-900 font-semibold text-center">Registration Failed!</h3>
15+
<p class="text-gray-600 my-2">Your registration for the event is incomplete due to payment failure.</p>
16+
<p class="text-gray-600">Registration #{{registration.id}}</p>
17+
{% if remark %}
18+
<p class="text-red-600 mt-2">{{remark}}</p>
19+
{% endif %}
20+
</div>
21+
22+
<div class="my-8">
23+
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
24+
<tbody>
25+
<tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
26+
<th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
27+
Event
28+
</th>
29+
<td class="px-6 py-4">
30+
{{event.name}}
31+
</td>
32+
</tr>
33+
34+
<tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
35+
<th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
36+
Name
37+
</th>
38+
<td class="px-6 py-4">
39+
{% if registration.user %}
40+
{{registration.user.get_full_name|default:"Anonymous User"}}
41+
{% else %}
42+
{{registration.form_responses.name|default:"Anonymous User"}}
43+
{% endif %}
44+
</td>
45+
</tr>
46+
47+
{% if registration.user %}
48+
<tr class="bg-white border-b dark:bg-gray-800">
49+
<th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
50+
Email
51+
</th>
52+
<td class="px-6 py-4">
53+
{{registration.user.email}}
54+
</td>
55+
</tr>
56+
{% endif %}
57+
58+
{% if not registration.user %}
59+
<tr class="bg-white border-b dark:bg-gray-800">
60+
<th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
61+
WhatsApp
62+
</th>
63+
<td class="px-6 py-4">
64+
{{registration.form_responses.whatsapp_number}}
65+
</td>
66+
</tr>
67+
{% endif %}
68+
69+
<tr class="bg-white border-b dark:bg-gray-800">
70+
<th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
71+
Date
72+
</th>
73+
<td class="px-6 py-4">
74+
{{event.start_time}}
75+
</td>
76+
</tr>
77+
78+
<tr class="bg-white border-b dark:bg-gray-800">
79+
<th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
80+
Venue
81+
</th>
82+
<td class="px-6 py-4">
83+
{{event.venue}}
84+
</td>
85+
</tr>
86+
87+
{% if registration.datetime %}
88+
<tr class="bg-white border-b dark:bg-gray-800">
89+
<th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
90+
Registration Time
91+
</th>
92+
<td class="px-6 py-4">
93+
{{registration.datetime}}
94+
</td>
95+
</tr>
96+
{% endif %}
97+
98+
<tr class="bg-white border-b dark:bg-gray-800">
99+
<th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
100+
Registration Fee
101+
</th>
102+
<td class="px-6 py-4">
103+
₹{{event.registration_fee}}
104+
<span class="ml-2 text-xs px-2 py-1 rounded-full bg-red-100 text-red-800">
105+
Payment Failed
106+
</span>
107+
</td>
108+
</tr>
109+
110+
{% for field in event.form_fields.all %}
111+
<tr class="bg-white border-b dark:bg-gray-800">
112+
<th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
113+
{{field.field_label}}
114+
</th>
115+
<td class="px-6 py-4">
116+
{{registration.form_responses|get_item:field.field_label}}
117+
</td>
118+
</tr>
119+
{% endfor %}
120+
</tbody>
121+
</table>
122+
</div>
123+
124+
<div class="flex justify-center space-x-4">
125+
<a href="{% url 'haps:retry_payment' registration_id=registration.id %}"
126+
class="text-white bg-primary-700 hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-primary-600 dark:hover:bg-primary-700 focus:outline-none dark:focus:ring-primary-800">
127+
Retry Payment
128+
</a>
129+
130+
<a href="{% url 'haps:event_item' slug=event.slug %}"
131+
class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
132+
Back to Event
133+
</a>
134+
</div>
135+
</div>
136+
</div>
137+
{% endblock content %}
+125-81
Original file line numberDiff line numberDiff line change
@@ -1,104 +1,148 @@
11
{% extends "commons.html" %}
22

33
{% block title %} {{ event.name }} {% endblock title %}
4+
{% load haps_extras %}
45

56

67
{% block content %}
7-
8-
<div class="bg-gray-100 h-screen">
9-
<div class="bg-white p-6 md:mx-auto">
8+
<div class="bg-gray-100 min-h-screen py-8">
9+
<div class="bg-white p-6 md:mx-auto max-w-2xl rounded-lg shadow">
1010
<svg viewBox="0 0 24 24" class="text-green-600 w-16 h-16 mx-auto my-6">
1111
<path fill="currentColor"
1212
d="M12,0A12,12,0,1,0,24,12,12.014,12.014,0,0,0,12,0Zm6.927,8.2-6.845,9.289a1.011,1.011,0,0,1-1.43.188L5.764,13.769a1,1,0,1,1,1.25-1.562l4.076,3.261,6.227-8.451A1,1,0,1,1,18.927,8.2Z">
1313
</path>
1414
</svg>
1515
<div class="text-center">
1616
<h3 class="md:text-2xl text-base text-gray-900 font-semibold text-center">Registration Done!</h3>
17-
<p class="text-gray-600 my-2">Thank you for completing your registration for event</p>
18-
<p> Looking forward to see you in the event! </p>
19-
17+
<p class="text-gray-600 my-2">Thank you for completing your registration.</p>
18+
<p class="text-gray-600">Registration #{{registration.id}}</p>
19+
<p class="text-gray-600"> Looking forward to seeing you at the event! </p>
2020
</div>
2121

22-
23-
<div class="flex justify-center my-4">
24-
<div class="max-w-2xl text-center">
25-
<table class="w-full text-lg text-left rtl:text-right text-gray-500 dark:text-gray-400">
26-
27-
<tbody>
28-
<tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
29-
<th scope="row"
30-
class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
31-
Event
32-
</th>
33-
<td class="px-6 py-4">
34-
{{event.name}}
35-
</td>
36-
37-
</tr>
38-
<tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
39-
<th scope="row"
40-
class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
41-
Name
42-
</th>
43-
<td class="px-6 py-4">
44-
{{user.first_name}} {{user.last_name}}
45-
</td>
46-
47-
</tr>
48-
<tr class="bg-white border-b dark:bg-gray-800">
49-
<th scope="row"
50-
class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
51-
Email
52-
</th>
53-
<td class="px-6 py-4">
54-
{{user.email}}
55-
</td>
56-
57-
</tr>
58-
59-
<tr class="bg-white border-b dark:bg-gray-800">
60-
<th scope="row"
61-
class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
62-
Date
63-
</th>
64-
<td class="px-6 py-4">
65-
{{event.start_time}}
66-
</td>
67-
68-
</tr>
69-
70-
<tr class="bg-white border-b dark:bg-gray-800">
71-
<th scope="row"
72-
class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
73-
Venue
74-
</th>
75-
<td class="px-6 py-4">
76-
{{event.venue}}
77-
</td>
78-
79-
</tr>
80-
{# show registration date time #}
81-
</tbody>
82-
</table>
83-
</div>
84-
85-
86-
22+
<div class="my-8">
23+
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
24+
<tbody>
25+
<tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
26+
<th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
27+
Event
28+
</th>
29+
<td class="px-6 py-4">
30+
{{event.name}}
31+
</td>
32+
</tr>
33+
34+
<tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
35+
<th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
36+
Name
37+
</th>
38+
<td class="px-6 py-4">
39+
{% if registration.user %}
40+
{{registration.user.get_full_name}}
41+
{% else %}
42+
{{registration.form_responses.name}}
43+
{% endif %}
44+
</td>
45+
</tr>
46+
47+
{% if registration.user %}
48+
<tr class="bg-white border-b dark:bg-gray-800">
49+
<th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
50+
Email
51+
</th>
52+
<td class="px-6 py-4">
53+
{{registration.user.email}}
54+
</td>
55+
</tr>
56+
{% endif %}
57+
58+
{% if not registration.user %}
59+
<tr class="bg-white border-b dark:bg-gray-800">
60+
<th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
61+
WhatsApp
62+
</th>
63+
<td class="px-6 py-4">
64+
{{registration.form_responses.whatsapp_number}}
65+
</td>
66+
</tr>
67+
{% endif %}
68+
69+
<tr class="bg-white border-b dark:bg-gray-800">
70+
<th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
71+
Date
72+
</th>
73+
<td class="px-6 py-4">
74+
{{event.start_time}}
75+
</td>
76+
</tr>
77+
78+
<tr class="bg-white border-b dark:bg-gray-800">
79+
<th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
80+
Venue
81+
</th>
82+
<td class="px-6 py-4">
83+
{{event.venue}}
84+
</td>
85+
</tr>
86+
87+
{% if registration.datetime %}
88+
<tr class="bg-white border-b dark:bg-gray-800">
89+
<th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
90+
Registration Time
91+
</th>
92+
<td class="px-6 py-4">
93+
{{registration.datetime}}
94+
</td>
95+
</tr>
96+
{% endif %}
97+
98+
{% if event.registration_fee %}
99+
<tr class="bg-white border-b dark:bg-gray-800">
100+
<th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
101+
Registration Fee
102+
</th>
103+
<td class="px-6 py-4">
104+
₹{{event.registration_fee}}
105+
<span class="ml-2 text-xs px-2 py-1 rounded-full
106+
{% if registration.payment_status == 'success' %}
107+
bg-green-100 text-green-800
108+
{% elif registration.payment_status == 'pending' %}
109+
bg-yellow-100 text-yellow-800
110+
{% else %}
111+
bg-red-100 text-red-800
112+
{% endif %}">
113+
{{registration.payment_status|title}}
114+
</span>
115+
</td>
116+
</tr>
117+
{% endif %}
118+
119+
{% for field in event.form_fields.all %}
120+
<tr class="bg-white border-b dark:bg-gray-800">
121+
<th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
122+
{{field.field_label}}
123+
</th>
124+
<td class="px-6 py-4">
125+
{{registration.form_responses|get_item:field.field_label}}
126+
</td>
127+
</tr>
128+
{% endfor %}
129+
</tbody>
130+
</table>
87131
</div>
88132

89-
<div class="my-4 flex justify-center">
90-
<a href="{{hap.slug}}">
91-
<button type="button"
92-
class="my-4 text-white bg-primary-700 hover:bg-primary-600 focus:ring-4 focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-primary-600 dark:hover:bg-primary-700 focus:outline-none dark:focus:ring-primary-800">
93-
Print PDF
94-
</button>
133+
<div class="flex justify-center space-x-4">
134+
{% if event.registration_fee and registration.payment_status == 'pending' %}
135+
<a href="{% url 'donations:payment' registration_id=registration.id %}"
136+
class="text-white bg-primary-700 hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-primary-600 dark:hover:bg-primary-700 focus:outline-none dark:focus:ring-primary-800">
137+
Pay Now
95138
</a>
139+
{% endif %}
96140

141+
<button type="button" onclick="handlePrint()"
142+
class="text-white bg-primary-700 hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-primary-600 dark:hover:bg-primary-700 focus:outline-none dark:focus:ring-primary-800">
143+
Print
144+
</button>
97145
</div>
98-
99-
100-
101146
</div>
102147
</div>
103-
104148
{% endblock content %}

‎src/haps/templatetags/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

‎src/haps/templatetags/haps_extras.py

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from django import template
2+
3+
register = template.Library()
4+
5+
@register.filter
6+
def get_item(dictionary, key):
7+
"""Get an item from a dictionary using template filter"""
8+
return dictionary.get(key, '')

‎src/haps/urls.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@
66

77
urlpatterns = [
88
path("", views.haps_list, name="events"),
9-
path("<slug>", views.haps_item, name="event_item"),
10-
path("<slug>/register", views.register_for_event, name="event_register"),
9+
path("<slug:slug>", views.haps_item, name="event_item"),
10+
path("<slug:slug>/register", views.register_for_event, name="register"),
11+
path("registration/<int:registration_id>/failure", views.register_failure, name="register_failure"),
12+
path("registration/<int:registration_id>/success", views.register_success, name="register_success"),
13+
path("registration/<int:registration_id>/pay", views.initiate_payment, name="initiate_payment"),
14+
path("registration/<int:registration_id>/retry", views.retry_payment, name="retry_payment"),
15+
path("payment/callback", views.payment_callback, name="payment_callback"),
1116
]

‎src/haps/views.py

+256-8
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1-
from django.shortcuts import render
2-
from django.http import HttpRequest, Http404
3-
from .models import Event, EventRegistration
1+
from django.shortcuts import render, redirect
2+
from django.http import HttpRequest, HttpResponse
43
from django.contrib.auth.decorators import login_required
4+
from django.contrib import messages
5+
from .models import Event, EventRegistration
56
import logging
6-
7+
import json
8+
from utils.payment.upi_gateway import create_order, check_order_status
9+
from django.urls import reverse
10+
from uuid import uuid4
11+
from datetime import date
12+
from utils.logging import critical_logger
713

814
log = logging.getLogger(__name__)
915

@@ -23,10 +29,252 @@ def haps_item(request: HttpRequest, slug: str):
2329

2430
@login_required(login_url="/users/register")
2531
def register_for_event(request: HttpRequest, slug: str):
26-
event = Event.objects.get(slug=slug)
27-
registration = EventRegistration.objects.get_or_create(
28-
event=event, user=request.user
32+
"""Register for an event"""
33+
try:
34+
event = Event.objects.get(slug=slug)
35+
except Event.DoesNotExist:
36+
raise Http404("Event not found")
37+
38+
if not event.accept_reg:
39+
messages.error(request, "Registration is closed for this event.")
40+
return redirect('haps:event_item', slug=slug)
41+
42+
if event.login_required and not request.user.is_authenticated:
43+
messages.error(request, "Please login to register for this event.")
44+
return redirect('haps:event_item', slug=slug)
45+
46+
if request.method == 'POST':
47+
# Collect form responses
48+
form_responses = {}
49+
50+
# If not login required, collect basic info
51+
if not event.login_required:
52+
form_responses['name'] = request.POST.get('name')
53+
form_responses['whatsapp_number'] = request.POST.get('whatsapp_number')
54+
55+
# Collect custom field responses
56+
for field in event.form_fields.all():
57+
field_id = f'field_{field.id}'
58+
form_responses[field.field_label] = request.POST.get(field_id)
59+
60+
# Create registration
61+
registration = EventRegistration(
62+
event=event,
63+
user=request.user if request.user.is_authenticated else None,
64+
form_responses=form_responses,
65+
amount=event.registration_fee if event.registration_fee else None
66+
)
67+
registration.save()
68+
69+
# If payment required, redirect to payment page
70+
if event.registration_fee:
71+
return redirect('haps:initiate_payment', registration_id=registration.id)
72+
73+
messages.success(request, "Successfully registered for the event!")
74+
return render(request, "haps/register_success.html",
75+
context={"registration": registration, "event": event})
76+
77+
# If GET request with registration modal, return to event page
78+
return redirect('haps:event_item', slug=slug)
79+
80+
81+
def register_failure(request: HttpRequest, registration_id: int):
82+
"""View for displaying registration failure page.
83+
This is typically shown when payment fails."""
84+
85+
try:
86+
registration = EventRegistration.objects.select_related('event', 'user').get(id=registration_id)
87+
except EventRegistration.DoesNotExist:
88+
raise Http404("Registration not found")
89+
90+
# For testing, you can pass a remark through URL query parameter
91+
remark = request.GET.get('remark', 'Payment was not completed')
92+
93+
context = {
94+
"registration": registration,
95+
"event": registration.event,
96+
"remark": remark
97+
}
98+
return render(request, "haps/register_failure.html", context=context)
99+
100+
101+
def initiate_payment(request: HttpRequest, registration_id: int):
102+
"""Initiate payment for event registration"""
103+
try:
104+
registration = EventRegistration.objects.select_related('event', 'user').get(id=registration_id)
105+
except EventRegistration.DoesNotExist:
106+
raise Http404("Registration not found")
107+
108+
# Validate amount
109+
if not registration.amount:
110+
log.error(f"Registration {registration.id} has no amount set")
111+
messages.error(request, "Invalid registration amount")
112+
return redirect('haps:register_failure', registration_id=registration.id)
113+
114+
if registration.payment_status == 'success':
115+
messages.info(request, "Payment already completed")
116+
return redirect('haps:register_success', registration_id=registration.id)
117+
118+
# Generate unique transaction ID if not exists
119+
if not registration.client_txn_id:
120+
registration.client_txn_id = f"TempleWebPay-event-{registration.id}-{uuid4().hex[:8]}"
121+
registration.save(update_fields=['client_txn_id'])
122+
123+
# Create callback URL
124+
callback_url = request.build_absolute_uri(
125+
reverse('haps:payment_callback')
126+
)
127+
128+
# Get user details
129+
name = None
130+
if registration.user:
131+
name = registration.user.get_full_name()
132+
if not name:
133+
name = registration.user.username
134+
else:
135+
name = registration.form_responses.get('name')
136+
137+
if not name:
138+
log.error(f"Registration {registration.id} has no customer name")
139+
140+
# return redirect('haps:register_failure', registration_id=registration.id)
141+
name = "Anonymous User"
142+
143+
mobile = registration.form_responses.get('whatsapp_number', '')
144+
email = registration.user.email if registration.user else ''
145+
146+
try:
147+
# Create payment order
148+
status, api_resp = create_order(
149+
client_txn_id=registration.client_txn_id,
150+
redirect_url=callback_url,
151+
amount=registration.amount,
152+
product_info=f"Registration for {registration.event.name}",
153+
customer_name=name,
154+
# customer_email=email,
155+
# customer_mobile=mobile
156+
)
157+
158+
if not status:
159+
log.error(f"Payment gateway error for registration {registration.id}: {api_resp.text}")
160+
messages.error(request, "Payment gateway error. Please try again later.")
161+
return redirect('haps:register_failure',
162+
registration_id=registration.id,
163+
remark=f"Payment gateway error: {api_resp.text}")
164+
165+
# Save order ID and redirect to payment URL
166+
registration.order_id = api_resp["order_id"]
167+
registration.save(update_fields=['order_id'])
168+
169+
return redirect(api_resp["payment_url"])
170+
171+
except Exception as e:
172+
log.error(f"Payment initiation failed for registration {registration.id}: {str(e)}")
173+
messages.error(request, "Failed to initiate payment. Please try again.")
174+
return redirect('haps:register_failure', registration_id=registration.id)
175+
176+
177+
def payment_callback(request: HttpRequest):
178+
"""Handle payment gateway callback"""
179+
log.debug("Payment callback called")
180+
client_txn_id = request.GET.get('client_txn_id')
181+
if not client_txn_id:
182+
messages.error(request, "Invalid payment callback")
183+
return redirect('haps:events')
184+
185+
try:
186+
registration = EventRegistration.objects.get(client_txn_id=client_txn_id)
187+
except EventRegistration.DoesNotExist:
188+
messages.error(request, "Registration not found")
189+
return redirect('haps:events')
190+
191+
# Prevent duplicate processing
192+
if registration.payment_status == 'success':
193+
return redirect('haps:register_success', registration_id=registration.id)
194+
195+
# Check payment status
196+
try:
197+
status_data = check_order_status(client_txn_id)
198+
if not status_data:
199+
raise ValueError("Empty response from payment gateway")
200+
201+
# Store all payment diagnostic data
202+
registration.payment_data = {
203+
'customer_vpa': status_data.get('customer_vpa'),
204+
'upi_txn_id': status_data.get('upi_txn_id'),
205+
'status': status_data.get('status'),
206+
'remark': status_data.get('remark'),
207+
'txnAt': status_data.get('txnAt'),
208+
'merchant': status_data.get('Merchant', {}),
209+
'udf1': status_data.get('udf1'),
210+
'udf2': status_data.get('udf2'),
211+
'udf3': status_data.get('udf3'),
212+
'redirect_url': status_data.get('redirect_url'),
213+
'createdAt': status_data.get('createdAt')
214+
}
215+
216+
# Update payment status
217+
if status_data['status'] == 'success':
218+
registration.payment_status = 'success'
219+
registration.payment_date_time = status_data.get('txnAt')
220+
registration.save(update_fields=['payment_status', 'payment_date_time', 'payment_data'])
221+
messages.success(request, "Payment successful!")
222+
return redirect('haps:register_success', registration_id=registration.id)
223+
else:
224+
registration.payment_status = 'failure'
225+
registration.save(update_fields=['payment_status', 'payment_data'])
226+
return redirect('haps:register_failure',
227+
registration_id=registration.id,
228+
remark=status_data.get('remark', 'Payment failed'))
229+
230+
except Exception as e:
231+
log.error(f"Payment verification failed for registration {registration.id}: {str(e)}")
232+
registration.payment_data = {'error': str(e)}
233+
registration.payment_status = 'failure'
234+
registration.save(update_fields=['payment_status', 'payment_data'])
235+
return redirect('haps:register_failure',
236+
registration_id=registration.id,
237+
remark="Payment verification failed")
238+
239+
240+
def retry_payment(request: HttpRequest, registration_id: int):
241+
"""Retry failed payment"""
242+
try:
243+
registration = EventRegistration.objects.get(id=registration_id)
244+
except EventRegistration.DoesNotExist:
245+
raise Http404("Registration not found")
246+
247+
if registration.payment_status == 'success':
248+
messages.info(request, "Payment already completed")
249+
return redirect('haps:register_success', registration_id=registration.id)
250+
251+
# Log critical payment data before reset
252+
critical_logger.critical(
253+
f"Payment retry initiated for registration {registration.id}. "
254+
f"Previous payment data: order_id={registration.order_id}, "
255+
f"client_txn_id={registration.client_txn_id}, "
256+
f"payment_status={registration.payment_status}, "
257+
f"payment_data={registration.payment_data}"
29258
)
30259

31-
context = {"event": event, "user": request.user, "reg": registration}
260+
# Reset payment fields for retry
261+
registration.order_id = None
262+
registration.client_txn_id = None
263+
registration.payment_status = 'pending'
264+
registration.save(update_fields=['order_id', 'client_txn_id', 'payment_status'])
265+
266+
return redirect('haps:initiate_payment', registration_id=registration.id)
267+
268+
269+
def register_success(request: HttpRequest, registration_id: int):
270+
"""View for displaying registration success page."""
271+
try:
272+
registration = EventRegistration.objects.select_related('event', 'user').get(id=registration_id)
273+
except EventRegistration.DoesNotExist:
274+
raise Http404("Registration not found")
275+
276+
context = {
277+
"registration": registration,
278+
"event": registration.event,
279+
}
32280
return render(request, "haps/register_success.html", context=context)

‎src/static/styles.css

+79-189
Large diffs are not rendered by default.

‎src/utils/logging.py

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import logging
2+
import os
3+
from datetime import datetime
4+
from pathlib import Path
5+
6+
def setup_critical_logger():
7+
"""Setup logger for critical events that need to be preserved"""
8+
9+
# Create logs directory if it doesn't exist
10+
logs_dir = Path(__file__).parent.parent.parent / 'logs'
11+
logs_dir.mkdir(exist_ok=True)
12+
13+
# Create critical logger
14+
critical_logger = logging.getLogger('critical')
15+
critical_logger.setLevel(logging.CRITICAL)
16+
17+
# Create file handler
18+
log_file = logs_dir / 'critical.log'
19+
handler = logging.FileHandler(str(log_file))
20+
handler.setLevel(logging.CRITICAL)
21+
22+
# Create formatter
23+
formatter = logging.Formatter(
24+
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
25+
)
26+
handler.setFormatter(formatter)
27+
28+
# Add handler to logger
29+
critical_logger.addHandler(handler)
30+
31+
return critical_logger
32+
33+
# Create the logger instance
34+
critical_logger = setup_critical_logger()

‎src/donations/upi_gateway.py ‎src/utils/payment/upi_gateway.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ def create_order(
4040
"""
4141
if not customer_email:
4242
customer_email = "example@example.com"
43+
if not customer_mobile:
44+
customer_mobile = "9999999999"
4345
payload_dict = {
4446
"key": str(PaymentGatewayConfig.API_KEY),
4547
"client_txn_id": client_txn_id,
@@ -55,19 +57,25 @@ def create_order(
5557

5658
payload_json_str = json.dumps(payload_dict)
5759

60+
log.info(f"making payment request for {amount=} and {client_txn_id=}")
61+
log.debug(payload_dict)
62+
5863
response = requests.request(
5964
"POST",
6065
PaymentGatewayConfig.CREATE_ORDER,
6166
headers=PaymentGatewayConfig.REQUEST_HEADERS,
6267
data=payload_json_str,
6368
)
64-
69+
log.debug(response.text)
6570
if response.status_code == 200:
6671
rj = response.json()
6772
if rj["status"] is True:
73+
log.warning(rj)
6874
return True, rj["data"]
6975
else:
7076
log.warning("Failed to create order \n%s", response.text)
77+
log.warning("rj[data]", rj["data"])
78+
log.warning("rj", rj)
7179
return False, response
7280
else:
7381
log.warning(

0 commit comments

Comments
 (0)
Please sign in to comment.