Update project

This commit is contained in:
2026-08-19 17:15:55 +03:30
parent 02d40e1238
commit f945f960c9
25 changed files with 1092 additions and 287 deletions

View File

@@ -1,3 +1,4 @@
from django.db import transaction
from django.utils import timezone
from statuslog.signals import status_changed
@@ -12,23 +13,32 @@ def activate_subscription(subscription_id: int):
(قبلاً چون Plan فیلد duration‌ای نداشت، این مقدار با یک قاعده‌ی stub
بر اساس متن title حدس زده می‌شد؛ الان duration_days مستقیماً از مدل
Plan خونده می‌شه.)
⚛️ atomic: خودِ این تابع هم مستقل (بدون اتکا به atomic بودنِ caller)
اتمیکه، چون به‌عنوان یک سرویسِ مشترک از جاهای مختلف (payment webhook،
bazaar verify) صدا زده می‌شه؛ save روی Subscription و نوشتنِ
StatusChangeLog که از signal.send نتیجه می‌شه، باید یک واحد بمونن.
"""
try:
subscription = Subscription.objects.get(pk=subscription_id)
except Subscription.DoesNotExist:
return None
with transaction.atomic():
try:
# 🔒 select_for_update اضافه شد تا این تابع هم مثل
# grant_free_subscription، صرف‌نظر از caller، در برابر
# concurrent update ایمن باشه.
subscription = Subscription.objects.select_for_update().get(pk=subscription_id)
except Subscription.DoesNotExist:
return None
old_status = subscription.status
duration_days = subscription.plan.duration_days if subscription.plan else 30
subscription.status = 'active'
subscription.expires_at = timezone.now() + timezone.timedelta(days=duration_days)
subscription.save(update_fields=['status', 'expires_at'])
old_status = subscription.status
duration_days = subscription.plan.duration_days if subscription.plan else 30
subscription.status = 'active'
subscription.expires_at = timezone.now() + timezone.timedelta(days=duration_days)
subscription.save(update_fields=['status', 'expires_at'])
status_changed.send(
sender='subscription.Subscription', object_id=subscription.id,
old_status=old_status, new_status='active',
event='payment_confirmed', user_id=subscription.user_id,
)
status_changed.send(
sender='subscription.Subscription', object_id=subscription.id,
old_status=old_status, new_status='active',
event='payment_confirmed', user_id=subscription.user_id,
)
return subscription
@@ -41,19 +51,32 @@ def grant_free_subscription(user_id: int, *, duration_days: int | None = None, e
از discount/services.py (apply_offer) صدا زده می‌شه؛ مثل بقیه‌ی
ارتباطات بین‌اپیِ این پروژه، یک import مستقیم به تابع سرویس، نه FK
واقعی و نه HTTP call به خودمون.
⚛️ atomic: مثل activate_subscription، مستقل از caller اتمیکه — چون
get_or_create + save + signal.send (که StatusChangeLog می‌سازه) باید
یک واحد باشن، چه apply_offer از قبل توی atomic باشه چه نه.
⚛️🔒 اصلاح race condition: قبلاً بعد از get_or_create هیچ قفلی گرفته
نمی‌شد؛ دو فراخوانیِ هم‌زمان (مثلاً دو آفر که تقریباً هم‌زمان اعمال
می‌شن) می‌تونستن هر دو همون Subscription رو بخونن، هر دو expires_at
رو بر اساس همون old_status/duration محاسبه کنن و save کنن — یکی از
دو نتیجه گم می‌شد (lost update). حالا ردیفِ Subscription قبل از
خوندن/تغییرش با select_for_update قفل می‌شه.
"""
subscription, _ = Subscription.objects.get_or_create(user_id=user_id)
old_status = subscription.status
with transaction.atomic():
Subscription.objects.get_or_create(user_id=user_id)
subscription = Subscription.objects.select_for_update().get(user_id=user_id)
old_status = subscription.status
subscription.status = 'active'
subscription.expires_at = (
timezone.now() + timezone.timedelta(days=duration_days) if duration_days else None
)
subscription.save(update_fields=['status', 'expires_at'])
subscription.status = 'active'
subscription.expires_at = (
timezone.now() + timezone.timedelta(days=duration_days) if duration_days else None
)
subscription.save(update_fields=['status', 'expires_at'])
status_changed.send(
sender='subscription.Subscription', object_id=subscription.id,
old_status=old_status, new_status='active',
event=event, user_id=user_id,
)
status_changed.send(
sender='subscription.Subscription', object_id=subscription.id,
old_status=old_status, new_status='active',
event=event, user_id=user_id,
)
return subscription