Files
Amirhossein Khalili 88b793ed9f
Some checks failed
Backend CI/CD / test (push) Has been cancelled
Backend CI/CD / deploy (push) Has been cancelled
initial commit
2026-05-19 20:53:08 +03:30

123 lines
5.5 KiB
Python

from django.db import models
from django.db.models import Q, Count
from django.core.exceptions import ValidationError
from django.conf import settings
from django.utils import timezone
from core.models import BaseModel
from apps.events.models import Event
from ninja.errors import HttpError
User = settings.AUTH_USER_MODEL
class DiscountCode(BaseModel):
class Type(models.TextChoices):
PERCENT = "percent", "Percent"
FIXED = "fixed", "Fixed (IRR)"
code = models.CharField(max_length=64, unique=True)
type = models.CharField(max_length=10, choices=Type.choices, default=Type.PERCENT)
value = models.PositiveIntegerField()
max_discount = models.PositiveIntegerField(null=True, blank=True)
is_active = models.BooleanField(default=True)
starts_at = models.DateTimeField(null=True, blank=True)
ends_at = models.DateTimeField(null=True, blank=True)
usage_limit_total = models.PositiveIntegerField(null=True, blank=True)
usage_limit_per_user = models.PositiveIntegerField(null=True, blank=True)
min_amount = models.PositiveIntegerField(null=True, blank=True)
applicable_events = models.ManyToManyField(Event, blank=True, related_name="discount_codes")
def __str__(self):
return f"{self.code} ({self.get_type_display()} {self.value})"
def calculate_discount(self, event: Event, user: User):
if not event.price:
return (0, 0)
if not self.is_active:
raise HttpError(400, "کد تخفیف نامعتبر یا غیرفعال است.")
n = timezone.now()
if self.starts_at and n < self.starts_at:
raise HttpError(400, "کد تخفیف هنوز فعال نشده است.")
if self.ends_at and n > self.ends_at:
raise HttpError(400, "کد تخفیف منقضی شده است.")
if self.applicable_events.exists() and not self.applicable_events.filter(pk=event.pk).exists():
raise HttpError(400, "کد تخفیف برای این رویداد قابل استفاده نیست.")
if self.min_amount and event.price < self.min_amount:
raise HttpError(400, "مبلغ سفارش کمتر از حداقل لازم برای این کد است.")
used_qs = Payment.objects.filter(discount_code=self, status__in=[Payment.OrderStatusChoices.PAID, Payment.OrderStatusChoices.PENDING])
if self.usage_limit_total is not None and used_qs.count() >= self.usage_limit_total:
raise HttpError(400, "حداکثر تعداد استفاده از این کد تخفیف تکمیل شده است.")
used_by_user = used_qs.filter(user=user).count()
if self.usage_limit_per_user is not None and used_by_user >= self.usage_limit_per_user:
raise HttpError(400, "شما حداکثر تعداد مجاز استفاده از این کد تخفیف را مصرف کرده‌اید.")
if self.type == DiscountCode.Type.FIXED:
disc = min(self.value, event.price)
else:
disc = (event.price * self.value) // 100
if self.max_discount:
disc = min(disc, self.max_discount)
final_amount = max(event.price - disc, 0)
if 0 < final_amount < 10_000:
raise HttpError(400, "با این تخفیف مبلغ قابل پرداخت به کمتر از ۱۰٬۰۰۰ ریال می‌رسد.")
return (final_amount, disc)
class Payment(BaseModel):
class OrderStatusChoices(models.IntegerChoices):
INIT = 0, "Initiated"
PENDING = 1, "Pending"
PAID = 2, "Paid"
FAILED = 3, "Failed"
CANCELED = 4, "Canceled"
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='payments', editable=False)
event = models.ForeignKey(Event, on_delete=models.PROTECT, related_name='payments', editable=False)
base_amount = models.PositiveIntegerField(editable=False)
discount_code = models.ForeignKey(DiscountCode, on_delete=models.PROTECT, null=True, blank=True, editable=False, related_name="payments")
discount_amount = models.PositiveIntegerField(default=0, editable=False)
amount = models.PositiveIntegerField(editable=False)
registration = models.ForeignKey(
"events.Registration",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="payments",
editable=False,
)
authority = models.CharField(max_length=64, unique=True, null=True, blank=True, editable=False)
status = models.IntegerField(choices=OrderStatusChoices.choices, default=OrderStatusChoices.INIT, editable=False)
ref_id = models.CharField(max_length=64, null=True, blank=True, editable=False)
card_pan = models.CharField(max_length=32, null=True, blank=True, editable=False)
card_hash = models.CharField(max_length=128, null=True, blank=True, editable=False)
verified_at = models.DateTimeField(null=True, blank=True, editable=False)
def clean(self):
if self.discount_amount and self.amount + self.discount_amount != self.base_amount:
raise ValidationError({"amount": "amount + discount_amount must equal base_amount"})
def save(self, *args, **kwargs):
self.full_clean()
return super().save(*args, **kwargs)
@property
def status_label(self):
"""Human-readable label for the payment status."""
return self.get_status_display()
def __str__(self):
return f"{self.user.email}:{self.event} - {self.get_status_display()}"