from django.db import transaction from django.utils import timezone from statuslog.signals import status_changed from .models import Subscription def activate_subscription(subscription_id: int): """ بعد از تایید پرداخت (payment webhook) صدا زده می‌شه؛ اشتراک رو active می‌کنه و expires_at رو بر اساس Plan.duration_days محاسبه می‌کنه. (قبلاً چون Plan فیلد duration‌ای نداشت، این مقدار با یک قاعده‌ی stub بر اساس متن title حدس زده می‌شد؛ الان duration_days مستقیماً از مدل Plan خونده می‌شه.) ⚛️ atomic: خودِ این تابع هم مستقل (بدون اتکا به atomic بودنِ caller) اتمیکه، چون به‌عنوان یک سرویسِ مشترک از جاهای مختلف (payment webhook، bazaar verify) صدا زده می‌شه؛ save روی Subscription و نوشتنِ StatusChangeLog که از signal.send نتیجه می‌شه، باید یک واحد بمونن. """ 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']) 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 def grant_free_subscription(user_id: int, *, duration_days: int | None = None, event: str = 'offer_applied'): """ فعال‌سازی اشتراک بدون هیچ Order/پرداختی — برای Offer (آفر رایگان محدود/کامل). duration_days=None یعنی نامحدود (free_full)؛ عدد یعنی trial با همون تعداد روز. از 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 قفل می‌شه. """ 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']) 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