REFACTOR(promotions): split admin panel into views/serializers/urls subpackages

Restructures the admin promotions/referrals endpoints to match the
admin/users folder split used by the sibling advertising service
(apps/crm, apps/escrow, apps/stores there): views/admin/, serializers/admin/,
and a shared urls/router.py with the two resources as GenericViewSets
registered on one DefaultRouter, instead of flat _admin-suffixed modules
with separate ListAPIView/RetrieveAPIView classes.

Scoped to the admin slice only -- the pre-existing user-token/
application-token views, urls, and serializers keep their original
_user/_application file layout and URL namespaces, since converting
those too would rename routes tests.py and external callers depend on.
filters.py stays a single flat file, matching how advertising's own
filters.py is not split into subfolders either.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Ali Asadi 2026-08-23 17:27:15 +03:30
parent d27ae32e27
commit 1d77303f00
16 changed files with 319 additions and 298 deletions

View file

@ -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']

View file

@ -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)

View file

@ -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',
]

View file

@ -0,0 +1,8 @@
from .promotion import AdminPlanSummarySerializer, AdminPromotionSerializer
from .referral import AdminReferralSerializer
__all__ = [
'AdminPlanSummarySerializer',
'AdminPromotionSerializer',
'AdminReferralSerializer',
]

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -0,0 +1,6 @@
from .router import router
from . import admin_urls
app_name = 'promotions-admin'
urlpatterns = router.urls + admin_urls.urlpatterns

View file

@ -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 = []

View file

@ -0,0 +1,3 @@
from rest_framework.routers import DefaultRouter
router = DefaultRouter()

View file

@ -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/<uuid:uuid>/', views_admin.AdminPromotionDetailApiView.as_view(), name='promotion-detail'),
path('referrals/', views_admin.AdminReferralListApiView.as_view(), name='referral-list'),
path('referrals/<uuid:uuid>/', views_admin.AdminReferralDetailApiView.as_view(), name='referral-detail'),
]

View file

@ -0,0 +1,6 @@
from .admin import AdminPromotionViewSet, AdminReferralViewSet
__all__ = [
'AdminPromotionViewSet',
'AdminReferralViewSet',
]

View file

@ -0,0 +1,7 @@
from .promotion import AdminPromotionViewSet
from .referral import AdminReferralViewSet
__all__ = [
'AdminPromotionViewSet',
'AdminReferralViewSet',
]

View file

@ -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"]],
}

View file

@ -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')

View file

@ -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)