reevit 0.9.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
reevit-0.9.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Reevit
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
reevit-0.9.1/PKG-INFO ADDED
@@ -0,0 +1,452 @@
1
+ Metadata-Version: 2.4
2
+ Name: reevit
3
+ Version: 0.9.1
4
+ Summary: Reevit Python SDK
5
+ Home-page: https://github.com/Reevit-Platform/python-sdk
6
+ Author: Reevit
7
+ License: MIT
8
+ Project-URL: Documentation, https://docs.reevit.io
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.6
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: requests>=2.25.0
16
+ Dynamic: author
17
+ Dynamic: classifier
18
+ Dynamic: description
19
+ Dynamic: description-content-type
20
+ Dynamic: home-page
21
+ Dynamic: license
22
+ Dynamic: license-file
23
+ Dynamic: project-url
24
+ Dynamic: requires-dist
25
+ Dynamic: requires-python
26
+ Dynamic: summary
27
+
28
+ # Reevit Python SDK
29
+
30
+ The official Python SDK for [Reevit](https://reevit.io) — a unified payment orchestration platform for Africa.
31
+
32
+ [![PyPI version](https://img.shields.io/pypi/v/reevit.svg)](https://pypi.org/project/reevit/)
33
+ [![Python versions](https://img.shields.io/pypi/pyversions/reevit.svg)](https://pypi.org/project/reevit/)
34
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
35
+
36
+ ## Installation
37
+
38
+ ```bash
39
+ pip install reevit==0.9.1
40
+ ```
41
+
42
+ ## Quick Start
43
+
44
+ ```python
45
+ from reevit import Reevit
46
+
47
+ client = Reevit(api_key="pfk_live_xxx", org_id="org_123")
48
+
49
+ # Create a payment
50
+ try:
51
+ payment = client.payments.create_intent({
52
+ "amount": 5000, # 50.00 GHS
53
+ "currency": "GHS",
54
+ "method": "momo",
55
+ "country": "GH",
56
+ "customer_id": "cust_123",
57
+ "metadata": {
58
+ "order_id": "12345"
59
+ }
60
+ }, idempotency_key="order_12345")
61
+ print(f"Payment created: {payment['id']}")
62
+ except Exception as e:
63
+ print(f"Error: {e}")
64
+
65
+ # List payments
66
+ payments = client.payments.list()
67
+ print(payments)
68
+ ```
69
+
70
+ ## Server-created checkout sessions
71
+
72
+ Create checkout sessions on your server, then pass `session["session_secret"]` to a browser SDK — [`@reevit/react`](https://www.npmjs.com/package/@reevit/react), [`@reevit/vue`](https://www.npmjs.com/package/@reevit/vue), or [`@reevit/svelte`](https://www.npmjs.com/package/@reevit/svelte) — to render the checkout UI.
73
+
74
+ ```python
75
+ session = client.checkout_sessions.create(
76
+ {
77
+ "amount": 5000,
78
+ "currency": "GHS",
79
+ "method": "mobile_money",
80
+ "country": "GH",
81
+ },
82
+ idempotency_key="order_12345",
83
+ )
84
+ ```
85
+
86
+ ## Idempotency
87
+
88
+ Pass `idempotency_key` to safely retry intent creation without duplicates.
89
+
90
+ ```python
91
+ payment = client.payments.create_intent(
92
+ {
93
+ "amount": 5000,
94
+ "currency": "GHS",
95
+ "method": "momo",
96
+ "country": "GH",
97
+ },
98
+ idempotency_key="order_12345",
99
+ )
100
+ ```
101
+
102
+ ## Features
103
+
104
+ - **Payments**: Create intents, update intents, confirm, confirm intent, cancel, retry, refund, stats
105
+ - **Connections**: Manage PSP integrations, validation, labels, status, audit
106
+ - **Subscriptions**: Manage recurring billing lifecycle
107
+ - **Fraud**: Configure fraud rules
108
+ - **Customers / Payment Links / Checkout Sessions / Webhooks / Routing Rules / Invoices**: Additional backend services
109
+
110
+ `org_id` is supported directly on the client. Omitting it for authenticated requests still works for backward compatibility, but that mode is deprecated.
111
+
112
+ ---
113
+
114
+ ## Webhook Verification
115
+
116
+ Reevit sends webhooks to notify your application of payment events. Always verify webhook signatures.
117
+
118
+ ### Understanding Webhooks
119
+
120
+ There are **two types of webhooks** in Reevit:
121
+
122
+ 1. **Inbound Webhooks (PSP → Reevit)**: Webhooks from payment providers (Paystack, Flutterwave, etc.) to Reevit. Configure these in the PSP dashboard. Reevit handles them automatically.
123
+
124
+ 2. **Outbound Webhooks (Reevit → Your App)**: Webhooks from Reevit to your application. Configure in Reevit Dashboard and create a handler in your app.
125
+
126
+ ### Signature Format
127
+
128
+ - **Header**: `X-Reevit-Signature: sha256=<hex-signature>`
129
+ - **Signature**: `HMAC-SHA256(request_body, signing_secret)`
130
+
131
+ ### Getting Your Signing Secret
132
+
133
+ 1. Go to **Reevit Dashboard > Developers > Webhooks**
134
+ 2. Configure your webhook endpoint URL
135
+ 3. Copy the signing secret (starts with `whsec_`)
136
+ 4. Set environment variable: `REEVIT_WEBHOOK_SECRET=whsec_xxx...`
137
+
138
+ ### Flask Webhook Handler
139
+
140
+ The SDK ships a constant-time verifier — `verify_webhook_signature(payload, signature, secret)` — so you do not have to reimplement HMAC. Pass the **raw** request body (not parsed-and-reserialized JSON), the `X-Reevit-Signature` header, and your signing secret.
141
+
142
+ ```python
143
+ import os
144
+ import logging
145
+ from dataclasses import dataclass
146
+ from typing import Optional, Dict, Any
147
+ from flask import Flask, request, jsonify
148
+ from reevit import verify_webhook_signature
149
+
150
+ app = Flask(__name__)
151
+ logging.basicConfig(level=logging.INFO)
152
+ logger = logging.getLogger(__name__)
153
+
154
+ @dataclass
155
+ class PaymentData:
156
+ id: str
157
+ status: str
158
+ amount: int
159
+ currency: str
160
+ provider: str
161
+ customer_id: Optional[str] = None
162
+ metadata: Optional[Dict[str, str]] = None
163
+
164
+ @dataclass
165
+ class SubscriptionData:
166
+ id: str
167
+ customer_id: str
168
+ plan_id: str
169
+ status: str
170
+ amount: int
171
+ currency: str
172
+ interval: str
173
+ next_renewal_at: Optional[str] = None
174
+
175
+ @app.route('/webhooks/reevit', methods=['POST'])
176
+ def webhook():
177
+ payload = request.get_data() # raw bytes — do not re-serialize
178
+ signature = request.headers.get('X-Reevit-Signature', '')
179
+ secret = os.environ.get('REEVIT_WEBHOOK_SECRET', '')
180
+
181
+ # Verify signature (required in production)
182
+ if not verify_webhook_signature(payload, signature, secret):
183
+ logger.warning('[Webhook] Invalid signature')
184
+ return jsonify({'error': 'Invalid signature'}), 401
185
+
186
+ event = request.get_json()
187
+ event_type = event.get('type')
188
+ event_id = event.get('id')
189
+
190
+ logger.info(f'[Webhook] Received: {event_type} ({event_id})')
191
+
192
+ # Handle different event types
193
+ if event_type == 'reevit.webhook.test':
194
+ logger.info(f'[Webhook] Test received: {event.get("message")}')
195
+
196
+ # Payment events
197
+ elif event_type == 'payment.succeeded':
198
+ data = PaymentData(**event.get('data', {}))
199
+ handle_payment_succeeded(data)
200
+
201
+ elif event_type == 'payment.failed':
202
+ data = PaymentData(**event.get('data', {}))
203
+ handle_payment_failed(data)
204
+
205
+ elif event_type == 'payment.refunded':
206
+ data = PaymentData(**event.get('data', {}))
207
+ handle_payment_refunded(data)
208
+
209
+ elif event_type == 'payment.pending':
210
+ data = PaymentData(**event.get('data', {}))
211
+ logger.info(f'[Webhook] Payment pending: {data.id}')
212
+
213
+ # Subscription events
214
+ elif event_type == 'subscription.created':
215
+ data = SubscriptionData(**event.get('data', {}))
216
+ handle_subscription_created(data)
217
+
218
+ elif event_type == 'subscription.renewed':
219
+ data = SubscriptionData(**event.get('data', {}))
220
+ handle_subscription_renewed(data)
221
+
222
+ elif event_type == 'subscription.canceled':
223
+ data = SubscriptionData(**event.get('data', {}))
224
+ handle_subscription_canceled(data)
225
+
226
+ else:
227
+ logger.info(f'[Webhook] Unhandled event: {event_type}')
228
+
229
+ return jsonify({'received': True})
230
+
231
+ # Payment handlers
232
+ def handle_payment_succeeded(data: PaymentData):
233
+ order_id = data.metadata.get('order_id') if data.metadata else None
234
+ logger.info(f'[Webhook] Payment succeeded: {data.id} for order {order_id}')
235
+
236
+ # TODO: Implement your business logic
237
+ # - Update order status to "paid"
238
+ # - Send confirmation email to customer
239
+ # - Trigger fulfillment process
240
+
241
+ def handle_payment_failed(data: PaymentData):
242
+ logger.info(f'[Webhook] Payment failed: {data.id}')
243
+
244
+ # TODO: Implement your business logic
245
+ # - Update order status to "payment_failed"
246
+ # - Send notification to customer
247
+ # - Allow retry
248
+
249
+ def handle_payment_refunded(data: PaymentData):
250
+ order_id = data.metadata.get('order_id') if data.metadata else None
251
+ logger.info(f'[Webhook] Payment refunded: {data.id} for order {order_id}')
252
+
253
+ # TODO: Implement your business logic
254
+ # - Update order status to "refunded"
255
+ # - Restore inventory if applicable
256
+
257
+ # Subscription handlers
258
+ def handle_subscription_created(data: SubscriptionData):
259
+ logger.info(f'[Webhook] Subscription created: {data.id} for customer {data.customer_id}')
260
+
261
+ # TODO: Implement your business logic
262
+ # - Grant access to subscription features
263
+ # - Send welcome email
264
+
265
+ def handle_subscription_renewed(data: SubscriptionData):
266
+ logger.info(f'[Webhook] Subscription renewed: {data.id}')
267
+
268
+ # TODO: Implement your business logic
269
+ # - Extend access period
270
+ # - Send renewal confirmation
271
+
272
+ def handle_subscription_canceled(data: SubscriptionData):
273
+ logger.info(f'[Webhook] Subscription canceled: {data.id}')
274
+
275
+ # TODO: Implement your business logic
276
+ # - Revoke access at end of billing period
277
+ # - Send cancellation confirmation
278
+
279
+ if __name__ == '__main__':
280
+ app.run(port=8080)
281
+ ```
282
+
283
+ ### Django Webhook Handler
284
+
285
+ ```python
286
+ # views.py
287
+ import json
288
+ import os
289
+ import logging
290
+ from django.http import JsonResponse
291
+ from django.views.decorators.csrf import csrf_exempt
292
+ from django.views.decorators.http import require_POST
293
+ from reevit import verify_webhook_signature
294
+
295
+ logger = logging.getLogger(__name__)
296
+
297
+ @csrf_exempt
298
+ @require_POST
299
+ def reevit_webhook(request):
300
+ payload = request.body # raw bytes — do not re-serialize
301
+ signature = request.headers.get('X-Reevit-Signature', '')
302
+ secret = os.environ.get('REEVIT_WEBHOOK_SECRET', '')
303
+
304
+ if not verify_webhook_signature(payload, signature, secret):
305
+ return JsonResponse({'error': 'Invalid signature'}, status=401)
306
+
307
+ event = json.loads(payload)
308
+ event_type = event.get('type')
309
+
310
+ logger.info(f'[Webhook] Received: {event_type}')
311
+
312
+ if event_type == 'payment.succeeded':
313
+ data = event.get('data', {})
314
+ order_id = data.get('metadata', {}).get('order_id')
315
+ # Fulfill order, send confirmation email
316
+ logger.info(f'Payment succeeded for order {order_id}')
317
+
318
+ elif event_type == 'payment.failed':
319
+ # Notify customer, allow retry
320
+ pass
321
+
322
+ elif event_type == 'subscription.renewed':
323
+ # Extend access
324
+ pass
325
+
326
+ elif event_type == 'subscription.canceled':
327
+ # Revoke access
328
+ pass
329
+
330
+ return JsonResponse({'received': True})
331
+ ```
332
+
333
+ ### FastAPI Webhook Handler
334
+
335
+ ```python
336
+ from fastapi import FastAPI, Request, HTTPException
337
+ from pydantic import BaseModel
338
+ from typing import Optional, Dict
339
+ import os
340
+ import logging
341
+ from reevit import verify_webhook_signature
342
+
343
+ app = FastAPI()
344
+ logger = logging.getLogger(__name__)
345
+
346
+ class PaymentData(BaseModel):
347
+ id: str
348
+ status: str
349
+ amount: int
350
+ currency: str
351
+ provider: str
352
+ customer_id: Optional[str] = None
353
+ metadata: Optional[Dict[str, str]] = None
354
+
355
+ class SubscriptionData(BaseModel):
356
+ id: str
357
+ customer_id: str
358
+ plan_id: str
359
+ status: str
360
+ amount: int
361
+ currency: str
362
+ interval: str
363
+ next_renewal_at: Optional[str] = None
364
+
365
+ @app.post('/webhooks/reevit')
366
+ async def webhook(request: Request):
367
+ payload = await request.body() # raw bytes — do not re-serialize
368
+ signature = request.headers.get('X-Reevit-Signature', '')
369
+ secret = os.environ.get('REEVIT_WEBHOOK_SECRET', '')
370
+
371
+ if not verify_webhook_signature(payload, signature, secret):
372
+ raise HTTPException(status_code=401, detail='Invalid signature')
373
+
374
+ event = await request.json()
375
+ event_type = event.get('type')
376
+
377
+ logger.info(f'[Webhook] Received: {event_type}')
378
+
379
+ # Payment events
380
+ if event_type == 'payment.succeeded':
381
+ data = PaymentData(**event.get('data', {}))
382
+ order_id = data.metadata.get('order_id') if data.metadata else None
383
+ logger.info(f'Payment succeeded: {data.id} for order {order_id}')
384
+ # Fulfill order, send confirmation email
385
+
386
+ elif event_type == 'payment.failed':
387
+ # Notify customer, allow retry
388
+ pass
389
+
390
+ # Subscription events
391
+ elif event_type == 'subscription.renewed':
392
+ data = SubscriptionData(**event.get('data', {}))
393
+ logger.info(f'Subscription renewed: {data.id}')
394
+ # Extend access
395
+
396
+ elif event_type == 'subscription.canceled':
397
+ data = SubscriptionData(**event.get('data', {}))
398
+ logger.info(f'Subscription canceled: {data.id}')
399
+ # Revoke access
400
+
401
+ return {'received': True}
402
+ ```
403
+
404
+ ## Supported PSPs
405
+
406
+ | Provider | Countries | Payment Methods |
407
+ |----------|-----------|-----------------|
408
+ | Paystack | NG, GH, ZA, KE | Card, Mobile Money, Bank Transfer |
409
+ | Flutterwave | NG, GH, KE, ZA + | Card, Mobile Money, Bank Transfer |
410
+ | Hubtel | GH | Mobile Money |
411
+ | Stripe | Global (50+) | Card, Apple Pay, Google Pay |
412
+ | Monnify | NG | Card, Bank Transfer, USSD |
413
+ | M-Pesa | KE, TZ | Mobile Money (STK Push) |
414
+
415
+ ---
416
+
417
+ ## Release Notes
418
+
419
+ ### v0.9.1
420
+
421
+ - Added `verify_webhook_signature` / `sign_webhook_payload` helpers (constant-time HMAC-SHA256 verification of the `X-Reevit-Signature` header)
422
+ - Version is now sourced from `reevit._version` and sent as the `X-Reevit-Client-Version` header
423
+
424
+ ### v0.9.0
425
+
426
+ - Added server-created checkout sessions
427
+ - Version alignment across all Reevit SDKs
428
+ - Updated documentation and webhook examples
429
+ - Added support for Apple Pay and Google Pay
430
+ - Updated supported PSPs and payment methods documentation
431
+
432
+ ---
433
+
434
+ ## Environment Variables
435
+
436
+ ```bash
437
+ export REEVIT_API_KEY=pfk_live_xxx
438
+ export REEVIT_ORG_ID=org_xxx
439
+ export REEVIT_WEBHOOK_SECRET=whsec_xxx # Get from Dashboard > Developers > Webhooks
440
+ ```
441
+
442
+ ---
443
+
444
+ ## Support
445
+
446
+ - **Documentation**: [https://docs.reevit.io](https://docs.reevit.io)
447
+ - **GitHub Issues**: [https://github.com/Reevit-Platform/backend/issues](https://github.com/Reevit-Platform/backend/issues)
448
+ - **Email**: support@reevit.io
449
+
450
+ ## License
451
+
452
+ MIT License - see [LICENSE](LICENSE) for details.