FEATURE(promotions): adding promotion is used status for user

adding promotion is used status for user
This commit is contained in:
Haydar Ghasemi 2026-07-26 15:25:49 +03:30
parent ab348d5134
commit 3cef84a346
6 changed files with 213 additions and 5 deletions

View file

@ -135,6 +135,18 @@ class Plan(BaseModel):
def __str__(self):
return self.title
def get_configured_promotion_amount(self):
amount = 0
for recipient in self.recipients.filter(recipient_uuid_field='->event:user'): #TODO: instead of hard code use better soloution
base_amount, promotion_amount = recipient.get_promotion_amount()
amount += promotion_amount
return amount
def is_promotion_used_by(self, user_uuid):
return self.promotions.filter(user_uuid=user_uuid).exclude(
state__in=[PaymentStateChoices.FAILED, PaymentStateChoices.EXPECTED_FAILURE]
).exists()
def process_event(self, event=None):
# TODO: processor
return self.promote_all(event=event)
@ -343,6 +355,23 @@ class PromotionQuerySet(models.QuerySet):
def used(self, user):
return self.filter(user=user, state__in=[Promotion.PaymentStateChoices.CREATED])
def is_processed_for_user(self, user_uuid, event_label):
return self.filter(user_uuid=user_uuid, event__label=event_label).exists()
def get_event_status_for_user(user_uuid, event_label):
processed = Promotion.objects.is_processed_for_user(user_uuid, event_label)
promotion_amount = None
if not processed:
plan = Plan.objects.filter(event_list__icontains=event_label).first()
if plan:
promotion_amount = plan.get_configured_promotion_amount()
return {
'event_label': event_label,
'processed': processed,
'promotion_amount': promotion_amount,
}
class Promotion(BaseModel):
user_uuid = models.UUIDField() # user_uuid

View file

@ -84,6 +84,12 @@ class UserRecipientSerializer(serializers.ModelSerializer):
"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:

View file

@ -719,3 +719,111 @@ class ApplicationApiFlowsTests(APITestCase):
response = self.client.post(url_promote, event_create_data, HTTP_AUTHORIZATION=auth, format='json')
print(response.json())
self.assertEqual(response.status_code, 404)
def _create_first_ad_create_plan(self):
event_label = 'ads::first-ad-create'
plan_title = 'first-ad-create'
promotion_amount = 30000
EventSaver.objects.get_or_create(
event_label=event_label,
defaults={
'title': plan_title,
'save_once': True,
},
)
plan, _ = Plan.objects.get_or_create(
title=plan_title,
defaults={
'balance': promotion_amount * 1000,
'user': self.user_1,
'application': self.application,
'event_list': [event_label],
'processor': ProcessorTypeChoices.OTHERS,
},
)
Recipient.objects.update_or_create(
label=plan_title,
plan=plan,
defaults={
'recipient_uuid_field': '->event:user',
'base_amount_field': str(promotion_amount),
},
)
return plan, event_label, promotion_amount
def test_first_ad_create_event_status_not_processed(self):
plan, event_label, promotion_amount = self._create_first_ad_create_plan()
auth = self._create_authorization_header(self.user_access_token.token)
response = self.client.get(
reverse('promotions:event-status', kwargs={'event_label': event_label}),
HTTP_AUTHORIZATION=auth,
format='json',
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {
'event_label': event_label,
'processed': False,
'promotion_amount': promotion_amount,
})
def test_first_ad_create_event_status_processed(self):
plan, event_label, promotion_amount = self._create_first_ad_create_plan()
auth = self._create_authorization_header(self.user_access_token.token)
event_create_data = {
'label': event_label,
'data': {
'user': str(self.user_1_uuid),
},
}
response = self.client.post(
reverse('promotions:promotion-create', kwargs={'plan': plan.uuid}),
event_create_data,
HTTP_AUTHORIZATION=auth,
format='json',
)
self.assertEqual(response.status_code, 201)
response = self.client.get(
reverse('promotions:event-status', kwargs={'event_label': event_label}),
HTTP_AUTHORIZATION=auth,
format='json',
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {
'event_label': event_label,
'processed': True,
'promotion_amount': None,
})
plan.refresh_from_db()
self.assertEqual(plan.balance, promotion_amount * 1000 - promotion_amount)
def test_first_ad_create_event_status_not_processed_when_only_event_exists(self):
plan, event_label, promotion_amount = self._create_first_ad_create_plan()
auth = self._create_authorization_header(self.user_access_token.token)
Event.objects.create(
user=self.user_1_uuid,
application=self.application,
label=event_label,
data={'user': str(self.user_1_uuid)},
)
response = self.client.get(
reverse('promotions:event-status', kwargs={'event_label': event_label}),
HTTP_AUTHORIZATION=auth,
format='json',
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {
'event_label': event_label,
'processed': False,
'promotion_amount': promotion_amount,
})

View file

@ -5,7 +5,12 @@ from utils.router import ProfileRouter
from . import views_user
from django.urls import NoReverseMatch, path, re_path, include
from .views_user import ApplicationPromoteUserApiView, ApplicationEventSubmitAPIView, ApplicationPromotionListApiView
from .views_user import (
ApplicationPromoteUserApiView,
ApplicationEventSubmitAPIView,
ApplicationPromotionListApiView,
ApplicationEventStatusApiView,
)
app_name = 'promotions'
@ -19,6 +24,7 @@ router.register(r'plans', views_user.UserPlanViewSet, basename='user-plans')
urlpatterns = [
path('api/v1/plans/<plan>/', ApplicationPromoteUserApiView.as_view(), name='promotion-create'),
path('api/v1/plans/<plan>/promotins', ApplicationPromotionListApiView.as_view(), name='promotion-list'),
path('api/v1/events/<path:event_label>/status/', ApplicationEventStatusApiView.as_view(), name='event-status'),
path('api/v1/events/submit', ApplicationEventSubmitAPIView.as_view(), name='event-create'),
# path('api/v1/events/get', ApplicationEventRetrieveAPIView.as_view(), name='event-retrieve'),
path('api/v2/', include(router.urls)),

View file

@ -12,8 +12,15 @@ from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRe
from apps.gooyal_oauth2.utils import get_application
from utils.clients.accounts_client import get_user_info
from utils.exceptions import UnprocessableEntity
from .models import Plan, Promotion, EventSaver
from .serializers import PlanSerializer, PromotionSerializer, EventSerializer, PromoteSerializer, UserPlanSerializer
from .models import Plan, Promotion, EventSaver, get_event_status_for_user
from .serializers import (
PlanSerializer,
PromotionSerializer,
EventSerializer,
PromoteSerializer,
UserPlanSerializer,
PromotionStatusSerializer,
)
from .tasks import analyze_event_task
from ..users.models import User
from django.utils.translation import gettext as _
@ -105,8 +112,38 @@ class ApplicationEventViewSet(
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
required_alternate_scopes = {
"POST": [["promotions.application.event:submit"]],
"GET": [["promotions.application.event:submit"]],
}
def _resolve_user(self):
user = getattr(self.request, 'user', None)
if user and getattr(user, 'uuid', None):
return user
user_uuid = self.kwargs.get('user_uuid')
user = User.objects.filter(uuid=user_uuid).first()
if not user:
user_info = get_user_info(user_uuid)
if user_info:
user, _ = User.objects.get_or_create(uuid=user_uuid)
if not user:
raise NotFound(_('user {user_uuid} not found').format(user_uuid=user_uuid))
self.request.user = user
return user
@action(
detail=False,
methods=['GET'],
url_path=r'(?P<event_label>.+)/status',
serializer_class=PromotionStatusSerializer,
)
def status(self, request, user_uuid=None, event_label=None):
user = self._resolve_user()
serializer = self.get_serializer(get_event_status_for_user(user.uuid, event_label))
return Response(serializer.data)
def perform_create(self, serializer: EventSerializer):
if hasattr(self.request, 'user'):
user = self.request.user

View file

@ -9,8 +9,15 @@ from rest_framework.viewsets import GenericViewSet
from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements
from apps.gooyal_oauth2.utils import get_application
from utils.exceptions import UnprocessableEntity
from .models import Plan, Promotion, EventSaver
from .serializers import PlanSerializer, PromotionSerializer, EventSerializer, PromoteSerializer, UserPlanSerializer
from .models import Plan, Promotion, EventSaver, get_event_status_for_user
from .serializers import (
PlanSerializer,
PromotionSerializer,
EventSerializer,
PromoteSerializer,
UserPlanSerializer,
PromotionStatusSerializer,
)
from .tasks import analyze_event_task
from ..users.models import User
@ -107,6 +114,21 @@ class ApplicationPromoteUserApiView(CreateAPIView):
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
class ApplicationEventStatusApiView(RetrieveAPIView):
serializer_class = PromotionStatusSerializer
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
required_alternate_scopes = {
"GET": [[]],
}
def retrieve(self, request, *args, **kwargs):
event_label = kwargs['event_label']
user_uuid = request.user.uuid
serializer = self.get_serializer(get_event_status_for_user(user_uuid, event_label))
return Response(serializer.data)
class ApplicationPromotionListApiView(ListAPIView):
model = Promotion
serializer_class = PromotionSerializer