paystack-django 2.0.0__tar.gz → 2.0.1__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.
- {paystack_django-2.0.0 → paystack_django-2.0.1}/CHANGELOG.md +4 -1
- {paystack_django-2.0.0 → paystack_django-2.0.1}/PKG-INFO +124 -80
- {paystack_django-2.0.0 → paystack_django-2.0.1}/README.md +123 -79
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/__init__.py +1 -1
- {paystack_django-2.0.0 → paystack_django-2.0.1}/paystack_django.egg-info/PKG-INFO +124 -80
- {paystack_django-2.0.0 → paystack_django-2.0.1}/pyproject.toml +1 -1
- {paystack_django-2.0.0 → paystack_django-2.0.1}/CONTRIBUTING.md +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/LICENSE +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/MANIFEST.in +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/admin.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/__init__.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/apple_pay.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/base.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/bulk_charges.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/charge.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/customers.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/dedicated_accounts.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/direct_debit.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/disputes.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/integration.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/miscellaneous.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/order.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/pages.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/payment_requests.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/plans.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/products.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/refunds.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/settlements.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/splits.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/storefront.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/subaccounts.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/subscriptions.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/terminal.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/transactions.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/transfer_control.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/transfer_recipients.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/transfers.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/verification.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/virtual_terminal.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/apps.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/client.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/decorators.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/dev/__init__.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/dev/ngrok_tunnel.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/dev/webhook_tester.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/exceptions.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/management/__init__.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/management/commands/__init__.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/management/commands/cleanup_paystack_logs.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/management/commands/list_webhook_events.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/management/commands/start_webhook_tunnel.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/management/commands/sync_paystack_data.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/management/commands/test_webhook.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/management/commands/verify_paystack_config.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/middleware.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/migrations/0001_initial.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/migrations/0002_alter_paystacktransfer_status.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/migrations/__init__.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/models.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/py.typed +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/settings.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/signals.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/tests/__init__.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/tests/conftest.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/tests/settings.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/tests/test_api_compliance.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/tests/test_client.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/tests/test_customers.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/tests/test_models.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/tests/test_new_endpoints.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/tests/test_production_hardening.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/tests/test_security_remediation.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/tests/test_transactions.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/tests/test_utils.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/tests/test_webhooks.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/utils.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/views.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/webhooks/__init__.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/webhooks/events.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/webhooks/handlers.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/webhooks/urls.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/webhooks/views.py +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/paystack_django.egg-info/SOURCES.txt +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/paystack_django.egg-info/dependency_links.txt +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/paystack_django.egg-info/requires.txt +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/paystack_django.egg-info/top_level.txt +0 -0
- {paystack_django-2.0.0 → paystack_django-2.0.1}/setup.cfg +0 -0
|
@@ -5,7 +5,10 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [
|
|
8
|
+
## [2.0.0] - 2026-06-13
|
|
9
|
+
|
|
10
|
+
This is a major release: it completes Paystack API coverage and includes
|
|
11
|
+
behavioural **breaking changes** (see the *Breaking changes* section below).
|
|
9
12
|
|
|
10
13
|
### Added
|
|
11
14
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: paystack-django
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.1
|
|
4
4
|
Summary: A comprehensive Django integration for Paystack Payment Gateway
|
|
5
5
|
Author-email: Humming Byte <dev@hummingbyte.org>
|
|
6
6
|
License: MIT
|
|
@@ -62,19 +62,25 @@ Dynamic: license-file
|
|
|
62
62
|
A comprehensive Django integration for the **Paystack Payment Gateway**. This package provides a complete, production-ready solution for integrating Paystack payments into your Django applications.
|
|
63
63
|
|
|
64
64
|
[](https://badge.fury.io/py/paystack-django)
|
|
65
|
-
[](https://www.djangoproject.com)
|
|
66
66
|
[](https://www.python.org)
|
|
67
67
|
[](https://opensource.org/licenses/MIT)
|
|
68
68
|
|
|
69
|
+
> **New in 2.0** — full coverage of the Paystack API (including Virtual Terminal,
|
|
70
|
+
> Direct Debit, Orders and Storefronts), memory-safe pagination with lazy
|
|
71
|
+
> `iter_all()` iterators, race-safe webhook deduplication, and a fail-closed
|
|
72
|
+
> webhook verification model. See the [CHANGELOG](CHANGELOG.md) for the full
|
|
73
|
+
> list, including **breaking changes**.
|
|
74
|
+
|
|
69
75
|
## Features
|
|
70
76
|
|
|
71
|
-
- **
|
|
72
|
-
- **Django Models** - Pre-built models for transactions, customers, plans, and
|
|
73
|
-
- **Webhook Support** - Built-in webhook handling
|
|
74
|
-
- **Signal Support** - Django signals for payment events
|
|
77
|
+
- **Full Paystack API Coverage** - Django-native clients for every Paystack API category
|
|
78
|
+
- **Django Models** - Pre-built models for transactions, customers, plans, subscriptions, transfers, and webhook events
|
|
79
|
+
- **Webhook Support** - Built-in webhook handling, HMAC-SHA512 signature verification (fails closed), and race-safe deduplication
|
|
80
|
+
- **Signal Support** - Django signals for payment, transfer, refund, subscription, and dispute events
|
|
81
|
+
- **Memory-safe Pagination** - Single-page `list()` plus lazy `iter_all()` iterators
|
|
75
82
|
- **Type Hints** - Typed public interface with a shipped `py.typed` marker
|
|
76
83
|
- **Comprehensive Documentation** - Detailed docs and examples
|
|
77
|
-
- **Production Ready** - Used in production by multiple companies
|
|
78
84
|
|
|
79
85
|
## Supported Services
|
|
80
86
|
|
|
@@ -141,13 +147,14 @@ INSTALLED_APPS = [
|
|
|
141
147
|
PAYSTACK = {
|
|
142
148
|
'SECRET_KEY': 'sk_live_your_secret_key_here',
|
|
143
149
|
'PUBLIC_KEY': 'pk_live_your_public_key_here',
|
|
144
|
-
# Paystack signs webhooks with your API SECRET KEY (the same sk_... value).
|
|
145
|
-
# Set this to that key; if left unset, webhooks are REJECTED (fail closed).
|
|
146
|
-
'WEBHOOK_SECRET': 'sk_live_your_secret_key_here',
|
|
147
150
|
'ENVIRONMENT': 'production', # or 'test'
|
|
148
151
|
}
|
|
149
152
|
```
|
|
150
153
|
|
|
154
|
+
> Paystack signs webhooks with your account **secret key**, so `WEBHOOK_SECRET`
|
|
155
|
+
> is optional and defaults to `SECRET_KEY`. Webhooks are **rejected** if no
|
|
156
|
+
> signing key can be resolved (fail closed).
|
|
157
|
+
|
|
151
158
|
### 2. Create PaystackClient Instance
|
|
152
159
|
|
|
153
160
|
```python
|
|
@@ -156,7 +163,7 @@ from djpaystack import PaystackClient
|
|
|
156
163
|
client = PaystackClient()
|
|
157
164
|
|
|
158
165
|
# Initialize a transaction
|
|
159
|
-
response = client.
|
|
166
|
+
response = client.transactions.initialize(
|
|
160
167
|
email='customer@example.com',
|
|
161
168
|
amount=50000, # in kobo (500 NGN)
|
|
162
169
|
reference='unique-reference-123'
|
|
@@ -170,7 +177,7 @@ print(f"Redirect user to: {authorization_url}")
|
|
|
170
177
|
|
|
171
178
|
```python
|
|
172
179
|
# After user completes payment
|
|
173
|
-
verified = client.
|
|
180
|
+
verified = client.transactions.verify(reference='unique-reference-123')
|
|
174
181
|
|
|
175
182
|
if verified['data']['status'] == 'success':
|
|
176
183
|
print("Payment successful!")
|
|
@@ -183,15 +190,16 @@ else:
|
|
|
183
190
|
|
|
184
191
|
```python
|
|
185
192
|
# urls.py
|
|
186
|
-
from django.urls import path
|
|
187
|
-
from djpaystack.webhooks import views as webhook_views
|
|
193
|
+
from django.urls import include, path
|
|
188
194
|
|
|
189
195
|
urlpatterns = [
|
|
190
|
-
|
|
196
|
+
# Exposes the webhook endpoint at /paystack/webhook/
|
|
197
|
+
path('paystack/', include('djpaystack.webhooks.urls')),
|
|
191
198
|
]
|
|
192
199
|
```
|
|
193
200
|
|
|
194
|
-
Then
|
|
201
|
+
Then set the webhook URL (e.g. `https://yoursite.com/paystack/webhook/`) in your
|
|
202
|
+
Paystack dashboard.
|
|
195
203
|
|
|
196
204
|
## Configuration
|
|
197
205
|
|
|
@@ -235,7 +243,7 @@ from djpaystack import PaystackClient
|
|
|
235
243
|
client = PaystackClient()
|
|
236
244
|
|
|
237
245
|
# Initialize transaction
|
|
238
|
-
response = client.
|
|
246
|
+
response = client.transactions.initialize(
|
|
239
247
|
email='user@example.com',
|
|
240
248
|
amount=100000,
|
|
241
249
|
reference='unique-ref-001',
|
|
@@ -243,20 +251,20 @@ response = client.transaction.initialize(
|
|
|
243
251
|
)
|
|
244
252
|
|
|
245
253
|
# Verify transaction
|
|
246
|
-
response = client.
|
|
254
|
+
response = client.transactions.verify(reference='unique-ref-001')
|
|
247
255
|
|
|
248
|
-
# List transactions
|
|
249
|
-
response = client.
|
|
256
|
+
# List transactions (one page; use iter_all() to stream everything)
|
|
257
|
+
response = client.transactions.list(page=1, per_page=10)
|
|
250
258
|
|
|
251
259
|
# Fetch transaction
|
|
252
|
-
response = client.
|
|
260
|
+
response = client.transactions.fetch(id_or_reference=123456)
|
|
253
261
|
```
|
|
254
262
|
|
|
255
263
|
### Customers
|
|
256
264
|
|
|
257
265
|
```python
|
|
258
266
|
# Create customer
|
|
259
|
-
response = client.
|
|
267
|
+
response = client.customers.create(
|
|
260
268
|
email='customer@example.com',
|
|
261
269
|
first_name='John',
|
|
262
270
|
last_name='Doe',
|
|
@@ -264,56 +272,55 @@ response = client.customer.create(
|
|
|
264
272
|
)
|
|
265
273
|
|
|
266
274
|
# List customers
|
|
267
|
-
response = client.
|
|
275
|
+
response = client.customers.list(page=1, per_page=50)
|
|
268
276
|
|
|
269
277
|
# Fetch customer
|
|
270
|
-
response = client.
|
|
278
|
+
response = client.customers.fetch(email_or_code='CUS_xxxxx')
|
|
271
279
|
```
|
|
272
280
|
|
|
273
281
|
### Subscriptions
|
|
274
282
|
|
|
275
283
|
```python
|
|
276
284
|
# Create subscription
|
|
277
|
-
response = client.
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
285
|
+
response = client.subscriptions.create(
|
|
286
|
+
customer='CUS_xxxxx',
|
|
287
|
+
plan='PLN_xxxxx',
|
|
288
|
+
authorization='AUTH_xxxxx'
|
|
281
289
|
)
|
|
282
290
|
|
|
283
291
|
# Enable subscription
|
|
284
|
-
response = client.
|
|
292
|
+
response = client.subscriptions.enable(
|
|
285
293
|
code='SUB_xxxxx',
|
|
286
294
|
token='tok_xxxxx'
|
|
287
295
|
)
|
|
288
296
|
|
|
289
297
|
# Disable subscription
|
|
290
|
-
response = client.
|
|
298
|
+
response = client.subscriptions.disable(code='SUB_xxxxx')
|
|
291
299
|
```
|
|
292
300
|
|
|
293
301
|
### Plans
|
|
294
302
|
|
|
295
303
|
```python
|
|
296
304
|
# Create plan
|
|
297
|
-
response = client.
|
|
305
|
+
response = client.plans.create(
|
|
298
306
|
name='Monthly Plan',
|
|
299
|
-
description='Premium monthly subscription',
|
|
300
307
|
amount=500000, # 5000 NGN
|
|
301
308
|
interval='monthly',
|
|
302
|
-
|
|
309
|
+
description='Premium monthly subscription'
|
|
303
310
|
)
|
|
304
311
|
|
|
305
312
|
# List plans
|
|
306
|
-
response = client.
|
|
313
|
+
response = client.plans.list(page=1)
|
|
307
314
|
|
|
308
315
|
# Fetch plan
|
|
309
|
-
response = client.
|
|
316
|
+
response = client.plans.fetch(id_or_code='PLN_xxxxx')
|
|
310
317
|
```
|
|
311
318
|
|
|
312
319
|
### Transfers
|
|
313
320
|
|
|
314
321
|
```python
|
|
315
322
|
# Create transfer recipient
|
|
316
|
-
response = client.
|
|
323
|
+
response = client.transfer_recipients.create(
|
|
317
324
|
type='nuban',
|
|
318
325
|
name='John Doe',
|
|
319
326
|
account_number='0000000000',
|
|
@@ -321,7 +328,7 @@ response = client.transfer_recipient.create(
|
|
|
321
328
|
)
|
|
322
329
|
|
|
323
330
|
# Initiate transfer
|
|
324
|
-
response = client.
|
|
331
|
+
response = client.transfers.initiate(
|
|
325
332
|
source='balance',
|
|
326
333
|
amount=50000,
|
|
327
334
|
recipient='RCP_xxxxx',
|
|
@@ -329,22 +336,42 @@ response = client.transfer.initiate(
|
|
|
329
336
|
)
|
|
330
337
|
|
|
331
338
|
# Finalize transfer
|
|
332
|
-
response = client.
|
|
339
|
+
response = client.transfers.finalize(transfer_code='TRF_xxxxx', otp='123456')
|
|
333
340
|
```
|
|
334
341
|
|
|
335
342
|
### Refunds
|
|
336
343
|
|
|
337
344
|
```python
|
|
338
345
|
# Create refund
|
|
339
|
-
response = client.
|
|
346
|
+
response = client.refunds.create(
|
|
340
347
|
transaction='123456'
|
|
341
348
|
)
|
|
342
349
|
|
|
343
350
|
# List refunds
|
|
344
|
-
response = client.
|
|
351
|
+
response = client.refunds.list(page=1)
|
|
345
352
|
|
|
346
353
|
# Fetch refund
|
|
347
|
-
response = client.
|
|
354
|
+
response = client.refunds.fetch(reference='123456')
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### Orders, Storefronts & Virtual Terminal (new in 2.0)
|
|
358
|
+
|
|
359
|
+
```python
|
|
360
|
+
# Create a virtual terminal
|
|
361
|
+
client.virtual_terminal.create(
|
|
362
|
+
name='In-store till',
|
|
363
|
+
destinations=[{'target': '+2348000000000', 'name': 'Sales'}],
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
# Create a storefront and publish it
|
|
367
|
+
sf = client.storefront.create(name='My Shop', slug='my-shop', currency='NGN')
|
|
368
|
+
client.storefront.publish(sf['data']['id'])
|
|
369
|
+
|
|
370
|
+
# Customer direct-debit onboarding
|
|
371
|
+
init = client.customers.initialize_authorization(
|
|
372
|
+
email='customer@example.com', channel='direct_debit',
|
|
373
|
+
)
|
|
374
|
+
client.customers.verify_authorization(init['data']['reference'])
|
|
348
375
|
```
|
|
349
376
|
|
|
350
377
|
## Database Models
|
|
@@ -356,8 +383,9 @@ from djpaystack.models import (
|
|
|
356
383
|
PaystackTransaction,
|
|
357
384
|
PaystackCustomer,
|
|
358
385
|
PaystackPlan,
|
|
359
|
-
|
|
360
|
-
|
|
386
|
+
PaystackSubscription,
|
|
387
|
+
PaystackTransfer,
|
|
388
|
+
PaystackWebhookEvent,
|
|
361
389
|
)
|
|
362
390
|
|
|
363
391
|
# Query transactions
|
|
@@ -368,36 +396,48 @@ customer_transactions = PaystackTransaction.objects.filter(
|
|
|
368
396
|
customer_email='user@example.com'
|
|
369
397
|
)
|
|
370
398
|
|
|
371
|
-
#
|
|
372
|
-
|
|
373
|
-
request = PaymentRequest.objects.create(
|
|
374
|
-
reference='req-001',
|
|
375
|
-
amount=100000,
|
|
376
|
-
description='Course enrollment'
|
|
377
|
-
)
|
|
399
|
+
# Inspect stored webhook events
|
|
400
|
+
events = PaystackWebhookEvent.objects.filter(event_type='charge.success')
|
|
378
401
|
```
|
|
379
402
|
|
|
403
|
+
> Persistence is controlled by the `ENABLE_MODELS` setting (default `True`).
|
|
404
|
+
> Webhook handlers populate these models automatically.
|
|
405
|
+
|
|
380
406
|
## Webhooks
|
|
381
407
|
|
|
382
408
|
Handle Paystack webhooks automatically:
|
|
383
409
|
|
|
384
410
|
```python
|
|
385
|
-
# Webhook signals are automatically
|
|
386
|
-
from djpaystack.signals import transaction_verified, transaction_failed
|
|
387
|
-
|
|
411
|
+
# Webhook signals are dispatched automatically as events arrive
|
|
388
412
|
from django.dispatch import receiver
|
|
389
413
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
414
|
+
from djpaystack.signals import (
|
|
415
|
+
paystack_payment_successful,
|
|
416
|
+
paystack_payment_failed,
|
|
417
|
+
paystack_transfer_successful,
|
|
418
|
+
paystack_refund_processed,
|
|
419
|
+
paystack_dispute_created,
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
@receiver(paystack_payment_successful)
|
|
423
|
+
def on_payment_success(sender, transaction_data, **kwargs):
|
|
424
|
+
print(f"Payment successful: {transaction_data['reference']}")
|
|
393
425
|
# Update your application
|
|
394
426
|
|
|
395
|
-
@receiver(
|
|
396
|
-
def on_payment_failed(sender,
|
|
397
|
-
print(f"Payment failed: {
|
|
427
|
+
@receiver(paystack_payment_failed)
|
|
428
|
+
def on_payment_failed(sender, transaction_data, **kwargs):
|
|
429
|
+
print(f"Payment failed: {transaction_data['reference']}")
|
|
398
430
|
# Handle failed payment
|
|
399
431
|
```
|
|
400
432
|
|
|
433
|
+
Available signals: `paystack_payment_successful`, `paystack_payment_failed`,
|
|
434
|
+
`paystack_subscription_created`, `paystack_subscription_cancelled`,
|
|
435
|
+
`paystack_transfer_successful`, `paystack_transfer_failed`,
|
|
436
|
+
`paystack_refund_processed`, `paystack_dispute_created`,
|
|
437
|
+
`paystack_dispute_resolved`. Each receiver is called with a keyword argument
|
|
438
|
+
carrying the event payload (e.g. `transaction_data`, `transfer_data`,
|
|
439
|
+
`refund_data`, `dispute_data`).
|
|
440
|
+
|
|
401
441
|
## Testing
|
|
402
442
|
|
|
403
443
|
Run the test suite:
|
|
@@ -421,9 +461,11 @@ tox
|
|
|
421
461
|
|
|
422
462
|
## Django Compatibility
|
|
423
463
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
|
464
|
+
The 2.x line is tested against the current and LTS Django releases:
|
|
465
|
+
|
|
466
|
+
| Package Version | Django 4.2 (LTS) | Django 5.2 (LTS) | Django 6.0 |
|
|
467
|
+
| --------------- | ---------------- | ---------------- | ---------- |
|
|
468
|
+
| 2.0.x | ✅ | ✅ | ✅ |
|
|
427
469
|
|
|
428
470
|
## Python Compatibility
|
|
429
471
|
|
|
@@ -433,7 +475,6 @@ tox
|
|
|
433
475
|
- Python 3.11
|
|
434
476
|
- Python 3.12
|
|
435
477
|
- Python 3.13
|
|
436
|
-
- Python 3.14
|
|
437
478
|
|
|
438
479
|
## Environment Variables
|
|
439
480
|
|
|
@@ -447,19 +488,21 @@ PAYSTACK_WEBHOOK_SECRET=sk_live_xxx
|
|
|
447
488
|
PAYSTACK_ENVIRONMENT=production
|
|
448
489
|
```
|
|
449
490
|
|
|
450
|
-
|
|
491
|
+
Load them however you prefer — for example with the standard library:
|
|
451
492
|
|
|
452
493
|
```python
|
|
453
|
-
|
|
494
|
+
import os
|
|
454
495
|
|
|
455
496
|
PAYSTACK = {
|
|
456
|
-
'SECRET_KEY':
|
|
457
|
-
'PUBLIC_KEY':
|
|
458
|
-
'
|
|
459
|
-
'ENVIRONMENT': config('PAYSTACK_ENVIRONMENT', default='test'),
|
|
497
|
+
'SECRET_KEY': os.environ['PAYSTACK_SECRET_KEY'],
|
|
498
|
+
'PUBLIC_KEY': os.environ['PAYSTACK_PUBLIC_KEY'],
|
|
499
|
+
'ENVIRONMENT': os.environ.get('PAYSTACK_ENVIRONMENT', 'test'),
|
|
460
500
|
}
|
|
461
501
|
```
|
|
462
502
|
|
|
503
|
+
> `python-decouple` is **not** a dependency of this package. If you prefer
|
|
504
|
+
> `decouple.config(...)`, install it in your own project.
|
|
505
|
+
|
|
463
506
|
## Error Handling
|
|
464
507
|
|
|
465
508
|
The package provides specific exception classes:
|
|
@@ -474,7 +517,7 @@ from djpaystack.exceptions import (
|
|
|
474
517
|
)
|
|
475
518
|
|
|
476
519
|
try:
|
|
477
|
-
client.
|
|
520
|
+
client.transactions.verify(reference='ref-123')
|
|
478
521
|
except PaystackAuthenticationError:
|
|
479
522
|
print("Invalid API credentials")
|
|
480
523
|
except PaystackNetworkError:
|
|
@@ -509,7 +552,7 @@ for txn in client.transactions.iter_all(status='success', from_date='2024-01-01'
|
|
|
509
552
|
process(txn) # one record at a time; pages fetched on demand
|
|
510
553
|
```
|
|
511
554
|
|
|
512
|
-
> **Upgrading from 1.
|
|
555
|
+
> **Upgrading from 1.x?** Previously `list()` eagerly fetched *all* pages.
|
|
513
556
|
> It now returns one page — switch full scans to `iter_all()`. See the
|
|
514
557
|
> [CHANGELOG](CHANGELOG.md) for the full list of breaking changes.
|
|
515
558
|
|
|
@@ -535,29 +578,30 @@ logger = logging.getLogger('djpaystack')
|
|
|
535
578
|
|
|
536
579
|
### Environment Variables
|
|
537
580
|
|
|
538
|
-
Never hardcode secrets:
|
|
581
|
+
Never hardcode secrets — load them from the environment:
|
|
539
582
|
|
|
540
583
|
```python
|
|
541
584
|
import os
|
|
542
|
-
from decouple import config
|
|
543
585
|
|
|
544
586
|
PAYSTACK = {
|
|
545
|
-
'SECRET_KEY':
|
|
546
|
-
'PUBLIC_KEY':
|
|
587
|
+
'SECRET_KEY': os.environ['PAYSTACK_SECRET_KEY'],
|
|
588
|
+
'PUBLIC_KEY': os.environ['PAYSTACK_PUBLIC_KEY'],
|
|
547
589
|
}
|
|
548
590
|
```
|
|
549
591
|
|
|
550
592
|
### Webhook Verification
|
|
551
593
|
|
|
552
|
-
|
|
594
|
+
The built-in `PaystackWebhookView` verifies the HMAC-SHA512 signature on every
|
|
595
|
+
request and **rejects** anything it cannot verify (fail closed), so you normally
|
|
596
|
+
don't need to verify manually. If you handle webhooks yourself, use the helper:
|
|
553
597
|
|
|
554
598
|
```python
|
|
555
|
-
from djpaystack.
|
|
599
|
+
from djpaystack.utils import verify_webhook_signature
|
|
556
600
|
|
|
557
601
|
is_valid = verify_webhook_signature(
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
602
|
+
request.body, # payload (bytes)
|
|
603
|
+
request.headers.get('X-Paystack-Signature', ''), # signature
|
|
604
|
+
settings.PAYSTACK['SECRET_KEY'], # secret (the signing key)
|
|
561
605
|
)
|
|
562
606
|
|
|
563
607
|
if not is_valid:
|