Update project
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
آفرها (مثلاً بجای اشتراک، دسترسی به یک اپ خاص) فقط همین فایل رو لمس میکنه،
|
||||
نه apps.py/receivers.py و نه هیچ اپ دیگهای.
|
||||
"""
|
||||
from django.db import IntegrityError
|
||||
from django.db import IntegrityError, transaction
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from .models import Offer, UserOffer
|
||||
@@ -17,27 +17,48 @@ def apply_offer(*, user_id: int, offer: Offer, source: str) -> UserOffer | None:
|
||||
یک آفر رو به یک کاربر اختصاص میده و اثرش رو اعمال میکنه (فعلاً: اشتراک
|
||||
رایگان/محدود). اگه کاربر قبلاً همین آفر رو گرفته باشه (unique constraint)
|
||||
None برمیگردونه، نه خطا — چون auto-assign هنگام ثبتنام نباید کرش کنه.
|
||||
|
||||
⚛️ atomic: ساخت UserOffer، فعالسازی Subscription (grant_free_subscription)
|
||||
و آپدیت expires_at روی همون UserOffer، هر سه یک واحد اتمیکاند. بدون این،
|
||||
اگه grant_free_subscription (یا هر چیزی که داخلش صدا زده میشه، مثل
|
||||
سیگنال status_changed) وسط راه fail کنه، یک UserOffer «یتیم» توی
|
||||
دیتابیس میمونه که هیچ اشتراکی واقعاً براش فعال نشده.
|
||||
"""
|
||||
if not offer.is_active:
|
||||
return None
|
||||
|
||||
try:
|
||||
user_offer = UserOffer.objects.create(
|
||||
user_id=user_id, offer=offer, source=source,
|
||||
expires_at=None, # پایینتر بعد از فعالسازی اشتراک ست میشه
|
||||
# ⚠️ اصلاح: قبلاً هر IntegrityError داخل کل بلوکِ atomic (که شاملِ
|
||||
# grant_free_subscription هم میشد) بهعنوان «duplicate UserOffer»
|
||||
# تفسیر میشد و None برمیگردوند — یعنی اگه یک IntegrityError واقعیِ
|
||||
# دیگه (مثلاً داخل grant_free_subscription) اتفاق میافتاد، خطا قورت
|
||||
# داده میشد و caller فکر میکرد «کاربر قبلاً این آفر رو گرفته»، در
|
||||
# حالی که اصلاً چنین چیزی نبود. حالا فقط IntegrityError ناشی از
|
||||
# UniqueConstraintِ خودِ UserOffer.create (duplicate واقعی) گرفته
|
||||
# میشه — با یک savepoint جداگانه (atomic تودرتو) که فقط همون create
|
||||
# رو rollback میکنه، نه کل تراکنش رو — و خطاهای دیگه (مثلاً از داخل
|
||||
# grant_free_subscription) عادی propagate میشن. atomic بودنِ کلِ
|
||||
# عملیات (UserOffer + Subscription + expires_at) هم دستنخورده باقی
|
||||
# مونده، دقیقاً طبق همون دلیلی که در docstring بالا توضیح داده شده.
|
||||
with transaction.atomic():
|
||||
try:
|
||||
with transaction.atomic(): # savepoint مجزا فقط برای create
|
||||
user_offer = UserOffer.objects.create(
|
||||
user_id=user_id, offer=offer, source=source,
|
||||
expires_at=None, # پایینتر بعد از فعالسازی اشتراک ست میشه
|
||||
)
|
||||
except IntegrityError:
|
||||
return None
|
||||
|
||||
from subscription.services import grant_free_subscription
|
||||
|
||||
duration_days = offer.duration_days if offer.kind == 'trial' else None
|
||||
subscription = grant_free_subscription(
|
||||
user_id, duration_days=duration_days, event=f'offer_{offer.kind}',
|
||||
)
|
||||
except IntegrityError:
|
||||
return None
|
||||
|
||||
from subscription.services import grant_free_subscription
|
||||
user_offer.expires_at = subscription.expires_at
|
||||
user_offer.save(update_fields=['expires_at'])
|
||||
|
||||
duration_days = offer.duration_days if offer.kind == 'trial' else None
|
||||
subscription = grant_free_subscription(
|
||||
user_id, duration_days=duration_days, event=f'offer_{offer.kind}',
|
||||
)
|
||||
|
||||
user_offer.expires_at = subscription.expires_at
|
||||
user_offer.save(update_fields=['expires_at'])
|
||||
return user_offer
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user