Files
MathApp/affiliate/services.py
Zareian 0580aec7b0 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>
2026-09-25 12:28:06 +03:30

48 lines
2.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
پورسانتِ معرف: فقط روی پرداختِ واقعی، فقط یک بار برای هر کاربرِ معرفی‌شده.
تا ۲۰۲۶-۰۹-۲۵ هیچ کدی Conversion را از «pending» جلو نمی‌برد و پورسانتی
حساب نمی‌شد؛ برنامه‌ی معرفی فقط یک جدولِ ثبت بود. حالا اولین سفارشِ
پرداخت‌شده‌ی کاربرِ معرفی‌شده *بعد از ثبتِ کد* Conversion را `paid` می‌کند و
`commission_amount = مبلغِ پرداختی × commission_rate` را ثبت می‌کند.
⚠️ «کاربرِ تازه» بررسی نمی‌شود: مشتریِ قدیمی هم هر وقت بخواهد کد ثبت می‌کند
و تمدیدِ بعدی‌اش پورسانت می‌سازد (بازبینیِ ۲۰۲۶-۰۹-۲۵، بازتولید شد). جلوی
معرفیِ خود با شماره‌ی دوم هم چیزی نیست.
پرداختِ خودِ پورسانت به معرف (تسویه) هنوز دستی و از پنلِ ادمین است.
ثبت‌نام یا ورودِ مهمان هیچ پورسانتی نمی‌سازد؛ فقط پول جابه‌جا شدنِ واقعی.
"""
import logging
from django.db import transaction
from .models import Conversion
logger = logging.getLogger(__name__)
def record_paid_order(order) -> Conversion | None:
"""از apply_order_payment_result صدا زده می‌شود، داخلِ همان تراکنش."""
if not order.amount:
return None
try:
with transaction.atomic():
conversion = (
Conversion.objects.select_for_update()
.select_related('affiliate_account')
.filter(referred_user_id=order.user_id, status='pending')
.first()
)
if conversion is None:
return None
rate = conversion.affiliate_account.commission_rate or 0
conversion.status = 'paid'
conversion.order_id = order.id
conversion.commission_amount = int(order.amount * rate)
conversion.save(update_fields=['status', 'order_id', 'commission_amount'])
return conversion
except Exception: # noqa: BLE001 — پورسانت نباید تاییدِ پرداختِ خودِ کاربر را خراب کند
logger.exception('affiliate: ثبتِ پورسانتِ سفارش %s شکست خورد', order.id)
return None