-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathservices.py
More file actions
144 lines (111 loc) · 4.76 KB
/
services.py
File metadata and controls
144 lines (111 loc) · 4.76 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
import django.db.models
import django.db.transaction
import django.utils.timezone
import rest_framework.exceptions
import business.constants
import business.models
import user.antifraud_service
import user.models
class PromoActivationError(rest_framework.exceptions.APIException):
"""Base exception for all promo code activation errors."""
status_code = 403
default_detail = 'Failed to activate promo code.'
default_code = 'promo_activation_failed'
class TargetingError(PromoActivationError):
"""Error for targeting mismatch."""
default_detail = 'You do not meet the promotion requirements.'
default_code = 'targeting_mismatch'
class PromoInactiveError(PromoActivationError):
"""Error if the promo code is inactive."""
default_detail = 'This promotion is currently inactive.'
default_code = 'promo_inactive'
class PromoUnavailableError(PromoActivationError):
"""Error if all promo codes have been used."""
default_detail = (
'Unfortunately, all codes for this promotion have been used.'
)
default_code = 'promo_unavailable'
class AntiFraudError(PromoActivationError):
"""Error from the anti-fraud system."""
default_detail = 'Activation is blocked by the security system.'
default_code = 'antifraud_block'
class PromoActivationService:
"""Service to encapsulate promo code activation logic."""
def __init__(self, user: user.models.User, promo: business.models.Promo):
self.user = user
self.promo = promo
def activate(self) -> str:
"""
Main method that starts the validation and activation process.
Returns the promo code on success.
"""
self._validate_targeting()
self._validate_is_active()
self._validate_antifraud()
return self._issue_promo_code()
def _validate_targeting(self):
"""Checks if the user matches the promotion's targeting settings."""
target = self.promo.target
user_age = self.user.other.get('age')
user_country = (
self.user.other.get('country', '').lower()
if self.user.other.get('country')
else None
)
if target.get('country') and user_country != target['country'].lower():
raise TargetingError('Country mismatch.')
if target.get('age_from') and user_age < target['age_from']:
raise TargetingError('Age mismatch.')
if target.get('age_until') and user_age > target['age_until']:
raise TargetingError('Age mismatch.')
def _validate_is_active(self):
"""Checks if the promo is active and codes are available."""
if not self.promo.active or not self.promo.is_active:
raise PromoInactiveError()
def _validate_antifraud(self):
"""Sends a request to the anti-fraud system."""
antifraud_response = (
user.antifraud_service.antifraud_service.get_verdict(
self.user.email,
str(self.promo.id),
)
)
if not antifraud_response.get('ok'):
raise AntiFraudError()
def _issue_promo_code(self) -> str:
"""
Issues a promo code in an atomic transaction, updates counters,
and creates a record in the history.
"""
try:
with django.db.transaction.atomic():
promo_locked = (
business.models.Promo.objects.select_for_update().get(
id=self.promo.id,
)
)
promo_code_value = None
if promo_locked.mode == business.constants.PROMO_MODE_COMMON:
if promo_locked.used_count < promo_locked.max_count:
promo_locked.used_count = (
django.db.models.F('used_count') + 1
)
promo_locked.save(update_fields=['used_count'])
promo_code_value = promo_locked.promo_common
else:
unique_code = promo_locked.unique_codes.filter(
is_used=False,
).first()
if unique_code:
unique_code.is_used = True
unique_code.used_at = django.utils.timezone.now()
unique_code.save(update_fields=['is_used', 'used_at'])
promo_code_value = unique_code.code
if promo_code_value:
user.models.PromoActivationHistory.objects.create(
user=self.user,
promo=promo_locked,
)
return promo_code_value
except business.models.Promo.DoesNotExist:
raise PromoActivationError('Promo not found.')