Skip to content

Commit cfac401

Browse files
Merge pull request #28 from RandomProgramm3r/feature/promo-refactor
refactor(business): DRY‑up promo validation & optimize creation logic - Apply DRY: extract shared validation rules into dedicated `PromoValidator` to eliminate duplication between `PromoCreateSerializer` and `PromoDetailSerializer` - Implement atomic promo creation via `PromoManager.create_promo()` using `@transaction.atomic` for full rollback on error - Move custom model managers into `managers.py` for cleaner structure
2 parents 5d8975c + 98f5725 commit cfac401

File tree

7 files changed

+163
-176
lines changed

7 files changed

+163
-176
lines changed

promo_code/business/managers.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import django.contrib.auth.models
2+
import django.db.models
3+
4+
import business.models
5+
6+
7+
class CompanyManager(django.contrib.auth.models.BaseUserManager):
8+
def create_company(self, email, name, password=None, **extra_fields):
9+
if not email:
10+
raise ValueError('The Email must be set')
11+
12+
email = self.normalize_email(email)
13+
company = self.model(
14+
email=email,
15+
name=name,
16+
**extra_fields,
17+
)
18+
company.set_password(password)
19+
company.save(using=self._db)
20+
return company
21+
22+
23+
class PromoManager(django.db.models.Manager):
24+
@django.db.transaction.atomic
25+
def create_promo(
26+
self,
27+
user,
28+
target_data,
29+
promo_common,
30+
promo_unique,
31+
**kwargs,
32+
):
33+
promo = self.create(
34+
company=user,
35+
target=target_data,
36+
**kwargs,
37+
)
38+
39+
if promo.mode == business.models.Promo.MODE_COMMON:
40+
promo.promo_common = promo_common
41+
promo.save(update_fields=['promo_common'])
42+
elif promo.mode == business.models.Promo.MODE_UNIQUE and promo_unique:
43+
self._create_unique_codes(promo, promo_unique)
44+
45+
return promo
46+
47+
def _create_unique_codes(self, promo, codes):
48+
business.models.PromoCode.objects.bulk_create(
49+
[
50+
business.models.PromoCode(promo=promo, code=code)
51+
for code in codes
52+
],
53+
)

promo_code/business/models.py

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,7 @@
33
import django.contrib.auth.models
44
import django.db.models
55

6-
7-
class CompanyManager(django.contrib.auth.models.BaseUserManager):
8-
def create_company(self, email, name, password=None, **extra_fields):
9-
if not email:
10-
raise ValueError('The Email must be set')
11-
12-
email = self.normalize_email(email)
13-
company = self.model(
14-
email=email,
15-
name=name,
16-
**extra_fields,
17-
)
18-
company.set_password(password)
19-
company.save(using=self._db)
20-
return company
6+
import business.managers
217

228

239
class Company(django.contrib.auth.models.AbstractBaseUser):
@@ -37,7 +23,7 @@ class Company(django.contrib.auth.models.AbstractBaseUser):
3723
created_at = django.db.models.DateTimeField(auto_now_add=True)
3824
is_active = django.db.models.BooleanField(default=True)
3925

40-
objects = CompanyManager()
26+
objects = business.managers.CompanyManager()
4127

4228
USERNAME_FIELD = 'email'
4329
REQUIRED_FIELDS = ['name']
@@ -87,6 +73,8 @@ class Promo(django.db.models.Model):
8773

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

76+
objects = business.managers.PromoManager()
77+
9078
def __str__(self):
9179
return f'Promo {self.id} ({self.mode})'
9280

promo_code/business/serializers.py

Lines changed: 14 additions & 157 deletions
Original file line numberDiff line numberDiff line change
@@ -246,104 +246,23 @@ class Meta:
246246
)
247247

248248
def validate(self, data):
249-
mode = data.get('mode')
250-
promo_common = data.get('promo_common')
251-
promo_unique = data.get('promo_unique')
252-
max_count = data.get('max_count')
253-
254-
if mode == business_models.Promo.MODE_COMMON:
255-
if not promo_common:
256-
raise rest_framework.serializers.ValidationError(
257-
{
258-
'promo_common': (
259-
'This field is required for COMMON mode.'
260-
),
261-
},
262-
)
263-
264-
if promo_unique is not None:
265-
raise rest_framework.serializers.ValidationError(
266-
{
267-
'promo_unique': (
268-
'This field is not allowed for COMMON mode.'
269-
),
270-
},
271-
)
272-
273-
if max_count < 0 or max_count > 100000000:
274-
raise rest_framework.serializers.ValidationError(
275-
{
276-
'max_count': (
277-
'Must be between 0 and 100,000,000 '
278-
'for COMMON mode.'
279-
),
280-
},
281-
)
282-
283-
elif mode == business_models.Promo.MODE_UNIQUE:
284-
if not promo_unique:
285-
raise rest_framework.serializers.ValidationError(
286-
{
287-
'promo_unique': (
288-
'This field is required for UNIQUE mode.'
289-
),
290-
},
291-
)
292-
293-
if promo_common is not None:
294-
raise rest_framework.serializers.ValidationError(
295-
{
296-
'promo_common': (
297-
'This field is not allowed for UNIQUE mode.'
298-
),
299-
},
300-
)
301-
302-
if max_count != 1:
303-
raise rest_framework.serializers.ValidationError(
304-
{'max_count': 'Must be 1 for UNIQUE mode.'},
305-
)
306-
307-
else:
308-
raise rest_framework.serializers.ValidationError(
309-
{'mode': 'Invalid mode.'},
310-
)
311-
312-
active_from = data.get('active_from')
313-
active_until = data.get('active_until')
314-
if active_from and active_until and active_from > active_until:
315-
raise rest_framework.serializers.ValidationError(
316-
{'active_until': 'Must be after or equal to active_from.'},
317-
)
318-
319-
return data
249+
data = super().validate(data)
250+
validator = business.validators.PromoValidator(data=data)
251+
return validator.validate()
320252

321253
def create(self, validated_data):
322254
target_data = validated_data.pop('target')
323255
promo_common = validated_data.pop('promo_common', None)
324256
promo_unique = validated_data.pop('promo_unique', None)
325-
mode = validated_data['mode']
326-
327-
user = self.context['request'].user
328-
validated_data['company'] = user
329257

330-
promo = business_models.Promo.objects.create(
258+
return business_models.Promo.objects.create_promo(
259+
user=self.context['request'].user,
260+
target_data=target_data,
261+
promo_common=promo_common,
262+
promo_unique=promo_unique,
331263
**validated_data,
332-
target=target_data,
333264
)
334265

335-
if mode == business_models.Promo.MODE_COMMON:
336-
promo.promo_common = promo_common
337-
promo.save()
338-
elif mode == business_models.Promo.MODE_UNIQUE and promo_unique:
339-
promo_codes = [
340-
business_models.PromoCode(promo=promo, code=code)
341-
for code in promo_unique
342-
]
343-
business_models.PromoCode.objects.bulk_create(promo_codes)
344-
345-
return promo
346-
347266
def to_representation(self, instance):
348267
data = super().to_representation(instance)
349268
data['target'] = instance.target
@@ -505,74 +424,12 @@ def update(self, instance, validated_data):
505424
return instance
506425

507426
def validate(self, data):
508-
instance = self.instance
509-
full_data = {
510-
'mode': instance.mode,
511-
'promo_common': instance.promo_common,
512-
'promo_unique': None,
513-
'max_count': instance.max_count,
514-
'active_from': instance.active_from,
515-
'active_until': instance.active_until,
516-
'target': instance.target if instance.target is not None else {},
517-
}
518-
full_data.update(data)
519-
mode = full_data.get('mode')
520-
promo_common = full_data.get('promo_common')
521-
promo_unique = full_data.get('promo_unique')
522-
max_count = full_data.get('max_count')
523-
524-
if mode == business_models.Promo.MODE_COMMON:
525-
if not promo_common:
526-
raise rest_framework.serializers.ValidationError(
527-
{
528-
'promo_common': (
529-
'This field is required for COMMON mode.'
530-
),
531-
},
532-
)
533-
534-
if promo_unique is not None:
535-
raise rest_framework.serializers.ValidationError(
536-
{
537-
'promo_unique': (
538-
'This field is not allowed for COMMON mode.'
539-
),
540-
},
541-
)
542-
543-
if max_count < 0 or max_count > 100000000:
544-
raise rest_framework.serializers.ValidationError(
545-
{'max_count': 'Must be between 0 and 100,000,000.'},
546-
)
547-
548-
elif mode == business_models.Promo.MODE_UNIQUE:
549-
if promo_common is not None:
550-
raise rest_framework.serializers.ValidationError(
551-
{
552-
'promo_common': (
553-
'This field is not allowed for UNIQUE mode.'
554-
),
555-
},
556-
)
557-
558-
if max_count != 1:
559-
raise rest_framework.serializers.ValidationError(
560-
{'max_count': 'Must be 1 for UNIQUE mode.'},
561-
)
562-
else:
563-
raise rest_framework.serializers.ValidationError(
564-
{'mode': 'Invalid mode.'},
565-
)
566-
567-
active_from = full_data.get('active_from')
568-
active_until = full_data.get('active_until')
569-
570-
if active_from and active_until and active_from > active_until:
571-
raise rest_framework.serializers.ValidationError(
572-
{'active_until': 'Must be after or equal to active_from.'},
573-
)
574-
575-
return data
427+
data = super().validate(data)
428+
validator = business.validators.PromoValidator(
429+
data=data,
430+
instance=self.instance,
431+
)
432+
return validator.validate()
576433

577434
def get_like_count(self, obj):
578435
return 0

promo_code/business/tests/promocodes/operations/test_detail.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ def setUpTestData(cls):
4545
cls.promo2_id = response2.data['id']
4646

4747
def test_get_promo_company1(self):
48-
4948
promo_detail_url = django.urls.reverse(
5049
'api-business:promo-detail',
5150
kwargs={'id': self.__class__.promo1_id},

promo_code/business/tests/promocodes/validations/test_create_validation.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
class TestPromoCreate(
88
business.tests.promocodes.base.BasePromoTestCase,
99
):
10-
1110
def setUp(self):
1211
super().setUp()
1312
self.client.credentials(

promo_code/business/tests/promocodes/validations/test_detail_validation.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66

77
class TestPromoDetail(business.tests.promocodes.base.BasePromoTestCase):
8-
98
@classmethod
109
def setUpClass(cls):
1110
super().setUpClass()

0 commit comments

Comments
 (0)