promotion with new method works

This commit is contained in:
Sayyid Hamid Mahdavi 2025-10-16 19:05:13 +03:30
parent e30005021d
commit 26c7e765a3
2 changed files with 151 additions and 42 deletions

View file

@ -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 django.db.models.deletion
import uuid import uuid
@ -16,18 +16,67 @@ class Migration(migrations.Migration):
] ]
operations = [ 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( migrations.CreateModel(
name='Plan', name='Plan',
fields=[ fields=[
('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), ('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)), ('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=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')), ('title', models.CharField(db_index=True, max_length=255, verbose_name='title')),
('description', models.TextField(blank=True, verbose_name='description')), ('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')), ('promotion_type', models.CharField(choices=[('percentage', 'percentage'), ('referral', 'referral'), ('others', 'others')], db_index=True, max_length=64, verbose_name='promotion type')),
('policy', models.JSONField(blank=True, default=dict, null=True, verbose_name='policy')), ('data', models.JSONField(blank=True, default=dict, null=True, verbose_name='data')),
('balance', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='balance')), ('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)), ('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={ options={
'abstract': False, '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)), ('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)), ('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=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)), ('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)), ('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')), ('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)), ('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={ options={
'abstract': False, 'abstract': False,

View file

@ -1,3 +1,5 @@
import uuid
from django.conf import settings from django.conf import settings
from django.db import models, transaction from django.db import models, transaction
from django.db.models import F from django.db.models import F
@ -5,17 +7,17 @@ from django.utils import timezone
from oauth2_provider.settings import oauth2_settings from oauth2_provider.settings import oauth2_settings
from apps.promotions import handlers from apps.promotions import handlers
import logging
from apps.promotions.handlers import PromotionTypeChoices, BasePromotionHandler from apps.promotions.handlers import PromotionTypeChoices, BasePromotionHandler
from apps.users.models import User 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 utils.models import BaseModel
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
logger = logging.getLogger(__name__)
class EventSaverQuerySet(models.QuerySet): class EventSaverQuerySet(models.QuerySet):
pass pass
@ -28,6 +30,9 @@ class EventSaver(BaseModel):
objects = EventSaverQuerySet.as_manager() objects = EventSaverQuerySet.as_manager()
def __str__(self):
return self.title
def save_event(self, **kwargs): def save_event(self, **kwargs):
user = kwargs.get('user') user = kwargs.get('user')
application = kwargs.get('application') application = kwargs.get('application')
@ -35,7 +40,8 @@ class EventSaver(BaseModel):
data = kwargs.get('data') data = kwargs.get('data')
if self.save_once: 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: if created:
return event return event
else: else:
@ -87,7 +93,7 @@ class Plan(BaseModel):
title = models.CharField(max_length=255, verbose_name=_('title'), db_index=True) title = models.CharField(max_length=255, verbose_name=_('title'), db_index=True)
description = models.TextField(verbose_name=_('description'), blank=True) description = models.TextField(verbose_name=_('description'), blank=True)
promotion_type = models.CharField(max_length=64, verbose_name=_('promotion type'), db_index=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) data = models.JSONField(default=dict, verbose_name=_('data'), null=True, blank=True)
balance = models.IntegerField(verbose_name=_('balance'), default=0) balance = models.IntegerField(verbose_name=_('balance'), default=0)
@ -97,6 +103,7 @@ class Plan(BaseModel):
return self.title return self.title
def process_event(self, event=None): def process_event(self, event=None):
# TODO: processor
self.promote(event=event) self.promote(event=event)
def promote(self, event=None): def promote(self, event=None):
@ -115,40 +122,59 @@ class Plan(BaseModel):
class Recipient(BaseModel): class Recipient(BaseModel):
label = models.CharField(max_length=255, db_index=True) label = models.CharField(max_length=255, db_index=True)
plan = models.ForeignKey(Plan, on_delete=models.PROTECT, related_name='recipients', null=True, blank=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, promotion_type = models.CharField(max_length=64, verbose_name=_('promotion type'), db_index=True,
choices=PromotionTypeChoices.choices) 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) data = models.JSONField(null=True, blank=True, default=dict)
def get_wallet_category_uuid(self): def __str__(self):
return settings.WALLET_PROMOTION_CATEGORY_UUID return f"{self.label} --> {self.plan}"
def get_recipient_uuid(self, event=None): def get_wallet_category_uuid(self):
return self.wallet_uuid or settings.WALLET_PROMOTION_CATEGORY_UUID
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': if model_name == 'event':
obj = event obj = event
elif model_name == 'plan': elif model_name == 'plan':
obj = self.plan obj = plan
else: else:
raise NotImplementedError() 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): def get_promotion_amount(self, plan=None, event=None):
base_amount = self.base_amount # TODO: get base amount from policy_choice
base_amount = 0
if base_amount <= base_amount and self.plan.balance >= base_amount: try:
promotion_amount = base_amount model_name, key = self.base_amount_field.split('::')
if model_name == 'event':
elif self.plan.balance >= base_amount: obj = event
promotion_amount = base_amount elif model_name == 'plan':
obj = plan
else: else:
raise raise NotImplementedError()
return promotion_amount
base_amount = obj.data.get(key)
except Exception as e:
try:
base_amount = int(self.base_amount_field) # todo: to float
except:
pass
return base_amount
# @property # @property
# def handler(self) -> BasePromotionHandler: # def handler(self) -> BasePromotionHandler:
@ -156,15 +182,15 @@ class Recipient(BaseModel):
def promote(self, plan=None, event=None): def promote(self, plan=None, event=None):
with transaction.atomic(): 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) reserved = self.plan.reserve_promotion_amount(promotion_amount)
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(event), user_id=self.get_recipient_uuid(plan, event),
plan=plan, plan=plan,
recipient=self, recipient=self,
base_amount=self.base_amount, base_amount=promotion_amount,
) )
if created: if created:
promotion.promote() promotion.promote()
@ -186,7 +212,7 @@ class Promotion(BaseModel):
PROMOTED = 2, _('Promoted') PROMOTED = 2, _('Promoted')
EXPIRED = 10, _('Expired') 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, 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)
@ -223,8 +249,39 @@ 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)
handler = self.plan.handler payee_uuid = self.user_id
if handler.promote(promotion=self, **kwargs):
self.change_state(from_states=[Promotion.StateChoices.ASSIGNED], to_state=Promotion.StateChoices.PROMOTED) data = {
return True "uuid": str(self.uuid),
return False "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')