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 __str__(self):
return f"{self.label} --> {self.plan}"
def get_wallet_category_uuid(self): 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): def get_recipient_uuid(self, plan=None, event=None):
model_name, key = self.recipient_uuid_field.split('::') try:
if model_name == 'event': model_name, key = self.recipient_uuid_field.split('::')
obj = event if model_name == 'event':
elif model_name == 'plan': obj = event
obj = self.plan elif model_name == 'plan':
else: obj = plan
raise NotImplementedError() 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): def get_promotion_amount(self, plan=None, event=None):
base_amount = self.base_amount # 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: base_amount = obj.data.get(key)
promotion_amount = base_amount except Exception as e:
try:
base_amount = int(self.base_amount_field) # todo: to float
except:
pass
elif self.plan.balance >= base_amount: return base_amount
promotion_amount = base_amount
else:
raise
return promotion_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')