send the student back to the web app after paying, not to a raw JSON page
ZARINPAL_REDIRECT_URL was empty on the server, so after a real (non-sandbox)
payment the callback answered with {"orderId","status"} on the API domain and
the student was stranded there. It now defaults to FRONTEND_URL
(https://math.tadrisino2.ir/) with ?orderId=&status=; reopening a finished
order's pay link goes back to the app the same way. "-" keeps the old JSON.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -692,10 +692,17 @@ ZARINPAL_CALLBACK_URL = os.environ.get(
|
|||||||
'ZARINPAL_CALLBACK_URL',
|
'ZARINPAL_CALLBACK_URL',
|
||||||
f'{PUBLIC_BASE_URL}/api/payment/zarinpal/callback' if PUBLIC_BASE_URL else '',
|
f'{PUBLIC_BASE_URL}/api/payment/zarinpal/callback' if PUBLIC_BASE_URL else '',
|
||||||
)
|
)
|
||||||
# اختیاری: بعدِ verify، مرورگر/کلاینت را به این URL (مثلاً یک deep link اپ)
|
# آدرسِ خودِ اپ (فرانت) برای برگرداندنِ کاربر بعد از پرداخت.
|
||||||
# با ?orderId=...&status=... ریدایرکت میکنیم. اگه خالی باشه، همونجا یک
|
FRONTEND_URL = (
|
||||||
# پاسخِ JSON ساده برمیگرده.
|
os.environ.get('FRONTEND_URL', '').strip() or 'https://math.tadrisino2.ir'
|
||||||
ZARINPAL_REDIRECT_URL = os.environ.get('ZARINPAL_REDIRECT_URL', '')
|
).rstrip('/')
|
||||||
|
# بعدِ verify، مرورگر را به این URL با ?orderId=...&status=... برمیگردانیم
|
||||||
|
# (برای اپِ موبایل میتواند یک deep link باشد). پیشفرض خودِ اپِ وب است؛ قبلاً
|
||||||
|
# خالی بود و دانشآموز بعد از پرداختِ واقعی روی یک صفحهی JSONِ خام روی دامنهی
|
||||||
|
# API میماند. مقدارِ صریحِ `-` یعنی همان پاسخِ JSON (برای تست/ابزار).
|
||||||
|
ZARINPAL_REDIRECT_URL = os.environ.get('ZARINPAL_REDIRECT_URL', '').strip() or f'{FRONTEND_URL}/'
|
||||||
|
if ZARINPAL_REDIRECT_URL == '-':
|
||||||
|
ZARINPAL_REDIRECT_URL = ''
|
||||||
|
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
# RAG / AI provider (اپ rag + ai_tutor.generate_tutor_answer)
|
# RAG / AI provider (اپ rag + ai_tutor.generate_tutor_answer)
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ curl https://api.math.tadrisino2.ir/api/health
|
|||||||
| `CACHE_URL` | سرویس بالا میآید ولی نرخ بینِ workerها مشترک نیست؛ `/api/health` هشدار میدهد |
|
| `CACHE_URL` | سرویس بالا میآید ولی نرخ بینِ workerها مشترک نیست؛ `/api/health` هشدار میدهد |
|
||||||
| `KAVENEGAR_API_KEY` | `POST /api/auth/otp/request` خطای صریحِ `503` میدهد، نه موفقیتِ ساختگی |
|
| `KAVENEGAR_API_KEY` | `POST /api/auth/otp/request` خطای صریحِ `503` میدهد، نه موفقیتِ ساختگی |
|
||||||
| `ZARINPAL_MERCHANT_ID` | مسیرِ پرداخت fail-closed میماند (`503`) |
|
| `ZARINPAL_MERCHANT_ID` | مسیرِ پرداخت fail-closed میماند (`503`) |
|
||||||
|
| `FRONTEND_URL` · `ZARINPAL_REDIRECT_URL` | بعد از پرداخت کاربر به `https://math.tadrisino2.ir/?orderId=…&status=paid\|failed` برمیگردد. `ZARINPAL_REDIRECT_URL=-` یعنی همان پاسخِ JSONِ قدیمی |
|
||||||
| `OPENAI_API_KEY` · `QDRANT_API_KEY` | `POST /api/ai/ask` همان پاسخِ stub را میدهد |
|
| `OPENAI_API_KEY` · `QDRANT_API_KEY` | `POST /api/ai/ask` همان پاسخِ stub را میدهد |
|
||||||
| `TEST_LOGIN_PHONES` | هیچ حسابِ تستی باز نیست (بخشِ بعد) |
|
| `TEST_LOGIN_PHONES` | هیچ حسابِ تستی باز نیست (بخشِ بعد) |
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import importlib
|
import importlib
|
||||||
import os
|
import os
|
||||||
from unittest.mock import patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
@@ -560,6 +560,7 @@ class PaymentRedirectViewTests(TestCase):
|
|||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(ZARINPAL_REDIRECT_URL='')
|
||||||
class ZarinpalCallbackViewTests(TestCase):
|
class ZarinpalCallbackViewTests(TestCase):
|
||||||
"""
|
"""
|
||||||
GET /api/payment/zarinpal/callback: امنیت از verify_payment میاد، نه از
|
GET /api/payment/zarinpal/callback: امنیت از verify_payment میاد، نه از
|
||||||
@@ -647,3 +648,51 @@ class ZarinpalCallbackViewTests(TestCase):
|
|||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
response.url, f'tadrisino://payment-result?orderId={self.order.id}&status=paid',
|
response.url, f'tadrisino://payment-result?orderId={self.order.id}&status=paid',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PaymentReturnsToTheAppTests(TestCase):
|
||||||
|
"""
|
||||||
|
بعد از پرداخت، کاربر به خودِ اپِ وب برمیگردد. قبلاً ZARINPAL_REDIRECT_URL
|
||||||
|
روی سرور خالی بود و دانشآموز بعد از پرداختِ واقعی روی یک صفحهی JSONِ
|
||||||
|
خام روی دامنهی API میماند.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
cache.clear()
|
||||||
|
self.client = APIClient()
|
||||||
|
|
||||||
|
def test_default_redirect_is_the_web_app(self):
|
||||||
|
from django.conf import settings
|
||||||
|
self.assertEqual(settings.ZARINPAL_REDIRECT_URL, f'{settings.FRONTEND_URL}/')
|
||||||
|
|
||||||
|
@override_settings(ZARINPAL_REDIRECT_URL='https://math.tadrisino2.ir/')
|
||||||
|
@patch('payment.views.zarinpal.verify_payment')
|
||||||
|
def test_callback_sends_the_student_back_with_the_result(self, mock_verify):
|
||||||
|
mock_verify.return_value = MagicMock(is_valid=False, ref_id='')
|
||||||
|
order = Order.objects.create(
|
||||||
|
user_id=1, amount=100000, currency='IRT', provider='gateway',
|
||||||
|
gateway_authority='A-return-1', idempotency_key='ret-1',
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.get('/api/payment/zarinpal/callback?Authority=A-return-1&Status=NOK')
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(
|
||||||
|
response['Location'],
|
||||||
|
f'https://math.tadrisino2.ir/?orderId={order.id}&status=failed',
|
||||||
|
)
|
||||||
|
|
||||||
|
@override_settings(ZARINPAL_REDIRECT_URL='https://math.tadrisino2.ir/')
|
||||||
|
def test_reopening_a_finished_order_goes_back_to_the_app(self):
|
||||||
|
order = Order.objects.create(
|
||||||
|
user_id=1, amount=100000, currency='IRT', provider='gateway',
|
||||||
|
status='paid', idempotency_key='ret-2',
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.get(f'/api/payment/orders/{order.id}/pay')
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(
|
||||||
|
response['Location'],
|
||||||
|
f'https://math.tadrisino2.ir/?orderId={order.id}&status=paid',
|
||||||
|
)
|
||||||
|
|||||||
@@ -201,6 +201,11 @@ class PaymentRedirectView(APIView):
|
|||||||
|
|
||||||
if order.status != 'pending':
|
if order.status != 'pending':
|
||||||
# سفارش از قبل نتیجه گرفته (paid/failed)؛ چیزی برای پرداختِ تازه نیست.
|
# سفارش از قبل نتیجه گرفته (paid/failed)؛ چیزی برای پرداختِ تازه نیست.
|
||||||
|
# کاربر به اپ برمیگردد با همان نتیجه، نه به ریشهی دامنهی API.
|
||||||
|
if settings.ZARINPAL_REDIRECT_URL:
|
||||||
|
return _redirect_to_configured_url(
|
||||||
|
f'{settings.ZARINPAL_REDIRECT_URL}?orderId={order.id}&status={order.status}'
|
||||||
|
)
|
||||||
return HttpResponseRedirect(settings.PUBLIC_BASE_URL or '/')
|
return HttpResponseRedirect(settings.PUBLIC_BASE_URL or '/')
|
||||||
|
|
||||||
if not order.gateway_authority:
|
if not order.gateway_authority:
|
||||||
|
|||||||
Reference in New Issue
Block a user