Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions promo_code/business/migrations/0002_promo_promocode.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Generated by Django 5.2b1 on 2025-03-28 16:03

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


class Migration(migrations.Migration):

dependencies = [
('business', '0001_initial'),
]

operations = [
migrations.CreateModel(
name='Promo',
fields=[
(
'id',
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID',
),
),
('description', models.CharField(max_length=300)),
(
'image_url',
models.URLField(blank=True, max_length=350, null=True),
),
('target', models.JSONField(default=dict)),
('max_count', models.IntegerField()),
('active_from', models.DateField(blank=True, null=True)),
('active_until', models.DateField(blank=True, null=True)),
(
'mode',
models.CharField(
choices=[('COMMON', 'Common'), ('UNIQUE', 'Unique')],
max_length=10,
),
),
(
'promo_common',
models.CharField(blank=True, max_length=30, null=True),
),
('active', models.BooleanField(default=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
(
'company',
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to='business.company',
),
),
],
),
migrations.CreateModel(
name='PromoCode',
fields=[
(
'id',
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID',
),
),
('code', models.CharField(max_length=30)),
('is_used', models.BooleanField(default=False)),
('used_at', models.DateTimeField(blank=True, null=True)),
(
'promo',
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name='unique_codes',
to='business.promo',
),
),
],
options={
'unique_together': {('promo', 'code')},
},
),
]
55 changes: 55 additions & 0 deletions promo_code/business/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,58 @@ class Company(django.contrib.auth.models.AbstractBaseUser):

def __str__(self):
return self.name


class Promo(django.db.models.Model):
MODE_COMMON = 'COMMON'
MODE_UNIQUE = 'UNIQUE'
MODE_CHOICES = [
(MODE_COMMON, 'Common'),
(MODE_UNIQUE, 'Unique'),
]

company = django.db.models.ForeignKey(
Company,
on_delete=django.db.models.CASCADE,
null=True,
blank=True,
)
description = django.db.models.CharField(max_length=300)
image_url = django.db.models.URLField(
max_length=350,
blank=True,
null=True,
)
target = django.db.models.JSONField(default=dict)
max_count = django.db.models.IntegerField()
active_from = django.db.models.DateField(null=True, blank=True)
active_until = django.db.models.DateField(null=True, blank=True)
mode = django.db.models.CharField(max_length=10, choices=MODE_CHOICES)
promo_common = django.db.models.CharField(
max_length=30,
blank=True,
null=True,
)
active = django.db.models.BooleanField(default=True)

created_at = django.db.models.DateTimeField(auto_now_add=True)

def __str__(self):
return f'Promo {self.id} ({self.mode})'


class PromoCode(django.db.models.Model):
promo = django.db.models.ForeignKey(
Promo,
on_delete=django.db.models.CASCADE,
related_name='unique_codes',
)
code = django.db.models.CharField(max_length=30)
is_used = django.db.models.BooleanField(default=False)
used_at = django.db.models.DateTimeField(null=True, blank=True)

class Meta:
unique_together = ('promo', 'code')

def __str__(self):
return self.code
7 changes: 7 additions & 0 deletions promo_code/business/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import business.models
import rest_framework.permissions


class IsCompanyUser(rest_framework.permissions.BasePermission):
def has_permission(self, request, view):
return isinstance(request.user, business.models.Company)
207 changes: 207 additions & 0 deletions promo_code/business/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import django.contrib.auth.password_validation
import django.core.exceptions
import django.core.validators
import pycountry
import rest_framework.exceptions
import rest_framework.serializers
import rest_framework.status
Expand Down Expand Up @@ -130,3 +131,209 @@ def validate(self, attrs):
)

return super().validate(attrs)


class TargetSerializer(rest_framework.serializers.Serializer):
age_from = rest_framework.serializers.IntegerField(
min_value=0,
max_value=100,
required=False,
allow_null=True,
)
age_until = rest_framework.serializers.IntegerField(
min_value=0,
max_value=100,
required=False,
allow_null=True,
)
country = rest_framework.serializers.CharField(
required=False,
allow_null=True,
allow_blank=True,
)
categories = rest_framework.serializers.ListField(
child=rest_framework.serializers.CharField(
min_length=2,
max_length=20,
),
max_length=20,
required=False,
allow_empty=True,
)

def validate(self, data):
age_from = data.get('age_from')
age_until = data.get('age_until')
if (
age_from is not None
and age_until is not None
and age_from > age_until
):
raise rest_framework.serializers.ValidationError(
{'age_until': 'Must be greater than or equal to age_from.'},
)

# change validation
country = data.get('country')
if country:
country = country.strip().upper()
try:
pycountry.countries.lookup(country)
data['country'] = country
except LookupError:
raise rest_framework.serializers.ValidationError(
{'country': 'Invalid ISO 3166-1 alpha-2 country code.'},
)

return data


class PromoCreateSerializer(rest_framework.serializers.ModelSerializer):
target = TargetSerializer(required=True)
promo_common = rest_framework.serializers.CharField(
min_length=5,
max_length=30,
required=False,
allow_null=True,
)
promo_unique = rest_framework.serializers.ListField(
child=rest_framework.serializers.CharField(
min_length=3,
max_length=30,
),
min_length=1,
max_length=5000,
required=False,
allow_null=True,
)

class Meta:
model = business_models.Promo
fields = (
'description',
'image_url',
'target',
'max_count',
'active_from',
'active_until',
'mode',
'promo_common',
'promo_unique',
)
extra_kwargs = {
'description': {'min_length': 10, 'max_length': 300},
'image_url': {'max_length': 350},
}

def validate(self, data):
mode = data.get('mode')
promo_common = data.get('promo_common')
promo_unique = data.get('promo_unique')
max_count = data.get('max_count')

if mode == business_models.Promo.MODE_COMMON:
if not promo_common:
raise rest_framework.serializers.ValidationError(
{
'promo_common': (
'This field is required for COMMON mode.'
),
},
)

if promo_unique is not None:
raise rest_framework.serializers.ValidationError(
{
'promo_unique': (
'This field is not allowed for COMMON mode.'
),
},
)

if max_count < 0 or max_count > 100000000:
raise rest_framework.serializers.ValidationError(
{
'max_count': (
'Must be between 0 and 100,000,000 '
'for COMMON mode.'
),
},
)

elif mode == business_models.Promo.MODE_UNIQUE:
if not promo_unique:
raise rest_framework.serializers.ValidationError(
{
'promo_unique': (
'This field is required for UNIQUE mode.'
),
},
)

if promo_common is not None:
raise rest_framework.serializers.ValidationError(
{
'promo_common': (
'This field is not allowed for UNIQUE mode.'
),
},
)

if max_count != 1:
raise rest_framework.serializers.ValidationError(
{'max_count': 'Must be 1 for UNIQUE mode.'},
)

else:
raise rest_framework.serializers.ValidationError(
{'mode': 'Invalid mode.'},
)

active_from = data.get('active_from')
active_until = data.get('active_until')
if active_from and active_until and active_from > active_until:
raise rest_framework.serializers.ValidationError(
{'active_until': 'Must be after or equal to active_from.'},
)

return data

def create(self, validated_data):
target_data = validated_data.pop('target')
promo_common = validated_data.pop('promo_common', None)
promo_unique = validated_data.pop('promo_unique', None)
mode = validated_data['mode']

user = self.context['request'].user
validated_data['company'] = user

promo = business_models.Promo.objects.create(
**validated_data,
target=target_data,
)

if mode == business_models.Promo.MODE_COMMON:
promo.promo_common = promo_common
promo.save()
elif mode == business_models.Promo.MODE_UNIQUE and promo_unique:
promo_codes = [
business_models.PromoCode(promo=promo, code=code)
for code in promo_unique
]
business_models.PromoCode.objects.bulk_create(promo_codes)

return promo

def to_representation(self, instance):
data = super().to_representation(instance)
data['target'] = instance.target

if instance.mode == business_models.Promo.MODE_UNIQUE:
data['promo_unique'] = [
code.code for code in instance.unique_codes.all()
]
data.pop('promo_common', None)
else:
data.pop('promo_unique', None)

return data
5 changes: 5 additions & 0 deletions promo_code/business/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,9 @@
business.views.CompanyTokenRefreshView.as_view(),
name='company-token-refresh',
),
django.urls.path(
'promo/create',
business.views.PromoCreateView.as_view(),
name='promo-create',
),
]
Loading