cleanup: trim bloated multi-line comments across the project
This commit is contained in:
@@ -14,31 +14,21 @@ class Plan(models.Model):
|
||||
price = models.PositiveIntegerField()
|
||||
currency = models.CharField(max_length=10, default='IRT')
|
||||
discount_percent = models.PositiveSmallIntegerField(default=0)
|
||||
popular = models.BooleanField(default=False) # <-- اصلاح/افزوده شده
|
||||
# ⚠️ اصلاح دیگه: سند طراحی هیچ فیلد durationای برای Plan نداره، در
|
||||
# نتیجهی این نبود، activate_subscription قبلاً مدت اشتراک رو با یک
|
||||
# قاعدهی stub (چک کردن کلمهی «سالانه»/yearly در عنوان) حدس میزد که
|
||||
# شکننده و وابسته به متن title بود. duration_days اینجا بهعنوان منبع
|
||||
# صریح و قابلاتکای مدت اشتراک اضافه شده؛ پیشفرض ۳۰ روز.
|
||||
duration_days = models.PositiveIntegerField(default=30) # <-- اصلاح/افزوده شده
|
||||
popular = models.BooleanField(default=False)
|
||||
# منبعِ صریحِ مدتِ اشتراک؛ بدونش activate_subscription مجبور بود مدت رو
|
||||
# از رویِ متنِ title (مثلاً کلمهی «سالانه») حدس بزنه.
|
||||
duration_days = models.PositiveIntegerField(default=30)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
|
||||
class Subscription(models.Model):
|
||||
"""
|
||||
⚠️ اصلاح نسبت به سند اولیه: STATUS_CHOICES در سند فقط شامل
|
||||
active/expired/canceled بود. اما در جریان POST /api/subscription/purchase
|
||||
یک Subscription قبل از تایید پرداخت (که با payment webhook میاد) ساخته
|
||||
میشه؛ بدون یک وضعیت میانی، راهی برای نمایش «سفارش ثبت شده ولی هنوز
|
||||
پرداخت تایید نشده» وجود نداره. برای همین وضعیت pending اضافه و بهعنوان
|
||||
پیشفرض ساخت (بهجای active در سند) تنظیم شده؛ فعالسازی واقعی توسط
|
||||
subscription.services.activate_subscription بعد از webhook پرداخت انجام
|
||||
میشه.
|
||||
"""
|
||||
"""وضعیتِ pending برای فاصلهی بینِ ساختنِ Subscription (در purchase) و
|
||||
تاییدِ پرداخت (webhook)؛ فعالسازیِ واقعی توسطِ
|
||||
subscription.services.activate_subscription انجام میشه."""
|
||||
STATUS_CHOICES = [
|
||||
('pending', 'در انتظار پرداخت'), # <-- اصلاح/افزوده شده
|
||||
('pending', 'در انتظار پرداخت'),
|
||||
('active', 'فعال'),
|
||||
('expired', 'منقضی'),
|
||||
('canceled', 'لغوشده'),
|
||||
@@ -50,19 +40,11 @@ class Subscription(models.Model):
|
||||
started_at = models.DateTimeField(auto_now_add=True)
|
||||
expires_at = models.DateTimeField(null=True, blank=True)
|
||||
order_id = models.IntegerField(null=True, blank=True) # ارجاع سبک به payment.Order
|
||||
# 🔒 اصلاحِ رگرسیونِ concurrency در PurchaseView.post: چون Subscription
|
||||
# فقط یک ردیف در ازای هر user_id داره، دو درخواستِ همزمانِ خریدِ همون
|
||||
# کاربر (مثلاً با همون plan) میتونن دقیقاً به یک وضعیتِ میانیِ یکسان
|
||||
# برسن (status='pending', همون plan_id, order_id=None) — یعنی مقایسهی
|
||||
# مقادیرِ فیلدها (که قبلاً بهعنوانِ CAS guard در cleanup استفاده میشد)
|
||||
# نمیتونه بینِ «ردیفِ نوشتهشده توسطِ خودِ همین درخواست» و «ردیفِ
|
||||
# نوشتهشده توسطِ یک درخواستِ همزمانِ دیگه» تمایز بذاره. version یک
|
||||
# شمارندهی صریحه که PurchaseView.post هر بار که واقعاً روی این ردیف
|
||||
# مینویسه (با UPDATE اتمیکِ version=F('version')+1، بدونِ نیاز به
|
||||
# select_for_update/نگهداشتنِ قفل) افزایشش میده؛ پس حتی وقتی محتوای
|
||||
# semantic فیلدهای دیگه بینِ دو نویسندهی همزمان یکسانه، version همیشه
|
||||
# بینِ نوشتههای مجزا فرق میکنه و میتونه بهعنوانِ CAS token قابلاتکا
|
||||
# برای cleanup استفاده بشه.
|
||||
# چون Subscription فقط یک ردیف در ازای هر user_id داره، دو خریدِ همزمانِ
|
||||
# همون کاربر میتونن به وضعیتِ میانیِ کاملاً یکسانی برسن (مقایسهی
|
||||
# فیلدها کافی نیست برای CAS). version با UPDATEِ اتمیکِ
|
||||
# version=F('version')+1 (بدونِ select_for_update) هر نوشتنِ واقعی رو
|
||||
# از نوشتنِ همزمانِ دیگری تمیز میده.
|
||||
version = models.PositiveIntegerField(default=0)
|
||||
|
||||
def __str__(self):
|
||||
|
||||
@@ -362,12 +362,9 @@ class PurchaseThrottleTests(TestCase):
|
||||
self.assertEqual(blocked_response.status_code, 429)
|
||||
|
||||
def test_changing_client_ip_does_not_bypass_the_user_limit(self):
|
||||
# ⚠️ توجه: از یک کاربرِ *غیرمهمان* استفاده میشه (نه self.user که
|
||||
# مهمان است)، چون طبقِ فیکسِ GuestAwarePurchaseThrottle حالا کلیدِ
|
||||
# bucket برای کاربرهای مهمان عمداً بر اساسِ IP است (نگاه کنید به
|
||||
# PurchaseGuestThrottleBypassTests پایینتر) — این تست فقط رفتارِ
|
||||
# per-user (نه IP) برای کاربرهایِ ثبتنامشده رو تضمین میکنه که
|
||||
# باید دقیقاً مثلِ قبل دستنخورده بمونه.
|
||||
# کاربرِ غیرمهمان لازمه، نه self.user، چون کلیدِ bucketِ مهمانها
|
||||
# عمداً بر اساسِ IP است (نگاه کنید به PurchaseGuestThrottleBypassTests)
|
||||
# و این تست رفتارِ per-user برای ثبتنامشدهها را میسنجد.
|
||||
registered_user = User.objects.create(is_guest=False, name='', phone='+989120000501')
|
||||
registered_token = issue_access_token(registered_user.id)
|
||||
|
||||
|
||||
@@ -21,27 +21,10 @@ from .serializers import (
|
||||
|
||||
|
||||
class GuestAwarePurchaseThrottle(ScopedRateThrottle):
|
||||
"""
|
||||
⚠️ اضافه شده: همون مشکلِ GuestAwareAIThrottle (ai_tutor/views.py)،
|
||||
GuestAwareFamilyThrottle (family/views.py) و GuestAwareDiscountThrottle
|
||||
(discount/views.py) اینجا هم هست. ScopedRateThrottle استاندارد برای
|
||||
درخواستِ احرازهویتشده از request.user.pk بهعنوانِ کلیدِ throttle
|
||||
استفاده میکنه؛ چون GuestSessionView با هر deviceId جدید یک کاربرِ
|
||||
مهمانِ تازه (pk جدید) میسازه، یک مهاجم میتونست با ساختنِ پیدرپیِ
|
||||
حسابهای مهمان (تا سقفِ guest_session=20/hour)، برای هر حساب یک
|
||||
bucket تازه از throttle_scope 'purchase' بگیره — یعنی سقفِ ۱۰/ساعتِ
|
||||
درخواستِ purchase عملاً با چرخشِ حساب مهمان چند برابر میشد. این
|
||||
endpoint حساسِ مالیه: هر درخواستِ پذیرفتهشده وارد جریانِ purchase
|
||||
میشه، یک Subscription میسازه/آپدیت میکنه و create_order() رو صدا
|
||||
میزنه (که در پروداکشن با درگاهِ واقعی جایگزینِ stub فعلی میشه).
|
||||
|
||||
دقیقاً طبقِ همون الگو: فقط برای کاربرهای مهمان (is_guest=True) کلیدِ
|
||||
cache از pk کاربر به IP کلاینت (get_ident) تغییر میکنه، پس همهی
|
||||
حسابهای مهمانِ ساختهشده از یک IP یک bucket مشترک دارن و چرخشِ حساب
|
||||
دیگه bucket تازه نمیده. رفتارِ throttle برای کاربرهای غیرمهمان
|
||||
(ثبتنامشده) دقیقاً مثلِ قبل (کلید بر اساسِ pk) باقی میمونه؛ منطقِ
|
||||
purchase/subscription/order/Idempotency-Key/plan-validation دستنخوردهست.
|
||||
"""
|
||||
"""همون الگوی GuestAwareAIThrottle/GuestAwareFamilyThrottle/etc: چرخشِ
|
||||
حساب مهمان نباید bucket تازهی purchase بده — این endpoint حساسِ مالیه
|
||||
(Subscription میسازه/آپدیت میکنه، create_order رو صدا میزنه). برای
|
||||
مهمانها کلیدِ cache بر اساسِ IP هست، نه pk."""
|
||||
|
||||
def get_cache_key(self, request, view):
|
||||
user = request.user
|
||||
@@ -85,20 +68,12 @@ class PurchaseView(APIView):
|
||||
to create an Order."""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
# scope کیدهای cache رو با pk کاربرِ احرازهویتشده میسازه (نه فقط IP)؛
|
||||
# چون Idempotency-Key بهتنهایی جلوی حجمِ بالای درخواستهای *متفاوت*
|
||||
# (با کلیدهای idempotency متفاوت) رو نمیگیره، این throttle مستقل و
|
||||
# مکملِ اون محافظته. ScopedRateThrottle خودش وقتی request.user
|
||||
# احرازهویتشده باشه (is_authenticated=True) بهجای IP از
|
||||
# request.user.pk بهعنوانِ ident استفاده میکنه، پس تغییرِ IP از
|
||||
# همون اکانت این محدودیت رو دور نمیزنه.
|
||||
# ⚠️ اصلاح شده: چون کاربرهای مهمان (GuestSessionView) با هر deviceId
|
||||
# جدید یک pk تازه میگیرن، ScopedRateThrottleِ ساده (کلید بر اساسِ
|
||||
# user.pk) توسطِ چرخشِ حساب مهمان قابلدورزدن بود. حالا از
|
||||
# GuestAwarePurchaseThrottle استفاده میشه که برای کاربرهای مهمان
|
||||
# کلید رو بر اساسِ IP میسازه (دقیقاً همون الگویِ GuestAwareAIThrottle،
|
||||
# GuestAwareFamilyThrottle و GuestAwareDiscountThrottle) و برای
|
||||
# کاربرهای غیرمهمان رفتارِ per-user قبلی رو دستنخورده نگه میداره.
|
||||
# این throttle مستقلِ Idempotency-Key است چون اون فقط جلوی تکرارِ *همون*
|
||||
# کلید رو میگیره، نه حجمِ درخواستهای متفاوت. کاربرهای مهمان با هر
|
||||
# deviceId یک pk تازه میگیرن، پس کلیدِ per-user ساده قابلدورزدنه؛
|
||||
# GuestAwarePurchaseThrottle برای مهمانها بر اساسِ IP کلید میسازه
|
||||
# (مثلِ GuestAwareAIThrottle/FamilyThrottle/DiscountThrottle) و برای
|
||||
# کاربرهای غیرمهمان رفتارِ per-user را حفظ میکنه.
|
||||
throttle_scope = 'purchase'
|
||||
throttle_classes = [GuestAwarePurchaseThrottle]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user