Skip to content

Commit

Permalink
feat(backend): add discount code (#52)
Browse files Browse the repository at this point in the history
* feat(backend): add discount code

* fix(backend): only count paid payments in discount remaining capacity

* feat(backend): rollback discount on payment threshold
  • Loading branch information
Adibov authored Dec 4, 2023
1 parent 4ae9ef7 commit 4c10930
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 15 deletions.
11 changes: 11 additions & 0 deletions backend/backend_api/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from backend_api import models
from backend_api.email import MailerThread
from backend_api.models import Discount


def desc_creator(selected_model):
Expand Down Expand Up @@ -101,6 +102,15 @@ def execute_mailer(self, request, obj):
actions = ['execute_mailer']


class DiscountAdmin(admin.ModelAdmin):
list_display = ('__str__', 'is_active', 'discount_percent', 'capacity', 'expiration_date')
readonly_fields = ('participants',)

class Meta:
model = Discount
fields = '__all__'


class PaymentAdmin(admin.ModelAdmin):
rdfields = []
for field in models.Payment._meta.get_fields():
Expand All @@ -123,6 +133,7 @@ class Meta:
admin.site.register(models.Presenter, PresenterAdmin)
admin.site.register(models.User, UserAdmin)
admin.site.register(models.Mailer, MailerAdmin)
admin.site.register(models.Discount, DiscountAdmin)
admin.site.register(models.Payment, PaymentAdmin)
admin.site.register(models.WorkshopRegistration)
admin.site.register(models.PresentationParticipation)
Expand Down
31 changes: 31 additions & 0 deletions backend/backend_api/migrations/0054_discount_payment_discount.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Generated by Django 4.2.4 on 2023-12-03 20:08

from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone


class Migration(migrations.Migration):

dependencies = [
('backend_api', '0053_alter_staff_role_alter_staff_section_name'),
]

operations = [
migrations.CreateModel(
name='Discount',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('code', models.CharField(default='BTusxOmw', max_length=8, unique=True)),
('discount_percent', models.IntegerField(default=0)),
('is_active', models.BooleanField(default=False)),
('capacity', models.IntegerField(default=0)),
('expiration_date', models.DateTimeField(default=django.utils.timezone.now)),
],
),
migrations.AddField(
model_name='payment',
name='discount',
field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='backend_api.discount'),
),
]
56 changes: 51 additions & 5 deletions backend/backend_api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from django.db import models
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from rest_framework.exceptions import ValidationError
from rest_framework import status
Expand All @@ -21,6 +22,7 @@
from utils.random import create_random_string
from utils.renderers import new_detailed_response
from urllib.parse import quote

SMALL_MAX_LENGTH = 255
BIG_MAX_LENGTH = 65535

Expand Down Expand Up @@ -173,7 +175,7 @@ class Presentation(models.Model):
cost = models.PositiveIntegerField(default=0)
capacity = models.PositiveIntegerField(default=50)
has_project = models.BooleanField(default=False, blank=False)

NOT_ASSIGNED = 'NOT_ASSIGNED'
ELEMENTARY = 'Elementary'
INTERMEDIATE = 'Intermediate'
Expand Down Expand Up @@ -341,6 +343,44 @@ def __str__(self):
return f"Mailer with id {self.id}: subject= {self.subject}"


class Discount(models.Model):
_CODE_LENGTH = 8
code = models.CharField(max_length=_CODE_LENGTH, null=False, default=create_random_string(_CODE_LENGTH),
unique=True)
discount_percent = models.IntegerField(default=0)
is_active = models.BooleanField(default=False)
capacity = models.IntegerField(default=0)
expiration_date = models.DateTimeField(default=timezone.now)

def __str__(self):
return self.code

@property
def participants(self):
users = []
for payment in self.payment_set.all():
users.append(payment.user)
return users

def _remaining_capacity(self) -> int:
return self.capacity - self.payment_set.filter(status=Payment.PaymentStatus.PAYMENT_CONFIRMED).count()

def is_usable(self, user: User) -> bool:
if not self.is_active:
raise ValidationError(new_detailed_response(status.HTTP_400_BAD_REQUEST,
"Discount is not active"))
if self._remaining_capacity() <= 0:
raise ValidationError(new_detailed_response(status.HTTP_400_BAD_REQUEST,
"Discount capacity is full"))
if self.expiration_date < timezone.now():
raise ValidationError(new_detailed_response(status.HTTP_400_BAD_REQUEST,
"Discount has expired"))
if self.payment_set.filter(user=user).exists():
raise ValidationError(new_detailed_response(status.HTTP_400_BAD_REQUEST,
"Discount has already been used by this user"))
return True


class Payment(models.Model):
class PaymentStatus(models.IntegerChoices):
AWAITING_PAYMENT = 0, _('Awaiting payment')
Expand All @@ -361,6 +401,7 @@ class PaymentStatus(models.IntegerChoices):
default=datetime.datetime(year=2020, month=7, day=1, hour=0, minute=0, second=0,
microsecond=0))
track_id = models.CharField(max_length=20, null=True, default=None)
discount = models.ForeignKey(Discount, on_delete=models.SET_NULL, null=True, default=None)

def __str__(self):
return f"Payment for {self.user.account} ({self.amount}) in {str(self.date)}"
Expand All @@ -385,7 +426,7 @@ def update_payment_status(self, payment_status: PaymentStatus):
self.save()

@staticmethod
def create_payment_for_user(user: User):
def create_payment_for_user(user: User, discount: Discount):
total_cost = 0
workshops: list[Workshop] = []
presentations: list[Presentation] = []
Expand Down Expand Up @@ -419,15 +460,20 @@ def create_payment_for_user(user: User):
if len(workshops) == 0 and len(presentations) == 0:
raise ValidationError(
new_detailed_response(status.HTTP_400_BAD_REQUEST, f"User {user} has no unpaid registrations"))
if total_cost >= Payment._DISCOUNT_THRESHOLD:
total_cost = int(total_cost * (1 - Payment._DISCOUNT_PERCENTAGE / 100))
payment = Payment.objects.create(user=user, amount=total_cost, year=datetime.date.today().year,
date=datetime.datetime.now())
date=datetime.datetime.now(), discount=discount)
payment.workshops.set(workshops)
payment.presentations.set(presentations)
payment.save()
return payment

@property
def discounted_amount(self):
if self.discount is None:
return self.amount if self.amount < self._DISCOUNT_THRESHOLD else int(
self.amount * (1 - self._DISCOUNT_PERCENTAGE / 100))
return int(self.amount * (1 - self.discount.discount_percent / 100))


class Committee(models.Model):
profile = models.ImageField(verbose_name='profile', null=True, blank=True)
Expand Down
32 changes: 22 additions & 10 deletions backend/backend_api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import os
import urllib.parse

from django.core.exceptions import ObjectDoesNotExist
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.shortcuts import get_object_or_404, redirect
from rest_framework import status, mixins
from rest_framework import viewsets
Expand All @@ -17,7 +17,7 @@
from aaiss_backend.settings import BASE_URL
from backend_api import models
from backend_api import serializers
from backend_api.models import User, Account, Payment, Staff, WorkshopRegistration, PresentationParticipation
from backend_api.models import User, Account, Payment, Staff, WorkshopRegistration, PresentationParticipation, Discount
from backend_api.serializers import WorkshopRegistrationSerializer, PresentationParticipationSerializer
from payment_backends.zify import ZIFYRequest, ZIFY_STATUS_OK
from utils.renderers import new_detailed_response
Expand Down Expand Up @@ -215,17 +215,29 @@ class PaymentViewSet(viewsets.GenericViewSet):
@action(methods=['POST'], detail=False, permission_classes=[IsAuthenticated])
def payment(self, request):
account = request.user
call_back = request.data.get('call_back')
if call_back is None:
return Response(new_detailed_response(status.HTTP_400_BAD_REQUEST, "call_back field is required"))
try:
user = User.objects.get(account=account)
except ObjectDoesNotExist:
return Response(new_detailed_response(
status.HTTP_400_BAD_REQUEST, "User not found"))

payment = Payment.create_payment_for_user(user)
response = ZIFYRequest().create_payment(str(payment.pk), payment.amount, user.name, user.phone_number,
call_back = request.data.get('call_back')
if call_back is None:
return Response(new_detailed_response(status.HTTP_400_BAD_REQUEST, "call_back field is required"))
discount = None
discount_code = request.data.get('discount_code')
if discount_code is not None:
try:
discount = Discount.objects.get(code=discount_code)
except ObjectDoesNotExist:
return Response(new_detailed_response(
status.HTTP_400_BAD_REQUEST, "Discount code not found"))
try:
discount.is_usable(user)
except ValidationError as e:
return Response(e)
payment = Payment.create_payment_for_user(user, discount)
response = ZIFYRequest().create_payment(str(payment.pk), payment.discounted_amount, user.name,
user.phone_number,
user.account.email, call_back)
if response['status'] == ZIFY_STATUS_OK:
payment.track_id = response['data']['order']
Expand All @@ -250,7 +262,7 @@ def verify(self, request):
response = ZIFYRequest().verify_payment(payment.track_id)
if response['status'] == ZIFY_STATUS_OK:
payment.update_payment_status(Payment.PaymentStatus.PAYMENT_CONFIRMED)
return Response(new_detailed_response(status.HTTP_200_OK, "Payment verified successfully", payment.pk))
return Response(new_detailed_response(status.HTTP_200_OK, "Payment verified successfully", payment.pk))
else:
payment.update_payment_status(Payment.PaymentStatus.PAYMENT_REJECTED)
return Response(
Expand Down Expand Up @@ -278,7 +290,7 @@ def list(self, request, *args, **kwargs):
}

section_data['people'].append(person_data)

if len(section_data['people']) != 0:
section_data['people'] = sorted(section_data['people'], key=lambda x: x['role'])[::-1]
data.append(section_data)
Expand Down

0 comments on commit 4c10930

Please sign in to comment.