diff --git a/apps/promotions/migrations/0002_promotion_event_alter_plan_processor_and_more.py b/apps/promotions/migrations/0002_promotion_event_alter_plan_processor_and_more.py new file mode 100644 index 0000000..2a37b73 --- /dev/null +++ b/apps/promotions/migrations/0002_promotion_event_alter_plan_processor_and_more.py @@ -0,0 +1,29 @@ +# Generated by Django 5.1.7 on 2025-11-06 08:42 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('promotions', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='promotion', + name='event', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='promotions', to='promotions.event'), + ), + migrations.AlterField( + model_name='plan', + name='processor', + field=models.CharField(choices=[('percentage', 'percentage'), ('referral', 'referral'), ('others', 'others')], db_index=True, max_length=64, verbose_name='processor'), + ), + migrations.AlterField( + model_name='recipient', + name='promotion_type', + field=models.CharField(choices=[], db_index=True, max_length=64, verbose_name='promotion type'), + ), + ] diff --git a/apps/promotions/models.py b/apps/promotions/models.py index 5db1149..b04bf23 100644 --- a/apps/promotions/models.py +++ b/apps/promotions/models.py @@ -45,6 +45,7 @@ class EventSaver(BaseModel): if created: return event else: + # TODO: good exception raise Exception(_('already saved')) else: @@ -104,11 +105,11 @@ class Plan(BaseModel): def process_event(self, event=None): # TODO: processor - self.promote(event=event) + return self.promote_all(event=event) - def promote(self, event=None): + def promote_all(self, event=None): for recipient in self.recipients.all(): - recipient.promote(plan=self, event=event) + yield recipient.promote(plan=self, event=event) def reserve_promotion_amount(self, amount): updated = False @@ -139,14 +140,13 @@ class Recipient(BaseModel): def get_recipient_uuid(self, plan=None, event=None): try: - model_name, key = self.recipient_uuid_field.split('::') + model_name, key = self.recipient_uuid_field.split(':') if model_name == 'event': obj = event elif model_name == 'plan': obj = plan else: - raise NotImplement - edError() + raise NotImplementedError() return obj.data.get(key) except Exception as e: @@ -160,7 +160,7 @@ class Recipient(BaseModel): # TODO: get base amount from policy_choice base_amount = 0 try: - model_name, key = self.base_amount_field.split('::') + model_name, key = self.base_amount_field.split(':') if model_name == 'event': obj = event elif model_name == 'plan': @@ -188,7 +188,8 @@ class Recipient(BaseModel): if reserved: promotion, created = Promotion.objects.get_or_create( promotion_amount=promotion_amount, - user_id=self.get_recipient_uuid(plan, event), + user_uuid=self.get_recipient_uuid(plan, event), + event=event, plan=plan, recipient=self, base_amount=promotion_amount, @@ -196,9 +197,11 @@ class Recipient(BaseModel): if created: promotion.promote() else: - raise + raise Exception('promotion object not created') else: - raise + raise Exception('promotion reward not reserved') + + return promotion class PromotionQuerySet(models.QuerySet): @@ -217,6 +220,7 @@ class Promotion(BaseModel): application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.PROTECT, related_name='+', null=True, blank=True) plan = models.ForeignKey(Plan, on_delete=models.PROTECT, related_name='promotions', null=True, blank=True) + event = models.ForeignKey(Event, on_delete=models.PROTECT, related_name='promotions', null=True, blank=True) recipient = models.ForeignKey(Recipient, on_delete=models.PROTECT, related_name='promotions', null=True, blank=True) state = models.IntegerField(choices=StateChoices.choices, default=StateChoices.CREATED) base_amount = models.IntegerField(null=True, blank=True) @@ -250,7 +254,7 @@ class Promotion(BaseModel): self.change_state(from_states=[Promotion.StateChoices.CREATED], to_state=Promotion.StateChoices.ASSIGNED, same_ok=False, raise_exception=True) - payee_uuid = self.user_id + payee_uuid = self.user_uuid data = { "uuid": str(self.uuid), @@ -274,7 +278,7 @@ class Promotion(BaseModel): verify_response = deposit_to_user_wallet_verify(str(self.uuid)) logger.debug(f'verify response: {verify_response}') try: - if verify_response.state != 5: + if verify_response.state == 5: self.change_state(from_states=[Promotion.StateChoices.ASSIGNED], to_state=Promotion.StateChoices.PROMOTED) else: diff --git a/apps/promotions/serializers.py b/apps/promotions/serializers.py index beb86ba..91ab52e 100644 --- a/apps/promotions/serializers.py +++ b/apps/promotions/serializers.py @@ -4,19 +4,19 @@ from .models import Promotion, Plan, Event class PromotionSerializer(serializers.ModelSerializer): - base_amount = serializers.IntegerField(required=True) - promotion_amount = serializers.IntegerField(read_only=True) + # base_amount = serializers.IntegerField(required=True) + # promotion_amount = serializers.IntegerField(read_only=True) + user = serializers.UUIDField(source='user_uuid') class Meta: model = Promotion fields = ( + "uuid", "base_amount", "promotion_amount", - "plan", - "user_uuid", - "application", + "state", + "user", ) - read_only_fields = ('user_uuid', 'application', "promotion_amount",) class EventSerializer(serializers.ModelSerializer): @@ -58,3 +58,19 @@ class PlanPromotSerializer(serializers.ModelSerializer): "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", + ) diff --git a/apps/promotions/tests.py b/apps/promotions/tests.py index 141e78d..5a04449 100644 --- a/apps/promotions/tests.py +++ b/apps/promotions/tests.py @@ -1,5 +1,6 @@ import uuid from datetime import timedelta +from unittest.mock import patch from django.urls import reverse from django.utils import timezone @@ -15,6 +16,49 @@ Application = get_application_model() application_uuid_hex = 'b14e8b86-8f4a-44d9-b29d-badceb470053' + +def mock_submit_deposit_success(data): + import uuid as sys_uuid + class Tmp(): + uuid = sys_uuid.uuid4() + + + data = Tmp() + + return data + + +def mock_verify_deposit_success(uuid): + import uuid as sys_uuid + class Tmp(): + uuid = sys_uuid.uuid4() + state = 5 + + data = Tmp() + + return data + +def mock_submit_withdraw_success(data): + import uuid as sys_uuid + class Tmp(): + uuid = sys_uuid.uuid4() + + data = Tmp() + + return data + + +def mock_verify_withdraw_success(uuid): + import uuid as sys_uuid + class Tmp(): + uuid = sys_uuid.uuid4() + state = 5 + + data = Tmp() + + return data + + class ApplicationApiFlowsTests(APITestCase): client_id = '4INGOCMoulE0fNY1SQlTbPtsWqqxGj2DdqjADq6u' application_uuid = uuid.UUID(application_uuid_hex) @@ -33,6 +77,21 @@ class ApplicationApiFlowsTests(APITestCase): ] def setUp(self): + self.submit_deposit_success_patcher = patch('apps.promotions.models.deposit_to_user_wallet_submit', + mock_submit_deposit_success) + self.verify_deposit_success_patcher = patch('apps.promotions.models.deposit_to_user_wallet_verify', + mock_verify_deposit_success) + # self.submit_withdraw_success_patcher = patch('apps.promotions.models.withdraw_from_user_wallet_submit', + # mock_submit_withdraw_success) + # self.verify_withdraw_success_patcher = patch('apps.promotions.models.withdraw_from_user_wallet_verify', + # mock_verify_withdraw_success) + + + self.submit_deposit_success_patcher.start() + self.verify_deposit_success_patcher.start() + # self.submit_withdraw_success_patcher.start() + # self.verify_withdraw_success_patcher.start() + self.user, _ = User.objects.get_or_create(pk=self.user_uuid) @@ -86,8 +145,8 @@ class ApplicationApiFlowsTests(APITestCase): label = self.first_ad_view_reward_label, plan = self.first_ad_view_reward_plan, wallet_uuid = self.user_uuid, - recipient_uuid_field="", - base_amount_field="", + recipient_uuid_field="event:user", + base_amount_field="event:base_amount", ) @@ -118,14 +177,18 @@ class ApplicationApiFlowsTests(APITestCase): 'label': self.first_ad_view_reward_label, "data": { 'user': self.user_uuid, - 'ad': self.user_uuid + 'base_amount':800 } } - response = self.client.post(reverse('promotions:user-promote', kwargs={"pk":self.first_ad_view_reward_plan.uuid}), event_create_data, HTTP_AUTHORIZATION=auth, format='json') + # response = self.client.post(reverse('promotions:user-promote', kwargs={"pk":self.first_ad_view_reward_plan.uuid}), event_create_data, HTTP_AUTHORIZATION=auth, format='json') + response = self.client.post(reverse('promotions:promote', kwargs={"plan":self.first_ad_view_reward_plan.uuid}), event_create_data, HTTP_AUTHORIZATION=auth, format='json') print(response.json()) - # event = Event.objects.get(user=self.user, label=self.first_ad_view_reward_label) - # event.analyze() - # print(event) + self.first_ad_view_reward_plan.refresh_from_db() + self.assertEqual(self.first_ad_view_reward_plan.balance, 10000 - 800) + + response = self.client.post(reverse('promotions:promote', kwargs={"plan": self.first_ad_view_reward_plan.uuid}), event_create_data, HTTP_AUTHORIZATION=auth, format='json') + print(response.json()) + return def test_referral_reward_success(self): diff --git a/apps/promotions/urls.py b/apps/promotions/urls.py index 2bfcdde..4d2214b 100644 --- a/apps/promotions/urls.py +++ b/apps/promotions/urls.py @@ -5,6 +5,8 @@ from utils.router import ProfileRouter from . import views from django.urls import NoReverseMatch, path, re_path, include +from .views import ApplicationPromoteUserApiView + app_name = 'promotions' router = DefaultRouter() @@ -16,4 +18,5 @@ router.register(r'plans', views.UserViewSet, basename='user') urlpatterns = [ path('api/v1/', include(router.urls), name='events'), + path('api/v1/promote//', ApplicationPromoteUserApiView.as_view(), name='promote'), ] diff --git a/apps/promotions/views.py b/apps/promotions/views.py index e4878ba..396f3a0 100644 --- a/apps/promotions/views.py +++ b/apps/promotions/views.py @@ -1,13 +1,15 @@ from drf_spectacular.utils import extend_schema -from rest_framework import mixins +from rest_framework import mixins, status from rest_framework.decorators import action from rest_framework import exceptions +from rest_framework.generics import CreateAPIView, get_object_or_404 +from rest_framework.response import Response 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, EventSaver -from .serializers import PlanSerializer, PromotionSerializer, EventSerializer +from .serializers import PlanSerializer, PromotionSerializer, EventSerializer, PromoteSerializer from .tasks import analyze_event_task from ..users.models import User @@ -145,8 +147,36 @@ class UserViewSet(mixins.RetrieveModelMixin, raise exceptions.ValidationError(str(e)) plan: Plan = self.get_object() - plan.process_event(event) return +class ApplicationPromoteUserApiView(CreateAPIView): + model = Promotion + serializer_class = PromoteSerializer + + def get_plan(self): + plan_uuid = self.kwargs.get('plan') + plan = get_object_or_404(Plan, uuid=plan_uuid) + return plan + + def create(self, request, *args, **kwargs): + plan = self.get_plan() + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + data = serializer.validated_data + application = get_application(self.request) + event_saver: EventSaver = EventSaver.objects.filter(event_label=serializer.validated_data['label']).first() + # try: + event = event_saver.save_event(user=None, application=application, **serializer.validated_data) + # except Exception as e: + # raise exceptions.ValidationError(str(e)) + + result_list = plan.process_event(event) + data["promotions"] = result_list + + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + + +