From 26c7e765a38ade17373c430a4f10fc5b18fd8132 Mon Sep 17 00:00:00 2001 From: Sayyid Hamid Mahdavi Date: Thu, 16 Oct 2025 19:05:13 +0330 Subject: [PATCH] promotion with new method works --- apps/promotions/migrations/0001_initial.py | 62 +++++++++- apps/promotions/models.py | 131 +++++++++++++++------ 2 files changed, 151 insertions(+), 42 deletions(-) diff --git a/apps/promotions/migrations/0001_initial.py b/apps/promotions/migrations/0001_initial.py index fcba557..8c8b6cb 100644 --- a/apps/promotions/migrations/0001_initial.py +++ b/apps/promotions/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.4 on 2025-08-02 16:18 +# Generated by Django 5.1.7 on 2025-10-16 15:00 import django.db.models.deletion import uuid @@ -16,18 +16,67 @@ class Migration(migrations.Migration): ] operations = [ + migrations.CreateModel( + name='EventSaver', + fields=[ + ('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('created_at', models.DateTimeField(auto_now_add=True, db_index=True)), + ('updated_at', models.DateTimeField(auto_now=True, db_index=True)), + ('title', models.CharField(max_length=255, verbose_name='title')), + ('event_label', models.CharField(db_index=True, max_length=255, unique=True, verbose_name='label')), + ('save_once', models.BooleanField(default=False, verbose_name='save once')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Event', + fields=[ + ('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('created_at', models.DateTimeField(auto_now_add=True, db_index=True)), + ('updated_at', models.DateTimeField(auto_now=True, db_index=True)), + ('label', models.CharField(db_index=True, max_length=255)), + ('data', models.JSONField()), + ('application', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), migrations.CreateModel( name='Plan', fields=[ ('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), ('created_at', models.DateTimeField(auto_now_add=True, db_index=True)), ('updated_at', models.DateTimeField(auto_now=True, db_index=True)), + ('event_list', models.JSONField(default=list)), ('title', models.CharField(db_index=True, max_length=255, verbose_name='title')), ('description', models.TextField(blank=True, verbose_name='description')), - ('promotion_type', models.CharField(choices=[('wallet percentage', 'Wallet percentage'), ('others', 'others')], db_index=True, max_length=64, verbose_name='promotion type')), - ('policy', models.JSONField(blank=True, default=dict, null=True, verbose_name='policy')), - ('balance', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='balance')), + ('promotion_type', models.CharField(choices=[('percentage', 'percentage'), ('referral', 'referral'), ('others', 'others')], db_index=True, max_length=64, verbose_name='promotion type')), + ('data', models.JSONField(blank=True, default=dict, null=True, verbose_name='data')), + ('balance', models.IntegerField(default=0, verbose_name='balance')), ('application', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Recipient', + fields=[ + ('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('created_at', models.DateTimeField(auto_now_add=True, db_index=True)), + ('updated_at', models.DateTimeField(auto_now=True, db_index=True)), + ('label', models.CharField(db_index=True, max_length=255)), + ('wallet_uuid', models.UUIDField(blank=True, null=True)), + ('promotion_type', models.CharField(choices=[('percentage', 'percentage'), ('referral', 'referral'), ('others', 'others')], db_index=True, max_length=64, verbose_name='promotion type')), + ('recipient_uuid_field', models.CharField(db_index=True, max_length=255)), + ('base_amount_field', models.CharField(db_index=True, max_length=255)), + ('data', models.JSONField(blank=True, default=dict, null=True)), + ('plan', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='recipients', to='promotions.plan')), ], options={ 'abstract': False, @@ -39,11 +88,14 @@ class Migration(migrations.Migration): ('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), ('created_at', models.DateTimeField(auto_now_add=True, db_index=True)), ('updated_at', models.DateTimeField(auto_now=True, db_index=True)), - ('state', models.IntegerField(choices=[(0, 'Created'), (1, 'Assigned'), (10, 'Expired')], default=0)), + ('state', models.IntegerField(choices=[(0, 'Created'), (1, 'Assigned'), (2, 'Promoted'), (10, 'Expired')], default=0)), + ('base_amount', models.IntegerField(blank=True, null=True)), + ('promotion_amount', models.IntegerField(blank=True, null=True)), ('data', models.JSONField(blank=True, null=True)), ('application', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)), ('plan', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='promotions', to='promotions.plan')), ('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), + ('recipient', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='promotions', to='promotions.recipient')), ], options={ 'abstract': False, diff --git a/apps/promotions/models.py b/apps/promotions/models.py index 7a0b68a..fc692a1 100644 --- a/apps/promotions/models.py +++ b/apps/promotions/models.py @@ -1,3 +1,5 @@ +import uuid + from django.conf import settings from django.db import models, transaction from django.db.models import F @@ -5,17 +7,17 @@ from django.utils import timezone from oauth2_provider.settings import oauth2_settings - from apps.promotions import handlers - +import logging from apps.promotions.handlers import PromotionTypeChoices, BasePromotionHandler - from apps.users.models import User +from utils.clients.wallet_client import deposit_to_user_wallet_submit, deposit_to_user_wallet_verify from utils.models import BaseModel from django.utils.translation import gettext_lazy as _ +logger = logging.getLogger(__name__) class EventSaverQuerySet(models.QuerySet): pass @@ -28,6 +30,9 @@ class EventSaver(BaseModel): objects = EventSaverQuerySet.as_manager() + def __str__(self): + return self.title + def save_event(self, **kwargs): user = kwargs.get('user') application = kwargs.get('application') @@ -35,7 +40,8 @@ class EventSaver(BaseModel): data = kwargs.get('data') if self.save_once: - event, created = Event.objects.get_or_create(user=user, application=application, label=label, defaults={"data": data}) + event, created = Event.objects.get_or_create(user=user, application=application, label=label, + defaults={"data": data}) if created: return event else: @@ -87,7 +93,7 @@ class Plan(BaseModel): title = models.CharField(max_length=255, verbose_name=_('title'), db_index=True) description = models.TextField(verbose_name=_('description'), blank=True) promotion_type = models.CharField(max_length=64, verbose_name=_('promotion type'), db_index=True, - choices=PromotionTypeChoices.choices) + choices=PromotionTypeChoices.choices) # TODO: processor type data = models.JSONField(default=dict, verbose_name=_('data'), null=True, blank=True) balance = models.IntegerField(verbose_name=_('balance'), default=0) @@ -97,6 +103,7 @@ class Plan(BaseModel): return self.title def process_event(self, event=None): + # TODO: processor self.promote(event=event) def promote(self, event=None): @@ -115,40 +122,59 @@ class Plan(BaseModel): class Recipient(BaseModel): label = models.CharField(max_length=255, db_index=True) plan = models.ForeignKey(Plan, on_delete=models.PROTECT, related_name='recipients', null=True, blank=True) - recipient_uuid_field = models.CharField(max_length=255, db_index=True) - # wallet_uuid = models.UUIDField(null=True, blank=True) + wallet_uuid = models.UUIDField(null=True, blank=True) promotion_type = models.CharField(max_length=64, verbose_name=_('promotion type'), db_index=True, choices=PromotionTypeChoices.choices) - base_amount = models.FloatField(null=True, blank=True) + recipient_uuid_field = models.CharField(max_length=255, db_index=True) + base_amount_field = models.CharField(max_length=255, db_index=True) + data = models.JSONField(null=True, blank=True, default=dict) + def __str__(self): + return f"{self.label} --> {self.plan}" + def get_wallet_category_uuid(self): - return settings.WALLET_PROMOTION_CATEGORY_UUID + return self.wallet_uuid or settings.WALLET_PROMOTION_CATEGORY_UUID - def get_recipient_uuid(self, event=None): - model_name, key = self.recipient_uuid_field.split('::') - if model_name == 'event': - obj = event - elif model_name == 'plan': - obj = self.plan - else: - raise NotImplementedError() + def get_recipient_uuid(self, plan=None, event=None): + try: + model_name, key = self.recipient_uuid_field.split('::') + if model_name == 'event': + obj = event + elif model_name == 'plan': + obj = plan + else: + raise NotImplementedError() - return obj.data.get(key) + return obj.data.get(key) + except Exception as e: + try: + uuid.UUID(self.recipient_uuid_field) + return self.recipient_uuid_field + except: + pass - def calculate(self, plan=None, event=None): - base_amount = self.base_amount + def get_promotion_amount(self, plan=None, event=None): + # TODO: get base amount from policy_choice + base_amount = 0 + try: + model_name, key = self.base_amount_field.split('::') + if model_name == 'event': + obj = event + elif model_name == 'plan': + obj = plan + else: + raise NotImplementedError() - if base_amount <= base_amount and self.plan.balance >= base_amount: - promotion_amount = base_amount + base_amount = obj.data.get(key) + except Exception as e: + try: + base_amount = int(self.base_amount_field) # todo: to float + except: + pass - elif self.plan.balance >= base_amount: - promotion_amount = base_amount - - else: - raise - return promotion_amount + return base_amount # @property # def handler(self) -> BasePromotionHandler: @@ -156,15 +182,15 @@ class Recipient(BaseModel): def promote(self, plan=None, event=None): with transaction.atomic(): - promotion_amount = self.calculate(plan=plan, event=event) + promotion_amount = self.get_promotion_amount(plan=plan, event=event) reserved = self.plan.reserve_promotion_amount(promotion_amount) if reserved: promotion, created = Promotion.objects.get_or_create( promotion_amount=promotion_amount, - user_id=self.get_recipient_uuid(event), + user_id=self.get_recipient_uuid(plan, event), plan=plan, recipient=self, - base_amount=self.base_amount, + base_amount=promotion_amount, ) if created: promotion.promote() @@ -186,7 +212,7 @@ class Promotion(BaseModel): PROMOTED = 2, _('Promoted') EXPIRED = 10, _('Expired') - user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT) + user_id = models.UUIDField() # user_uuid 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) @@ -223,8 +249,39 @@ class Promotion(BaseModel): self.change_state(from_states=[Promotion.StateChoices.CREATED], to_state=Promotion.StateChoices.ASSIGNED, same_ok=False, raise_exception=True) - handler = self.plan.handler - if handler.promote(promotion=self, **kwargs): - self.change_state(from_states=[Promotion.StateChoices.ASSIGNED], to_state=Promotion.StateChoices.PROMOTED) - return True - return False + payee_uuid = self.user_id + + data = { + "uuid": str(self.uuid), + "payee_id": str(payee_uuid), + "payee_type": 1, + "amount": self.promotion_amount, + "details": { + 'description': _("for promotion"), + 'reference_id': str(self.uuid), + 'application_details_url': '' + }, + } + + submit_response = deposit_to_user_wallet_submit(data) + try: + submit_response.uuid + except Exception as e: + logger.exception('Failed to submit payment') + raise Exception('Failed to submit payment') + + verify_response = deposit_to_user_wallet_verify(str(self.uuid)) + logger.debug(f'verify response: {verify_response}') + try: + if verify_response.state != 5: + self.change_state(from_states=[Promotion.StateChoices.ASSIGNED], + to_state=Promotion.StateChoices.PROMOTED) + else: + logger.debug('Failed to verify payment') + raise Exception('Failed to verify payment') + + except Exception as e: + logger.exception('Failed to verify payment') + raise Exception('Failed to verify payment') + +