From 30ccca8efc0feab28f5bb83a806e9ac4e888cfd9 Mon Sep 17 00:00:00 2001 From: Sayyid Hamid Mahdavi Date: Thu, 25 Jun 2026 11:52:04 +0330 Subject: [PATCH] get promotion amount from another promotion --- ...e_holder_alter_plan_description_details.py | 23 +++++++ apps/promotions/models.py | 34 ++++++--- apps/promotions/tests.py | 69 ++++++++++++++++++- 3 files changed, 116 insertions(+), 10 deletions(-) create mode 100644 apps/promotions/migrations/0008_plan_balance_holder_alter_plan_description_details.py diff --git a/apps/promotions/migrations/0008_plan_balance_holder_alter_plan_description_details.py b/apps/promotions/migrations/0008_plan_balance_holder_alter_plan_description_details.py new file mode 100644 index 0000000..a12d751 --- /dev/null +++ b/apps/promotions/migrations/0008_plan_balance_holder_alter_plan_description_details.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.4 on 2026-06-25 06:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('promotions', '0007_alter_event_options_plan_banner_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='plan', + name='balance_holder', + field=models.UUIDField(blank=True, null=True), + ), + migrations.AlterField( + model_name='plan', + name='description_details', + field=models.JSONField(blank=True, default=dict, verbose_name='description details'), + ), + ] diff --git a/apps/promotions/models.py b/apps/promotions/models.py index 6594fa8..12242db 100644 --- a/apps/promotions/models.py +++ b/apps/promotions/models.py @@ -112,6 +112,8 @@ class Plan(BaseModel): processor = models.CharField(max_length=64, verbose_name=_('processor'), db_index=True, choices=ProcessorTypeChoices.choices) # TODO: processor type data = models.JSONField(default=dict, verbose_name=_('data'), null=True, blank=True) + + balance_holder = models.UUIDField(null=True, blank=True) balance = models.IntegerField(verbose_name=_('balance'), default=0) objects = PlanQuerySet.as_manager() @@ -142,11 +144,27 @@ class Plan(BaseModel): yield recipient.promote(plan=self, event=event) def reserve_promotion_amount(self, amount): + + if self.balance_holder: + balance_holder_object = Promotion.objects.get(pk=self.balance_holder) + balance_field_name = 'promotion_amount' + else: + balance_holder_object = self + balance_field_name = 'balance' + updated = False - if amount and self.balance >= amount: - updated = Plan.objects.filter(pk=self.pk, balance__gte=amount).update( - balance=F('balance') - amount - ) + + select_query_kwargs = { + "pk": balance_holder_object.pk, + f"{balance_field_name}__gte": amount + } + update_query_kwargs = { + balance_field_name: F(balance_field_name) - amount + } + + if amount and getattr(balance_holder_object, balance_field_name) >= amount: + updated = balance_holder_object._meta.model.objects.filter(**select_query_kwargs).update( + **update_query_kwargs) return updated @@ -200,7 +218,6 @@ class Recipient(BaseModel): if obj_value != value: return None - model_name, key = recipient_candidate.split(':') if model_name == 'event': obj = event @@ -277,7 +294,6 @@ class Recipient(BaseModel): else: return base_amount, promotion_amount - # @property # def handler(self) -> BasePromotionHandler: # return PromotionTypeChoices(self.promotion_type).handler(self) @@ -287,7 +303,6 @@ class Recipient(BaseModel): if not recipient: return None - base_amount, promotion_amount = self.get_promotion_amount(plan=plan, event=event) promotion, created = Promotion.objects.get_or_create( promotion_amount=promotion_amount, @@ -306,7 +321,8 @@ class Recipient(BaseModel): else: raise Exception('promotion reward not reserved') except Exception as e: - promotion.change_state(from_states=[promotion.state], to_state=PaymentStateChoices.EXPECTED_FAILURE, same_ok=True) + promotion.change_state(from_states=[promotion.state], to_state=PaymentStateChoices.EXPECTED_FAILURE, + same_ok=True) promotion.refresh_from_db() return promotion @@ -382,7 +398,7 @@ class Promotion(BaseModel): if self.state not in [PaymentStateChoices.CREATED]: raise Exception(_('Cannot promote. not in correct state')) - if not self.promotion_amount: + if not self.promotion_amount or self.plan.balance_holder: to_not_payed = self.change_state( from_states=[PaymentStateChoices.CREATED], to_state=PaymentStateChoices.SUCCESS, diff --git a/apps/promotions/tests.py b/apps/promotions/tests.py index 68672a3..962ea2c 100644 --- a/apps/promotions/tests.py +++ b/apps/promotions/tests.py @@ -9,7 +9,8 @@ from rest_framework.test import APITestCase, override_settings, APIClient from apps.promotions.tasks import analyze_event_task from apps.users.models import User -from apps.promotions.models import Plan, Promotion, EventSaver, ProcessorTypeChoices, Event, Recipient +from apps.promotions.models import Plan, Promotion, EventSaver, ProcessorTypeChoices, Event, Recipient, \ + PaymentStateChoices AccessToken = get_access_token_model() Application = get_application_model() @@ -465,3 +466,69 @@ class ApplicationApiFlowsTests(APITestCase): self.assertEqual(self.serial_promotion_plan.balance, 100000) self.assertEqual(Event.objects.count(), 3) + + def test_plan_balance_from_another_promotion_success(self): + base_plan, _created = Plan.objects.get_or_create( + title='base_plan', + balance=0, + user=self.user_1, + application=self.application, + event_list=['advertising:manual'] + ) + + base_promotion, _created = Promotion.objects.get_or_create( + user_uuid = self.user_1_uuid, + application = self.application, + plan = base_plan, + state = PaymentStateChoices.SUCCESS, + base_amount = 10000, + promotion_amount = 10000 + ) + + promotion_event_saver, _created = EventSaver.objects.get_or_create( + title=self.first_ad_view_reward_label, + event_label=self.first_ad_view_reward_label, + save_once=True, + ) + promotion_plan, _created = Plan.objects.get_or_create( + title='promotion_plan', + balance=0, + balance_holder=base_promotion.pk, + user=self.user_1, + application=self.application, + event_list=[self.first_ad_view_reward_label] + ) + + self.promotion_recipient, _created = Recipient.objects.get_or_create( + label='promotion_recipient', + plan=promotion_plan, + wallet_uuid=self.user_1_uuid, + recipient_uuid_field=self.user_2_uuid, + base_amount_field=1000, + data={} + + ) + + auth = self._create_authorization_header(self.user_access_token.token) + + create_data = { + 'label': self.first_ad_view_reward_label, + "data": { + 'user': self.user_1_uuid, + } + } + + self.assertEqual(Event.objects.count(), 0) + response = self.client.post(reverse('promotions:promotion-create', kwargs={'plan': str(promotion_plan.uuid)}), + create_data, + HTTP_AUTHORIZATION=auth, format='json') + print(response.json()) + promotion_plan.refresh_from_db() + base_promotion.refresh_from_db() + self.assertEqual(base_promotion.base_amount, 10000) + self.assertEqual(base_promotion.promotion_amount, 9000) + self.assertEqual(Event.objects.count(), 1) + + response = self.client.post(reverse('promotions:promotion-create', kwargs={'plan': str(promotion_plan.uuid)}), create_data, + HTTP_AUTHORIZATION=auth, format='json') + print(response.json())