first test pass

This commit is contained in:
Sayyid Hamid Mahdavi 2025-11-06 14:11:00 +03:30
parent c6ad3cb82d
commit 450aadd6da
6 changed files with 173 additions and 28 deletions

View file

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

View file

@ -45,6 +45,7 @@ class EventSaver(BaseModel):
if created: if created:
return event return event
else: else:
# TODO: good exception
raise Exception(_('already saved')) raise Exception(_('already saved'))
else: else:
@ -104,11 +105,11 @@ class Plan(BaseModel):
def process_event(self, event=None): def process_event(self, event=None):
# TODO: processor # 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(): 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): def reserve_promotion_amount(self, amount):
updated = False updated = False
@ -139,14 +140,13 @@ class Recipient(BaseModel):
def get_recipient_uuid(self, plan=None, event=None): def get_recipient_uuid(self, plan=None, event=None):
try: try:
model_name, key = self.recipient_uuid_field.split('::') model_name, key = self.recipient_uuid_field.split(':')
if model_name == 'event': if model_name == 'event':
obj = event obj = event
elif model_name == 'plan': elif model_name == 'plan':
obj = plan obj = plan
else: else:
raise NotImplement raise NotImplementedError()
edError()
return obj.data.get(key) return obj.data.get(key)
except Exception as e: except Exception as e:
@ -160,7 +160,7 @@ class Recipient(BaseModel):
# TODO: get base amount from policy_choice # TODO: get base amount from policy_choice
base_amount = 0 base_amount = 0
try: try:
model_name, key = self.base_amount_field.split('::') model_name, key = self.base_amount_field.split(':')
if model_name == 'event': if model_name == 'event':
obj = event obj = event
elif model_name == 'plan': elif model_name == 'plan':
@ -188,7 +188,8 @@ class Recipient(BaseModel):
if reserved: if reserved:
promotion, created = Promotion.objects.get_or_create( promotion, created = Promotion.objects.get_or_create(
promotion_amount=promotion_amount, promotion_amount=promotion_amount,
user_id=self.get_recipient_uuid(plan, event), user_uuid=self.get_recipient_uuid(plan, event),
event=event,
plan=plan, plan=plan,
recipient=self, recipient=self,
base_amount=promotion_amount, base_amount=promotion_amount,
@ -196,9 +197,11 @@ class Recipient(BaseModel):
if created: if created:
promotion.promote() promotion.promote()
else: else:
raise raise Exception('promotion object not created')
else: else:
raise raise Exception('promotion reward not reserved')
return promotion
class PromotionQuerySet(models.QuerySet): class PromotionQuerySet(models.QuerySet):
@ -217,6 +220,7 @@ class Promotion(BaseModel):
application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.PROTECT, application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.PROTECT,
related_name='+', null=True, blank=True) related_name='+', null=True, blank=True)
plan = models.ForeignKey(Plan, on_delete=models.PROTECT, related_name='promotions', 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) recipient = models.ForeignKey(Recipient, on_delete=models.PROTECT, related_name='promotions', null=True, blank=True)
state = models.IntegerField(choices=StateChoices.choices, default=StateChoices.CREATED) state = models.IntegerField(choices=StateChoices.choices, default=StateChoices.CREATED)
base_amount = models.IntegerField(null=True, blank=True) 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, self.change_state(from_states=[Promotion.StateChoices.CREATED], to_state=Promotion.StateChoices.ASSIGNED,
same_ok=False, raise_exception=True) same_ok=False, raise_exception=True)
payee_uuid = self.user_id payee_uuid = self.user_uuid
data = { data = {
"uuid": str(self.uuid), "uuid": str(self.uuid),
@ -274,7 +278,7 @@ class Promotion(BaseModel):
verify_response = deposit_to_user_wallet_verify(str(self.uuid)) verify_response = deposit_to_user_wallet_verify(str(self.uuid))
logger.debug(f'verify response: {verify_response}') logger.debug(f'verify response: {verify_response}')
try: try:
if verify_response.state != 5: if verify_response.state == 5:
self.change_state(from_states=[Promotion.StateChoices.ASSIGNED], self.change_state(from_states=[Promotion.StateChoices.ASSIGNED],
to_state=Promotion.StateChoices.PROMOTED) to_state=Promotion.StateChoices.PROMOTED)
else: else:

View file

@ -4,19 +4,19 @@ from .models import Promotion, Plan, Event
class PromotionSerializer(serializers.ModelSerializer): class PromotionSerializer(serializers.ModelSerializer):
base_amount = serializers.IntegerField(required=True) # base_amount = serializers.IntegerField(required=True)
promotion_amount = serializers.IntegerField(read_only=True) # promotion_amount = serializers.IntegerField(read_only=True)
user = serializers.UUIDField(source='user_uuid')
class Meta: class Meta:
model = Promotion model = Promotion
fields = ( fields = (
"uuid",
"base_amount", "base_amount",
"promotion_amount", "promotion_amount",
"plan", "state",
"user_uuid", "user",
"application",
) )
read_only_fields = ('user_uuid', 'application', "promotion_amount",)
class EventSerializer(serializers.ModelSerializer): class EventSerializer(serializers.ModelSerializer):
@ -58,3 +58,19 @@ class PlanPromotSerializer(serializers.ModelSerializer):
"application", "application",
) )
read_only_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",
)

View file

@ -1,5 +1,6 @@
import uuid import uuid
from datetime import timedelta from datetime import timedelta
from unittest.mock import patch
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
@ -15,6 +16,49 @@ Application = get_application_model()
application_uuid_hex = 'b14e8b86-8f4a-44d9-b29d-badceb470053' 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): class ApplicationApiFlowsTests(APITestCase):
client_id = '4INGOCMoulE0fNY1SQlTbPtsWqqxGj2DdqjADq6u' client_id = '4INGOCMoulE0fNY1SQlTbPtsWqqxGj2DdqjADq6u'
application_uuid = uuid.UUID(application_uuid_hex) application_uuid = uuid.UUID(application_uuid_hex)
@ -33,6 +77,21 @@ class ApplicationApiFlowsTests(APITestCase):
] ]
def setUp(self): 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) 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, label = self.first_ad_view_reward_label,
plan = self.first_ad_view_reward_plan, plan = self.first_ad_view_reward_plan,
wallet_uuid = self.user_uuid, wallet_uuid = self.user_uuid,
recipient_uuid_field="", recipient_uuid_field="event:user",
base_amount_field="", base_amount_field="event:base_amount",
) )
@ -118,14 +177,18 @@ class ApplicationApiFlowsTests(APITestCase):
'label': self.first_ad_view_reward_label, 'label': self.first_ad_view_reward_label,
"data": { "data": {
'user': self.user_uuid, '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()) print(response.json())
# event = Event.objects.get(user=self.user, label=self.first_ad_view_reward_label) self.first_ad_view_reward_plan.refresh_from_db()
# event.analyze() self.assertEqual(self.first_ad_view_reward_plan.balance, 10000 - 800)
# print(event)
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 return
def test_referral_reward_success(self): def test_referral_reward_success(self):

View file

@ -5,6 +5,8 @@ from utils.router import ProfileRouter
from . import views from . import views
from django.urls import NoReverseMatch, path, re_path, include from django.urls import NoReverseMatch, path, re_path, include
from .views import ApplicationPromoteUserApiView
app_name = 'promotions' app_name = 'promotions'
router = DefaultRouter() router = DefaultRouter()
@ -16,4 +18,5 @@ router.register(r'plans', views.UserViewSet, basename='user')
urlpatterns = [ urlpatterns = [
path('api/v1/', include(router.urls), name='events'), path('api/v1/', include(router.urls), name='events'),
path('api/v1/promote/<plan>/', ApplicationPromoteUserApiView.as_view(), name='promote'),
] ]

View file

@ -1,13 +1,15 @@
from drf_spectacular.utils import extend_schema 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.decorators import action
from rest_framework import exceptions 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 rest_framework.viewsets import GenericViewSet
from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements
from apps.gooyal_oauth2.utils import get_application from apps.gooyal_oauth2.utils import get_application
from .models import Plan, Promotion, EventSaver 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 .tasks import analyze_event_task
from ..users.models import User from ..users.models import User
@ -145,8 +147,36 @@ class UserViewSet(mixins.RetrieveModelMixin,
raise exceptions.ValidationError(str(e)) raise exceptions.ValidationError(str(e))
plan: Plan = self.get_object() plan: Plan = self.get_object()
plan.process_event(event)
return 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)