smartpaystack 0.1.0__tar.gz → 0.1.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: smartpaystack
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: A smart, strategy-based, multi-currency Paystack SDK for Python.
5
5
  Author-email: Fidelis Chukwunyere <fidelchukwunyere@gmail.com>
6
6
  License-Expression: MIT
@@ -43,7 +43,7 @@ pip install smartpaystack
43
43
 
44
44
  ---
45
45
 
46
- ## Quickstart
46
+ ## 🚀 Quickstart
47
47
 
48
48
  ### 1. Initialization
49
49
 
@@ -99,7 +99,26 @@ print(response["authorization_url"])
99
99
 
100
100
  ```
101
101
 
102
- ### 3. Sending Money (Transfers)
102
+ ### 3. Passing Custom Metadata
103
+
104
+ You can easily attach your own custom data (like order IDs or user IDs) to a charge. The package will safely merge your custom dictionary with its own internal fee calculations so you can access both later in your webhook!
105
+
106
+ ```python
107
+ response = client.create_charge(
108
+ email="buyer@email.com",
109
+ amount=15000,
110
+ charge_strategy=ChargeStrategy.PASS,
111
+ metadata={
112
+ "custom_order_id": "ORD-88291",
113
+ "cart_items": 3
114
+ }
115
+ )
116
+
117
+ ```
118
+
119
+ *When this transaction succeeds, your webhook will receive your custom fields alongside `smartpaystack_strategy`, `merchant_expected`, and `customer_amount`.*
120
+
121
+ ### 4. Sending Money (Transfers)
103
122
 
104
123
  Sending money is a two-step process: create a recipient, then initiate the transfer.
105
124
 
@@ -108,11 +127,12 @@ Sending money is a two-step process: create a recipient, then initiate the trans
108
127
  account = client.resolve_account_number(account_number="0123456789", bank_code="033")
109
128
  print(f"Resolved Name: {account['account_name']}")
110
129
 
111
- # 2. Create the recipient
130
+ # 2. Create the recipient (You can pass metadata here, too!)
112
131
  recipient = client.create_transfer_recipient(
113
132
  name=account["account_name"],
114
133
  account_number="0123456789",
115
- bank_code="033"
134
+ bank_code="033",
135
+ metadata={"internal_worker_id": "W-990"}
116
136
  )
117
137
  recipient_code = recipient["recipient_code"]
118
138
 
@@ -184,7 +204,12 @@ async def paystack_webhook(request: Request, x_paystack_signature: str = Header(
184
204
 
185
205
  # Handle the event
186
206
  if event_data["event"] == "charge.success":
187
- print("Payment successful!")
207
+ data = event_data["data"]
208
+ # Retrieve the math breakdown and your custom metadata!
209
+ merchant_keeps = data["metadata"]["merchant_expected"]
210
+ order_id = data["metadata"].get("custom_order_id")
211
+
212
+ print(f"Payment successful for Order {order_id}! Expected payout: {merchant_keeps}")
188
213
 
189
214
  return {"status": "success"}
190
215
 
@@ -251,3 +276,5 @@ def paystack_webhook(request):
251
276
 
252
277
  ```
253
278
 
279
+ ```
280
+
@@ -23,7 +23,7 @@ pip install smartpaystack
23
23
 
24
24
  ---
25
25
 
26
- ## Quickstart
26
+ ## 🚀 Quickstart
27
27
 
28
28
  ### 1. Initialization
29
29
 
@@ -79,7 +79,26 @@ print(response["authorization_url"])
79
79
 
80
80
  ```
81
81
 
82
- ### 3. Sending Money (Transfers)
82
+ ### 3. Passing Custom Metadata
83
+
84
+ You can easily attach your own custom data (like order IDs or user IDs) to a charge. The package will safely merge your custom dictionary with its own internal fee calculations so you can access both later in your webhook!
85
+
86
+ ```python
87
+ response = client.create_charge(
88
+ email="buyer@email.com",
89
+ amount=15000,
90
+ charge_strategy=ChargeStrategy.PASS,
91
+ metadata={
92
+ "custom_order_id": "ORD-88291",
93
+ "cart_items": 3
94
+ }
95
+ )
96
+
97
+ ```
98
+
99
+ *When this transaction succeeds, your webhook will receive your custom fields alongside `smartpaystack_strategy`, `merchant_expected`, and `customer_amount`.*
100
+
101
+ ### 4. Sending Money (Transfers)
83
102
 
84
103
  Sending money is a two-step process: create a recipient, then initiate the transfer.
85
104
 
@@ -88,11 +107,12 @@ Sending money is a two-step process: create a recipient, then initiate the trans
88
107
  account = client.resolve_account_number(account_number="0123456789", bank_code="033")
89
108
  print(f"Resolved Name: {account['account_name']}")
90
109
 
91
- # 2. Create the recipient
110
+ # 2. Create the recipient (You can pass metadata here, too!)
92
111
  recipient = client.create_transfer_recipient(
93
112
  name=account["account_name"],
94
113
  account_number="0123456789",
95
- bank_code="033"
114
+ bank_code="033",
115
+ metadata={"internal_worker_id": "W-990"}
96
116
  )
97
117
  recipient_code = recipient["recipient_code"]
98
118
 
@@ -164,7 +184,12 @@ async def paystack_webhook(request: Request, x_paystack_signature: str = Header(
164
184
 
165
185
  # Handle the event
166
186
  if event_data["event"] == "charge.success":
167
- print("Payment successful!")
187
+ data = event_data["data"]
188
+ # Retrieve the math breakdown and your custom metadata!
189
+ merchant_keeps = data["metadata"]["merchant_expected"]
190
+ order_id = data["metadata"].get("custom_order_id")
191
+
192
+ print(f"Payment successful for Order {order_id}! Expected payout: {merchant_keeps}")
168
193
 
169
194
  return {"status": "success"}
170
195
 
@@ -231,3 +256,5 @@ def paystack_webhook(request):
231
256
 
232
257
  ```
233
258
 
259
+ ```
260
+
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "smartpaystack"
7
- version = "0.1.0"
7
+ version = "0.1.2"
8
8
  description = "A smart, strategy-based, multi-currency Paystack SDK for Python."
9
9
  readme = "README.md"
10
10
  authors = [{ name = "Fidelis Chukwunyere", email = "fidelchukwunyere@gmail.com" }]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: smartpaystack
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: A smart, strategy-based, multi-currency Paystack SDK for Python.
5
5
  Author-email: Fidelis Chukwunyere <fidelchukwunyere@gmail.com>
6
6
  License-Expression: MIT
@@ -43,7 +43,7 @@ pip install smartpaystack
43
43
 
44
44
  ---
45
45
 
46
- ## Quickstart
46
+ ## 🚀 Quickstart
47
47
 
48
48
  ### 1. Initialization
49
49
 
@@ -99,7 +99,26 @@ print(response["authorization_url"])
99
99
 
100
100
  ```
101
101
 
102
- ### 3. Sending Money (Transfers)
102
+ ### 3. Passing Custom Metadata
103
+
104
+ You can easily attach your own custom data (like order IDs or user IDs) to a charge. The package will safely merge your custom dictionary with its own internal fee calculations so you can access both later in your webhook!
105
+
106
+ ```python
107
+ response = client.create_charge(
108
+ email="buyer@email.com",
109
+ amount=15000,
110
+ charge_strategy=ChargeStrategy.PASS,
111
+ metadata={
112
+ "custom_order_id": "ORD-88291",
113
+ "cart_items": 3
114
+ }
115
+ )
116
+
117
+ ```
118
+
119
+ *When this transaction succeeds, your webhook will receive your custom fields alongside `smartpaystack_strategy`, `merchant_expected`, and `customer_amount`.*
120
+
121
+ ### 4. Sending Money (Transfers)
103
122
 
104
123
  Sending money is a two-step process: create a recipient, then initiate the transfer.
105
124
 
@@ -108,11 +127,12 @@ Sending money is a two-step process: create a recipient, then initiate the trans
108
127
  account = client.resolve_account_number(account_number="0123456789", bank_code="033")
109
128
  print(f"Resolved Name: {account['account_name']}")
110
129
 
111
- # 2. Create the recipient
130
+ # 2. Create the recipient (You can pass metadata here, too!)
112
131
  recipient = client.create_transfer_recipient(
113
132
  name=account["account_name"],
114
133
  account_number="0123456789",
115
- bank_code="033"
134
+ bank_code="033",
135
+ metadata={"internal_worker_id": "W-990"}
116
136
  )
117
137
  recipient_code = recipient["recipient_code"]
118
138
 
@@ -184,7 +204,12 @@ async def paystack_webhook(request: Request, x_paystack_signature: str = Header(
184
204
 
185
205
  # Handle the event
186
206
  if event_data["event"] == "charge.success":
187
- print("Payment successful!")
207
+ data = event_data["data"]
208
+ # Retrieve the math breakdown and your custom metadata!
209
+ merchant_keeps = data["metadata"]["merchant_expected"]
210
+ order_id = data["metadata"].get("custom_order_id")
211
+
212
+ print(f"Payment successful for Order {order_id}! Expected payout: {merchant_keeps}")
188
213
 
189
214
  return {"status": "success"}
190
215
 
@@ -251,3 +276,5 @@ def paystack_webhook(request):
251
276
 
252
277
  ```
253
278
 
279
+ ```
280
+
@@ -1,11 +1,5 @@
1
1
  README.md
2
- calculator.py
3
- client.py
4
- config.py
5
- enums.py
6
- exceptions.py
7
2
  pyproject.toml
8
- webhooks.py
9
3
  smartpaystack.egg-info/PKG-INFO
10
4
  smartpaystack.egg-info/SOURCES.txt
11
5
  smartpaystack.egg-info/dependency_links.txt
@@ -1,37 +0,0 @@
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)
@@ -1,127 +0,0 @@
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)
@@ -1,30 +0,0 @@
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
- }
@@ -1,21 +0,0 @@
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"
@@ -1,11 +0,0 @@
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
@@ -1,26 +0,0 @@
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")
File without changes