harden OTP for real SMS and close the second audit round
Kavenegar is being switched on, so every OTP request now costs credit: - phone numbers are normalised to 09XXXXXXXXX everywhere (login, OTP, family invite, TEST_LOGIN_PHONES keys) and existing rows are migrated; non-Iranian and landline numbers are rejected before any SMS - cooldown and a daily per-phone cap are counted in the database, so the three gunicorn workers no longer triple them - a global daily OTP SMS cap (default 300) stops SMS pumping - per-IP rates widened for CGNAT / school Wi-Fi (env-overridable) - provider message id is logged; SMS text warns not to share the code Second audit round: an offer no longer shortens a longer paid plan, Zarinpal error bodies no longer 500, island XP reaches the parent report, a non-numeric questionId no longer 500s after the answer is generated. 596 tests green. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -125,6 +125,11 @@ class PaymentRedirectView(APIView):
|
||||
URL باز شود authority ساخته و روی سفارش ذخیره میشود؛ بازکردنِ دوباره
|
||||
(مثلاً دکمهی back مرورگر) همان authority قبلی را دوباره استفاده
|
||||
میکند، نه یک PaymentRequest تازه.
|
||||
|
||||
⚠️ AllowAny است و مالکیتِ سفارش را نمیسنجد (مرورگر هدرِ Bearer نمیفرستد)؛
|
||||
شناسهی سفارش ترتیبی است. هر کسی با باز کردنِ `/orders/<n>/pay` برای سفارشِ
|
||||
دیگران authority میسازد و از آن به بعد صاحبِ سفارش دیگر نمیتواند کدِ
|
||||
تخفیف بزند (بازبینیِ ۲۰۲۶-۰۹-۲۵، بازتولید شد).
|
||||
"""
|
||||
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
@@ -120,12 +120,23 @@ def verify_payment(*, authority: str, amount: int, currency: str) -> ZarinpalVer
|
||||
raise ZarinpalAPIError()
|
||||
|
||||
try:
|
||||
result_data = resp.json()['data']
|
||||
code = result_data.get('code')
|
||||
body = resp.json()
|
||||
result_data = body['data']
|
||||
except (ValueError, KeyError, TypeError):
|
||||
logger.error('Zarinpal payment-verify response malformed: status=%s', resp.status_code)
|
||||
raise ZarinpalAPIError()
|
||||
|
||||
# زرینپال روی خطا (مثلاً -51 پرداختِ ناموفق) `"data": []` و کدِ خطا را در
|
||||
# `errors` برمیگرداند. این یک «رد»ِ صریح است، نه خرابیِ شبکه، پس مثلِ هر کدِ
|
||||
# غیرِ ۱۰۰/۱۰۱ نامعتبر حساب میشود. پیش از ۲۰۲۶-۰۹-۲۵ اینجا `[].get` کرش
|
||||
# میکرد و کاربر بهجای برگشت به اپ صفحهی ۵۰۰ میدید. با بدنهی مستندِ
|
||||
# زرینپال تست شده، نه با درگاهِ واقعی.
|
||||
if not isinstance(result_data, dict):
|
||||
errors = body.get('errors') if isinstance(body, dict) else None
|
||||
logger.warning('Zarinpal payment-verify rejected: errors=%s', errors)
|
||||
return ZarinpalVerifyResult(is_valid=False, raw=body if isinstance(body, dict) else None)
|
||||
code = result_data.get('code')
|
||||
|
||||
# ۱۰۰ = تاییدِ موفقِ تازه، ۱۰۱ = قبلاً تایید شده (idempotent از سمتِ زرینپال) — هر دو معتبرن.
|
||||
if code in (100, 101):
|
||||
return ZarinpalVerifyResult(is_valid=True, ref_id=str(result_data.get('ref_id', '')), raw=result_data)
|
||||
|
||||
Reference in New Issue
Block a user