101 lines
4.9 KiB
Python
101 lines
4.9 KiB
Python
from django.db import models
|
|
|
|
|
|
class DiscountCode(models.Model):
|
|
TYPE_CHOICES = [('percent', 'درصدی'), ('fixed', 'مبلغ ثابت')]
|
|
|
|
code = models.CharField(max_length=30, unique=True)
|
|
type = models.CharField(max_length=10, choices=TYPE_CHOICES)
|
|
value = models.PositiveIntegerField()
|
|
max_uses = models.PositiveIntegerField(default=1)
|
|
expires_at = models.DateTimeField(null=True, blank=True)
|
|
|
|
def __str__(self):
|
|
return self.code
|
|
|
|
|
|
class Redemption(models.Model):
|
|
discount_code = models.ForeignKey(DiscountCode, on_delete=models.CASCADE, related_name='redemptions')
|
|
user_id = models.IntegerField(db_index=True) # ارجاع سبک به accounts.User
|
|
order_id = models.IntegerField() # ارجاع سبک به payment.Order
|
|
redeemed_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
class Meta:
|
|
# هر کاربر برای یک کد تخفیف حداکثر یکبار میتونه redeem کنه؛
|
|
# قبلاً هیچ محدودیتی در سطح دیتابیس نبود و همراه با نبود چک max_uses
|
|
# در RedeemDiscountView، امکان استفادهی نامحدود از یک کد وجود داشت.
|
|
constraints = [
|
|
models.UniqueConstraint(
|
|
fields=['discount_code', 'user_id'], name='unique_redemption_per_user_per_code'
|
|
)
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"redemption-{self.discount_code_id}-order{self.order_id}"
|
|
|
|
|
|
class Offer(models.Model):
|
|
"""
|
|
آفر رایگان/تخفیف مستقل از DiscountCode: برخلاف DiscountCode که روی
|
|
مبلغِ یک سفارش اثر میذاره، Offer دسترسی (اشتراک) رایگان میده — یا
|
|
برای مدت محدود ('trial'، مثلاً یک هفته بعد از ثبتنام) یا کامل و
|
|
نامحدود ('free_full'). طبق درخواست سینیور، اعمال این آفرها signal-based
|
|
و قابلتغییره: کدوم آفر، با چه شرطی، روی کدوم کاربر اعمال بشه بدون
|
|
دستزدن به accounts یا subscription قابل تغییره (فقط discount/receivers.py
|
|
و discount/services.py).
|
|
"""
|
|
KIND_CHOICES = [
|
|
('trial', 'دورهی رایگان محدود'),
|
|
('free_full', 'دسترسی کامل رایگان'),
|
|
]
|
|
|
|
name = models.CharField(max_length=100)
|
|
kind = models.CharField(max_length=10, choices=KIND_CHOICES)
|
|
# فقط برای kind='trial' معنی داره؛ برای free_full خالی میمونه (یعنی نامحدود).
|
|
duration_days = models.PositiveIntegerField(null=True, blank=True)
|
|
# اگه پر باشه، کاربر میتونه بعداً (نه فقط هنگام ثبتنام) با این کد آفر
|
|
# رو خودش اعمال کنه؛ دقیقاً مشابه DiscountCode.code ولی برای اشتراک،
|
|
# نه تخفیفِ یک سفارش.
|
|
code = models.CharField(max_length=30, unique=True, null=True, blank=True)
|
|
# اگه True باشه، همون لحظهی signal اول (accounts.signals.user_registered)
|
|
# بدون نیاز به وارد کردن کد، خودکار به کاربر تازهثبتنامشده اختصاص داده میشه.
|
|
auto_assign_on_signup = models.BooleanField(default=False)
|
|
is_active = models.BooleanField(default=True)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
|
|
class UserOffer(models.Model):
|
|
"""ثبتِ اینکه کدوم آفر به کدوم کاربر، از چه مسیری، چه زمانی اختصاص داده شده."""
|
|
|
|
SOURCE_CHOICES = [
|
|
('signup_auto', 'خودکار هنگام ثبتنام'),
|
|
('code', 'وارد کردن کد'),
|
|
('manual', 'اختصاص دستی/پشتیبانی'),
|
|
]
|
|
STATUS_CHOICES = [
|
|
('active', 'فعال'),
|
|
('expired', 'منقضی'),
|
|
('revoked', 'لغوشده'),
|
|
]
|
|
|
|
user_id = models.IntegerField(db_index=True) # ارجاع سبک به accounts.User
|
|
offer = models.ForeignKey(Offer, on_delete=models.CASCADE, related_name='user_offers')
|
|
source = models.CharField(max_length=15, choices=SOURCE_CHOICES)
|
|
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='active')
|
|
granted_at = models.DateTimeField(auto_now_add=True)
|
|
expires_at = models.DateTimeField(null=True, blank=True) # None یعنی نامحدود (free_full)
|
|
|
|
class Meta:
|
|
ordering = ['-granted_at']
|
|
# هر کاربر برای یک آفرِ مشخص فقط یکبار میتونه دریافت کنه (چه
|
|
# auto چه با کد)؛ جلوی گرفتن مکرر یک آفرِ trial رو میگیره.
|
|
constraints = [
|
|
models.UniqueConstraint(fields=['offer', 'user_id'], name='unique_offer_per_user')
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"useroffer-{self.offer_id}-user{self.user_id}-{self.status}"
|