first test pass
This commit is contained in:
parent
c6ad3cb82d
commit
450aadd6da
6 changed files with 173 additions and 28 deletions
|
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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'),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue