diff --git a/apps/promotions/admin.py b/apps/promotions/admin.py index e64b0cc..7677367 100644 --- a/apps/promotions/admin.py +++ b/apps/promotions/admin.py @@ -2,11 +2,11 @@ from functools import update_wrapper from django.contrib import admin -from .models import Promotion, Plan, Event, Recipient - +from .models import Promotion, Plan, Event, Recipient, EventSaver admin.site.register(Plan) admin.site.register(Promotion) admin.site.register(Event) +admin.site.register(EventSaver) admin.site.register(Recipient) diff --git a/apps/promotions/migrations/0011_eventsaver_remove_recipient_wallet_uuid.py b/apps/promotions/migrations/0011_eventsaver_remove_recipient_wallet_uuid.py new file mode 100644 index 0000000..810ba35 --- /dev/null +++ b/apps/promotions/migrations/0011_eventsaver_remove_recipient_wallet_uuid.py @@ -0,0 +1,32 @@ +# Generated by Django 5.1.7 on 2025-10-14 16:49 + +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('promotions', '0010_remove_plan_max_amount_remove_plan_min_amount_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='EventSaver', + fields=[ + ('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('created_at', models.DateTimeField(auto_now_add=True, db_index=True)), + ('updated_at', models.DateTimeField(auto_now=True, db_index=True)), + ('title', models.CharField(max_length=255, verbose_name='title')), + ('event_label', models.CharField(db_index=True, max_length=255, unique=True, verbose_name='label')), + ('save_once', models.BooleanField(default=False, verbose_name='save once')), + ], + options={ + 'abstract': False, + }, + ), + migrations.RemoveField( + model_name='recipient', + name='wallet_uuid', + ), + ] diff --git a/apps/promotions/models.py b/apps/promotions/models.py index ee9dc0d..7a0b68a 100644 --- a/apps/promotions/models.py +++ b/apps/promotions/models.py @@ -5,14 +5,47 @@ from django.utils import timezone from oauth2_provider.settings import oauth2_settings + from apps.promotions import handlers + from apps.promotions.handlers import PromotionTypeChoices, BasePromotionHandler + + from apps.users.models import User from utils.models import BaseModel from django.utils.translation import gettext_lazy as _ +class EventSaverQuerySet(models.QuerySet): + pass + + +class EventSaver(BaseModel): + title = models.CharField(_('title'), max_length=255) + event_label = models.CharField(_('label'), max_length=255, unique=True, db_index=True) + save_once = models.BooleanField(_('save once'), default=False) + + objects = EventSaverQuerySet.as_manager() + + def save_event(self, **kwargs): + user = kwargs.get('user') + application = kwargs.get('application') + label = kwargs.get('label') + data = kwargs.get('data') + + if self.save_once: + event, created = Event.objects.get_or_create(user=user, application=application, label=label, defaults={"data": data}) + if created: + return event + else: + raise Exception(_('already saved')) + + else: + event = Event.objects.create(user=user, application=application, label=label, defaults={"date": date}) + return event + + class EventQuerySet(models.QuerySet): pass @@ -63,20 +96,6 @@ class Plan(BaseModel): def __str__(self): return self.title - # @property - # def handler(self): - # if self.promotion_type == PromotionTypeChoices.PERCENTAGE.name: - # return handlers.PercentageHandler(self) - # - # elif self.promotion_type == PromotionTypeChoices.REFERRAL.name: - # return handlers.ReferralHandler(self) - # - # elif self.promotion_type == PromotionTypeChoices.OTHERS.name: - # return handlers.OtherHandler(self) - # - # else: - # raise NotImplementedError() - def process_event(self, event=None): self.promote(event=event) @@ -155,7 +174,6 @@ class Recipient(BaseModel): raise - class PromotionQuerySet(models.QuerySet): def used(self, user): return self.filter(user=user, state__in=[Promotion.StateChoices.CREATED]) diff --git a/apps/promotions/serializers.py b/apps/promotions/serializers.py index a8d5318..dea5e3c 100644 --- a/apps/promotions/serializers.py +++ b/apps/promotions/serializers.py @@ -16,7 +16,7 @@ class PromotionSerializer(serializers.ModelSerializer): "user", "application", ) - read_only_fields = ('user','application', "promotion_amount",) + read_only_fields = ('user', 'application', "promotion_amount",) class EventSerializer(serializers.ModelSerializer): @@ -28,7 +28,8 @@ class EventSerializer(serializers.ModelSerializer): "user", "application", ) - read_only_fields = ('user','application') + read_only_fields = ('user', 'application') + class PlanSerializer(serializers.ModelSerializer): base_amount = serializers.IntegerField(required=True) @@ -36,13 +37,13 @@ class PlanSerializer(serializers.ModelSerializer): class Meta: model = Plan - fields = ( - "title", - "description", - "base_amount", - "application", - "promotion_amount" - ) + fields = ("label" + "title", + "description", + "base_amount", + "application", + "promotion_amount" + ) read_only_fields = ('application', "promotion_amount", "title", "description") # def get_promotion_amount(self, obj): @@ -57,5 +58,4 @@ class PlanPromotSerializer(serializers.ModelSerializer): "user", "application", ) - read_only_fields = ('user','application') - + read_only_fields = ('user', 'application') diff --git a/apps/promotions/views.py b/apps/promotions/views.py index 38bc19f..f929ec1 100644 --- a/apps/promotions/views.py +++ b/apps/promotions/views.py @@ -1,11 +1,12 @@ from drf_spectacular.utils import extend_schema from rest_framework import mixins from rest_framework.decorators import action +from rest_framework import exceptions from rest_framework.viewsets import GenericViewSet from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements from apps.gooyal_oauth2.utils import get_application -from .models import Plan, Promotion +from .models import Plan, Promotion, EventSaver from .serializers import PlanSerializer, PromotionSerializer, EventSerializer from .tasks import analyze_event_task from ..users.models import User @@ -74,14 +75,20 @@ class ApplicationEventViewSet( "POST": [["promotions.application.event:submit"]], } - def perform_create(self, serializer): + def perform_create(self, serializer: EventSerializer): if hasattr(self.request, 'user'): user = self.request.user else: user = None application = get_application(self.request) - event = serializer.save(user=user, application=application) + # EventSaver.objects.first(event_label=serializer.validated_data['event_label'], valid_application__contains=application.uuid) + event_saver: EventSaver = EventSaver.objects.filter(event_label=serializer.validated_data['label']).first() + try: + event = event_saver.save_event(user=user, application=application, **serializer.validated_data) + except Exception as e: + raise exceptions.ValidationError(str(e)) + analyze_event_task.delay(event.uuid) diff --git a/main/settings.py b/main/settings.py index d63a6d6..ea0f0fd 100644 --- a/main/settings.py +++ b/main/settings.py @@ -188,7 +188,7 @@ REST_FRAMEWORK = { # 'otp_min': '1/min', # 'otp_day': '20/day', }, - 'EXCEPTION_HANDLER': 'utils.exceptions.ExceptionHandler', + 'EXCEPTION_HANDLER': 'utils.exceptions.exception_handler', } AUTHENTICATION_BACKENDS = ( diff --git a/utils/exceptions.py b/utils/exceptions.py index e1956ac..795d053 100644 --- a/utils/exceptions.py +++ b/utils/exceptions.py @@ -5,34 +5,33 @@ from rest_framework.views import exception_handler as drf_exception_handler logger = logging.getLogger(__name__) -class ExceptionHandler: - @classmethod - def __call__(cls, exc, context): +def exception_handler(exc, context): # پاسخ پیش‌فرض DRF را دریافت می‌کنیم - response = drf_exception_handler(exc, context) + response = drf_exception_handler(exc, context) - if response is not None: - # ساختار دلخواه خود را تعریف می‌کنیم - response_data = { - 'success': False, - 'status_code': response.status_code, - 'message': 'An error occurred', - 'details': {} - } + if response is not None: + # ساختار دلخواه خود را تعریف می‌کنیم + response_data = { + 'success': False, + 'status_code': response.status_code, + 'message': 'An error occurred', + 'details': {} + } - if isinstance(exc, APIException): - response_data['message'] = exc.detail if isinstance(exc.detail, str) else "Validation error" - response_data['details'] = exc.detail if isinstance(exc.detail, dict) else {} + if isinstance(exc, APIException): + response_data['message'] = exc.detail if isinstance(exc.detail, str) else "Validation error" + response_data['details'] = exc.detail if isinstance(exc.detail, dict) else {} - response.data = response_data + response.data = response_data - return response + return response # در فایل middleware.py from django.http import JsonResponse + class ErrorMiddleware: def __init__(self, get_response): self.get_response = get_response @@ -51,4 +50,4 @@ class ErrorMiddleware: "details": str(exception) # فقط در حالت توسعه! در تولید بهتر است لاگ شود. }, status=500 - ) \ No newline at end of file + )