Skip to content

Commit 052ecf1

Browse files
authored
Merge pull request #2563 from gtech-mulearn/dev
feat: Add Razorpay subscription for recurring donations and auth proxy endpoints for mobile app
2 parents ec01851 + 59b320b commit 052ecf1

File tree

12 files changed

+1101
-79
lines changed

12 files changed

+1101
-79
lines changed

api/auth/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Auth proxy module - forwards requests to auth server

api/auth/auth_views.py

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import decouple
2+
import requests
3+
from rest_framework.views import APIView
4+
5+
from utils.response import CustomResponse
6+
7+
8+
AUTH_DOMAIN = decouple.config("AUTH_DOMAIN")
9+
10+
11+
class GoogleMobileAuthProxyAPI(APIView):
12+
"""
13+
Proxy endpoint for Google mobile authentication.
14+
Forwards requests to the auth server and returns the response.
15+
"""
16+
17+
def post(self, request):
18+
id_token = request.data.get("id_token") or request.data.get("idToken")
19+
20+
if not id_token:
21+
return CustomResponse(
22+
general_message="ID token is required"
23+
).get_failure_response()
24+
25+
try:
26+
response = requests.post(
27+
f"{AUTH_DOMAIN}/api/v1/auth/google-mobile/",
28+
json={"id_token": id_token},
29+
headers={"Content-Type": "application/json"},
30+
timeout=30,
31+
)
32+
33+
data = response.json()
34+
35+
if data.get("hasError"):
36+
return CustomResponse(
37+
general_message=data.get("message", {}).get("general", ["Authentication failed"])[0]
38+
).get_failure_response()
39+
40+
return CustomResponse(
41+
general_message="Access Granted",
42+
response=data.get("response"),
43+
).get_success_response()
44+
45+
except requests.exceptions.Timeout:
46+
return CustomResponse(
47+
general_message="Authentication server timeout"
48+
).get_failure_response()
49+
except requests.exceptions.RequestException as e:
50+
return CustomResponse(
51+
general_message=f"Authentication server error: {str(e)}"
52+
).get_failure_response()
53+
54+
55+
class AppleMobileAuthProxyAPI(APIView):
56+
"""
57+
Proxy endpoint for Apple mobile authentication.
58+
Forwards requests to the auth server and returns the response.
59+
"""
60+
61+
def post(self, request):
62+
identity_token = request.data.get("identity_token") or request.data.get("identityToken")
63+
email = request.data.get("email")
64+
65+
if not identity_token:
66+
return CustomResponse(
67+
general_message="Identity token is required"
68+
).get_failure_response()
69+
70+
try:
71+
payload = {"identity_token": identity_token}
72+
if email:
73+
payload["email"] = email
74+
75+
response = requests.post(
76+
f"{AUTH_DOMAIN}/api/v1/auth/apple-mobile/",
77+
json=payload,
78+
headers={"Content-Type": "application/json"},
79+
timeout=30,
80+
)
81+
82+
data = response.json()
83+
84+
if data.get("hasError"):
85+
return CustomResponse(
86+
general_message=data.get("message", {}).get("general", ["Authentication failed"])[0]
87+
).get_failure_response()
88+
89+
return CustomResponse(
90+
general_message="Access Granted",
91+
response=data.get("response"),
92+
).get_success_response()
93+
94+
except requests.exceptions.Timeout:
95+
return CustomResponse(
96+
general_message="Authentication server timeout"
97+
).get_failure_response()
98+
except requests.exceptions.RequestException as e:
99+
return CustomResponse(
100+
general_message=f"Authentication server error: {str(e)}"
101+
).get_failure_response()
102+
103+
104+
class RefreshTokenProxyAPI(APIView):
105+
"""
106+
Proxy endpoint for token refresh.
107+
Forwards requests to the auth server and returns fresh tokens.
108+
"""
109+
110+
def post(self, request):
111+
refresh_token = request.data.get("refreshToken") or request.data.get("refresh_token")
112+
113+
if not refresh_token:
114+
return CustomResponse(
115+
general_message="Refresh token is required"
116+
).get_failure_response()
117+
118+
try:
119+
response = requests.post(
120+
f"{AUTH_DOMAIN}/api/v1/auth/get-access-token/",
121+
json={"refreshToken": refresh_token},
122+
headers={"Content-Type": "application/json"},
123+
timeout=30,
124+
)
125+
126+
data = response.json()
127+
128+
if data.get("hasError"):
129+
return CustomResponse(
130+
general_message=data.get("message", {}).get("general", ["Token refresh failed"])[0]
131+
).get_failure_response()
132+
133+
return CustomResponse(
134+
response=data.get("response"),
135+
).get_success_response()
136+
137+
except requests.exceptions.Timeout:
138+
return CustomResponse(
139+
general_message="Authentication server timeout"
140+
).get_failure_response()
141+
except requests.exceptions.RequestException as e:
142+
return CustomResponse(
143+
general_message=f"Authentication server error: {str(e)}"
144+
).get_failure_response()

api/auth/urls.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from django.urls import path
2+
3+
from . import auth_views
4+
5+
urlpatterns = [
6+
path('google-mobile/', auth_views.GoogleMobileAuthProxyAPI.as_view(), name='google_mobile_proxy'),
7+
path('apple-mobile/', auth_views.AppleMobileAuthProxyAPI.as_view(), name='apple_mobile_proxy'),
8+
path('refresh-token/', auth_views.RefreshTokenProxyAPI.as_view(), name='refresh_token_proxy'),
9+
]

api/donate/donate_serializer.py

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,65 @@
44
from django.conf import settings
55

66
from db.donor import Donor
7+
from db.donation import Donation
78
from utils.utils import DateTimeUtils
89

910

1011
class DonorSerializer(serializers.ModelSerializer):
11-
currency = serializers.CharField(allow_null=True, allow_blank=True, default='INR')
12+
"""Serializer for Donor model - personal info only"""
13+
company = serializers.CharField(allow_null=True, allow_blank=True, required=False)
14+
phone_number = serializers.CharField(allow_null=True, allow_blank=True, required=False)
15+
pan_number = serializers.CharField(allow_null=True, allow_blank=True, required=False)
16+
address = serializers.CharField(allow_null=True, allow_blank=True, required=False)
17+
is_organisation = serializers.BooleanField(default=False)
1218

1319
class Meta:
1420
model = Donor
15-
exclude = ['created_by', 'created_at', 'id', 'payment_id', 'payment_method']
21+
exclude = ['created_by', 'created_at', 'id']
1622

1723
def create(self, validated_data):
1824
validated_data["created_by_id"] = settings.SYSTEM_ADMIN_ID
1925
validated_data["id"] = uuid.uuid4()
2026
return Donor.objects.create(**validated_data)
27+
28+
29+
class DonationSerializer(serializers.ModelSerializer):
30+
"""Serializer for Donation model - payment tracking"""
31+
donation_type = serializers.ChoiceField(choices=['one-time', 'monthly', 'yearly'])
32+
33+
class Meta:
34+
model = Donation
35+
exclude = ['id', 'created_at']
36+
37+
def create(self, validated_data):
38+
validated_data["id"] = uuid.uuid4()
39+
return Donation.objects.create(**validated_data)
40+
41+
42+
class SubscriptionSerializer(serializers.Serializer):
43+
"""Serializer for subscription creation requests"""
44+
amount = serializers.DecimalField(max_digits=12, decimal_places=2)
45+
currency = serializers.CharField(default='INR')
46+
name = serializers.CharField(max_length=255)
47+
email = serializers.EmailField()
48+
phone_number = serializers.CharField(max_length=20, required=False, allow_blank=True)
49+
pan_number = serializers.CharField(max_length=10, required=False, allow_blank=True)
50+
address = serializers.CharField(required=False, allow_blank=True)
51+
company = serializers.CharField(max_length=255, required=False, allow_blank=True)
52+
donation_type = serializers.ChoiceField(choices=['one-time', 'monthly', 'yearly'])
53+
is_organisation = serializers.BooleanField(default=False)
54+
55+
56+
class OrderSerializer(serializers.Serializer):
57+
"""Serializer for one-time order creation requests"""
58+
amount = serializers.DecimalField(max_digits=12, decimal_places=2)
59+
currency = serializers.CharField(default='INR')
60+
name = serializers.CharField(max_length=255)
61+
email = serializers.EmailField()
62+
phone_number = serializers.CharField(max_length=20, required=False, allow_blank=True)
63+
pan_number = serializers.CharField(max_length=10, required=False, allow_blank=True)
64+
address = serializers.CharField(required=False, allow_blank=True)
65+
company = serializers.CharField(max_length=255, required=False, allow_blank=True)
66+
donation_type = serializers.ChoiceField(choices=['one-time', 'monthly', 'yearly'], default='one-time')
67+
is_organisation = serializers.BooleanField(default=False)
68+

api/donate/urls.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
1-
from . import views
21
from django.urls import path
2+
from .views import (
3+
RazorPayOrderAPI,
4+
RazorPayVerification,
5+
RazorPaySubscriptionAPI,
6+
RazorPaySubscriptionVerification
7+
)
38

49
urlpatterns = [
5-
path('order/', views.RazorPayOrderAPI.as_view()),
6-
path('verify/', views.RazorPayVerification.as_view())
10+
# One-time payments
11+
path('order/', RazorPayOrderAPI.as_view(), name='donate-order'),
12+
path('verify/', RazorPayVerification.as_view(), name='donate-verify'),
13+
14+
# Recurring payments (subscriptions)
15+
path('subscription/create/', RazorPaySubscriptionAPI.as_view(), name='donate-subscription-create'),
16+
path('subscription/verify/', RazorPaySubscriptionVerification.as_view(), name='donate-subscription-verify'),
717
]

0 commit comments

Comments
 (0)