diff --git a/apps/promotions/filters_admin.py b/apps/promotions/filters_admin.py deleted file mode 100644 index c7bbe77..0000000 --- a/apps/promotions/filters_admin.py +++ /dev/null @@ -1,36 +0,0 @@ -import django_filters - -from .handlers import ProcessorTypeChoices -from .models import Promotion, PaymentStateChoices - - -class AdminPromotionFilter(django_filters.FilterSet): - user_uuid = django_filters.UUIDFilter(field_name='user_uuid') - plan = django_filters.UUIDFilter(field_name='plan__uuid') - promotion_type = django_filters.ChoiceFilter(field_name='plan__processor', choices=ProcessorTypeChoices.choices) - state = django_filters.ChoiceFilter(field_name='state', choices=PaymentStateChoices.choices) - event_label = django_filters.CharFilter(field_name='event__label', lookup_expr='exact') - created_after = django_filters.DateTimeFilter(field_name='created_at', lookup_expr='gte') - created_before = django_filters.DateTimeFilter(field_name='created_at', lookup_expr='lte') - - class Meta: - model = Promotion - fields = ['user_uuid', 'plan', 'promotion_type', 'state', 'event_label', 'created_after', 'created_before'] - - -class AdminReferralFilter(django_filters.FilterSet): - # invited_by == Promotion.user_uuid: in the referral recipient DSL - # ("->event:referral") this is who gets paid, i.e. the referrer. - invited_by = django_filters.UUIDFilter(field_name='user_uuid') - # invited_user lives only in free-form JSON (event.data['user']), so this is a - # CharFilter, not UUIDFilter: JSONField key-transform lookups compare against - # the stored string, not a native UUID the adapter can serialize. - invited_user = django_filters.CharFilter(field_name='event__data__user') - plan = django_filters.UUIDFilter(field_name='plan__uuid') - state = django_filters.ChoiceFilter(field_name='state', choices=PaymentStateChoices.choices) - created_after = django_filters.DateTimeFilter(field_name='created_at', lookup_expr='gte') - created_before = django_filters.DateTimeFilter(field_name='created_at', lookup_expr='lte') - - class Meta: - model = Promotion - fields = ['invited_user', 'invited_by', 'plan', 'state', 'created_after', 'created_before'] diff --git a/apps/promotions/serializers.py b/apps/promotions/serializers.py deleted file mode 100644 index 939a7b7..0000000 --- a/apps/promotions/serializers.py +++ /dev/null @@ -1,188 +0,0 @@ -from rest_framework import serializers - -from .models import Promotion, Plan, Event, Recipient - - -class EventSerializer(serializers.ModelSerializer): - class Meta: - model = Event - fields = ( - "label", - "data", - "user", - "application", - ) - read_only_fields = ('user', 'application') - - -class PromotionSerializer(serializers.ModelSerializer): - # base_amount = serializers.IntegerField(required=True) - # promotion_amount = serializers.IntegerField(read_only=True) - event = EventSerializer(read_only=True) - user = serializers.UUIDField(source='user_uuid') - - class Meta: - model = Promotion - fields = ( - "uuid", - "event", - "promotion_amount", - "state", - "user", - ) - - -class PlanSerializer(serializers.ModelSerializer): - base_amount = serializers.IntegerField(required=True) - promotion_amount = serializers.IntegerField(read_only=True) - - class Meta: - model = Plan - fields = ("title", - "description", - "base_amount", - "application", - "promotion_amount" - ) - read_only_fields = ('application', "promotion_amount", "title", "description") - - # def get_promotion_amount(self, obj): - # user = self.context['request'].user - # return obj.calculate_promotion(user) - - -class PlanPromotSerializer(serializers.ModelSerializer): - class Meta: - model = Promotion - fields = ( - "user", - "application", - ) - read_only_fields = ('user', 'application') - - - -class PromoteSerializer(serializers.ModelSerializer): - label = serializers.CharField(write_only=True) - data = serializers.JSONField(write_only=True) - - promotions = PromotionSerializer(many=True, read_only=True) - - class Meta: - model = Promotion - fields = ( - "promotions", - "label", - "data", - ) - - -class UserRecipientSerializer(serializers.ModelSerializer): - class Meta: - model = Recipient - fields = ['label', - "base_amount_field"] - - -class PromotionStatusSerializer(serializers.Serializer): - event_label = serializers.CharField(read_only=True) - processed = serializers.BooleanField(read_only=True) - promotion_amount = serializers.IntegerField(read_only=True, allow_null=True) - - -class AdminPlanSummarySerializer(serializers.ModelSerializer): - class Meta: - model = Plan - fields = ("uuid", "title", "processor") - - -class AdminPromotionSerializer(serializers.ModelSerializer): - """Every Promotion row, i.e. every promotion received by a user.""" - user = serializers.UUIDField(source='user_uuid', read_only=True) - plan = AdminPlanSummarySerializer(read_only=True) - promotion_type = serializers.SerializerMethodField() - event_label = serializers.SerializerMethodField() - state_display = serializers.CharField(source='get_state_display', read_only=True) - reward_amount = serializers.IntegerField(source='promotion_amount', read_only=True) - date = serializers.DateTimeField(source='created_at', read_only=True) - - class Meta: - model = Promotion - fields = ( - "uuid", - "user", - "plan", - "promotion_type", - "event_label", - "base_amount", - "reward_amount", - "state", - "state_display", - "date", - "updated_at", - ) - - def get_promotion_type(self, obj): - return obj.plan.processor if obj.plan_id else None - - def get_event_label(self, obj): - return obj.event.label if obj.event_id else None - - -class AdminReferralSerializer(serializers.ModelSerializer): - """ - Referral activity, derived from Promotion rows on plans whose processor is - 'referral'. There is no dedicated referral model and no referral_code field - in this codebase today (see docs/adminpanel_technical.md, "Known limitations"). - - In the recipient DSL a referral plan's Recipient resolves to - "->event:referral", so Promotion.user_uuid is the referrer being paid - (invited_by) -- not the person who triggered the event. The invited user - only exists as free-form JSON on the triggering Event (event.data['user']). - """ - invited_by = serializers.UUIDField(source='user_uuid', read_only=True) - invited_user = serializers.SerializerMethodField() - plan = AdminPlanSummarySerializer(read_only=True) - event_label = serializers.SerializerMethodField() - reward_amount = serializers.IntegerField(source='promotion_amount', read_only=True) - state_display = serializers.CharField(source='get_state_display', read_only=True) - date = serializers.DateTimeField(source='created_at', read_only=True) - - class Meta: - model = Promotion - fields = ( - "uuid", - "invited_user", - "invited_by", - "plan", - "event_label", - "reward_amount", - "state", - "state_display", - "date", - ) - - def get_invited_user(self, obj): - if obj.event_id and obj.event.data: - return obj.event.data.get('user') - return None - - def get_event_label(self, obj): - return obj.event.label if obj.event_id else None - - -class UserPlanSerializer(serializers.ModelSerializer): - recipients = UserRecipientSerializer(many=True, read_only=True) - class Meta: - model = Plan - fields = ("title", - 'banner', - "description", - "description_details", - "recipients", - ) - read_only_fields = ("title", "description", "description_details", 'banner', "recipients") - - # def get_promotion_amount(self, obj): - # user = self.context['request'].user - # return obj.calculate_promotion(user) diff --git a/apps/promotions/serializers/__init__.py b/apps/promotions/serializers/__init__.py new file mode 100644 index 0000000..9fd6158 --- /dev/null +++ b/apps/promotions/serializers/__init__.py @@ -0,0 +1,25 @@ +from .common import ( + EventSerializer, + PromotionSerializer, + PlanSerializer, + PlanPromotSerializer, + PromoteSerializer, + UserRecipientSerializer, + PromotionStatusSerializer, + UserPlanSerializer, +) +from .admin import AdminPlanSummarySerializer, AdminPromotionSerializer, AdminReferralSerializer + +__all__ = [ + 'EventSerializer', + 'PromotionSerializer', + 'PlanSerializer', + 'PlanPromotSerializer', + 'PromoteSerializer', + 'UserRecipientSerializer', + 'PromotionStatusSerializer', + 'UserPlanSerializer', + 'AdminPlanSummarySerializer', + 'AdminPromotionSerializer', + 'AdminReferralSerializer', +] diff --git a/apps/promotions/serializers/admin/__init__.py b/apps/promotions/serializers/admin/__init__.py new file mode 100644 index 0000000..617b516 --- /dev/null +++ b/apps/promotions/serializers/admin/__init__.py @@ -0,0 +1,8 @@ +from .promotion import AdminPlanSummarySerializer, AdminPromotionSerializer +from .referral import AdminReferralSerializer + +__all__ = [ + 'AdminPlanSummarySerializer', + 'AdminPromotionSerializer', + 'AdminReferralSerializer', +] diff --git a/apps/promotions/serializers/admin/promotion.py b/apps/promotions/serializers/admin/promotion.py new file mode 100644 index 0000000..98666be --- /dev/null +++ b/apps/promotions/serializers/admin/promotion.py @@ -0,0 +1,42 @@ +from rest_framework import serializers + +from apps.promotions.models import Promotion, Plan + + +class AdminPlanSummarySerializer(serializers.ModelSerializer): + class Meta: + model = Plan + fields = ("uuid", "title", "processor") + + +class AdminPromotionSerializer(serializers.ModelSerializer): + """Every Promotion row, i.e. every promotion received by a user.""" + user = serializers.UUIDField(source='user_uuid', read_only=True) + plan = AdminPlanSummarySerializer(read_only=True) + promotion_type = serializers.SerializerMethodField() + event_label = serializers.SerializerMethodField() + state_display = serializers.CharField(source='get_state_display', read_only=True) + reward_amount = serializers.IntegerField(source='promotion_amount', read_only=True) + date = serializers.DateTimeField(source='created_at', read_only=True) + + class Meta: + model = Promotion + fields = ( + "uuid", + "user", + "plan", + "promotion_type", + "event_label", + "base_amount", + "reward_amount", + "state", + "state_display", + "date", + "updated_at", + ) + + def get_promotion_type(self, obj): + return obj.plan.processor if obj.plan_id else None + + def get_event_label(self, obj): + return obj.event.label if obj.event_id else None diff --git a/apps/promotions/serializers/admin/referral.py b/apps/promotions/serializers/admin/referral.py new file mode 100644 index 0000000..3deea23 --- /dev/null +++ b/apps/promotions/serializers/admin/referral.py @@ -0,0 +1,46 @@ +from rest_framework import serializers + +from apps.promotions.models import Promotion +from .promotion import AdminPlanSummarySerializer + + +class AdminReferralSerializer(serializers.ModelSerializer): + """ + Referral activity, derived from Promotion rows on plans whose processor is + 'referral'. There is no dedicated referral model and no referral_code field + in this codebase today (see docs/adminpanel_technical.md, "Known limitations"). + + In the recipient DSL a referral plan's Recipient resolves to + "->event:referral", so Promotion.user_uuid is the referrer being paid + (invited_by) -- not the person who triggered the event. The invited user + only exists as free-form JSON on the triggering Event (event.data['user']). + """ + invited_by = serializers.UUIDField(source='user_uuid', read_only=True) + invited_user = serializers.SerializerMethodField() + plan = AdminPlanSummarySerializer(read_only=True) + event_label = serializers.SerializerMethodField() + reward_amount = serializers.IntegerField(source='promotion_amount', read_only=True) + state_display = serializers.CharField(source='get_state_display', read_only=True) + date = serializers.DateTimeField(source='created_at', read_only=True) + + class Meta: + model = Promotion + fields = ( + "uuid", + "invited_user", + "invited_by", + "plan", + "event_label", + "reward_amount", + "state", + "state_display", + "date", + ) + + def get_invited_user(self, obj): + if obj.event_id and obj.event.data: + return obj.event.data.get('user') + return None + + def get_event_label(self, obj): + return obj.event.label if obj.event_id else None diff --git a/apps/promotions/serializers/common.py b/apps/promotions/serializers/common.py new file mode 100644 index 0000000..df22f47 --- /dev/null +++ b/apps/promotions/serializers/common.py @@ -0,0 +1,107 @@ +from rest_framework import serializers + +from apps.promotions.models import Promotion, Plan, Event, Recipient + + +class EventSerializer(serializers.ModelSerializer): + class Meta: + model = Event + fields = ( + "label", + "data", + "user", + "application", + ) + read_only_fields = ('user', 'application') + + +class PromotionSerializer(serializers.ModelSerializer): + # base_amount = serializers.IntegerField(required=True) + # promotion_amount = serializers.IntegerField(read_only=True) + event = EventSerializer(read_only=True) + user = serializers.UUIDField(source='user_uuid') + + class Meta: + model = Promotion + fields = ( + "uuid", + "event", + "promotion_amount", + "state", + "user", + ) + + +class PlanSerializer(serializers.ModelSerializer): + base_amount = serializers.IntegerField(required=True) + promotion_amount = serializers.IntegerField(read_only=True) + + class Meta: + model = Plan + fields = ("title", + "description", + "base_amount", + "application", + "promotion_amount" + ) + read_only_fields = ('application', "promotion_amount", "title", "description") + + # def get_promotion_amount(self, obj): + # user = self.context['request'].user + # return obj.calculate_promotion(user) + + +class PlanPromotSerializer(serializers.ModelSerializer): + class Meta: + model = Promotion + fields = ( + "user", + "application", + ) + read_only_fields = ('user', 'application') + + + +class PromoteSerializer(serializers.ModelSerializer): + label = serializers.CharField(write_only=True) + data = serializers.JSONField(write_only=True) + + promotions = PromotionSerializer(many=True, read_only=True) + + class Meta: + model = Promotion + fields = ( + "promotions", + "label", + "data", + ) + + +class UserRecipientSerializer(serializers.ModelSerializer): + class Meta: + model = Recipient + fields = ['label', + "base_amount_field"] + + +class PromotionStatusSerializer(serializers.Serializer): + event_label = serializers.CharField(read_only=True) + processed = serializers.BooleanField(read_only=True) + promotion_amount = serializers.IntegerField(read_only=True, allow_null=True) + + +class UserPlanSerializer(serializers.ModelSerializer): + recipients = UserRecipientSerializer(many=True, read_only=True) + class Meta: + model = Plan + fields = ("title", + 'banner', + "description", + "description_details", + "recipients", + ) + read_only_fields = ("title", "description", "description_details", 'banner', "recipients") + + # def get_promotion_amount(self, obj): + # user = self.context['request'].user + # return obj.calculate_promotion(user) diff --git a/apps/promotions/urls/__init__.py b/apps/promotions/urls/__init__.py new file mode 100644 index 0000000..a6bcff4 --- /dev/null +++ b/apps/promotions/urls/__init__.py @@ -0,0 +1,6 @@ +from .router import router +from . import admin_urls + +app_name = 'promotions-admin' + +urlpatterns = router.urls + admin_urls.urlpatterns diff --git a/apps/promotions/urls/admin_urls.py b/apps/promotions/urls/admin_urls.py new file mode 100644 index 0000000..59dc60d --- /dev/null +++ b/apps/promotions/urls/admin_urls.py @@ -0,0 +1,8 @@ +from apps.promotions.views import AdminPromotionViewSet, AdminReferralViewSet + +from .router import router + +router.register('promotions', AdminPromotionViewSet, basename='admin-promotions') +router.register('referrals', AdminReferralViewSet, basename='admin-referrals') + +urlpatterns = [] diff --git a/apps/promotions/urls/router.py b/apps/promotions/urls/router.py new file mode 100644 index 0000000..fd9849d --- /dev/null +++ b/apps/promotions/urls/router.py @@ -0,0 +1,3 @@ +from rest_framework.routers import DefaultRouter + +router = DefaultRouter() diff --git a/apps/promotions/urls_admin.py b/apps/promotions/urls_admin.py deleted file mode 100644 index 098c66d..0000000 --- a/apps/promotions/urls_admin.py +++ /dev/null @@ -1,12 +0,0 @@ -from django.urls import path - -from . import views_admin - -app_name = 'promotions-admin' - -urlpatterns = [ - path('promotions/', views_admin.AdminPromotionListApiView.as_view(), name='promotion-list'), - path('promotions//', views_admin.AdminPromotionDetailApiView.as_view(), name='promotion-detail'), - path('referrals/', views_admin.AdminReferralListApiView.as_view(), name='referral-list'), - path('referrals//', views_admin.AdminReferralDetailApiView.as_view(), name='referral-detail'), -] diff --git a/apps/promotions/views/__init__.py b/apps/promotions/views/__init__.py new file mode 100644 index 0000000..351edd6 --- /dev/null +++ b/apps/promotions/views/__init__.py @@ -0,0 +1,6 @@ +from .admin import AdminPromotionViewSet, AdminReferralViewSet + +__all__ = [ + 'AdminPromotionViewSet', + 'AdminReferralViewSet', +] diff --git a/apps/promotions/views/admin/__init__.py b/apps/promotions/views/admin/__init__.py new file mode 100644 index 0000000..1572ca1 --- /dev/null +++ b/apps/promotions/views/admin/__init__.py @@ -0,0 +1,7 @@ +from .promotion import AdminPromotionViewSet +from .referral import AdminReferralViewSet + +__all__ = [ + 'AdminPromotionViewSet', + 'AdminReferralViewSet', +] diff --git a/apps/promotions/views/admin/promotion.py b/apps/promotions/views/admin/promotion.py new file mode 100644 index 0000000..de479b5 --- /dev/null +++ b/apps/promotions/views/admin/promotion.py @@ -0,0 +1,26 @@ +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework import mixins +from rest_framework.viewsets import GenericViewSet + +from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements +from apps.promotions.filters import AdminPromotionFilter +from apps.promotions.models import Promotion +from apps.promotions.serializers import AdminPromotionSerializer + + +class AdminPromotionViewSet( + mixins.ListModelMixin, + mixins.RetrieveModelMixin, + GenericViewSet +): + """Read-only: every Promotion received, per user, with type/amount/date.""" + queryset = Promotion.objects.select_related('plan', 'event').order_by('-created_at') + serializer_class = AdminPromotionSerializer + lookup_field = 'uuid' + filter_backends = (DjangoFilterBackend,) + filterset_class = AdminPromotionFilter + + permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements] + required_alternate_scopes = { + "GET": [["admin.promotions:retrieve"]], + } diff --git a/apps/promotions/views/admin/referral.py b/apps/promotions/views/admin/referral.py new file mode 100644 index 0000000..3483283 --- /dev/null +++ b/apps/promotions/views/admin/referral.py @@ -0,0 +1,35 @@ +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework import mixins +from rest_framework.viewsets import GenericViewSet + +from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements +from apps.promotions.filters import AdminReferralFilter +from apps.promotions.handlers import ProcessorTypeChoices +from apps.promotions.models import Promotion +from apps.promotions.serializers import AdminReferralSerializer + + +class AdminReferralViewSet( + mixins.ListModelMixin, + mixins.RetrieveModelMixin, + GenericViewSet +): + """ + Read-only: referral activity. Scoped to Promotion rows on plans whose + processor == 'referral'. No dedicated Referral model / referral_code + field exists in this codebase; see docs/adminpanel_technical.md. + """ + serializer_class = AdminReferralSerializer + lookup_field = 'uuid' + filter_backends = (DjangoFilterBackend,) + filterset_class = AdminReferralFilter + + permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements] + required_alternate_scopes = { + "GET": [["admin.referrals:retrieve"]], + } + + def get_queryset(self): + return Promotion.objects.select_related('plan', 'event').filter( + plan__processor=ProcessorTypeChoices.REFERRAL + ).order_by('-created_at') diff --git a/apps/promotions/views_admin.py b/apps/promotions/views_admin.py deleted file mode 100644 index f770dd1..0000000 --- a/apps/promotions/views_admin.py +++ /dev/null @@ -1,62 +0,0 @@ -from django_filters.rest_framework import DjangoFilterBackend -from rest_framework.generics import ListAPIView, RetrieveAPIView - -from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements -from .filters_admin import AdminPromotionFilter, AdminReferralFilter -from .handlers import ProcessorTypeChoices -from .models import Promotion -from .serializers import AdminPromotionSerializer, AdminReferralSerializer - -ADMIN_PROMOTION_QUERYSET = Promotion.objects.select_related('plan', 'event').order_by('-created_at') - - -class AdminPromotionListApiView(ListAPIView): - """Read-only: every Promotion received, per user, with type/amount/date.""" - queryset = ADMIN_PROMOTION_QUERYSET - serializer_class = AdminPromotionSerializer - permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements] - required_alternate_scopes = { - "GET": [["admin.promotions:retrieve"]], - } - filter_backends = [DjangoFilterBackend] - filterset_class = AdminPromotionFilter - - -class AdminPromotionDetailApiView(RetrieveAPIView): - queryset = ADMIN_PROMOTION_QUERYSET - serializer_class = AdminPromotionSerializer - lookup_field = 'uuid' - permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements] - required_alternate_scopes = { - "GET": [["admin.promotions:retrieve"]], - } - - -class AdminReferralListApiView(ListAPIView): - """ - Read-only: referral activity. Scoped to Promotion rows on plans whose - processor == 'referral'. No dedicated Referral model / referral_code - field exists in this codebase; see docs/adminpanel_technical.md. - """ - serializer_class = AdminReferralSerializer - permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements] - required_alternate_scopes = { - "GET": [["admin.referrals:retrieve"]], - } - filter_backends = [DjangoFilterBackend] - filterset_class = AdminReferralFilter - - def get_queryset(self): - return ADMIN_PROMOTION_QUERYSET.filter(plan__processor=ProcessorTypeChoices.REFERRAL) - - -class AdminReferralDetailApiView(RetrieveAPIView): - serializer_class = AdminReferralSerializer - lookup_field = 'uuid' - permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements] - required_alternate_scopes = { - "GET": [["admin.referrals:retrieve"]], - } - - def get_queryset(self): - return ADMIN_PROMOTION_QUERYSET.filter(plan__processor=ProcessorTypeChoices.REFERRAL)