You've already forked Promocode-API
mirror of
https://github.com/devitq/Promocode-API.git
synced 2026-05-23 02:47:12 +00:00
feat: added promocode creation and view
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from apps.promo.models import Promocode, PromocodeTarget
|
||||
|
||||
admin.site.register(Promocode)
|
||||
admin.site.register(PromocodeTarget)
|
||||
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PromoConfig(AppConfig):
|
||||
name = "apps.promo"
|
||||
label = "promo"
|
||||
@@ -0,0 +1,80 @@
|
||||
# Generated by Django 5.1.5 on 2025-01-21 14:30
|
||||
|
||||
import apps.promo.validators
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
import django_countries.fields
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('business', '0001_initial'),
|
||||
('user', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PromocodeTarget',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('age_from', models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100)])),
|
||||
('age_until', models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100)])),
|
||||
('country', django_countries.fields.CountryField(blank=True, max_length=2, null=True)),
|
||||
('categories', models.JSONField(blank=True, default=list, null=True, validators=[apps.promo.validators.TargetCategoriesValidator()])),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Promocode',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('description', models.TextField(max_length=300, validators=[django.core.validators.MinLengthValidator(10)])),
|
||||
('image_url', models.URLField(blank=True, max_length=350, null=True)),
|
||||
('max_count', models.PositiveIntegerField(validators=[django.core.validators.MaxValueValidator(100000000)])),
|
||||
('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=6)),
|
||||
('promo_common', models.CharField(blank=True, max_length=30, null=True, validators=[django.core.validators.MinLengthValidator(5)])),
|
||||
('promo_unique', models.JSONField(blank=True, default=list, null=True, validators=[apps.promo.validators.PromocodeUniqueValidator()])),
|
||||
('promo_unique_activated', models.JSONField(blank=True, default=list, editable=False, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('business', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='promocodes', to='business.business')),
|
||||
('target', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='promocodes', to='promo.promocodetarget')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PromocodeActivation',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('timestamp', models.DateTimeField(auto_now_add=True)),
|
||||
('promocode', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='activations', to='promo.promocode')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='activations', to='user.user')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PromocodeComment',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('text', models.TextField(max_length=1000, validators=[django.core.validators.MinLengthValidator(10)])),
|
||||
('date', models.DateTimeField(auto_now_add=True)),
|
||||
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='user.user')),
|
||||
('promocode', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='promo.promocode')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,171 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import (
|
||||
MaxValueValidator,
|
||||
MinLengthValidator,
|
||||
MinValueValidator,
|
||||
)
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django_countries.fields import CountryField
|
||||
|
||||
from apps.business.models import Business
|
||||
from apps.core.models import BaseModel
|
||||
from apps.promo.validators import (
|
||||
PromocodeDurationValidator,
|
||||
PromocodeUniqueValidator,
|
||||
TargetAgeValidator,
|
||||
TargetCategoriesValidator,
|
||||
)
|
||||
from apps.user.models import User
|
||||
|
||||
|
||||
class PromocodeTarget(BaseModel):
|
||||
age_from = models.PositiveSmallIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[MinValueValidator(0), MaxValueValidator(100)],
|
||||
)
|
||||
age_until = models.PositiveSmallIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[MinValueValidator(0), MaxValueValidator(100)],
|
||||
)
|
||||
country = CountryField(blank=True, null=True)
|
||||
categories = models.JSONField(
|
||||
blank=True,
|
||||
null=True,
|
||||
default=list,
|
||||
validators=[TargetCategoriesValidator()],
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return str(self.id)
|
||||
|
||||
def clean(self) -> None:
|
||||
super().clean()
|
||||
|
||||
TargetAgeValidator()(self)
|
||||
|
||||
|
||||
class Promocode(BaseModel):
|
||||
class ModeChoices(models.TextChoices):
|
||||
COMMON = "COMMON"
|
||||
UNIQUE = "UNIQUE"
|
||||
|
||||
business = models.ForeignKey(
|
||||
Business,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="promocodes",
|
||||
)
|
||||
description = models.TextField(
|
||||
max_length=300,
|
||||
validators=[MinLengthValidator(10)],
|
||||
)
|
||||
image_url = models.URLField(max_length=350, blank=True, null=True)
|
||||
target = models.ForeignKey(
|
||||
PromocodeTarget,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="promocodes",
|
||||
)
|
||||
max_count = models.PositiveIntegerField(
|
||||
validators=[MaxValueValidator(100000000)],
|
||||
)
|
||||
active_from = models.DateField(blank=True, null=True)
|
||||
active_until = models.DateField(blank=True, null=True)
|
||||
mode = models.CharField(max_length=6, choices=ModeChoices)
|
||||
promo_common = models.CharField(
|
||||
max_length=30,
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[MinLengthValidator(5)],
|
||||
)
|
||||
promo_unique = models.JSONField(
|
||||
blank=True,
|
||||
null=True,
|
||||
default=list,
|
||||
validators=[PromocodeUniqueValidator()],
|
||||
)
|
||||
promo_unique_activated = models.JSONField(
|
||||
blank=True,
|
||||
null=True,
|
||||
default=list,
|
||||
editable=False,
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def clean(self) -> None:
|
||||
super().clean()
|
||||
|
||||
if self.mode == self.ModeChoices.COMMON:
|
||||
if not self.promo_common:
|
||||
err = {
|
||||
"promo_common": "Field is required for COMMON mode.",
|
||||
}
|
||||
raise ValidationError(err)
|
||||
if self.promo_unique:
|
||||
err = {
|
||||
"promo_unique": "Field must be empty for COMMON mode.",
|
||||
}
|
||||
raise ValidationError(err)
|
||||
elif self.mode == self.ModeChoices.UNIQUE:
|
||||
if not self.promo_unique:
|
||||
err = {
|
||||
"promo_unique": "Field is required for UNIQUE mode.",
|
||||
}
|
||||
raise ValidationError(err)
|
||||
if self.promo_common:
|
||||
err = {
|
||||
"promo_common": "Field must be empty for UNIQUE mode.",
|
||||
}
|
||||
raise ValidationError(err)
|
||||
|
||||
PromocodeDurationValidator()(self)
|
||||
|
||||
@property
|
||||
def active(self) -> bool:
|
||||
current_date = timezone.datetime.today().date()
|
||||
|
||||
is_active_by_date = (
|
||||
self.active_from is None or self.active_from <= current_date
|
||||
) and (self.active_until is None or self.active_until >= current_date)
|
||||
|
||||
if self.mode == self.ModeChoices.COMMON:
|
||||
is_active_by_mode = self.activations.count() < self.max_count
|
||||
elif self.mode == self.ModeChoices.UNIQUE:
|
||||
is_active_by_mode = len(self.promo_unique) > len(
|
||||
self.promo_unique_activated
|
||||
)
|
||||
|
||||
return is_active_by_date and is_active_by_mode
|
||||
|
||||
|
||||
class PromocodeActivation(BaseModel):
|
||||
promocode = models.ForeignKey(
|
||||
Promocode,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="activations",
|
||||
)
|
||||
user = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="activations",
|
||||
)
|
||||
timestamp = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
|
||||
class PromocodeComment(BaseModel):
|
||||
promocode = models.ForeignKey(
|
||||
Promocode,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="comments",
|
||||
)
|
||||
author = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="comments",
|
||||
)
|
||||
text = models.TextField(
|
||||
max_length=1000,
|
||||
validators=[MinLengthValidator(10)],
|
||||
)
|
||||
date = models.DateTimeField(auto_now_add=True)
|
||||
@@ -0,0 +1,82 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import BaseValidator
|
||||
from django.utils.deconstruct import deconstructible
|
||||
|
||||
MAX_CATEGORIES_LIST_LEN = 20
|
||||
MIN_CATEGORY_LEN = 2
|
||||
MAX_CATEGORY_LEN = 20
|
||||
|
||||
MIN_UNIQUE_PROMOCODES_LIST_LEN = 1
|
||||
MAX_UNIQUE_PROMOCODES_LIST_LEN = 5000
|
||||
MIN_UNIQUE_PROMOCODE_LEN = 3
|
||||
MAX_UNIQUE_PROMOCODE_LEN = 30
|
||||
|
||||
|
||||
class TargetAgeValidator:
|
||||
def __call__(self, instance) -> None: # noqa: ANN001
|
||||
if (
|
||||
instance.age_from is not None
|
||||
and instance.age_until is not None
|
||||
and instance.age_from > instance.age_until
|
||||
):
|
||||
err = "age_from can't be greater than age_until"
|
||||
raise ValidationError(err)
|
||||
|
||||
|
||||
class PromocodeDurationValidator:
|
||||
def __call__(self, instance) -> None: # noqa: ANN001
|
||||
if (
|
||||
instance.active_from is not None
|
||||
and instance.active_until is not None
|
||||
and instance.active_from > instance.active_until
|
||||
):
|
||||
err = "active_from can't be greater than active_until"
|
||||
raise ValidationError(err)
|
||||
|
||||
|
||||
@deconstructible
|
||||
class TargetCategoriesValidator(BaseValidator):
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
def __call__(self, categories: list) -> None:
|
||||
if not isinstance(categories, list):
|
||||
err = "categories must be a list"
|
||||
raise ValidationError(err)
|
||||
|
||||
if len(categories) > MAX_CATEGORIES_LIST_LEN:
|
||||
err = "max. categories length is 20"
|
||||
raise ValidationError(err)
|
||||
|
||||
for category in categories:
|
||||
if not (MIN_CATEGORY_LEN <= len(category) <= MAX_CATEGORY_LEN):
|
||||
err = "category name length must be >=2 and <=20"
|
||||
raise ValidationError(err)
|
||||
|
||||
|
||||
@deconstructible
|
||||
class PromocodeUniqueValidator(BaseValidator):
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
def __call__(self, promocodes: list) -> None:
|
||||
if not isinstance(promocodes, list):
|
||||
err = "unque promocodes must be a list"
|
||||
raise ValidationError(err)
|
||||
|
||||
if not (
|
||||
MIN_UNIQUE_PROMOCODES_LIST_LEN
|
||||
<= len(promocodes)
|
||||
<= MAX_UNIQUE_PROMOCODES_LIST_LEN
|
||||
):
|
||||
err = "unique promocodes length must be >=1 and <=5000"
|
||||
raise ValidationError(err)
|
||||
|
||||
for promocode in promocodes:
|
||||
if not (
|
||||
MIN_UNIQUE_PROMOCODE_LEN
|
||||
<= len(promocode)
|
||||
<= MAX_UNIQUE_PROMOCODE_LEN
|
||||
):
|
||||
err = "unique promocode length must be >=3 and <=30"
|
||||
raise ValidationError(err)
|
||||
Reference in New Issue
Block a user