promotions/apps/promotions/models.py
Sayyid Hamid Mahdavi 61cb8aaf5a delme
2025-10-30 15:55:58 +03:30

287 lines
10 KiB
Python

import uuid
from django.conf import settings
from django.db import models, transaction
from django.db.models import F
from django.utils import timezone
from oauth2_provider.settings import oauth2_settings
from apps.promotions import handlers
import logging
from apps.promotions.handlers import ProcessorTypeChoices, 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
class EventSaver(BaseModel):
title = models.CharField(_('title'), max_length=255)
event_label = models.CharField(_('label'), max_length=255, unique=True, db_index=True)
save_once = models.BooleanField(_('save once'), default=False)
objects = EventSaverQuerySet.as_manager()
def __str__(self):
return self.title
def save_event(self, **kwargs):
user = kwargs.get('user')
application = kwargs.get('application')
label = kwargs.get('label')
data = kwargs.get('data')
if self.save_once:
event, created = Event.objects.get_or_create(user=user, application=application, label=label,
defaults={"data": data})
if created:
return event
else:
raise Exception(_('already saved'))
else:
event = Event.objects.create(user=user, application=application, label=label, data= data)
return event
class EventQuerySet(models.QuerySet):
pass
class Event(BaseModel):
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)
data = models.JSONField() # from to amount application user etc
objects = EventQuerySet.as_manager()
def __str__(self):
return self.label
def analyze(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)
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)
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)
processor = models.CharField(max_length=64, verbose_name=_('processor'), db_index=True,
choices=ProcessorTypeChoices.choices) # TODO: processor type
data = models.JSONField(default=dict, verbose_name=_('data'), null=True, blank=True)
balance = models.IntegerField(verbose_name=_('balance'), default=0)
objects = PlanQuerySet.as_manager()
def __str__(self):
return self.title
def process_event(self, event=None):
# TODO: processor
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
if amount and self.balance >= amount:
updated = Plan.objects.filter(pk=self.pk, balance__gte=amount).update(
balance=F('balance') - amount
)
return updated
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)
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)
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 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('::')
if model_name == 'event':
obj = event
elif model_name == 'plan':
obj = plan
else:
raise NotImplementedError()
return obj.data.get(key)
except Exception as e:
try:
uuid.UUID(self.recipient_uuid_field)
return self.recipient_uuid_field
except:
pass
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()
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
# def handler(self) -> BasePromotionHandler:
# return PromotionTypeChoices(self.promotion_type).handler(self)
def promote(self, plan=None, event=None):
with transaction.atomic():
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(plan, event),
plan=plan,
recipient=self,
base_amount=promotion_amount,
)
if created:
promotion.promote()
else:
raise
else:
raise
class PromotionQuerySet(models.QuerySet):
def used(self, user):
return self.filter(user=user, state__in=[Promotion.StateChoices.CREATED])
class Promotion(BaseModel):
class StateChoices(models.IntegerChoices):
CREATED = 0, _('Created')
ASSIGNED = 1, _('Assigned')
PROMOTED = 2, _('Promoted')
EXPIRED = 10, _('Expired')
user_uuid = 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)
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)
data = models.JSONField(null=True, blank=True)
objects = PromotionQuerySet.as_manager()
def change_state(self, from_states: list | tuple, to_state, same_ok=True, raise_exception=False,
error_message=None,
data: None | dict = None):
data = data or {}
data['updated_at'] = timezone.now()
if same_ok and self.state == to_state:
return self
if self.state in from_states:
updated = Promotion.objects.filter(pk=self.pk, state__in=from_states).update(
state=to_state,
**data
)
if updated: # double check in db level
self.state = to_state
return self
if raise_exception:
raise Exception(error_message)
def promote(self, **kwargs):
self.change_state(from_states=[Promotion.StateChoices.CREATED], to_state=Promotion.StateChoices.ASSIGNED,
same_ok=False, raise_exception=True)
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')