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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ requests>=2.25.1
@@ -0,0 +1,6 @@
1
+ calculator
2
+ client
3
+ config
4
+ enums
5
+ exceptions
6
+ webhooks
@@ -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")