recipient skeleton
This commit is contained in:
parent
32f94178cd
commit
a5854390cb
7 changed files with 285 additions and 69 deletions
|
|
@ -2,10 +2,11 @@ from functools import update_wrapper
|
|||
|
||||
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(Promotion)
|
||||
admin.site.register(Event)
|
||||
admin.site.register(Recipient)
|
||||
|
||||
|
|
|
|||
|
|
@ -10,32 +10,24 @@ logger = logging.getLogger("promotion_handler")
|
|||
|
||||
class PromotionTypeChoices(models.TextChoices):
|
||||
PERCENTAGE = 'percentage', _('percentage')
|
||||
REFERRAL = 'referral', _('referral')
|
||||
|
||||
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:
|
||||
def __init__(self, application=None, policy=None, balance=None):
|
||||
self.application = application
|
||||
self.policy = policy
|
||||
self.balance = balance
|
||||
def __init__(self, plan):
|
||||
self.plan = plan
|
||||
|
||||
def calculate(self, **kwargs):
|
||||
# condition and amount
|
||||
raise NotImplementedError()
|
||||
|
||||
def promote(self, **kwargs):
|
||||
raise NotImplementedError()
|
||||
|
||||
# def get_recipient_uuid(self, recipient, **kwargs):
|
||||
# raise NotImplementedError()
|
||||
|
||||
|
||||
class PercentageHandler(BasePromotionHandler):
|
||||
def calculate(self, **kwargs):
|
||||
|
|
@ -48,8 +40,8 @@ class PercentageHandler(BasePromotionHandler):
|
|||
promotion_amount = 1000 if self.balance >= 1000 else self.balance
|
||||
return promotion_amount
|
||||
|
||||
def promote(self, promotion, **kwargs):
|
||||
pass
|
||||
def promote(self, **kwargs):
|
||||
return
|
||||
payee_type = self.policy.get('payee_type')
|
||||
payee_uuid = self.policy.get('payee_uuid')
|
||||
|
||||
|
|
@ -89,5 +81,63 @@ class PercentageHandler(BasePromotionHandler):
|
|||
logger.exception('error in finish visit:')
|
||||
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):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
),
|
||||
]
|
||||
|
|
@ -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'),
|
||||
),
|
||||
]
|
||||
|
|
@ -5,6 +5,7 @@ from django.utils import timezone
|
|||
|
||||
from oauth2_provider.settings import oauth2_settings
|
||||
|
||||
from apps.promotions import handlers
|
||||
from apps.promotions.handlers import PromotionTypeChoices, BasePromotionHandler
|
||||
from apps.users.models import User
|
||||
from utils.models import BaseModel
|
||||
|
|
@ -17,7 +18,7 @@ class EventQuerySet(models.QuerySet):
|
|||
|
||||
|
||||
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,
|
||||
related_name='+', null=True, blank=True)
|
||||
label = models.CharField(max_length=255, db_index=True)
|
||||
|
|
@ -29,35 +30,32 @@ class Event(BaseModel):
|
|||
return self.label
|
||||
|
||||
def analyze(self):
|
||||
for plan in Plan.objects.get_related_to_event(self):
|
||||
print(plan)
|
||||
plan.process_event(self)
|
||||
for plan in Plan.objects.related_to_event(self):
|
||||
plan.process_event(event=self)
|
||||
|
||||
|
||||
class PlanQuerySet(models.QuerySet):
|
||||
|
||||
# this is for list of discount code for example
|
||||
def calculate(self, user, application=None):
|
||||
for plan in self.filter(application=application)[:10]:
|
||||
if not Promotion.objects.used(user):
|
||||
plan.calculate(user)
|
||||
# # this is for list of discount code for example
|
||||
# def calculate(self, user, application=None):
|
||||
# for plan in self.filter(application=application)[:10]:
|
||||
# if not Promotion.objects.used(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)
|
||||
|
||||
|
||||
class Plan(BaseModel):
|
||||
# user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
|
||||
application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.PROTECT,
|
||||
related_name='+', null=True, blank=True)
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
|
||||
application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.PROTECT, related_name='+',
|
||||
null=True, blank=True)
|
||||
event_list = models.JSONField(default=list)
|
||||
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)
|
||||
min_amount = models.FloatField(null=True, blank=True)
|
||||
max_amount = models.FloatField(null=True, blank=True)
|
||||
policy = models.JSONField(default=dict, verbose_name=_('policy'), null=True, blank=True)
|
||||
data = models.JSONField(default=dict, verbose_name=_('data'), null=True, blank=True)
|
||||
balance = models.IntegerField(verbose_name=_('balance'), default=0)
|
||||
|
||||
objects = PlanQuerySet.as_manager()
|
||||
|
|
@ -65,18 +63,26 @@ class Plan(BaseModel):
|
|||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
@property
|
||||
def handler(self) -> BasePromotionHandler:
|
||||
return PromotionTypeChoices(self.promotion_type).handler(
|
||||
application=self.application,
|
||||
policy=self.policy,
|
||||
balance=self.balance,
|
||||
)
|
||||
# @property
|
||||
# def handler(self):
|
||||
# if self.promotion_type == PromotionTypeChoices.PERCENTAGE.name:
|
||||
# return handlers.PercentageHandler(self)
|
||||
#
|
||||
# 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):
|
||||
handler = self.handler
|
||||
# self.base_amount = kwargs.get('base_amount', 0)
|
||||
return handler.calculate(**kwargs)
|
||||
def process_event(self, event=None):
|
||||
self.promote(event=event)
|
||||
|
||||
def promote(self, event=None):
|
||||
for recipient in self.recipients.all():
|
||||
recipient.promote(plan=self, event=event)
|
||||
|
||||
def reserve_promotion_amount(self, amount):
|
||||
updated = False
|
||||
|
|
@ -86,25 +92,68 @@ class Plan(BaseModel):
|
|||
)
|
||||
return updated
|
||||
|
||||
def process_event(self, event=None):
|
||||
print(event)
|
||||
referral_code = event.data.get('referral_code')
|
||||
user_uuid = event.data.get('user_uuid')
|
||||
promotion_amount = self.calculate_promotion(**self.policy)
|
||||
if referral_code and promotion_amount and not self.promotions.filter(user_id=referral_code,
|
||||
data__user_uuid=user_uuid).exists():
|
||||
referral_user, created = User.objects.get_or_create(uuid=referral_code)
|
||||
user = User.objects.get_or_create(uuid=user_uuid)
|
||||
with transaction.atomic():
|
||||
if self.reserve_promotion_amount(promotion_amount):
|
||||
promotion, created = Promotion.objects.get_or_create(plan=self, user=referral_user, defaults={
|
||||
"data": event.data
|
||||
})
|
||||
if not created:
|
||||
raise Exception('Promotion already reserved')
|
||||
|
||||
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)
|
||||
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)
|
||||
data = models.JSONField(null=True, blank=True, default=dict)
|
||||
|
||||
def get_wallet_category_uuid(self):
|
||||
return 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()
|
||||
|
||||
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()
|
||||
else:
|
||||
raise Exception('Promotion already reserved')
|
||||
promotion.promote()
|
||||
raise
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
|
||||
class PromotionQuerySet(models.QuerySet):
|
||||
|
|
@ -123,6 +172,7 @@ class Promotion(BaseModel):
|
|||
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)
|
||||
recipient = models.ForeignKey(Recipient, on_delete=models.PROTECT, related_name='promotions', null=True, blank=True)
|
||||
state = models.IntegerField(choices=StateChoices.choices, default=StateChoices.CREATED)
|
||||
base_amount = models.IntegerField(null=True, blank=True)
|
||||
promotion_amount = models.IntegerField(null=True, blank=True)
|
||||
|
|
@ -153,7 +203,7 @@ class Promotion(BaseModel):
|
|||
|
||||
def promote(self, **kwargs):
|
||||
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
|
||||
if handler.promote(promotion=self, **kwargs):
|
||||
|
|
|
|||
|
|
@ -85,10 +85,6 @@ class ApplicationEventViewSet(
|
|||
analyze_event_task.delay(event.uuid)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class UserPlanViewSet(mixins.RetrieveModelMixin,
|
||||
mixins.ListModelMixin,
|
||||
GenericViewSet):
|
||||
|
|
|
|||
|
|
@ -371,3 +371,6 @@ CELERY_TASK_TRACK_STARTED = True
|
|||
CELERY_TASK_TIME_LIMIT = 30 * 60
|
||||
CELERY_BROKER_URL = 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
|
||||
Loading…
Add table
Reference in a new issue