diff --git a/apps/promotions/models.py b/apps/promotions/models.py index 12242db..541ea1f 100644 --- a/apps/promotions/models.py +++ b/apps/promotions/models.py @@ -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 diff --git a/apps/promotions/serializers.py b/apps/promotions/serializers.py index 3fd744d..50ed72d 100644 --- a/apps/promotions/serializers.py +++ b/apps/promotions/serializers.py @@ -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: diff --git a/apps/promotions/tests.py b/apps/promotions/tests.py index d7c8c12..8f0305f 100644 --- a/apps/promotions/tests.py +++ b/apps/promotions/tests.py @@ -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, + }) diff --git a/apps/promotions/urls_user.py b/apps/promotions/urls_user.py index 10ef410..0183849 100644 --- a/apps/promotions/urls_user.py +++ b/apps/promotions/urls_user.py @@ -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//', ApplicationPromoteUserApiView.as_view(), name='promotion-create'), path('api/v1/plans//promotins', ApplicationPromotionListApiView.as_view(), name='promotion-list'), + path('api/v1/events//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)), diff --git a/apps/promotions/views_application.py b/apps/promotions/views_application.py index a75eb65..9664c7e 100644 --- a/apps/promotions/views_application.py +++ b/apps/promotions/views_application.py @@ -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.+)/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 diff --git a/apps/promotions/views_user.py b/apps/promotions/views_user.py index 843377f..c04edaf 100644 --- a/apps/promotions/views_user.py +++ b/apps/promotions/views_user.py @@ -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