recipient skeleton

This commit is contained in:
Sayyid Hamid Mahdavi 2025-10-12 21:02:48 +03:30
parent 32f94178cd
commit a5854390cb
7 changed files with 285 additions and 69 deletions

View file

@ -2,10 +2,11 @@ from functools import update_wrapper
from django.contrib import admin from django.contrib import admin
from .models import Promotion, Plan, Event from .models import Promotion, Plan, Event, Recipient
admin.site.register(Plan) admin.site.register(Plan)
admin.site.register(Promotion) admin.site.register(Promotion)
admin.site.register(Event) admin.site.register(Event)
admin.site.register(Recipient)

View file

@ -10,32 +10,24 @@ logger = logging.getLogger("promotion_handler")
class PromotionTypeChoices(models.TextChoices): class PromotionTypeChoices(models.TextChoices):
PERCENTAGE = 'percentage', _('percentage') PERCENTAGE = 'percentage', _('percentage')
REFERRAL = 'referral', _('referral')
OTHERS = 'others', _('others') OTHERS = 'others', _('others')
@enum_property
def handler(self):
if self.name == self.PERCENTAGE.name:
return PercentageHandler
elif self.name == self.OTHERS.name:
return OtherHandler
else:
raise NotImplementedError()
class BasePromotionHandler: class BasePromotionHandler:
def __init__(self, application=None, policy=None, balance=None): def __init__(self, plan):
self.application = application self.plan = plan
self.policy = policy
self.balance = balance
def calculate(self, **kwargs): def calculate(self, **kwargs):
# condition and amount
raise NotImplementedError() raise NotImplementedError()
def promote(self, **kwargs): def promote(self, **kwargs):
raise NotImplementedError() raise NotImplementedError()
# def get_recipient_uuid(self, recipient, **kwargs):
# raise NotImplementedError()
class PercentageHandler(BasePromotionHandler): class PercentageHandler(BasePromotionHandler):
def calculate(self, **kwargs): def calculate(self, **kwargs):
@ -48,8 +40,8 @@ class PercentageHandler(BasePromotionHandler):
promotion_amount = 1000 if self.balance >= 1000 else self.balance promotion_amount = 1000 if self.balance >= 1000 else self.balance
return promotion_amount return promotion_amount
def promote(self, promotion, **kwargs): def promote(self, **kwargs):
pass return
payee_type = self.policy.get('payee_type') payee_type = self.policy.get('payee_type')
payee_uuid = self.policy.get('payee_uuid') payee_uuid = self.policy.get('payee_uuid')
@ -89,5 +81,63 @@ class PercentageHandler(BasePromotionHandler):
logger.exception('error in finish visit:') logger.exception('error in finish visit:')
raise e raise e
class ReferralHandler(BasePromotionHandler):
def calculate(self, **kwargs):
event = kwargs.get('event')
plan = self.plan
if base_amount <= 1000 and self.balance >= 1000:
promotion_amount = base_amount
else:
promotion_amount = 1000 if self.balance >= 1000 else self.balance
return promotion_amount
def promote(self, **kwargs):
return
payee_type = self.policy.get('payee_type')
payee_uuid = self.policy.get('payee_uuid')
# promotion_amount = self.calculate(**kwargs)
try:
data = {
"uuid": str(promotion.uuid),
"payee_id": str(payee_uuid),
"payee_type": payee_type,
"amount": promotion.promotion_amount,
"details": {
'description': "for promotion",
'reference_id': str(promotion.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(promotion.uuid))
logger.debug(f'verify response: {verify_response}')
try:
if not verify_response.uuid or verify_response.state != 5:
logger.debug('Failed to verify payment')
raise Exception('Failed to verify payment')
else:
return True
except Exception as e:
logger.exception('Failed to verify payment')
raise Exception('Failed to verify payment')
except Exception as e:
logger.exception('error in finish visit:')
raise e
# def get_recipient_uuid(self, recipient, **kwargs):
# return recipient.uuid
class OtherHandler(BasePromotionHandler): class OtherHandler(BasePromotionHandler):
pass pass

View file

@ -0,0 +1,40 @@
# Generated by Django 5.2.6 on 2025-09-21 14:19
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('promotions', '0008_event_application_event_user'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name='event',
name='user',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
),
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)),
('plan', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='promotions.plan')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='promotion',
name='recipient',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='promotions', to='promotions.recipient'),
),
]

View file

@ -0,0 +1,76 @@
# Generated by Django 5.1.7 on 2025-10-12 13:39
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('promotions', '0009_alter_event_user_recipient_promotion_recipient'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.RemoveField(
model_name='plan',
name='max_amount',
),
migrations.RemoveField(
model_name='plan',
name='min_amount',
),
migrations.RemoveField(
model_name='plan',
name='policy',
),
migrations.AddField(
model_name='plan',
name='data',
field=models.JSONField(blank=True, default=dict, null=True, verbose_name='data'),
),
migrations.AddField(
model_name='plan',
name='user',
field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
preserve_default=False,
),
migrations.AddField(
model_name='recipient',
name='base_amount',
field=models.FloatField(blank=True, null=True),
),
migrations.AddField(
model_name='recipient',
name='data',
field=models.JSONField(blank=True, default=dict, null=True),
),
migrations.AddField(
model_name='recipient',
name='promotion_type',
field=models.CharField(choices=[('percentage', 'percentage'), ('referral', 'referral'), ('others', 'others')], db_index=True, default=None, max_length=64, verbose_name='promotion type'),
preserve_default=False,
),
migrations.AddField(
model_name='recipient',
name='recipient_uuid_field',
field=models.CharField(db_index=True, default=None, max_length=255),
preserve_default=False,
),
migrations.AddField(
model_name='recipient',
name='wallet_uuid',
field=models.UUIDField(blank=True, null=True),
),
migrations.AlterField(
model_name='plan',
name='promotion_type',
field=models.CharField(choices=[('percentage', 'percentage'), ('referral', 'referral'), ('others', 'others')], db_index=True, max_length=64, verbose_name='promotion type'),
),
migrations.AlterField(
model_name='recipient',
name='plan',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='recipients', to='promotions.plan'),
),
]

View file

@ -5,6 +5,7 @@ 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.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.models import BaseModel from utils.models import BaseModel
@ -17,7 +18,7 @@ class EventQuerySet(models.QuerySet):
class Event(BaseModel): class Event(BaseModel):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, null=True, blank=True)
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)
label = models.CharField(max_length=255, db_index=True) label = models.CharField(max_length=255, db_index=True)
@ -29,35 +30,32 @@ class Event(BaseModel):
return self.label return self.label
def analyze(self): def analyze(self):
for plan in Plan.objects.get_related_to_event(self): for plan in Plan.objects.related_to_event(self):
print(plan) plan.process_event(event=self)
plan.process_event(self)
class PlanQuerySet(models.QuerySet): class PlanQuerySet(models.QuerySet):
# this is for list of discount code for example # # this is for list of discount code for example
def calculate(self, user, application=None): # def calculate(self, user, application=None):
for plan in self.filter(application=application)[:10]: # for plan in self.filter(application=application)[:10]:
if not Promotion.objects.used(user): # if not Promotion.objects.used(user):
plan.calculate(user) # plan.calculate(user)
def get_related_to_event(self, event): def related_to_event(self, event):
return self.filter(event_list__icontains=event.label) return self.filter(event_list__icontains=event.label)
class Plan(BaseModel): class Plan(BaseModel):
# user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.PROTECT, application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.PROTECT, related_name='+',
related_name='+', null=True, blank=True) null=True, blank=True)
event_list = models.JSONField(default=list) event_list = models.JSONField(default=list)
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)
min_amount = models.FloatField(null=True, blank=True) data = models.JSONField(default=dict, verbose_name=_('data'), null=True, blank=True)
max_amount = models.FloatField(null=True, blank=True)
policy = models.JSONField(default=dict, verbose_name=_('policy'), null=True, blank=True)
balance = models.IntegerField(verbose_name=_('balance'), default=0) balance = models.IntegerField(verbose_name=_('balance'), default=0)
objects = PlanQuerySet.as_manager() objects = PlanQuerySet.as_manager()
@ -65,18 +63,26 @@ class Plan(BaseModel):
def __str__(self): def __str__(self):
return self.title return self.title
@property # @property
def handler(self) -> BasePromotionHandler: # def handler(self):
return PromotionTypeChoices(self.promotion_type).handler( # if self.promotion_type == PromotionTypeChoices.PERCENTAGE.name:
application=self.application, # return handlers.PercentageHandler(self)
policy=self.policy, #
balance=self.balance, # elif self.promotion_type == PromotionTypeChoices.REFERRAL.name:
) # return handlers.ReferralHandler(self)
#
# elif self.promotion_type == PromotionTypeChoices.OTHERS.name:
# return handlers.OtherHandler(self)
#
# else:
# raise NotImplementedError()
def calculate_promotion(self, **kwargs): def process_event(self, event=None):
handler = self.handler self.promote(event=event)
# self.base_amount = kwargs.get('base_amount', 0)
return handler.calculate(**kwargs) def promote(self, event=None):
for recipient in self.recipients.all():
recipient.promote(plan=self, event=event)
def reserve_promotion_amount(self, amount): def reserve_promotion_amount(self, amount):
updated = False updated = False
@ -86,25 +92,68 @@ class Plan(BaseModel):
) )
return updated return updated
def process_event(self, event=None):
print(event) class Recipient(BaseModel):
referral_code = event.data.get('referral_code') label = models.CharField(max_length=255, db_index=True)
user_uuid = event.data.get('user_uuid') plan = models.ForeignKey(Plan, on_delete=models.PROTECT, related_name='recipients', null=True, blank=True)
promotion_amount = self.calculate_promotion(**self.policy) recipient_uuid_field = models.CharField(max_length=255, db_index=True)
if referral_code and promotion_amount and not self.promotions.filter(user_id=referral_code, # wallet_uuid = models.UUIDField(null=True, blank=True)
data__user_uuid=user_uuid).exists(): promotion_type = models.CharField(max_length=64, verbose_name=_('promotion type'), db_index=True,
referral_user, created = User.objects.get_or_create(uuid=referral_code) choices=PromotionTypeChoices.choices)
user = User.objects.get_or_create(uuid=user_uuid)
with transaction.atomic(): base_amount = models.FloatField(null=True, blank=True)
if self.reserve_promotion_amount(promotion_amount): data = models.JSONField(null=True, blank=True, default=dict)
promotion, created = Promotion.objects.get_or_create(plan=self, user=referral_user, defaults={
"data": event.data def get_wallet_category_uuid(self):
}) return settings.WALLET_PROMOTION_CATEGORY_UUID
if not created:
raise Exception('Promotion already reserved') 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: else:
raise Exception('Promotion already reserved') raise NotImplementedError()
return obj.data.get(key)
def calculate(self, plan=None, event=None):
base_amount = self.base_amount
if base_amount <= base_amount and self.plan.balance >= base_amount:
promotion_amount = base_amount
elif self.plan.balance >= base_amount:
promotion_amount = base_amount
else:
raise
return promotion_amount
# @property
# def handler(self) -> BasePromotionHandler:
# return PromotionTypeChoices(self.promotion_type).handler(self)
def promote(self, plan=None, event=None):
with transaction.atomic():
promotion_amount = self.calculate(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),
plan=plan,
recipient=self,
base_amount=self.base_amount,
)
if created:
promotion.promote() promotion.promote()
else:
raise
else:
raise
class PromotionQuerySet(models.QuerySet): class PromotionQuerySet(models.QuerySet):
@ -123,6 +172,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)
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)
promotion_amount = models.IntegerField(null=True, blank=True) promotion_amount = models.IntegerField(null=True, blank=True)

View file

@ -85,10 +85,6 @@ class ApplicationEventViewSet(
analyze_event_task.delay(event.uuid) analyze_event_task.delay(event.uuid)
class UserPlanViewSet(mixins.RetrieveModelMixin, class UserPlanViewSet(mixins.RetrieveModelMixin,
mixins.ListModelMixin, mixins.ListModelMixin,
GenericViewSet): GenericViewSet):

View file

@ -371,3 +371,6 @@ CELERY_TASK_TRACK_STARTED = True
CELERY_TASK_TIME_LIMIT = 30 * 60 CELERY_TASK_TIME_LIMIT = 30 * 60
CELERY_BROKER_URL = REDIS_BASE_URL CELERY_BROKER_URL = REDIS_BASE_URL
CELERY_RESULT_BACKEND = REDIS_BASE_URL CELERY_RESULT_BACKEND = REDIS_BASE_URL
WALLET_SERVICE_CATEGORY_UUID = config('WALLET_SERVICE_CATEGORY_UUID', '') # TODO: better naming
WALLET_PROMOTION_CATEGORY_UUID = config('WALLET_PROMOTION_CATEGORY_UUID', '') # TODO: may be configurable