diff --git a/config/settings.py b/config/settings.py index 8d5135f..588d21f 100644 --- a/config/settings.py +++ b/config/settings.py @@ -692,10 +692,17 @@ ZARINPAL_CALLBACK_URL = os.environ.get( 'ZARINPAL_CALLBACK_URL', f'{PUBLIC_BASE_URL}/api/payment/zarinpal/callback' if PUBLIC_BASE_URL else '', ) -# اختیاری: بعدِ verify، مرورگر/کلاینت را به این URL (مثلاً یک deep link اپ) -# با ?orderId=...&status=... ریدایرکت می‌کنیم. اگه خالی باشه، همون‌جا یک -# پاسخِ JSON ساده برمی‌گرده. -ZARINPAL_REDIRECT_URL = os.environ.get('ZARINPAL_REDIRECT_URL', '') +# آدرسِ خودِ اپ (فرانت) برای برگرداندنِ کاربر بعد از پرداخت. +FRONTEND_URL = ( + os.environ.get('FRONTEND_URL', '').strip() or 'https://math.tadrisino2.ir' +).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) diff --git a/docs/operations.md b/docs/operations.md index 9d8d2f2..1973d93 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -106,6 +106,7 @@ curl https://api.math.tadrisino2.ir/api/health | `CACHE_URL` | سرویس بالا می‌آید ولی نرخ بینِ workerها مشترک نیست؛ `/api/health` هشدار می‌دهد | | `KAVENEGAR_API_KEY` | `POST /api/auth/otp/request` خطای صریحِ `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 را می‌دهد | | `TEST_LOGIN_PHONES` | هیچ حسابِ تستی باز نیست (بخشِ بعد) | diff --git a/payment/tests.py b/payment/tests.py index fb8c834..41a52ca 100644 --- a/payment/tests.py +++ b/payment/tests.py @@ -1,6 +1,6 @@ import importlib import os -from unittest.mock import patch +from unittest.mock import MagicMock, patch from django.core.cache import cache from django.test import TestCase, override_settings @@ -560,6 +560,7 @@ class PaymentRedirectViewTests(TestCase): self.assertEqual(response.status_code, 404) +@override_settings(ZARINPAL_REDIRECT_URL='') class ZarinpalCallbackViewTests(TestCase): """ GET /api/payment/zarinpal/callback: امنیت از verify_payment میاد، نه از @@ -647,3 +648,51 @@ class ZarinpalCallbackViewTests(TestCase): self.assertEqual( 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', + ) diff --git a/payment/views.py b/payment/views.py index 31ea0d9..2268b27 100644 --- a/payment/views.py +++ b/payment/views.py @@ -201,6 +201,11 @@ class PaymentRedirectView(APIView): if order.status != 'pending': # سفارش از قبل نتیجه گرفته (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 '/') if not order.gateway_authority: