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.
Files changed (87) hide show
  1. {paystack_django-2.0.0 → paystack_django-2.0.1}/CHANGELOG.md +4 -1
  2. {paystack_django-2.0.0 → paystack_django-2.0.1}/PKG-INFO +124 -80
  3. {paystack_django-2.0.0 → paystack_django-2.0.1}/README.md +123 -79
  4. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/__init__.py +1 -1
  5. {paystack_django-2.0.0 → paystack_django-2.0.1}/paystack_django.egg-info/PKG-INFO +124 -80
  6. {paystack_django-2.0.0 → paystack_django-2.0.1}/pyproject.toml +1 -1
  7. {paystack_django-2.0.0 → paystack_django-2.0.1}/CONTRIBUTING.md +0 -0
  8. {paystack_django-2.0.0 → paystack_django-2.0.1}/LICENSE +0 -0
  9. {paystack_django-2.0.0 → paystack_django-2.0.1}/MANIFEST.in +0 -0
  10. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/admin.py +0 -0
  11. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/__init__.py +0 -0
  12. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/apple_pay.py +0 -0
  13. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/base.py +0 -0
  14. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/bulk_charges.py +0 -0
  15. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/charge.py +0 -0
  16. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/customers.py +0 -0
  17. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/dedicated_accounts.py +0 -0
  18. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/direct_debit.py +0 -0
  19. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/disputes.py +0 -0
  20. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/integration.py +0 -0
  21. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/miscellaneous.py +0 -0
  22. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/order.py +0 -0
  23. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/pages.py +0 -0
  24. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/payment_requests.py +0 -0
  25. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/plans.py +0 -0
  26. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/products.py +0 -0
  27. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/refunds.py +0 -0
  28. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/settlements.py +0 -0
  29. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/splits.py +0 -0
  30. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/storefront.py +0 -0
  31. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/subaccounts.py +0 -0
  32. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/subscriptions.py +0 -0
  33. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/terminal.py +0 -0
  34. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/transactions.py +0 -0
  35. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/transfer_control.py +0 -0
  36. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/transfer_recipients.py +0 -0
  37. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/transfers.py +0 -0
  38. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/verification.py +0 -0
  39. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/api/virtual_terminal.py +0 -0
  40. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/apps.py +0 -0
  41. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/client.py +0 -0
  42. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/decorators.py +0 -0
  43. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/dev/__init__.py +0 -0
  44. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/dev/ngrok_tunnel.py +0 -0
  45. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/dev/webhook_tester.py +0 -0
  46. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/exceptions.py +0 -0
  47. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/management/__init__.py +0 -0
  48. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/management/commands/__init__.py +0 -0
  49. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/management/commands/cleanup_paystack_logs.py +0 -0
  50. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/management/commands/list_webhook_events.py +0 -0
  51. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/management/commands/start_webhook_tunnel.py +0 -0
  52. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/management/commands/sync_paystack_data.py +0 -0
  53. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/management/commands/test_webhook.py +0 -0
  54. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/management/commands/verify_paystack_config.py +0 -0
  55. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/middleware.py +0 -0
  56. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/migrations/0001_initial.py +0 -0
  57. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/migrations/0002_alter_paystacktransfer_status.py +0 -0
  58. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/migrations/__init__.py +0 -0
  59. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/models.py +0 -0
  60. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/py.typed +0 -0
  61. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/settings.py +0 -0
  62. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/signals.py +0 -0
  63. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/tests/__init__.py +0 -0
  64. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/tests/conftest.py +0 -0
  65. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/tests/settings.py +0 -0
  66. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/tests/test_api_compliance.py +0 -0
  67. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/tests/test_client.py +0 -0
  68. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/tests/test_customers.py +0 -0
  69. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/tests/test_models.py +0 -0
  70. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/tests/test_new_endpoints.py +0 -0
  71. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/tests/test_production_hardening.py +0 -0
  72. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/tests/test_security_remediation.py +0 -0
  73. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/tests/test_transactions.py +0 -0
  74. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/tests/test_utils.py +0 -0
  75. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/tests/test_webhooks.py +0 -0
  76. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/utils.py +0 -0
  77. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/views.py +0 -0
  78. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/webhooks/__init__.py +0 -0
  79. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/webhooks/events.py +0 -0
  80. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/webhooks/handlers.py +0 -0
  81. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/webhooks/urls.py +0 -0
  82. {paystack_django-2.0.0 → paystack_django-2.0.1}/djpaystack/webhooks/views.py +0 -0
  83. {paystack_django-2.0.0 → paystack_django-2.0.1}/paystack_django.egg-info/SOURCES.txt +0 -0
  84. {paystack_django-2.0.0 → paystack_django-2.0.1}/paystack_django.egg-info/dependency_links.txt +0 -0
  85. {paystack_django-2.0.0 → paystack_django-2.0.1}/paystack_django.egg-info/requires.txt +0 -0
  86. {paystack_django-2.0.0 → paystack_django-2.0.1}/paystack_django.egg-info/top_level.txt +0 -0
  87. {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
- ## [1.2.0] - 2026-06-13
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.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
  [![PyPI version](https://badge.fury.io/py/paystack-django.svg)](https://badge.fury.io/py/paystack-django)
65
- [![Django Versions](https://img.shields.io/badge/Django-3.2%2B-green)](https://www.djangoproject.com)
65
+ [![Django Versions](https://img.shields.io/badge/Django-4.2%2B-green)](https://www.djangoproject.com)
66
66
  [![Python Versions](https://img.shields.io/badge/Python-3.8%2B-blue)](https://www.python.org)
67
67
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
- - **Broad Paystack API Coverage** - Most Paystack endpoints across 25+ categories
72
- - **Django Models** - Pre-built models for transactions, customers, plans, and more
73
- - **Webhook Support** - Built-in webhook handling and HMAC-SHA512 signature verification (fails closed)
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.transaction.initialize(
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.transaction.verify(reference='unique-reference-123')
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
- path('webhooks/paystack/', webhook_views.handle_webhook, name='paystack_webhook'),
196
+ # Exposes the webhook endpoint at /paystack/webhook/
197
+ path('paystack/', include('djpaystack.webhooks.urls')),
191
198
  ]
192
199
  ```
193
200
 
194
- Then configure the webhook URL in your Paystack dashboard.
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.transaction.initialize(
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.transaction.verify(reference='unique-ref-001')
254
+ response = client.transactions.verify(reference='unique-ref-001')
247
255
 
248
- # List transactions
249
- response = client.transaction.list(page=1, per_page=10)
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.transaction.fetch(id=123456)
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.customer.create(
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.customer.list(page=1, per_page=50)
275
+ response = client.customers.list(page=1, per_page=50)
268
276
 
269
277
  # Fetch customer
270
- response = client.customer.fetch(customer_code='CUS_xxxxx')
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.subscription.create(
278
- customer_code='CUS_xxxxx',
279
- plan_code='PLN_xxxxx',
280
- authorization_code='AUTH_xxxxx'
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.subscription.enable(
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.subscription.disable(code='SUB_xxxxx')
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.plan.create(
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
- plan_code='PLN_custom'
309
+ description='Premium monthly subscription'
303
310
  )
304
311
 
305
312
  # List plans
306
- response = client.plan.list(page=1)
313
+ response = client.plans.list(page=1)
307
314
 
308
315
  # Fetch plan
309
- response = client.plan.fetch(plan_id=123)
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.transfer_recipient.create(
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.transfer.initiate(
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.transfer.finalize(transfer_code='TRF_xxxxx', otp='123456')
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.refund.create(
346
+ response = client.refunds.create(
340
347
  transaction='123456'
341
348
  )
342
349
 
343
350
  # List refunds
344
- response = client.refund.list(page=1)
351
+ response = client.refunds.list(page=1)
345
352
 
346
353
  # Fetch refund
347
- response = client.refund.fetch(refund_id='123')
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
- PaystackProduct,
360
- PaystackRefund,
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
- # Create a payment request
372
- from djpaystack.models import PaymentRequest
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 sent
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
- @receiver(transaction_verified)
391
- def on_payment_success(sender, transaction, **kwargs):
392
- print(f"Payment successful: {transaction.reference}")
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(transaction_failed)
396
- def on_payment_failed(sender, transaction, **kwargs):
397
- print(f"Payment failed: {transaction.reference}")
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
- | Package Version | Django 3.2 | Django 4.0 | Django 4.1 | Django 4.2 | Django 5.0 | Django 5.2 | Django 6.0 |
425
- | --------------- | ---------- | ---------- | ---------- | ---------- | ---------- | ---------- | ---------- |
426
- | 1.0.x | ✅ | | ✅ | ✅ | | | |
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
- Then use `python-decouple` to load them:
491
+ Load them however you prefer — for example with the standard library:
451
492
 
452
493
  ```python
453
- from decouple import config
494
+ import os
454
495
 
455
496
  PAYSTACK = {
456
- 'SECRET_KEY': config('PAYSTACK_SECRET_KEY'),
457
- 'PUBLIC_KEY': config('PAYSTACK_PUBLIC_KEY'),
458
- 'WEBHOOK_SECRET': config('PAYSTACK_WEBHOOK_SECRET'),
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.transaction.verify(reference='ref-123')
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.0.x?** Previously `list()` eagerly fetched *all* pages.
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': config('PAYSTACK_SECRET_KEY'),
546
- 'PUBLIC_KEY': config('PAYSTACK_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
- Verify all incoming webhooks:
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.webhooks.handlers import verify_webhook_signature
599
+ from djpaystack.utils import verify_webhook_signature
556
600
 
557
601
  is_valid = verify_webhook_signature(
558
- body=request.body,
559
- signature_header=request.META.get('HTTP_X_PAYSTACK_SIGNATURE'),
560
- webhook_secret=PAYSTACK['WEBHOOK_SECRET']
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: