smartpaystack 0.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.
- smartpaystack-0.1.0/PKG-INFO +253 -0
- smartpaystack-0.1.0/README.md +233 -0
- smartpaystack-0.1.0/calculator.py +37 -0
- smartpaystack-0.1.0/client.py +127 -0
- smartpaystack-0.1.0/config.py +30 -0
- smartpaystack-0.1.0/enums.py +21 -0
- smartpaystack-0.1.0/exceptions.py +11 -0
- smartpaystack-0.1.0/pyproject.toml +30 -0
- smartpaystack-0.1.0/setup.cfg +4 -0
- smartpaystack-0.1.0/smartpaystack.egg-info/PKG-INFO +253 -0
- smartpaystack-0.1.0/smartpaystack.egg-info/SOURCES.txt +13 -0
- smartpaystack-0.1.0/smartpaystack.egg-info/dependency_links.txt +1 -0
- smartpaystack-0.1.0/smartpaystack.egg-info/requires.txt +1 -0
- smartpaystack-0.1.0/smartpaystack.egg-info/top_level.txt +6 -0
- smartpaystack-0.1.0/webhooks.py +26 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: smartpaystack
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A smart, strategy-based, multi-currency Paystack SDK for Python.
|
|
5
|
+
Author-email: Fidelis Chukwunyere <fidelchukwunyere@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/fidel-c/smartpaystack
|
|
8
|
+
Keywords: paystack,payments,fintech,africa,api,wrapper
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Requires-Python: >=3.8
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: requests>=2.25.1
|
|
20
|
+
|
|
21
|
+
```markdown
|
|
22
|
+
# SmartPaystack
|
|
23
|
+
|
|
24
|
+
A smart, framework-agnostic, strategy-based Paystack integration for Python.
|
|
25
|
+
|
|
26
|
+
Stop writing manual math logic to calculate Paystack fees. Just declare your strategy (`ABSORB`, `PASS`, `SPLIT`) and let the package do the rest. Works seamlessly with **FastAPI, Flask, Django, Tornado, or plain Python scripts.**
|
|
27
|
+
|
|
28
|
+
## ✨ Features
|
|
29
|
+
* **Zero-Math API:** No more Kobo/Cents conversions. Pass native amounts (e.g., `5000` NGN).
|
|
30
|
+
* **Smart Fee Strategies:** Easily absorb fees, pass them to the customer, or split them.
|
|
31
|
+
* **Multi-Currency Routing:** Automatically applies the correct fee caps and percentages for NGN, GHS, ZAR, KES, and USD.
|
|
32
|
+
* **Framework Agnostic Webhooks:** Built-in HMAC SHA512 verifier that works with any web framework.
|
|
33
|
+
* **Fully Typed:** Sweet IDE auto-completion.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 📦 Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install smartpaystack
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Quickstart
|
|
47
|
+
|
|
48
|
+
### 1. Initialization
|
|
49
|
+
|
|
50
|
+
You can either pass your secret key directly or set it as an environment variable (`PAYSTACK_SECRET_KEY`).
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import os
|
|
54
|
+
from smartpaystack import SmartPaystack
|
|
55
|
+
|
|
56
|
+
# Option A: Uses the PAYSTACK_SECRET_KEY environment variable
|
|
57
|
+
os.environ["PAYSTACK_SECRET_KEY"] = "sk_live_xxxxxx"
|
|
58
|
+
client = SmartPaystack()
|
|
59
|
+
|
|
60
|
+
# Option B: Pass it explicitly
|
|
61
|
+
client = SmartPaystack(secret_key="sk_live_xxxxxx")
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### 2. Collecting Money (Charges)
|
|
66
|
+
|
|
67
|
+
Stop worrying about fee math. Tell the client how much you want, and who pays the fee.
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from smartpaystack import ChargeStrategy, Currency
|
|
71
|
+
|
|
72
|
+
# Scenario A: You want exactly ₦50,000. Customer pays the Paystack fee.
|
|
73
|
+
response = client.create_charge(
|
|
74
|
+
email="customer@email.com",
|
|
75
|
+
amount=50000,
|
|
76
|
+
currency=Currency.NGN,
|
|
77
|
+
charge_strategy=ChargeStrategy.PASS
|
|
78
|
+
)
|
|
79
|
+
print(response["authorization_url"])
|
|
80
|
+
|
|
81
|
+
# Scenario B: You absorb the fee for a Ghana Cedi transaction.
|
|
82
|
+
response = client.create_charge(
|
|
83
|
+
email="ghana@email.com",
|
|
84
|
+
amount=1000,
|
|
85
|
+
currency=Currency.GHS,
|
|
86
|
+
charge_strategy=ChargeStrategy.ABSORB
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Scenario C: You split the Paystack fee 50/50 with the customer.
|
|
90
|
+
# If the fee is ₦150, the customer is charged ₦10,075 and you receive ₦9,925.
|
|
91
|
+
response = client.create_charge(
|
|
92
|
+
email="split@email.com",
|
|
93
|
+
amount=10000,
|
|
94
|
+
currency=Currency.NGN,
|
|
95
|
+
charge_strategy=ChargeStrategy.SPLIT,
|
|
96
|
+
split_ratio=0.5 # The percentage of the fee the customer pays (0.5 = 50%)
|
|
97
|
+
)
|
|
98
|
+
print(response["authorization_url"])
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 3. Sending Money (Transfers)
|
|
103
|
+
|
|
104
|
+
Sending money is a two-step process: create a recipient, then initiate the transfer.
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
# 1. Resolve the account (Optional but recommended)
|
|
108
|
+
account = client.resolve_account_number(account_number="0123456789", bank_code="033")
|
|
109
|
+
print(f"Resolved Name: {account['account_name']}")
|
|
110
|
+
|
|
111
|
+
# 2. Create the recipient
|
|
112
|
+
recipient = client.create_transfer_recipient(
|
|
113
|
+
name=account["account_name"],
|
|
114
|
+
account_number="0123456789",
|
|
115
|
+
bank_code="033"
|
|
116
|
+
)
|
|
117
|
+
recipient_code = recipient["recipient_code"]
|
|
118
|
+
|
|
119
|
+
# 3. Send the money (e.g., Send ₦10,500)
|
|
120
|
+
transfer = client.initiate_transfer(
|
|
121
|
+
amount=10500,
|
|
122
|
+
recipient_code=recipient_code,
|
|
123
|
+
reason="Monthly Payout"
|
|
124
|
+
)
|
|
125
|
+
print(f"Transfer Status: {transfer['status']}")
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## 🛡️ Error Handling
|
|
132
|
+
|
|
133
|
+
When building fintech applications, you must handle failures gracefully. `smartpaystack` provides specific exceptions so you can catch exactly what went wrong.
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from smartpaystack import SmartPaystack
|
|
137
|
+
from smartpaystack.exceptions import PaystackAPIError, PaystackError
|
|
138
|
+
|
|
139
|
+
client = SmartPaystack()
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
account = client.resolve_account_number("invalid_number", "033")
|
|
143
|
+
except PaystackAPIError as e:
|
|
144
|
+
# Raised when Paystack returns a 400/500 response, or network fails
|
|
145
|
+
print(f"Paystack API failed: {str(e)}")
|
|
146
|
+
# Example Output: Paystack API failed: Could not resolve account name.
|
|
147
|
+
except PaystackError as e:
|
|
148
|
+
# A generic fallback for any other package-related error
|
|
149
|
+
print(f"An unexpected error occurred: {str(e)}")
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**Available Exceptions (from `smartpaystack.exceptions`):**
|
|
154
|
+
|
|
155
|
+
* `PaystackError`: The base class for all package exceptions.
|
|
156
|
+
* `PaystackAPIError`: Raised when the HTTP request to Paystack fails or returns an error message.
|
|
157
|
+
* `WebhookVerificationError`: Raised when the HMAC signature on an incoming webhook is invalid or missing.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## 📡 Verifying Webhooks
|
|
162
|
+
|
|
163
|
+
Paystack sends webhooks to your server when events happen (like a successful charge or transfer). `smartpaystack` provides a generic `WebhookVerifier` that works with any framework.
|
|
164
|
+
|
|
165
|
+
### Example: FastAPI
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
from fastapi import FastAPI, Request, Header, HTTPException
|
|
169
|
+
from smartpaystack import WebhookVerifier
|
|
170
|
+
from smartpaystack.exceptions import WebhookVerificationError
|
|
171
|
+
|
|
172
|
+
app = FastAPI()
|
|
173
|
+
verifier = WebhookVerifier(secret_key="sk_live_xxxxxx")
|
|
174
|
+
|
|
175
|
+
@app.post("/paystack/webhook")
|
|
176
|
+
async def paystack_webhook(request: Request, x_paystack_signature: str = Header(None)):
|
|
177
|
+
raw_body = await request.body()
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
# Verifies the HMAC SHA512 signature and parses the JSON
|
|
181
|
+
event_data = verifier.verify_and_parse(raw_body, x_paystack_signature)
|
|
182
|
+
except WebhookVerificationError as e:
|
|
183
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
184
|
+
|
|
185
|
+
# Handle the event
|
|
186
|
+
if event_data["event"] == "charge.success":
|
|
187
|
+
print("Payment successful!")
|
|
188
|
+
|
|
189
|
+
return {"status": "success"}
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Example: Flask
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
from flask import Flask, request, jsonify
|
|
197
|
+
from smartpaystack import WebhookVerifier
|
|
198
|
+
from smartpaystack.exceptions import WebhookVerificationError
|
|
199
|
+
|
|
200
|
+
app = Flask(__name__)
|
|
201
|
+
verifier = WebhookVerifier(secret_key="sk_live_xxxxxx")
|
|
202
|
+
|
|
203
|
+
@app.route("/paystack/webhook", methods=["POST"])
|
|
204
|
+
def paystack_webhook():
|
|
205
|
+
signature = request.headers.get("x-paystack-signature")
|
|
206
|
+
raw_body = request.get_data()
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
event_data = verifier.verify_and_parse(raw_body, signature)
|
|
210
|
+
except WebhookVerificationError as e:
|
|
211
|
+
return jsonify({"error": str(e)}), 400
|
|
212
|
+
|
|
213
|
+
if event_data["event"] == "transfer.success":
|
|
214
|
+
print("Transfer successful!")
|
|
215
|
+
|
|
216
|
+
return jsonify({"status": "success"}), 200
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Example: Django
|
|
221
|
+
|
|
222
|
+
In your `views.py`, use `@csrf_exempt` since Paystack (an external service) cannot send a CSRF token.
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
from django.http import JsonResponse
|
|
226
|
+
from django.views.decorators.csrf import csrf_exempt
|
|
227
|
+
from django.views.decorators.http import require_POST
|
|
228
|
+
from django.conf import settings
|
|
229
|
+
from smartpaystack import WebhookVerifier
|
|
230
|
+
from smartpaystack.exceptions import WebhookVerificationError
|
|
231
|
+
|
|
232
|
+
# Initialize the verifier (ideally load this from environment or settings)
|
|
233
|
+
verifier = WebhookVerifier(secret_key=getattr(settings, "PAYSTACK_SECRET_KEY", "sk_live_xxxxxx"))
|
|
234
|
+
|
|
235
|
+
@csrf_exempt
|
|
236
|
+
@require_POST
|
|
237
|
+
def paystack_webhook(request):
|
|
238
|
+
signature = request.headers.get("x-paystack-signature", "")
|
|
239
|
+
raw_body = request.body # Django provides the raw bytes here
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
event_data = verifier.verify_and_parse(raw_body, signature)
|
|
243
|
+
except WebhookVerificationError as e:
|
|
244
|
+
return JsonResponse({"error": str(e)}, status=400)
|
|
245
|
+
|
|
246
|
+
# Handle the event
|
|
247
|
+
if event_data["event"] == "charge.success":
|
|
248
|
+
print(f"Payment successful for amount: {event_data['data']['amount']}")
|
|
249
|
+
|
|
250
|
+
return JsonResponse({"status": "success"}, status=200)
|
|
251
|
+
|
|
252
|
+
```
|
|
253
|
+
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
```markdown
|
|
2
|
+
# SmartPaystack
|
|
3
|
+
|
|
4
|
+
A smart, framework-agnostic, strategy-based Paystack integration for Python.
|
|
5
|
+
|
|
6
|
+
Stop writing manual math logic to calculate Paystack fees. Just declare your strategy (`ABSORB`, `PASS`, `SPLIT`) and let the package do the rest. Works seamlessly with **FastAPI, Flask, Django, Tornado, or plain Python scripts.**
|
|
7
|
+
|
|
8
|
+
## ✨ Features
|
|
9
|
+
* **Zero-Math API:** No more Kobo/Cents conversions. Pass native amounts (e.g., `5000` NGN).
|
|
10
|
+
* **Smart Fee Strategies:** Easily absorb fees, pass them to the customer, or split them.
|
|
11
|
+
* **Multi-Currency Routing:** Automatically applies the correct fee caps and percentages for NGN, GHS, ZAR, KES, and USD.
|
|
12
|
+
* **Framework Agnostic Webhooks:** Built-in HMAC SHA512 verifier that works with any web framework.
|
|
13
|
+
* **Fully Typed:** Sweet IDE auto-completion.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 📦 Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install smartpaystack
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Quickstart
|
|
27
|
+
|
|
28
|
+
### 1. Initialization
|
|
29
|
+
|
|
30
|
+
You can either pass your secret key directly or set it as an environment variable (`PAYSTACK_SECRET_KEY`).
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
import os
|
|
34
|
+
from smartpaystack import SmartPaystack
|
|
35
|
+
|
|
36
|
+
# Option A: Uses the PAYSTACK_SECRET_KEY environment variable
|
|
37
|
+
os.environ["PAYSTACK_SECRET_KEY"] = "sk_live_xxxxxx"
|
|
38
|
+
client = SmartPaystack()
|
|
39
|
+
|
|
40
|
+
# Option B: Pass it explicitly
|
|
41
|
+
client = SmartPaystack(secret_key="sk_live_xxxxxx")
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 2. Collecting Money (Charges)
|
|
46
|
+
|
|
47
|
+
Stop worrying about fee math. Tell the client how much you want, and who pays the fee.
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from smartpaystack import ChargeStrategy, Currency
|
|
51
|
+
|
|
52
|
+
# Scenario A: You want exactly ₦50,000. Customer pays the Paystack fee.
|
|
53
|
+
response = client.create_charge(
|
|
54
|
+
email="customer@email.com",
|
|
55
|
+
amount=50000,
|
|
56
|
+
currency=Currency.NGN,
|
|
57
|
+
charge_strategy=ChargeStrategy.PASS
|
|
58
|
+
)
|
|
59
|
+
print(response["authorization_url"])
|
|
60
|
+
|
|
61
|
+
# Scenario B: You absorb the fee for a Ghana Cedi transaction.
|
|
62
|
+
response = client.create_charge(
|
|
63
|
+
email="ghana@email.com",
|
|
64
|
+
amount=1000,
|
|
65
|
+
currency=Currency.GHS,
|
|
66
|
+
charge_strategy=ChargeStrategy.ABSORB
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Scenario C: You split the Paystack fee 50/50 with the customer.
|
|
70
|
+
# If the fee is ₦150, the customer is charged ₦10,075 and you receive ₦9,925.
|
|
71
|
+
response = client.create_charge(
|
|
72
|
+
email="split@email.com",
|
|
73
|
+
amount=10000,
|
|
74
|
+
currency=Currency.NGN,
|
|
75
|
+
charge_strategy=ChargeStrategy.SPLIT,
|
|
76
|
+
split_ratio=0.5 # The percentage of the fee the customer pays (0.5 = 50%)
|
|
77
|
+
)
|
|
78
|
+
print(response["authorization_url"])
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 3. Sending Money (Transfers)
|
|
83
|
+
|
|
84
|
+
Sending money is a two-step process: create a recipient, then initiate the transfer.
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
# 1. Resolve the account (Optional but recommended)
|
|
88
|
+
account = client.resolve_account_number(account_number="0123456789", bank_code="033")
|
|
89
|
+
print(f"Resolved Name: {account['account_name']}")
|
|
90
|
+
|
|
91
|
+
# 2. Create the recipient
|
|
92
|
+
recipient = client.create_transfer_recipient(
|
|
93
|
+
name=account["account_name"],
|
|
94
|
+
account_number="0123456789",
|
|
95
|
+
bank_code="033"
|
|
96
|
+
)
|
|
97
|
+
recipient_code = recipient["recipient_code"]
|
|
98
|
+
|
|
99
|
+
# 3. Send the money (e.g., Send ₦10,500)
|
|
100
|
+
transfer = client.initiate_transfer(
|
|
101
|
+
amount=10500,
|
|
102
|
+
recipient_code=recipient_code,
|
|
103
|
+
reason="Monthly Payout"
|
|
104
|
+
)
|
|
105
|
+
print(f"Transfer Status: {transfer['status']}")
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## 🛡️ Error Handling
|
|
112
|
+
|
|
113
|
+
When building fintech applications, you must handle failures gracefully. `smartpaystack` provides specific exceptions so you can catch exactly what went wrong.
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from smartpaystack import SmartPaystack
|
|
117
|
+
from smartpaystack.exceptions import PaystackAPIError, PaystackError
|
|
118
|
+
|
|
119
|
+
client = SmartPaystack()
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
account = client.resolve_account_number("invalid_number", "033")
|
|
123
|
+
except PaystackAPIError as e:
|
|
124
|
+
# Raised when Paystack returns a 400/500 response, or network fails
|
|
125
|
+
print(f"Paystack API failed: {str(e)}")
|
|
126
|
+
# Example Output: Paystack API failed: Could not resolve account name.
|
|
127
|
+
except PaystackError as e:
|
|
128
|
+
# A generic fallback for any other package-related error
|
|
129
|
+
print(f"An unexpected error occurred: {str(e)}")
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Available Exceptions (from `smartpaystack.exceptions`):**
|
|
134
|
+
|
|
135
|
+
* `PaystackError`: The base class for all package exceptions.
|
|
136
|
+
* `PaystackAPIError`: Raised when the HTTP request to Paystack fails or returns an error message.
|
|
137
|
+
* `WebhookVerificationError`: Raised when the HMAC signature on an incoming webhook is invalid or missing.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## 📡 Verifying Webhooks
|
|
142
|
+
|
|
143
|
+
Paystack sends webhooks to your server when events happen (like a successful charge or transfer). `smartpaystack` provides a generic `WebhookVerifier` that works with any framework.
|
|
144
|
+
|
|
145
|
+
### Example: FastAPI
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
from fastapi import FastAPI, Request, Header, HTTPException
|
|
149
|
+
from smartpaystack import WebhookVerifier
|
|
150
|
+
from smartpaystack.exceptions import WebhookVerificationError
|
|
151
|
+
|
|
152
|
+
app = FastAPI()
|
|
153
|
+
verifier = WebhookVerifier(secret_key="sk_live_xxxxxx")
|
|
154
|
+
|
|
155
|
+
@app.post("/paystack/webhook")
|
|
156
|
+
async def paystack_webhook(request: Request, x_paystack_signature: str = Header(None)):
|
|
157
|
+
raw_body = await request.body()
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
# Verifies the HMAC SHA512 signature and parses the JSON
|
|
161
|
+
event_data = verifier.verify_and_parse(raw_body, x_paystack_signature)
|
|
162
|
+
except WebhookVerificationError as e:
|
|
163
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
164
|
+
|
|
165
|
+
# Handle the event
|
|
166
|
+
if event_data["event"] == "charge.success":
|
|
167
|
+
print("Payment successful!")
|
|
168
|
+
|
|
169
|
+
return {"status": "success"}
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Example: Flask
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
from flask import Flask, request, jsonify
|
|
177
|
+
from smartpaystack import WebhookVerifier
|
|
178
|
+
from smartpaystack.exceptions import WebhookVerificationError
|
|
179
|
+
|
|
180
|
+
app = Flask(__name__)
|
|
181
|
+
verifier = WebhookVerifier(secret_key="sk_live_xxxxxx")
|
|
182
|
+
|
|
183
|
+
@app.route("/paystack/webhook", methods=["POST"])
|
|
184
|
+
def paystack_webhook():
|
|
185
|
+
signature = request.headers.get("x-paystack-signature")
|
|
186
|
+
raw_body = request.get_data()
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
event_data = verifier.verify_and_parse(raw_body, signature)
|
|
190
|
+
except WebhookVerificationError as e:
|
|
191
|
+
return jsonify({"error": str(e)}), 400
|
|
192
|
+
|
|
193
|
+
if event_data["event"] == "transfer.success":
|
|
194
|
+
print("Transfer successful!")
|
|
195
|
+
|
|
196
|
+
return jsonify({"status": "success"}), 200
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Example: Django
|
|
201
|
+
|
|
202
|
+
In your `views.py`, use `@csrf_exempt` since Paystack (an external service) cannot send a CSRF token.
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
from django.http import JsonResponse
|
|
206
|
+
from django.views.decorators.csrf import csrf_exempt
|
|
207
|
+
from django.views.decorators.http import require_POST
|
|
208
|
+
from django.conf import settings
|
|
209
|
+
from smartpaystack import WebhookVerifier
|
|
210
|
+
from smartpaystack.exceptions import WebhookVerificationError
|
|
211
|
+
|
|
212
|
+
# Initialize the verifier (ideally load this from environment or settings)
|
|
213
|
+
verifier = WebhookVerifier(secret_key=getattr(settings, "PAYSTACK_SECRET_KEY", "sk_live_xxxxxx"))
|
|
214
|
+
|
|
215
|
+
@csrf_exempt
|
|
216
|
+
@require_POST
|
|
217
|
+
def paystack_webhook(request):
|
|
218
|
+
signature = request.headers.get("x-paystack-signature", "")
|
|
219
|
+
raw_body = request.body # Django provides the raw bytes here
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
event_data = verifier.verify_and_parse(raw_body, signature)
|
|
223
|
+
except WebhookVerificationError as e:
|
|
224
|
+
return JsonResponse({"error": str(e)}, status=400)
|
|
225
|
+
|
|
226
|
+
# Handle the event
|
|
227
|
+
if event_data["event"] == "charge.success":
|
|
228
|
+
print(f"Payment successful for amount: {event_data['data']['amount']}")
|
|
229
|
+
|
|
230
|
+
return JsonResponse({"status": "success"}, status=200)
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from decimal import Decimal, ROUND_HALF_UP
|
|
2
|
+
from .enums import Currency
|
|
3
|
+
from .config import CURRENCY_RULES
|
|
4
|
+
|
|
5
|
+
class PaystackFeeCalculator:
|
|
6
|
+
"""Calculates Paystack transaction fees accurately to avoid floating-point errors."""
|
|
7
|
+
|
|
8
|
+
@classmethod
|
|
9
|
+
def calculate_fee(cls, amount: Decimal, currency: Currency = Currency.NGN) -> Decimal:
|
|
10
|
+
rule = CURRENCY_RULES.get(currency, CURRENCY_RULES[Currency.NGN])
|
|
11
|
+
amount = Decimal(str(amount))
|
|
12
|
+
|
|
13
|
+
fee = amount * rule["percentage"]
|
|
14
|
+
|
|
15
|
+
if rule["flat_fee"] > 0 and amount >= rule["threshold"]:
|
|
16
|
+
fee += rule["flat_fee"]
|
|
17
|
+
|
|
18
|
+
if rule["cap"] is not None:
|
|
19
|
+
fee = min(fee, rule["cap"])
|
|
20
|
+
|
|
21
|
+
return fee.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def gross_up_amount(cls, desired_amount: Decimal, currency: Currency = Currency.NGN) -> Decimal:
|
|
25
|
+
"""Calculates the exact amount to charge a customer so the merchant receives the `desired_amount`."""
|
|
26
|
+
rule = CURRENCY_RULES.get(currency, CURRENCY_RULES[Currency.NGN])
|
|
27
|
+
desired_amount = Decimal(str(desired_amount))
|
|
28
|
+
|
|
29
|
+
gross = desired_amount / (Decimal("1") - rule["percentage"])
|
|
30
|
+
|
|
31
|
+
if rule["flat_fee"] > 0 and gross >= rule["threshold"]:
|
|
32
|
+
gross = (desired_amount + rule["flat_fee"]) / (Decimal("1") - rule["percentage"])
|
|
33
|
+
|
|
34
|
+
if rule["cap"] is not None and (gross - desired_amount) > rule["cap"]:
|
|
35
|
+
gross = desired_amount + rule["cap"]
|
|
36
|
+
|
|
37
|
+
return gross.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import uuid
|
|
3
|
+
import requests
|
|
4
|
+
from decimal import Decimal
|
|
5
|
+
from typing import Optional, Dict, Any, Union
|
|
6
|
+
|
|
7
|
+
from .calculator import PaystackFeeCalculator
|
|
8
|
+
from .enums import ChargeStrategy, Currency, RecipientType, TransferSource
|
|
9
|
+
from .exceptions import PaystackAPIError
|
|
10
|
+
|
|
11
|
+
class SmartPaystack:
|
|
12
|
+
"""The main client for interacting with the Paystack API."""
|
|
13
|
+
BASE_URL = "https://api.paystack.co"
|
|
14
|
+
|
|
15
|
+
def __init__(self, secret_key: Optional[str] = None) -> None:
|
|
16
|
+
self.secret_key = secret_key or os.environ.get("PAYSTACK_SECRET_KEY")
|
|
17
|
+
|
|
18
|
+
if not self.secret_key:
|
|
19
|
+
raise ValueError(
|
|
20
|
+
"Paystack secret key missing. Pass it to the client "
|
|
21
|
+
"or set the 'PAYSTACK_SECRET_KEY' environment variable."
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
self.headers = {
|
|
25
|
+
"Authorization": f"Bearer {self.secret_key}",
|
|
26
|
+
"Content-Type": "application/json",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
def _request(self, method: str, endpoint: str, data: Optional[Dict] = None, params: Optional[Dict] = None) -> Dict[str, Any]:
|
|
30
|
+
url = f"{self.BASE_URL}{endpoint}"
|
|
31
|
+
try:
|
|
32
|
+
response = requests.request(
|
|
33
|
+
method, url, json=data, params=params, headers=self.headers, timeout=30
|
|
34
|
+
)
|
|
35
|
+
result = response.json()
|
|
36
|
+
except requests.RequestException as e:
|
|
37
|
+
raise PaystackAPIError(f"Network error: {str(e)}")
|
|
38
|
+
except ValueError:
|
|
39
|
+
raise PaystackAPIError("Invalid JSON response from Paystack.")
|
|
40
|
+
|
|
41
|
+
if not response.ok or not result.get("status"):
|
|
42
|
+
raise PaystackAPIError(result.get("message", "API Request Failed"))
|
|
43
|
+
|
|
44
|
+
return result.get("data", result)
|
|
45
|
+
|
|
46
|
+
def create_charge(
|
|
47
|
+
self,
|
|
48
|
+
email: str,
|
|
49
|
+
amount: Union[int, float, Decimal],
|
|
50
|
+
currency: Currency = Currency.NGN,
|
|
51
|
+
charge_strategy: ChargeStrategy = ChargeStrategy.ABSORB,
|
|
52
|
+
split_ratio: float = 0.5,
|
|
53
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
54
|
+
**kwargs: Any
|
|
55
|
+
) -> Dict[str, Any]:
|
|
56
|
+
"""Initializes a transaction applying the correct fee strategy automatically."""
|
|
57
|
+
amount = Decimal(str(amount))
|
|
58
|
+
fee = PaystackFeeCalculator.calculate_fee(amount, currency)
|
|
59
|
+
|
|
60
|
+
if charge_strategy == ChargeStrategy.PASS:
|
|
61
|
+
customer_amount = PaystackFeeCalculator.gross_up_amount(amount, currency)
|
|
62
|
+
elif charge_strategy == ChargeStrategy.SPLIT:
|
|
63
|
+
customer_amount = amount + (fee * Decimal(str(split_ratio)))
|
|
64
|
+
else:
|
|
65
|
+
customer_amount = amount
|
|
66
|
+
|
|
67
|
+
payload = {
|
|
68
|
+
"email": email,
|
|
69
|
+
"amount": int(customer_amount * 100), # Convert to lowest denomination (kobo/cents)
|
|
70
|
+
"currency": currency.value,
|
|
71
|
+
"reference": kwargs.pop("reference", str(uuid.uuid4())),
|
|
72
|
+
"metadata": {
|
|
73
|
+
"smartpaystack_strategy": charge_strategy.value,
|
|
74
|
+
"merchant_expected": float(amount),
|
|
75
|
+
"customer_amount": float(customer_amount),
|
|
76
|
+
**(metadata or {}),
|
|
77
|
+
},
|
|
78
|
+
**kwargs
|
|
79
|
+
}
|
|
80
|
+
return self._request("POST", "/transaction/initialize", data=payload)
|
|
81
|
+
|
|
82
|
+
def resolve_account_number(self, account_number: str, bank_code: str) -> Dict[str, Any]:
|
|
83
|
+
"""Verifies an account number and bank code before creating a recipient."""
|
|
84
|
+
params = {"account_number": account_number, "bank_code": bank_code}
|
|
85
|
+
return self._request("GET", "/bank/resolve", params=params)
|
|
86
|
+
|
|
87
|
+
def create_transfer_recipient(
|
|
88
|
+
self,
|
|
89
|
+
name: str,
|
|
90
|
+
account_number: str,
|
|
91
|
+
bank_code: str,
|
|
92
|
+
recipient_type: RecipientType = RecipientType.NUBAN,
|
|
93
|
+
currency: Currency = Currency.NGN,
|
|
94
|
+
description: str = "",
|
|
95
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
96
|
+
) -> Dict[str, Any]:
|
|
97
|
+
"""Creates a recipient code needed to initiate a transfer."""
|
|
98
|
+
payload = {
|
|
99
|
+
"type": recipient_type.value,
|
|
100
|
+
"name": name,
|
|
101
|
+
"account_number": account_number,
|
|
102
|
+
"bank_code": bank_code,
|
|
103
|
+
"currency": currency.value,
|
|
104
|
+
"description": description,
|
|
105
|
+
"metadata": metadata or {}
|
|
106
|
+
}
|
|
107
|
+
return self._request("POST", "/transferrecipient", data=payload)
|
|
108
|
+
|
|
109
|
+
def initiate_transfer(
|
|
110
|
+
self,
|
|
111
|
+
amount: Union[int, float, Decimal],
|
|
112
|
+
recipient_code: str,
|
|
113
|
+
currency: Currency = Currency.NGN,
|
|
114
|
+
source: TransferSource = TransferSource.BALANCE,
|
|
115
|
+
reason: str = "",
|
|
116
|
+
reference: Optional[str] = None
|
|
117
|
+
) -> Dict[str, Any]:
|
|
118
|
+
"""Initiates a transfer from your Paystack balance to the recipient."""
|
|
119
|
+
payload = {
|
|
120
|
+
"source": source.value,
|
|
121
|
+
"amount": int(Decimal(str(amount)) * 100), # Convert to kobo/cents
|
|
122
|
+
"currency": currency.value,
|
|
123
|
+
"recipient": recipient_code,
|
|
124
|
+
"reason": reason,
|
|
125
|
+
"reference": reference or str(uuid.uuid4())
|
|
126
|
+
}
|
|
127
|
+
return self._request("POST", "/transfer", data=payload)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
from .enums import Currency
|
|
3
|
+
|
|
4
|
+
# Official Paystack fee structures per currency
|
|
5
|
+
CURRENCY_RULES = {
|
|
6
|
+
Currency.NGN: {
|
|
7
|
+
"percentage": Decimal("0.015"),
|
|
8
|
+
"flat_fee": Decimal("100"),
|
|
9
|
+
"threshold": Decimal("2500"),
|
|
10
|
+
"cap": Decimal("2000"),
|
|
11
|
+
},
|
|
12
|
+
Currency.GHS: {
|
|
13
|
+
"percentage": Decimal("0.0195"),
|
|
14
|
+
"flat_fee": Decimal("0"),
|
|
15
|
+
"threshold": Decimal("0"),
|
|
16
|
+
"cap": None,
|
|
17
|
+
},
|
|
18
|
+
Currency.ZAR: {
|
|
19
|
+
"percentage": Decimal("0.029"),
|
|
20
|
+
"flat_fee": Decimal("1"),
|
|
21
|
+
"threshold": Decimal("0"),
|
|
22
|
+
"cap": None,
|
|
23
|
+
},
|
|
24
|
+
Currency.KES: {
|
|
25
|
+
"percentage": Decimal("0.029"),
|
|
26
|
+
"flat_fee": Decimal("0"),
|
|
27
|
+
"threshold": Decimal("0"),
|
|
28
|
+
"cap": None,
|
|
29
|
+
},
|
|
30
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
class ChargeStrategy(str, Enum):
|
|
4
|
+
ABSORB = "absorb"
|
|
5
|
+
PASS = "pass"
|
|
6
|
+
SPLIT = "split"
|
|
7
|
+
|
|
8
|
+
class Currency(str, Enum):
|
|
9
|
+
NGN = "NGN"
|
|
10
|
+
GHS = "GHS"
|
|
11
|
+
ZAR = "ZAR"
|
|
12
|
+
KES = "KES"
|
|
13
|
+
USD = "USD"
|
|
14
|
+
|
|
15
|
+
class RecipientType(str, Enum):
|
|
16
|
+
NUBAN = "nuban"
|
|
17
|
+
MOBILE_MONEY = "mobile_money"
|
|
18
|
+
BASA = "basa"
|
|
19
|
+
|
|
20
|
+
class TransferSource(str, Enum):
|
|
21
|
+
BALANCE = "balance"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
class PaystackError(Exception):
|
|
2
|
+
"""Base exception for all SmartPaystack errors."""
|
|
3
|
+
pass
|
|
4
|
+
|
|
5
|
+
class PaystackAPIError(PaystackError):
|
|
6
|
+
"""Raised when the Paystack API returns an error or fails."""
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
class WebhookVerificationError(PaystackError):
|
|
10
|
+
"""Raised when a webhook signature fails validation."""
|
|
11
|
+
pass
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=65.5.1", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "smartpaystack"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A smart, strategy-based, multi-currency Paystack SDK for Python."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [{ name = "Fidelis Chukwunyere", email = "fidelchukwunyere@gmail.com" }]
|
|
11
|
+
license = "MIT"
|
|
12
|
+
keywords = ["paystack", "payments", "fintech", "africa", "api", "wrapper"]
|
|
13
|
+
dependencies = ["requests>=2.25.1"]
|
|
14
|
+
requires-python = ">=3.8"
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.8",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Operating System :: OS Independent"
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/fidel-c/smartpaystack"
|
|
28
|
+
|
|
29
|
+
[tool.setuptools]
|
|
30
|
+
py-modules = ["calculator", "client", "config", "enums", "exceptions", "webhooks"]
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: smartpaystack
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A smart, strategy-based, multi-currency Paystack SDK for Python.
|
|
5
|
+
Author-email: Fidelis Chukwunyere <fidelchukwunyere@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/fidel-c/smartpaystack
|
|
8
|
+
Keywords: paystack,payments,fintech,africa,api,wrapper
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Requires-Python: >=3.8
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: requests>=2.25.1
|
|
20
|
+
|
|
21
|
+
```markdown
|
|
22
|
+
# SmartPaystack
|
|
23
|
+
|
|
24
|
+
A smart, framework-agnostic, strategy-based Paystack integration for Python.
|
|
25
|
+
|
|
26
|
+
Stop writing manual math logic to calculate Paystack fees. Just declare your strategy (`ABSORB`, `PASS`, `SPLIT`) and let the package do the rest. Works seamlessly with **FastAPI, Flask, Django, Tornado, or plain Python scripts.**
|
|
27
|
+
|
|
28
|
+
## ✨ Features
|
|
29
|
+
* **Zero-Math API:** No more Kobo/Cents conversions. Pass native amounts (e.g., `5000` NGN).
|
|
30
|
+
* **Smart Fee Strategies:** Easily absorb fees, pass them to the customer, or split them.
|
|
31
|
+
* **Multi-Currency Routing:** Automatically applies the correct fee caps and percentages for NGN, GHS, ZAR, KES, and USD.
|
|
32
|
+
* **Framework Agnostic Webhooks:** Built-in HMAC SHA512 verifier that works with any web framework.
|
|
33
|
+
* **Fully Typed:** Sweet IDE auto-completion.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 📦 Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install smartpaystack
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Quickstart
|
|
47
|
+
|
|
48
|
+
### 1. Initialization
|
|
49
|
+
|
|
50
|
+
You can either pass your secret key directly or set it as an environment variable (`PAYSTACK_SECRET_KEY`).
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import os
|
|
54
|
+
from smartpaystack import SmartPaystack
|
|
55
|
+
|
|
56
|
+
# Option A: Uses the PAYSTACK_SECRET_KEY environment variable
|
|
57
|
+
os.environ["PAYSTACK_SECRET_KEY"] = "sk_live_xxxxxx"
|
|
58
|
+
client = SmartPaystack()
|
|
59
|
+
|
|
60
|
+
# Option B: Pass it explicitly
|
|
61
|
+
client = SmartPaystack(secret_key="sk_live_xxxxxx")
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### 2. Collecting Money (Charges)
|
|
66
|
+
|
|
67
|
+
Stop worrying about fee math. Tell the client how much you want, and who pays the fee.
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from smartpaystack import ChargeStrategy, Currency
|
|
71
|
+
|
|
72
|
+
# Scenario A: You want exactly ₦50,000. Customer pays the Paystack fee.
|
|
73
|
+
response = client.create_charge(
|
|
74
|
+
email="customer@email.com",
|
|
75
|
+
amount=50000,
|
|
76
|
+
currency=Currency.NGN,
|
|
77
|
+
charge_strategy=ChargeStrategy.PASS
|
|
78
|
+
)
|
|
79
|
+
print(response["authorization_url"])
|
|
80
|
+
|
|
81
|
+
# Scenario B: You absorb the fee for a Ghana Cedi transaction.
|
|
82
|
+
response = client.create_charge(
|
|
83
|
+
email="ghana@email.com",
|
|
84
|
+
amount=1000,
|
|
85
|
+
currency=Currency.GHS,
|
|
86
|
+
charge_strategy=ChargeStrategy.ABSORB
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Scenario C: You split the Paystack fee 50/50 with the customer.
|
|
90
|
+
# If the fee is ₦150, the customer is charged ₦10,075 and you receive ₦9,925.
|
|
91
|
+
response = client.create_charge(
|
|
92
|
+
email="split@email.com",
|
|
93
|
+
amount=10000,
|
|
94
|
+
currency=Currency.NGN,
|
|
95
|
+
charge_strategy=ChargeStrategy.SPLIT,
|
|
96
|
+
split_ratio=0.5 # The percentage of the fee the customer pays (0.5 = 50%)
|
|
97
|
+
)
|
|
98
|
+
print(response["authorization_url"])
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 3. Sending Money (Transfers)
|
|
103
|
+
|
|
104
|
+
Sending money is a two-step process: create a recipient, then initiate the transfer.
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
# 1. Resolve the account (Optional but recommended)
|
|
108
|
+
account = client.resolve_account_number(account_number="0123456789", bank_code="033")
|
|
109
|
+
print(f"Resolved Name: {account['account_name']}")
|
|
110
|
+
|
|
111
|
+
# 2. Create the recipient
|
|
112
|
+
recipient = client.create_transfer_recipient(
|
|
113
|
+
name=account["account_name"],
|
|
114
|
+
account_number="0123456789",
|
|
115
|
+
bank_code="033"
|
|
116
|
+
)
|
|
117
|
+
recipient_code = recipient["recipient_code"]
|
|
118
|
+
|
|
119
|
+
# 3. Send the money (e.g., Send ₦10,500)
|
|
120
|
+
transfer = client.initiate_transfer(
|
|
121
|
+
amount=10500,
|
|
122
|
+
recipient_code=recipient_code,
|
|
123
|
+
reason="Monthly Payout"
|
|
124
|
+
)
|
|
125
|
+
print(f"Transfer Status: {transfer['status']}")
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## 🛡️ Error Handling
|
|
132
|
+
|
|
133
|
+
When building fintech applications, you must handle failures gracefully. `smartpaystack` provides specific exceptions so you can catch exactly what went wrong.
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from smartpaystack import SmartPaystack
|
|
137
|
+
from smartpaystack.exceptions import PaystackAPIError, PaystackError
|
|
138
|
+
|
|
139
|
+
client = SmartPaystack()
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
account = client.resolve_account_number("invalid_number", "033")
|
|
143
|
+
except PaystackAPIError as e:
|
|
144
|
+
# Raised when Paystack returns a 400/500 response, or network fails
|
|
145
|
+
print(f"Paystack API failed: {str(e)}")
|
|
146
|
+
# Example Output: Paystack API failed: Could not resolve account name.
|
|
147
|
+
except PaystackError as e:
|
|
148
|
+
# A generic fallback for any other package-related error
|
|
149
|
+
print(f"An unexpected error occurred: {str(e)}")
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**Available Exceptions (from `smartpaystack.exceptions`):**
|
|
154
|
+
|
|
155
|
+
* `PaystackError`: The base class for all package exceptions.
|
|
156
|
+
* `PaystackAPIError`: Raised when the HTTP request to Paystack fails or returns an error message.
|
|
157
|
+
* `WebhookVerificationError`: Raised when the HMAC signature on an incoming webhook is invalid or missing.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## 📡 Verifying Webhooks
|
|
162
|
+
|
|
163
|
+
Paystack sends webhooks to your server when events happen (like a successful charge or transfer). `smartpaystack` provides a generic `WebhookVerifier` that works with any framework.
|
|
164
|
+
|
|
165
|
+
### Example: FastAPI
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
from fastapi import FastAPI, Request, Header, HTTPException
|
|
169
|
+
from smartpaystack import WebhookVerifier
|
|
170
|
+
from smartpaystack.exceptions import WebhookVerificationError
|
|
171
|
+
|
|
172
|
+
app = FastAPI()
|
|
173
|
+
verifier = WebhookVerifier(secret_key="sk_live_xxxxxx")
|
|
174
|
+
|
|
175
|
+
@app.post("/paystack/webhook")
|
|
176
|
+
async def paystack_webhook(request: Request, x_paystack_signature: str = Header(None)):
|
|
177
|
+
raw_body = await request.body()
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
# Verifies the HMAC SHA512 signature and parses the JSON
|
|
181
|
+
event_data = verifier.verify_and_parse(raw_body, x_paystack_signature)
|
|
182
|
+
except WebhookVerificationError as e:
|
|
183
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
184
|
+
|
|
185
|
+
# Handle the event
|
|
186
|
+
if event_data["event"] == "charge.success":
|
|
187
|
+
print("Payment successful!")
|
|
188
|
+
|
|
189
|
+
return {"status": "success"}
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Example: Flask
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
from flask import Flask, request, jsonify
|
|
197
|
+
from smartpaystack import WebhookVerifier
|
|
198
|
+
from smartpaystack.exceptions import WebhookVerificationError
|
|
199
|
+
|
|
200
|
+
app = Flask(__name__)
|
|
201
|
+
verifier = WebhookVerifier(secret_key="sk_live_xxxxxx")
|
|
202
|
+
|
|
203
|
+
@app.route("/paystack/webhook", methods=["POST"])
|
|
204
|
+
def paystack_webhook():
|
|
205
|
+
signature = request.headers.get("x-paystack-signature")
|
|
206
|
+
raw_body = request.get_data()
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
event_data = verifier.verify_and_parse(raw_body, signature)
|
|
210
|
+
except WebhookVerificationError as e:
|
|
211
|
+
return jsonify({"error": str(e)}), 400
|
|
212
|
+
|
|
213
|
+
if event_data["event"] == "transfer.success":
|
|
214
|
+
print("Transfer successful!")
|
|
215
|
+
|
|
216
|
+
return jsonify({"status": "success"}), 200
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Example: Django
|
|
221
|
+
|
|
222
|
+
In your `views.py`, use `@csrf_exempt` since Paystack (an external service) cannot send a CSRF token.
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
from django.http import JsonResponse
|
|
226
|
+
from django.views.decorators.csrf import csrf_exempt
|
|
227
|
+
from django.views.decorators.http import require_POST
|
|
228
|
+
from django.conf import settings
|
|
229
|
+
from smartpaystack import WebhookVerifier
|
|
230
|
+
from smartpaystack.exceptions import WebhookVerificationError
|
|
231
|
+
|
|
232
|
+
# Initialize the verifier (ideally load this from environment or settings)
|
|
233
|
+
verifier = WebhookVerifier(secret_key=getattr(settings, "PAYSTACK_SECRET_KEY", "sk_live_xxxxxx"))
|
|
234
|
+
|
|
235
|
+
@csrf_exempt
|
|
236
|
+
@require_POST
|
|
237
|
+
def paystack_webhook(request):
|
|
238
|
+
signature = request.headers.get("x-paystack-signature", "")
|
|
239
|
+
raw_body = request.body # Django provides the raw bytes here
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
event_data = verifier.verify_and_parse(raw_body, signature)
|
|
243
|
+
except WebhookVerificationError as e:
|
|
244
|
+
return JsonResponse({"error": str(e)}, status=400)
|
|
245
|
+
|
|
246
|
+
# Handle the event
|
|
247
|
+
if event_data["event"] == "charge.success":
|
|
248
|
+
print(f"Payment successful for amount: {event_data['data']['amount']}")
|
|
249
|
+
|
|
250
|
+
return JsonResponse({"status": "success"}, status=200)
|
|
251
|
+
|
|
252
|
+
```
|
|
253
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
calculator.py
|
|
3
|
+
client.py
|
|
4
|
+
config.py
|
|
5
|
+
enums.py
|
|
6
|
+
exceptions.py
|
|
7
|
+
pyproject.toml
|
|
8
|
+
webhooks.py
|
|
9
|
+
smartpaystack.egg-info/PKG-INFO
|
|
10
|
+
smartpaystack.egg-info/SOURCES.txt
|
|
11
|
+
smartpaystack.egg-info/dependency_links.txt
|
|
12
|
+
smartpaystack.egg-info/requires.txt
|
|
13
|
+
smartpaystack.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
requests>=2.25.1
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import hmac
|
|
2
|
+
import hashlib
|
|
3
|
+
import json
|
|
4
|
+
from typing import Dict, Any
|
|
5
|
+
from .exceptions import WebhookVerificationError
|
|
6
|
+
|
|
7
|
+
class WebhookVerifier:
|
|
8
|
+
"""A framework-agnostic utility for validating Paystack webhook signatures."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, secret_key: str):
|
|
11
|
+
self.secret_key = secret_key.encode("utf-8")
|
|
12
|
+
|
|
13
|
+
def verify_and_parse(self, payload: bytes, signature: str) -> Dict[str, Any]:
|
|
14
|
+
if not signature:
|
|
15
|
+
raise WebhookVerificationError("Missing Paystack signature header")
|
|
16
|
+
|
|
17
|
+
# Paystack uses HMAC SHA512 to sign webhooks
|
|
18
|
+
hash_obj = hmac.new(self.secret_key, msg=payload, digestmod=hashlib.sha512)
|
|
19
|
+
|
|
20
|
+
if not hmac.compare_digest(hash_obj.hexdigest(), signature):
|
|
21
|
+
raise WebhookVerificationError("Invalid Paystack webhook signature")
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
return json.loads(payload)
|
|
25
|
+
except json.JSONDecodeError:
|
|
26
|
+
raise WebhookVerificationError("Invalid JSON payload")
|