coupons 1.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- coupons-1.1.0/LICENSE +21 -0
- coupons-1.1.0/MANIFEST.in +3 -0
- coupons-1.1.0/PKG-INFO +246 -0
- coupons-1.1.0/README.md +206 -0
- coupons-1.1.0/coupons/__init__.py +3 -0
- coupons-1.1.0/coupons/actions.py +31 -0
- coupons-1.1.0/coupons/admin.py +53 -0
- coupons-1.1.0/coupons/apps.py +7 -0
- coupons-1.1.0/coupons/helpers.py +19 -0
- coupons-1.1.0/coupons/migrations/0001_initial.py +112 -0
- coupons-1.1.0/coupons/migrations/__init__.py +0 -0
- coupons-1.1.0/coupons/models.py +135 -0
- coupons-1.1.0/coupons/tests.py +59 -0
- coupons-1.1.0/coupons/validations.py +74 -0
- coupons-1.1.0/coupons/views.py +1 -0
- coupons-1.1.0/coupons.egg-info/PKG-INFO +246 -0
- coupons-1.1.0/coupons.egg-info/SOURCES.txt +20 -0
- coupons-1.1.0/coupons.egg-info/dependency_links.txt +1 -0
- coupons-1.1.0/coupons.egg-info/requires.txt +1 -0
- coupons-1.1.0/coupons.egg-info/top_level.txt +1 -0
- coupons-1.1.0/pyproject.toml +71 -0
- coupons-1.1.0/setup.cfg +4 -0
coupons-1.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020-2026 Nitesh Kumar Singh (nkscoder)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
coupons-1.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: coupons
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Django coupon and discount code system with validation rules — by Nitesh Kumar Singh (nkscoder)
|
|
5
|
+
Author-email: Nitesh Kumar Singh <nkscoder@gmail.com>
|
|
6
|
+
Maintainer-email: Nitesh Kumar Singh <nkscoder@gmail.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/nkscoder/coupons
|
|
9
|
+
Project-URL: Documentation, https://github.com/nkscoder/coupons#readme
|
|
10
|
+
Project-URL: Repository, https://github.com/nkscoder/coupons
|
|
11
|
+
Project-URL: Bug Tracker, https://github.com/nkscoder/coupons/issues
|
|
12
|
+
Project-URL: Author GitHub, https://github.com/nkscoder
|
|
13
|
+
Keywords: django,coupons,coupon,discount,promo-code,promotional-code,ecommerce,nkscoder,nitesh-kumar-singh,python,django-coupons,coupon-validation,discount-code
|
|
14
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
15
|
+
Classifier: Environment :: Web Environment
|
|
16
|
+
Classifier: Framework :: Django
|
|
17
|
+
Classifier: Framework :: Django :: 3.2
|
|
18
|
+
Classifier: Framework :: Django :: 4.0
|
|
19
|
+
Classifier: Framework :: Django :: 4.1
|
|
20
|
+
Classifier: Framework :: Django :: 4.2
|
|
21
|
+
Classifier: Framework :: Django :: 5.0
|
|
22
|
+
Classifier: Framework :: Django :: 5.1
|
|
23
|
+
Classifier: Intended Audience :: Developers
|
|
24
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
25
|
+
Classifier: Operating System :: OS Independent
|
|
26
|
+
Classifier: Programming Language :: Python
|
|
27
|
+
Classifier: Programming Language :: Python :: 3
|
|
28
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
29
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
30
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
31
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
32
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
33
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
34
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
35
|
+
Requires-Python: >=3.8
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
License-File: LICENSE
|
|
38
|
+
Requires-Dist: Django>=3.2
|
|
39
|
+
Dynamic: license-file
|
|
40
|
+
|
|
41
|
+
# Coupons — Django Coupon & Discount Code System
|
|
42
|
+
|
|
43
|
+
[](https://pypi.org/project/coupons/)
|
|
44
|
+
[](https://pypi.org/project/coupons/)
|
|
45
|
+
[](https://www.djangoproject.com/)
|
|
46
|
+
[](LICENSE)
|
|
47
|
+
|
|
48
|
+
**Author:** [Nitesh Kumar Singh](https://github.com/nkscoder) · **GitHub:** [@nkscoder](https://github.com/nkscoder)
|
|
49
|
+
|
|
50
|
+
A reusable **Django coupons app** for managing promotional codes, discount rules, and coupon validation in Python web applications. Built by **Nitesh Kumar Singh (nkscoder)** for e-commerce, SaaS, and any Django project that needs flexible coupon functionality.
|
|
51
|
+
|
|
52
|
+
> **Keywords:** django coupons · python coupon system · discount code · promo code · coupon validation · nkscoder · nitesh kumar singh
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Features
|
|
57
|
+
|
|
58
|
+
- **Percentage or fixed-amount discounts** on any order total
|
|
59
|
+
- **User-specific or global coupons** — restrict codes to selected users or allow all
|
|
60
|
+
- **Usage limits** — max total uses, uses per user, or unlimited
|
|
61
|
+
- **Expiration & active/inactive rules** with datetime-based validity
|
|
62
|
+
- **Django Admin integration** with bulk actions (reset usage, delete expired)
|
|
63
|
+
- **Simple validation API** — one function call to check any coupon code
|
|
64
|
+
- **Configurable coupon code length** via `DSC_COUPON_CODE_LENGTH` setting
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Requirements
|
|
69
|
+
|
|
70
|
+
- Python 3.8+
|
|
71
|
+
- Django 3.2+
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Installation
|
|
76
|
+
|
|
77
|
+
### From PyPI (recommended)
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
pip install coupons
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### From GitHub
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pip install git+https://github.com/nkscoder/coupons.git
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Setup
|
|
92
|
+
|
|
93
|
+
### Step 1 — Add to `INSTALLED_APPS`
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
# settings.py
|
|
97
|
+
INSTALLED_APPS = [
|
|
98
|
+
...
|
|
99
|
+
"coupons",
|
|
100
|
+
]
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Step 2 — Run migrations
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
python manage.py migrate coupons
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Step 3 — Create coupons in Django Admin
|
|
110
|
+
|
|
111
|
+
Go to `/admin/` and create:
|
|
112
|
+
|
|
113
|
+
1. **Discount** — set value and type (percentage or fixed)
|
|
114
|
+
2. **Allowed Users Rule** — select users or enable "All users"
|
|
115
|
+
3. **Max Uses Rule** — set limits or enable infinite uses
|
|
116
|
+
4. **Validity Rule** — set expiration date and active status
|
|
117
|
+
5. **Ruleset** — link the three rules above
|
|
118
|
+
6. **Coupon** — assign discount and ruleset (code auto-generated)
|
|
119
|
+
|
|
120
|
+
### Step 4 (optional) — Custom coupon code length
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
# settings.py
|
|
124
|
+
DSC_COUPON_CODE_LENGTH = 16 # default is 12
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Usage
|
|
130
|
+
|
|
131
|
+
### Validate a coupon
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from coupons.validations import validate_coupon
|
|
135
|
+
|
|
136
|
+
coupon_code = "COUPONTEST01"
|
|
137
|
+
user = User.objects.get(username="nitesh")
|
|
138
|
+
|
|
139
|
+
status = validate_coupon(coupon_code=coupon_code, user=user)
|
|
140
|
+
# {'valid': True}
|
|
141
|
+
|
|
142
|
+
if status["valid"]:
|
|
143
|
+
print("Coupon is valid!")
|
|
144
|
+
else:
|
|
145
|
+
print(status["message"]) # e.g. "Coupon does not exist!"
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Apply a coupon (record usage)
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
from coupons.models import Coupon
|
|
152
|
+
|
|
153
|
+
coupon = Coupon.objects.get(code=coupon_code)
|
|
154
|
+
coupon.use_coupon(user=user)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Get discount details
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
coupon = Coupon.objects.get(code=coupon_code)
|
|
161
|
+
discount = coupon.get_discount()
|
|
162
|
+
# {'value': 50, 'is_percentage': True}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Calculate discounted price
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
discounted = coupon.get_discounted_value(initial_value=100.0)
|
|
169
|
+
# Returns 50.0 for 50% off, or 80.0 for $20 off
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Validation Rules
|
|
175
|
+
|
|
176
|
+
| Rule | Description |
|
|
177
|
+
|------|-------------|
|
|
178
|
+
| **Allowed Users** | Coupon valid only for selected users, or all users |
|
|
179
|
+
| **Max Uses** | Global usage cap, per-user limit, or infinite |
|
|
180
|
+
| **Validity** | Active flag + expiration datetime |
|
|
181
|
+
|
|
182
|
+
Invalid responses include a human-readable `message` key:
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
validate_coupon("DUMMYCODE", user)
|
|
186
|
+
# {'valid': False, 'message': 'Coupon does not exist!'}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## REST API Example
|
|
192
|
+
|
|
193
|
+
See [`examples/`](examples/) for a Django REST Framework integration sample (`ajax_views.py`, `ajax_urls.py`).
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Development
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
git clone git@github.com:nkscoder/coupons.git
|
|
201
|
+
cd coupons
|
|
202
|
+
pip install -e ".[dev]"
|
|
203
|
+
python manage.py migrate
|
|
204
|
+
python manage.py runserver
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Build & publish to PyPI
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
pip install build twine
|
|
211
|
+
python -m build
|
|
212
|
+
twine upload dist/*
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Or tag a release on GitHub — the included GitHub Action publishes automatically.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Changelog
|
|
220
|
+
|
|
221
|
+
### 1.1.0
|
|
222
|
+
- Modernized for Django 3.2–5.x and Python 3.8+
|
|
223
|
+
- Added PyPI packaging (`pyproject.toml`)
|
|
224
|
+
- Fixed per-coupon usage validation bug
|
|
225
|
+
- Fixed template mutation in validation responses
|
|
226
|
+
- SEO & documentation update by **Nitesh Kumar Singh (nkscoder)**
|
|
227
|
+
|
|
228
|
+
### 1.0.0
|
|
229
|
+
- Initial release
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## Author & Links
|
|
234
|
+
|
|
235
|
+
| | |
|
|
236
|
+
|---|---|
|
|
237
|
+
| **Author** | Nitesh Kumar Singh |
|
|
238
|
+
| **GitHub** | [github.com/nkscoder](https://github.com/nkscoder) |
|
|
239
|
+
| **Repository** | [github.com/nkscoder/coupons](https://github.com/nkscoder/coupons) |
|
|
240
|
+
| **PyPI** | [pypi.org/project/coupons](https://pypi.org/project/coupons) |
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## License
|
|
245
|
+
|
|
246
|
+
MIT License — Copyright (c) 2020-2026 [Nitesh Kumar Singh (nkscoder)](https://github.com/nkscoder)
|
coupons-1.1.0/README.md
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# Coupons — Django Coupon & Discount Code System
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/coupons/)
|
|
4
|
+
[](https://pypi.org/project/coupons/)
|
|
5
|
+
[](https://www.djangoproject.com/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
**Author:** [Nitesh Kumar Singh](https://github.com/nkscoder) · **GitHub:** [@nkscoder](https://github.com/nkscoder)
|
|
9
|
+
|
|
10
|
+
A reusable **Django coupons app** for managing promotional codes, discount rules, and coupon validation in Python web applications. Built by **Nitesh Kumar Singh (nkscoder)** for e-commerce, SaaS, and any Django project that needs flexible coupon functionality.
|
|
11
|
+
|
|
12
|
+
> **Keywords:** django coupons · python coupon system · discount code · promo code · coupon validation · nkscoder · nitesh kumar singh
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- **Percentage or fixed-amount discounts** on any order total
|
|
19
|
+
- **User-specific or global coupons** — restrict codes to selected users or allow all
|
|
20
|
+
- **Usage limits** — max total uses, uses per user, or unlimited
|
|
21
|
+
- **Expiration & active/inactive rules** with datetime-based validity
|
|
22
|
+
- **Django Admin integration** with bulk actions (reset usage, delete expired)
|
|
23
|
+
- **Simple validation API** — one function call to check any coupon code
|
|
24
|
+
- **Configurable coupon code length** via `DSC_COUPON_CODE_LENGTH` setting
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Requirements
|
|
29
|
+
|
|
30
|
+
- Python 3.8+
|
|
31
|
+
- Django 3.2+
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
### From PyPI (recommended)
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install coupons
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### From GitHub
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install git+https://github.com/nkscoder/coupons.git
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Setup
|
|
52
|
+
|
|
53
|
+
### Step 1 — Add to `INSTALLED_APPS`
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
# settings.py
|
|
57
|
+
INSTALLED_APPS = [
|
|
58
|
+
...
|
|
59
|
+
"coupons",
|
|
60
|
+
]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Step 2 — Run migrations
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
python manage.py migrate coupons
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Step 3 — Create coupons in Django Admin
|
|
70
|
+
|
|
71
|
+
Go to `/admin/` and create:
|
|
72
|
+
|
|
73
|
+
1. **Discount** — set value and type (percentage or fixed)
|
|
74
|
+
2. **Allowed Users Rule** — select users or enable "All users"
|
|
75
|
+
3. **Max Uses Rule** — set limits or enable infinite uses
|
|
76
|
+
4. **Validity Rule** — set expiration date and active status
|
|
77
|
+
5. **Ruleset** — link the three rules above
|
|
78
|
+
6. **Coupon** — assign discount and ruleset (code auto-generated)
|
|
79
|
+
|
|
80
|
+
### Step 4 (optional) — Custom coupon code length
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
# settings.py
|
|
84
|
+
DSC_COUPON_CODE_LENGTH = 16 # default is 12
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Usage
|
|
90
|
+
|
|
91
|
+
### Validate a coupon
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from coupons.validations import validate_coupon
|
|
95
|
+
|
|
96
|
+
coupon_code = "COUPONTEST01"
|
|
97
|
+
user = User.objects.get(username="nitesh")
|
|
98
|
+
|
|
99
|
+
status = validate_coupon(coupon_code=coupon_code, user=user)
|
|
100
|
+
# {'valid': True}
|
|
101
|
+
|
|
102
|
+
if status["valid"]:
|
|
103
|
+
print("Coupon is valid!")
|
|
104
|
+
else:
|
|
105
|
+
print(status["message"]) # e.g. "Coupon does not exist!"
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Apply a coupon (record usage)
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from coupons.models import Coupon
|
|
112
|
+
|
|
113
|
+
coupon = Coupon.objects.get(code=coupon_code)
|
|
114
|
+
coupon.use_coupon(user=user)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Get discount details
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
coupon = Coupon.objects.get(code=coupon_code)
|
|
121
|
+
discount = coupon.get_discount()
|
|
122
|
+
# {'value': 50, 'is_percentage': True}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Calculate discounted price
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
discounted = coupon.get_discounted_value(initial_value=100.0)
|
|
129
|
+
# Returns 50.0 for 50% off, or 80.0 for $20 off
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Validation Rules
|
|
135
|
+
|
|
136
|
+
| Rule | Description |
|
|
137
|
+
|------|-------------|
|
|
138
|
+
| **Allowed Users** | Coupon valid only for selected users, or all users |
|
|
139
|
+
| **Max Uses** | Global usage cap, per-user limit, or infinite |
|
|
140
|
+
| **Validity** | Active flag + expiration datetime |
|
|
141
|
+
|
|
142
|
+
Invalid responses include a human-readable `message` key:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
validate_coupon("DUMMYCODE", user)
|
|
146
|
+
# {'valid': False, 'message': 'Coupon does not exist!'}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## REST API Example
|
|
152
|
+
|
|
153
|
+
See [`examples/`](examples/) for a Django REST Framework integration sample (`ajax_views.py`, `ajax_urls.py`).
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Development
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
git clone git@github.com:nkscoder/coupons.git
|
|
161
|
+
cd coupons
|
|
162
|
+
pip install -e ".[dev]"
|
|
163
|
+
python manage.py migrate
|
|
164
|
+
python manage.py runserver
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Build & publish to PyPI
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
pip install build twine
|
|
171
|
+
python -m build
|
|
172
|
+
twine upload dist/*
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Or tag a release on GitHub — the included GitHub Action publishes automatically.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Changelog
|
|
180
|
+
|
|
181
|
+
### 1.1.0
|
|
182
|
+
- Modernized for Django 3.2–5.x and Python 3.8+
|
|
183
|
+
- Added PyPI packaging (`pyproject.toml`)
|
|
184
|
+
- Fixed per-coupon usage validation bug
|
|
185
|
+
- Fixed template mutation in validation responses
|
|
186
|
+
- SEO & documentation update by **Nitesh Kumar Singh (nkscoder)**
|
|
187
|
+
|
|
188
|
+
### 1.0.0
|
|
189
|
+
- Initial release
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Author & Links
|
|
194
|
+
|
|
195
|
+
| | |
|
|
196
|
+
|---|---|
|
|
197
|
+
| **Author** | Nitesh Kumar Singh |
|
|
198
|
+
| **GitHub** | [github.com/nkscoder](https://github.com/nkscoder) |
|
|
199
|
+
| **Repository** | [github.com/nkscoder/coupons](https://github.com/nkscoder/coupons) |
|
|
200
|
+
| **PyPI** | [pypi.org/project/coupons](https://pypi.org/project/coupons) |
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## License
|
|
205
|
+
|
|
206
|
+
MIT License — Copyright (c) 2020-2026 [Nitesh Kumar Singh (nkscoder)](https://github.com/nkscoder)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
|
|
2
|
+
from django.contrib.admin import ModelAdmin
|
|
3
|
+
from django.utils import timezone
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# Create your actions here
|
|
7
|
+
# ========================
|
|
8
|
+
def reset_coupon_usage(modeladmin, request, queryset):
|
|
9
|
+
for coupon_user in queryset:
|
|
10
|
+
coupon_user.times_used = 0
|
|
11
|
+
coupon_user.save()
|
|
12
|
+
|
|
13
|
+
ModelAdmin.message_user(modeladmin, request, "Coupons reseted!")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def delete_expired_coupons(modeladmin, request, queryset):
|
|
17
|
+
count = 0
|
|
18
|
+
for coupon in queryset:
|
|
19
|
+
expiration_date = coupon.ruleset.validity.expiration_date
|
|
20
|
+
if timezone.now() >= expiration_date:
|
|
21
|
+
coupon.delete()
|
|
22
|
+
count += 1
|
|
23
|
+
|
|
24
|
+
ModelAdmin.message_user(modeladmin, request, "{0} Expired coupons deleted!".format(count))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Actions short descriptions
|
|
28
|
+
# ==========================
|
|
29
|
+
reset_coupon_usage.short_description = "Reset coupon usage"
|
|
30
|
+
delete_expired_coupons.short_description = "Delete expired coupons"
|
|
31
|
+
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from django.contrib import admin
|
|
2
|
+
|
|
3
|
+
from .models import (Coupon,
|
|
4
|
+
Discount,
|
|
5
|
+
Ruleset,
|
|
6
|
+
CouponUser,
|
|
7
|
+
AllowedUsersRule,
|
|
8
|
+
MaxUsesRule,
|
|
9
|
+
ValidityRule)
|
|
10
|
+
|
|
11
|
+
from .actions import (reset_coupon_usage, delete_expired_coupons)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Register your models here.
|
|
15
|
+
# ==========================
|
|
16
|
+
@admin.register(Coupon)
|
|
17
|
+
class CouponAdmin(admin.ModelAdmin):
|
|
18
|
+
list_display = ('code', 'discount', 'ruleset', 'times_used', 'created', )
|
|
19
|
+
actions = [delete_expired_coupons]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@admin.register(Discount)
|
|
23
|
+
class DiscountAdmin(admin.ModelAdmin):
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@admin.register(Ruleset)
|
|
28
|
+
class RulesetAdmin(admin.ModelAdmin):
|
|
29
|
+
list_display = ('__str__', 'allowed_users', 'max_uses', 'validity', )
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@admin.register(CouponUser)
|
|
33
|
+
class CouponUserAdmin(admin.ModelAdmin):
|
|
34
|
+
list_display = ('user', 'coupon', 'times_used', )
|
|
35
|
+
actions = [reset_coupon_usage]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@admin.register(AllowedUsersRule)
|
|
39
|
+
class AllowedUsersRuleAdmin(admin.ModelAdmin):
|
|
40
|
+
def get_model_perms(self, request):
|
|
41
|
+
return {}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@admin.register(MaxUsesRule)
|
|
45
|
+
class MaxUsesRuleAdmin(admin.ModelAdmin):
|
|
46
|
+
def get_model_perms(self, request):
|
|
47
|
+
return {}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@admin.register(ValidityRule)
|
|
51
|
+
class ValidityRuleAdmin(admin.ModelAdmin):
|
|
52
|
+
def get_model_perms(self, request):
|
|
53
|
+
return {}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import string
|
|
2
|
+
import random
|
|
3
|
+
|
|
4
|
+
from django.conf import settings
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_coupon_code_length(length=12):
|
|
8
|
+
return settings.DSC_COUPON_CODE_LENGTH if hasattr(settings, 'DSC_COUPON_CODE_LENGTH') else length
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_user_model():
|
|
12
|
+
return settings.AUTH_USER_MODEL
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_random_code(length=12):
|
|
16
|
+
length = get_coupon_code_length(length=length)
|
|
17
|
+
return ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(length))
|
|
18
|
+
|
|
19
|
+
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Generated by Django 3.0.8 on 2023-01-13 12:57
|
|
2
|
+
|
|
3
|
+
import coupons.helpers
|
|
4
|
+
from django.conf import settings
|
|
5
|
+
from django.db import migrations, models
|
|
6
|
+
import django.db.models.deletion
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Migration(migrations.Migration):
|
|
10
|
+
|
|
11
|
+
initial = True
|
|
12
|
+
|
|
13
|
+
dependencies = [
|
|
14
|
+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
operations = [
|
|
18
|
+
migrations.CreateModel(
|
|
19
|
+
name='AllowedUsersRule',
|
|
20
|
+
fields=[
|
|
21
|
+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
22
|
+
('all_users', models.BooleanField(default=False, verbose_name='All users?')),
|
|
23
|
+
('users', models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Users')),
|
|
24
|
+
],
|
|
25
|
+
options={
|
|
26
|
+
'verbose_name': 'Allowed User Rule',
|
|
27
|
+
'verbose_name_plural': 'Allowed User Rules',
|
|
28
|
+
},
|
|
29
|
+
),
|
|
30
|
+
migrations.CreateModel(
|
|
31
|
+
name='Coupon',
|
|
32
|
+
fields=[
|
|
33
|
+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
34
|
+
('code', models.CharField(default=coupons.helpers.get_random_code, max_length=12, unique=True, verbose_name='Coupon Code')),
|
|
35
|
+
('times_used', models.IntegerField(default=0, editable=False, verbose_name='Times used')),
|
|
36
|
+
('created', models.DateTimeField(editable=False, verbose_name='Created')),
|
|
37
|
+
],
|
|
38
|
+
),
|
|
39
|
+
migrations.CreateModel(
|
|
40
|
+
name='Discount',
|
|
41
|
+
fields=[
|
|
42
|
+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
43
|
+
('value', models.IntegerField(default=0, verbose_name='Value')),
|
|
44
|
+
('is_percentage', models.BooleanField(default=False, verbose_name='Is percentage?')),
|
|
45
|
+
],
|
|
46
|
+
options={
|
|
47
|
+
'verbose_name': 'Discount',
|
|
48
|
+
'verbose_name_plural': 'Discounts',
|
|
49
|
+
},
|
|
50
|
+
),
|
|
51
|
+
migrations.CreateModel(
|
|
52
|
+
name='MaxUsesRule',
|
|
53
|
+
fields=[
|
|
54
|
+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
55
|
+
('max_uses', models.BigIntegerField(default=0, verbose_name='Maximum uses')),
|
|
56
|
+
('is_infinite', models.BooleanField(default=False, verbose_name='Infinite uses?')),
|
|
57
|
+
('uses_per_user', models.IntegerField(default=1, verbose_name='Uses per user')),
|
|
58
|
+
],
|
|
59
|
+
options={
|
|
60
|
+
'verbose_name': 'Max Uses Rule',
|
|
61
|
+
'verbose_name_plural': 'Max Uses Rules',
|
|
62
|
+
},
|
|
63
|
+
),
|
|
64
|
+
migrations.CreateModel(
|
|
65
|
+
name='ValidityRule',
|
|
66
|
+
fields=[
|
|
67
|
+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
68
|
+
('expiration_date', models.DateTimeField(verbose_name='Expiration date')),
|
|
69
|
+
('is_active', models.BooleanField(default=False, verbose_name='Is active?')),
|
|
70
|
+
],
|
|
71
|
+
options={
|
|
72
|
+
'verbose_name': 'Validity Rule',
|
|
73
|
+
'verbose_name_plural': 'Validity Rules',
|
|
74
|
+
},
|
|
75
|
+
),
|
|
76
|
+
migrations.CreateModel(
|
|
77
|
+
name='Ruleset',
|
|
78
|
+
fields=[
|
|
79
|
+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
80
|
+
('allowed_users', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='coupons.AllowedUsersRule', verbose_name='Allowed users rule')),
|
|
81
|
+
('max_uses', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='coupons.MaxUsesRule', verbose_name='Max uses rule')),
|
|
82
|
+
('validity', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='coupons.ValidityRule', verbose_name='Validity rule')),
|
|
83
|
+
],
|
|
84
|
+
options={
|
|
85
|
+
'verbose_name': 'Ruleset',
|
|
86
|
+
'verbose_name_plural': 'Rulesets',
|
|
87
|
+
},
|
|
88
|
+
),
|
|
89
|
+
migrations.CreateModel(
|
|
90
|
+
name='CouponUser',
|
|
91
|
+
fields=[
|
|
92
|
+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
93
|
+
('times_used', models.IntegerField(default=0, editable=False, verbose_name='Times used')),
|
|
94
|
+
('coupon', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='coupons.Coupon', verbose_name='Coupon')),
|
|
95
|
+
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User')),
|
|
96
|
+
],
|
|
97
|
+
options={
|
|
98
|
+
'verbose_name': 'Coupon User',
|
|
99
|
+
'verbose_name_plural': 'Coupon Users',
|
|
100
|
+
},
|
|
101
|
+
),
|
|
102
|
+
migrations.AddField(
|
|
103
|
+
model_name='coupon',
|
|
104
|
+
name='discount',
|
|
105
|
+
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='coupons.Discount'),
|
|
106
|
+
),
|
|
107
|
+
migrations.AddField(
|
|
108
|
+
model_name='coupon',
|
|
109
|
+
name='ruleset',
|
|
110
|
+
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='coupons.Ruleset', verbose_name='Ruleset'),
|
|
111
|
+
),
|
|
112
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from django.utils import timezone
|
|
3
|
+
|
|
4
|
+
from .helpers import (get_random_code,
|
|
5
|
+
get_coupon_code_length,
|
|
6
|
+
get_user_model)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# Create your models here.
|
|
10
|
+
# ========================
|
|
11
|
+
class Ruleset(models.Model):
|
|
12
|
+
allowed_users = models.ForeignKey('AllowedUsersRule', on_delete=models.CASCADE, verbose_name="Allowed users rule")
|
|
13
|
+
max_uses = models.ForeignKey('MaxUsesRule', on_delete=models.CASCADE, verbose_name="Max uses rule")
|
|
14
|
+
validity = models.ForeignKey('ValidityRule', on_delete=models.CASCADE, verbose_name="Validity rule")
|
|
15
|
+
|
|
16
|
+
def __str__(self):
|
|
17
|
+
return "Ruleset Nº{0}".format(self.id)
|
|
18
|
+
|
|
19
|
+
class Meta:
|
|
20
|
+
verbose_name = "Ruleset"
|
|
21
|
+
verbose_name_plural = "Rulesets"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AllowedUsersRule(models.Model):
|
|
25
|
+
user_model = get_user_model()
|
|
26
|
+
|
|
27
|
+
users = models.ManyToManyField(user_model, verbose_name="Users", blank=True)
|
|
28
|
+
all_users = models.BooleanField(default=False, verbose_name="All users?")
|
|
29
|
+
|
|
30
|
+
def __str__(self):
|
|
31
|
+
return "AllowedUsersRule Nº{0}".format(self.id)
|
|
32
|
+
|
|
33
|
+
class Meta:
|
|
34
|
+
verbose_name = "Allowed User Rule"
|
|
35
|
+
verbose_name_plural = "Allowed User Rules"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class MaxUsesRule(models.Model):
|
|
39
|
+
max_uses = models.BigIntegerField(default=0, verbose_name="Maximum uses")
|
|
40
|
+
is_infinite = models.BooleanField(default=False, verbose_name="Infinite uses?")
|
|
41
|
+
uses_per_user = models.IntegerField(default=1, verbose_name="Uses per user")
|
|
42
|
+
|
|
43
|
+
def __str__(self):
|
|
44
|
+
return "MaxUsesRule Nº{0}".format(self.id)
|
|
45
|
+
|
|
46
|
+
class Meta:
|
|
47
|
+
verbose_name = "Max Uses Rule"
|
|
48
|
+
verbose_name_plural = "Max Uses Rules"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ValidityRule(models.Model):
|
|
52
|
+
expiration_date = models.DateTimeField(verbose_name="Expiration date")
|
|
53
|
+
is_active = models.BooleanField(default=False, verbose_name="Is active?")
|
|
54
|
+
|
|
55
|
+
def __str__(self):
|
|
56
|
+
return "ValidityRule Nº{0}".format(self.id)
|
|
57
|
+
|
|
58
|
+
class Meta:
|
|
59
|
+
verbose_name = "Validity Rule"
|
|
60
|
+
verbose_name_plural = "Validity Rules"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class CouponUser(models.Model):
|
|
64
|
+
user_model = get_user_model()
|
|
65
|
+
|
|
66
|
+
user = models.ForeignKey(user_model, on_delete=models.CASCADE, verbose_name="User")
|
|
67
|
+
coupon = models.ForeignKey('Coupon', on_delete=models.CASCADE, verbose_name="Coupon")
|
|
68
|
+
times_used = models.IntegerField(default=0, editable=False, verbose_name="Times used")
|
|
69
|
+
|
|
70
|
+
def __str__(self):
|
|
71
|
+
return str(self.user)
|
|
72
|
+
|
|
73
|
+
class Meta:
|
|
74
|
+
verbose_name = "Coupon User"
|
|
75
|
+
verbose_name_plural = "Coupon Users"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class Discount(models.Model):
|
|
79
|
+
value = models.IntegerField(default=0, verbose_name="Value")
|
|
80
|
+
is_percentage = models.BooleanField(default=False, verbose_name="Is percentage?")
|
|
81
|
+
|
|
82
|
+
def __str__(self):
|
|
83
|
+
if self.is_percentage:
|
|
84
|
+
return "{0}% - Discount".format(self.value)
|
|
85
|
+
|
|
86
|
+
return "${0} - Discount".format(self.value)
|
|
87
|
+
|
|
88
|
+
class Meta:
|
|
89
|
+
verbose_name = "Discount"
|
|
90
|
+
verbose_name_plural = "Discounts"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class Coupon(models.Model):
|
|
94
|
+
code_length = get_coupon_code_length()
|
|
95
|
+
|
|
96
|
+
code = models.CharField(max_length=code_length, default=get_random_code, verbose_name="Coupon Code", unique=True)
|
|
97
|
+
discount = models.ForeignKey('Discount', on_delete=models.CASCADE)
|
|
98
|
+
times_used = models.IntegerField(default=0, editable=False, verbose_name="Times used")
|
|
99
|
+
created = models.DateTimeField(editable=False, verbose_name="Created")
|
|
100
|
+
|
|
101
|
+
ruleset = models.ForeignKey('Ruleset', on_delete=models.CASCADE, verbose_name="Ruleset")
|
|
102
|
+
|
|
103
|
+
def __str__(self):
|
|
104
|
+
return self.code
|
|
105
|
+
|
|
106
|
+
def use_coupon(self, user):
|
|
107
|
+
coupon_user, created = CouponUser.objects.get_or_create(user=user, coupon=self)
|
|
108
|
+
coupon_user.times_used += 1
|
|
109
|
+
coupon_user.save()
|
|
110
|
+
|
|
111
|
+
self.times_used += 1
|
|
112
|
+
self.save()
|
|
113
|
+
|
|
114
|
+
def get_discount(self):
|
|
115
|
+
return {
|
|
116
|
+
"value": self.discount.value,
|
|
117
|
+
"is_percentage": self.discount.is_percentage
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
def get_discounted_value(self, initial_value):
|
|
121
|
+
discount = self.get_discount()
|
|
122
|
+
|
|
123
|
+
if discount['is_percentage']:
|
|
124
|
+
new_price = initial_value - ((initial_value * discount['value']) / 100)
|
|
125
|
+
new_price = new_price if new_price >= 0.0 else 0.0
|
|
126
|
+
else:
|
|
127
|
+
new_price = initial_value - discount['value']
|
|
128
|
+
new_price = new_price if new_price >= 0.0 else 0.0
|
|
129
|
+
|
|
130
|
+
return new_price
|
|
131
|
+
|
|
132
|
+
def save(self, *args, **kwargs):
|
|
133
|
+
if not self.id:
|
|
134
|
+
self.created = timezone.now()
|
|
135
|
+
return super().save(*args, **kwargs)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from django.test import TestCase
|
|
2
|
+
from django.contrib.auth import get_user_model
|
|
3
|
+
from django.utils import timezone
|
|
4
|
+
from datetime import timedelta
|
|
5
|
+
|
|
6
|
+
from coupons.models import (
|
|
7
|
+
AllowedUsersRule,
|
|
8
|
+
Coupon,
|
|
9
|
+
Discount,
|
|
10
|
+
MaxUsesRule,
|
|
11
|
+
Ruleset,
|
|
12
|
+
ValidityRule,
|
|
13
|
+
)
|
|
14
|
+
from coupons.validations import validate_coupon
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
User = get_user_model()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CouponValidationTests(TestCase):
|
|
21
|
+
def setUp(self):
|
|
22
|
+
self.user = User.objects.create_user(username="testuser", password="pass")
|
|
23
|
+
self.discount = Discount.objects.create(value=10, is_percentage=True)
|
|
24
|
+
self.allowed = AllowedUsersRule.objects.create(all_users=True)
|
|
25
|
+
self.max_uses = MaxUsesRule.objects.create(
|
|
26
|
+
max_uses=100, is_infinite=False, uses_per_user=1
|
|
27
|
+
)
|
|
28
|
+
self.validity = ValidityRule.objects.create(
|
|
29
|
+
expiration_date=timezone.now() + timedelta(days=30),
|
|
30
|
+
is_active=True,
|
|
31
|
+
)
|
|
32
|
+
self.ruleset = Ruleset.objects.create(
|
|
33
|
+
allowed_users=self.allowed,
|
|
34
|
+
max_uses=self.max_uses,
|
|
35
|
+
validity=self.validity,
|
|
36
|
+
)
|
|
37
|
+
self.coupon = Coupon.objects.create(
|
|
38
|
+
code="TESTCODE1234",
|
|
39
|
+
discount=self.discount,
|
|
40
|
+
ruleset=self.ruleset,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def test_valid_coupon(self):
|
|
44
|
+
result = validate_coupon("TESTCODE1234", self.user)
|
|
45
|
+
self.assertTrue(result["valid"])
|
|
46
|
+
|
|
47
|
+
def test_invalid_coupon_code(self):
|
|
48
|
+
result = validate_coupon("NOTEXIST", self.user)
|
|
49
|
+
self.assertFalse(result["valid"])
|
|
50
|
+
self.assertEqual(result["message"], "Coupon does not exist!")
|
|
51
|
+
|
|
52
|
+
def test_get_discounted_value_percentage(self):
|
|
53
|
+
self.assertEqual(self.coupon.get_discounted_value(100.0), 90.0)
|
|
54
|
+
|
|
55
|
+
def test_get_discounted_value_fixed(self):
|
|
56
|
+
self.discount.is_percentage = False
|
|
57
|
+
self.discount.value = 20
|
|
58
|
+
self.discount.save()
|
|
59
|
+
self.assertEqual(self.coupon.get_discounted_value(100.0), 80.0)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from .models import Coupon, CouponUser
|
|
2
|
+
from django.utils import timezone
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
INVALID_TEMPLATE = {
|
|
6
|
+
"valid": False,
|
|
7
|
+
"message": ""
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
VALID_TEMPLATE = {
|
|
11
|
+
"valid": True
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def assemble_invalid_message(message=""):
|
|
16
|
+
return {"valid": False, "message": message}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def validate_allowed_users_rule(coupon_object, user):
|
|
20
|
+
allowed_users_rule = coupon_object.ruleset.allowed_users
|
|
21
|
+
if user not in allowed_users_rule.users.all():
|
|
22
|
+
return False if not allowed_users_rule.all_users else True
|
|
23
|
+
|
|
24
|
+
return True
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def validate_max_uses_rule(coupon_object, user):
|
|
28
|
+
max_uses_rule = coupon_object.ruleset.max_uses
|
|
29
|
+
if coupon_object.times_used >= max_uses_rule.max_uses and not max_uses_rule.is_infinite:
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
coupon_user = CouponUser.objects.get(user=user, coupon=coupon_object)
|
|
34
|
+
if coupon_user.times_used >= max_uses_rule.uses_per_user:
|
|
35
|
+
return False
|
|
36
|
+
except CouponUser.DoesNotExist:
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
return True
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def validate_validity_rule(coupon_object):
|
|
43
|
+
validity_rule = coupon_object.ruleset.validity
|
|
44
|
+
if timezone.now() > validity_rule.expiration_date:
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
return validity_rule.is_active
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def validate_coupon(coupon_code, user):
|
|
51
|
+
if not coupon_code:
|
|
52
|
+
return assemble_invalid_message(message="No coupon code provided!")
|
|
53
|
+
|
|
54
|
+
if not user:
|
|
55
|
+
return assemble_invalid_message(message="No user provided!")
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
coupon_object = Coupon.objects.get(code=coupon_code)
|
|
59
|
+
except Coupon.DoesNotExist:
|
|
60
|
+
return assemble_invalid_message(message="Coupon does not exist!")
|
|
61
|
+
|
|
62
|
+
valid_allowed_users_rule = validate_allowed_users_rule(coupon_object=coupon_object, user=user)
|
|
63
|
+
if not valid_allowed_users_rule:
|
|
64
|
+
return assemble_invalid_message(message="Invalid coupon for this user!")
|
|
65
|
+
|
|
66
|
+
valid_max_uses_rule = validate_max_uses_rule(coupon_object=coupon_object, user=user)
|
|
67
|
+
if not valid_max_uses_rule:
|
|
68
|
+
return assemble_invalid_message(message="Coupon uses exceeded for this user!")
|
|
69
|
+
|
|
70
|
+
valid_validity_rule = validate_validity_rule(coupon_object=coupon_object)
|
|
71
|
+
if not valid_validity_rule:
|
|
72
|
+
return assemble_invalid_message(message="Invalid coupon!")
|
|
73
|
+
|
|
74
|
+
return VALID_TEMPLATE
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Create your views here.
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: coupons
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Django coupon and discount code system with validation rules — by Nitesh Kumar Singh (nkscoder)
|
|
5
|
+
Author-email: Nitesh Kumar Singh <nkscoder@gmail.com>
|
|
6
|
+
Maintainer-email: Nitesh Kumar Singh <nkscoder@gmail.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/nkscoder/coupons
|
|
9
|
+
Project-URL: Documentation, https://github.com/nkscoder/coupons#readme
|
|
10
|
+
Project-URL: Repository, https://github.com/nkscoder/coupons
|
|
11
|
+
Project-URL: Bug Tracker, https://github.com/nkscoder/coupons/issues
|
|
12
|
+
Project-URL: Author GitHub, https://github.com/nkscoder
|
|
13
|
+
Keywords: django,coupons,coupon,discount,promo-code,promotional-code,ecommerce,nkscoder,nitesh-kumar-singh,python,django-coupons,coupon-validation,discount-code
|
|
14
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
15
|
+
Classifier: Environment :: Web Environment
|
|
16
|
+
Classifier: Framework :: Django
|
|
17
|
+
Classifier: Framework :: Django :: 3.2
|
|
18
|
+
Classifier: Framework :: Django :: 4.0
|
|
19
|
+
Classifier: Framework :: Django :: 4.1
|
|
20
|
+
Classifier: Framework :: Django :: 4.2
|
|
21
|
+
Classifier: Framework :: Django :: 5.0
|
|
22
|
+
Classifier: Framework :: Django :: 5.1
|
|
23
|
+
Classifier: Intended Audience :: Developers
|
|
24
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
25
|
+
Classifier: Operating System :: OS Independent
|
|
26
|
+
Classifier: Programming Language :: Python
|
|
27
|
+
Classifier: Programming Language :: Python :: 3
|
|
28
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
29
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
30
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
31
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
32
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
33
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
34
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
35
|
+
Requires-Python: >=3.8
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
License-File: LICENSE
|
|
38
|
+
Requires-Dist: Django>=3.2
|
|
39
|
+
Dynamic: license-file
|
|
40
|
+
|
|
41
|
+
# Coupons — Django Coupon & Discount Code System
|
|
42
|
+
|
|
43
|
+
[](https://pypi.org/project/coupons/)
|
|
44
|
+
[](https://pypi.org/project/coupons/)
|
|
45
|
+
[](https://www.djangoproject.com/)
|
|
46
|
+
[](LICENSE)
|
|
47
|
+
|
|
48
|
+
**Author:** [Nitesh Kumar Singh](https://github.com/nkscoder) · **GitHub:** [@nkscoder](https://github.com/nkscoder)
|
|
49
|
+
|
|
50
|
+
A reusable **Django coupons app** for managing promotional codes, discount rules, and coupon validation in Python web applications. Built by **Nitesh Kumar Singh (nkscoder)** for e-commerce, SaaS, and any Django project that needs flexible coupon functionality.
|
|
51
|
+
|
|
52
|
+
> **Keywords:** django coupons · python coupon system · discount code · promo code · coupon validation · nkscoder · nitesh kumar singh
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Features
|
|
57
|
+
|
|
58
|
+
- **Percentage or fixed-amount discounts** on any order total
|
|
59
|
+
- **User-specific or global coupons** — restrict codes to selected users or allow all
|
|
60
|
+
- **Usage limits** — max total uses, uses per user, or unlimited
|
|
61
|
+
- **Expiration & active/inactive rules** with datetime-based validity
|
|
62
|
+
- **Django Admin integration** with bulk actions (reset usage, delete expired)
|
|
63
|
+
- **Simple validation API** — one function call to check any coupon code
|
|
64
|
+
- **Configurable coupon code length** via `DSC_COUPON_CODE_LENGTH` setting
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Requirements
|
|
69
|
+
|
|
70
|
+
- Python 3.8+
|
|
71
|
+
- Django 3.2+
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Installation
|
|
76
|
+
|
|
77
|
+
### From PyPI (recommended)
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
pip install coupons
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### From GitHub
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pip install git+https://github.com/nkscoder/coupons.git
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Setup
|
|
92
|
+
|
|
93
|
+
### Step 1 — Add to `INSTALLED_APPS`
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
# settings.py
|
|
97
|
+
INSTALLED_APPS = [
|
|
98
|
+
...
|
|
99
|
+
"coupons",
|
|
100
|
+
]
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Step 2 — Run migrations
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
python manage.py migrate coupons
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Step 3 — Create coupons in Django Admin
|
|
110
|
+
|
|
111
|
+
Go to `/admin/` and create:
|
|
112
|
+
|
|
113
|
+
1. **Discount** — set value and type (percentage or fixed)
|
|
114
|
+
2. **Allowed Users Rule** — select users or enable "All users"
|
|
115
|
+
3. **Max Uses Rule** — set limits or enable infinite uses
|
|
116
|
+
4. **Validity Rule** — set expiration date and active status
|
|
117
|
+
5. **Ruleset** — link the three rules above
|
|
118
|
+
6. **Coupon** — assign discount and ruleset (code auto-generated)
|
|
119
|
+
|
|
120
|
+
### Step 4 (optional) — Custom coupon code length
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
# settings.py
|
|
124
|
+
DSC_COUPON_CODE_LENGTH = 16 # default is 12
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Usage
|
|
130
|
+
|
|
131
|
+
### Validate a coupon
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from coupons.validations import validate_coupon
|
|
135
|
+
|
|
136
|
+
coupon_code = "COUPONTEST01"
|
|
137
|
+
user = User.objects.get(username="nitesh")
|
|
138
|
+
|
|
139
|
+
status = validate_coupon(coupon_code=coupon_code, user=user)
|
|
140
|
+
# {'valid': True}
|
|
141
|
+
|
|
142
|
+
if status["valid"]:
|
|
143
|
+
print("Coupon is valid!")
|
|
144
|
+
else:
|
|
145
|
+
print(status["message"]) # e.g. "Coupon does not exist!"
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Apply a coupon (record usage)
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
from coupons.models import Coupon
|
|
152
|
+
|
|
153
|
+
coupon = Coupon.objects.get(code=coupon_code)
|
|
154
|
+
coupon.use_coupon(user=user)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Get discount details
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
coupon = Coupon.objects.get(code=coupon_code)
|
|
161
|
+
discount = coupon.get_discount()
|
|
162
|
+
# {'value': 50, 'is_percentage': True}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Calculate discounted price
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
discounted = coupon.get_discounted_value(initial_value=100.0)
|
|
169
|
+
# Returns 50.0 for 50% off, or 80.0 for $20 off
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Validation Rules
|
|
175
|
+
|
|
176
|
+
| Rule | Description |
|
|
177
|
+
|------|-------------|
|
|
178
|
+
| **Allowed Users** | Coupon valid only for selected users, or all users |
|
|
179
|
+
| **Max Uses** | Global usage cap, per-user limit, or infinite |
|
|
180
|
+
| **Validity** | Active flag + expiration datetime |
|
|
181
|
+
|
|
182
|
+
Invalid responses include a human-readable `message` key:
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
validate_coupon("DUMMYCODE", user)
|
|
186
|
+
# {'valid': False, 'message': 'Coupon does not exist!'}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## REST API Example
|
|
192
|
+
|
|
193
|
+
See [`examples/`](examples/) for a Django REST Framework integration sample (`ajax_views.py`, `ajax_urls.py`).
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Development
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
git clone git@github.com:nkscoder/coupons.git
|
|
201
|
+
cd coupons
|
|
202
|
+
pip install -e ".[dev]"
|
|
203
|
+
python manage.py migrate
|
|
204
|
+
python manage.py runserver
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Build & publish to PyPI
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
pip install build twine
|
|
211
|
+
python -m build
|
|
212
|
+
twine upload dist/*
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Or tag a release on GitHub — the included GitHub Action publishes automatically.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Changelog
|
|
220
|
+
|
|
221
|
+
### 1.1.0
|
|
222
|
+
- Modernized for Django 3.2–5.x and Python 3.8+
|
|
223
|
+
- Added PyPI packaging (`pyproject.toml`)
|
|
224
|
+
- Fixed per-coupon usage validation bug
|
|
225
|
+
- Fixed template mutation in validation responses
|
|
226
|
+
- SEO & documentation update by **Nitesh Kumar Singh (nkscoder)**
|
|
227
|
+
|
|
228
|
+
### 1.0.0
|
|
229
|
+
- Initial release
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## Author & Links
|
|
234
|
+
|
|
235
|
+
| | |
|
|
236
|
+
|---|---|
|
|
237
|
+
| **Author** | Nitesh Kumar Singh |
|
|
238
|
+
| **GitHub** | [github.com/nkscoder](https://github.com/nkscoder) |
|
|
239
|
+
| **Repository** | [github.com/nkscoder/coupons](https://github.com/nkscoder/coupons) |
|
|
240
|
+
| **PyPI** | [pypi.org/project/coupons](https://pypi.org/project/coupons) |
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## License
|
|
245
|
+
|
|
246
|
+
MIT License — Copyright (c) 2020-2026 [Nitesh Kumar Singh (nkscoder)](https://github.com/nkscoder)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
coupons/__init__.py
|
|
6
|
+
coupons/actions.py
|
|
7
|
+
coupons/admin.py
|
|
8
|
+
coupons/apps.py
|
|
9
|
+
coupons/helpers.py
|
|
10
|
+
coupons/models.py
|
|
11
|
+
coupons/tests.py
|
|
12
|
+
coupons/validations.py
|
|
13
|
+
coupons/views.py
|
|
14
|
+
coupons.egg-info/PKG-INFO
|
|
15
|
+
coupons.egg-info/SOURCES.txt
|
|
16
|
+
coupons.egg-info/dependency_links.txt
|
|
17
|
+
coupons.egg-info/requires.txt
|
|
18
|
+
coupons.egg-info/top_level.txt
|
|
19
|
+
coupons/migrations/0001_initial.py
|
|
20
|
+
coupons/migrations/__init__.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Django>=3.2
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
coupons
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "coupons"
|
|
7
|
+
version = "1.1.0"
|
|
8
|
+
description = "Django coupon and discount code system with validation rules — by Nitesh Kumar Singh (nkscoder)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Nitesh Kumar Singh", email = "nkscoder@gmail.com" },
|
|
13
|
+
]
|
|
14
|
+
maintainers = [
|
|
15
|
+
{ name = "Nitesh Kumar Singh", email = "nkscoder@gmail.com" },
|
|
16
|
+
]
|
|
17
|
+
keywords = [
|
|
18
|
+
"django",
|
|
19
|
+
"coupons",
|
|
20
|
+
"coupon",
|
|
21
|
+
"discount",
|
|
22
|
+
"promo-code",
|
|
23
|
+
"promotional-code",
|
|
24
|
+
"ecommerce",
|
|
25
|
+
"nkscoder",
|
|
26
|
+
"nitesh-kumar-singh",
|
|
27
|
+
"python",
|
|
28
|
+
"django-coupons",
|
|
29
|
+
"coupon-validation",
|
|
30
|
+
"discount-code",
|
|
31
|
+
]
|
|
32
|
+
classifiers = [
|
|
33
|
+
"Development Status :: 5 - Production/Stable",
|
|
34
|
+
"Environment :: Web Environment",
|
|
35
|
+
"Framework :: Django",
|
|
36
|
+
"Framework :: Django :: 3.2",
|
|
37
|
+
"Framework :: Django :: 4.0",
|
|
38
|
+
"Framework :: Django :: 4.1",
|
|
39
|
+
"Framework :: Django :: 4.2",
|
|
40
|
+
"Framework :: Django :: 5.0",
|
|
41
|
+
"Framework :: Django :: 5.1",
|
|
42
|
+
"Intended Audience :: Developers",
|
|
43
|
+
"License :: OSI Approved :: MIT License",
|
|
44
|
+
"Operating System :: OS Independent",
|
|
45
|
+
"Programming Language :: Python",
|
|
46
|
+
"Programming Language :: Python :: 3",
|
|
47
|
+
"Programming Language :: Python :: 3.8",
|
|
48
|
+
"Programming Language :: Python :: 3.9",
|
|
49
|
+
"Programming Language :: Python :: 3.10",
|
|
50
|
+
"Programming Language :: Python :: 3.11",
|
|
51
|
+
"Programming Language :: Python :: 3.12",
|
|
52
|
+
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
|
|
53
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
54
|
+
]
|
|
55
|
+
requires-python = ">=3.8"
|
|
56
|
+
dependencies = [
|
|
57
|
+
"Django>=3.2",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
[project.urls]
|
|
61
|
+
Homepage = "https://github.com/nkscoder/coupons"
|
|
62
|
+
Documentation = "https://github.com/nkscoder/coupons#readme"
|
|
63
|
+
Repository = "https://github.com/nkscoder/coupons"
|
|
64
|
+
"Bug Tracker" = "https://github.com/nkscoder/coupons/issues"
|
|
65
|
+
"Author GitHub" = "https://github.com/nkscoder"
|
|
66
|
+
|
|
67
|
+
[tool.setuptools.packages.find]
|
|
68
|
+
include = ["coupons*"]
|
|
69
|
+
|
|
70
|
+
[tool.setuptools.package-data]
|
|
71
|
+
coupons = ["migrations/*.py"]
|
coupons-1.1.0/setup.cfg
ADDED