payplus-python 0.1.2__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. payplus_python-0.2.1/PKG-INFO +487 -0
  2. payplus_python-0.2.1/README.md +443 -0
  3. payplus_python-0.2.1/examples/subscription_saas.py +151 -0
  4. {payplus_python-0.1.2 → payplus_python-0.2.1}/payplus/__init__.py +4 -10
  5. payplus_python-0.2.1/payplus/api/customers.py +114 -0
  6. payplus_python-0.2.1/payplus/api/payment_pages.py +299 -0
  7. {payplus_python-0.1.2 → payplus_python-0.2.1}/payplus/api/recurring.py +83 -0
  8. {payplus_python-0.1.2 → payplus_python-0.2.1}/payplus/client.py +3 -1
  9. payplus_python-0.2.1/payplus/models/__init__.py +16 -0
  10. {payplus_python-0.1.2 → payplus_python-0.2.1}/payplus/models/subscription.py +3 -1
  11. {payplus_python-0.1.2 → payplus_python-0.2.1}/payplus/subscriptions/__init__.py +0 -2
  12. payplus_python-0.2.1/payplus/subscriptions/manager.py +600 -0
  13. {payplus_python-0.1.2 → payplus_python-0.2.1}/payplus/subscriptions/storage.py +100 -251
  14. {payplus_python-0.1.2 → payplus_python-0.2.1}/payplus/webhooks/handler.py +154 -162
  15. payplus_python-0.2.1/payplus_python.egg-info/PKG-INFO +487 -0
  16. {payplus_python-0.1.2 → payplus_python-0.2.1}/payplus_python.egg-info/SOURCES.txt +1 -3
  17. {payplus_python-0.1.2 → payplus_python-0.2.1}/payplus_python.egg-info/top_level.txt +1 -0
  18. {payplus_python-0.1.2 → payplus_python-0.2.1}/pyproject.toml +1 -1
  19. {payplus_python-0.1.2 → payplus_python-0.2.1}/tests/test_models.py +116 -171
  20. payplus_python-0.1.2/PKG-INFO +0 -446
  21. payplus_python-0.1.2/README.md +0 -402
  22. payplus_python-0.1.2/examples/subscription_saas.py +0 -206
  23. payplus_python-0.1.2/payplus/api/payment_pages.py +0 -176
  24. payplus_python-0.1.2/payplus/models/__init__.py +0 -23
  25. payplus_python-0.1.2/payplus/models/invoice.py +0 -242
  26. payplus_python-0.1.2/payplus/models/payment.py +0 -179
  27. payplus_python-0.1.2/payplus/subscriptions/billing.py +0 -231
  28. payplus_python-0.1.2/payplus/subscriptions/manager.py +0 -600
  29. payplus_python-0.1.2/payplus_python.egg-info/PKG-INFO +0 -446
  30. {payplus_python-0.1.2 → payplus_python-0.2.1}/LICENSE +0 -0
  31. {payplus_python-0.1.2 → payplus_python-0.2.1}/examples/basic_payment.py +0 -0
  32. {payplus_python-0.1.2 → payplus_python-0.2.1}/examples/fastapi_webhooks.py +0 -0
  33. {payplus_python-0.1.2 → payplus_python-0.2.1}/payplus/api/__init__.py +0 -0
  34. {payplus_python-0.1.2 → payplus_python-0.2.1}/payplus/api/base.py +0 -0
  35. {payplus_python-0.1.2 → payplus_python-0.2.1}/payplus/api/payments.py +0 -0
  36. {payplus_python-0.1.2 → payplus_python-0.2.1}/payplus/api/transactions.py +0 -0
  37. {payplus_python-0.1.2 → payplus_python-0.2.1}/payplus/exceptions.py +0 -0
  38. {payplus_python-0.1.2 → payplus_python-0.2.1}/payplus/models/customer.py +0 -0
  39. {payplus_python-0.1.2 → payplus_python-0.2.1}/payplus/models/tier.py +0 -0
  40. {payplus_python-0.1.2 → payplus_python-0.2.1}/payplus/webhooks/__init__.py +0 -0
  41. {payplus_python-0.1.2 → payplus_python-0.2.1}/payplus_python.egg-info/dependency_links.txt +0 -0
  42. {payplus_python-0.1.2 → payplus_python-0.2.1}/payplus_python.egg-info/requires.txt +0 -0
  43. {payplus_python-0.1.2 → payplus_python-0.2.1}/setup.cfg +0 -0
  44. {payplus_python-0.1.2 → payplus_python-0.2.1}/tests/__init__.py +0 -0
@@ -0,0 +1,487 @@
1
+ Metadata-Version: 2.4
2
+ Name: payplus-python
3
+ Version: 0.2.1
4
+ Summary: Python SDK for PayPlus payment gateway with subscription management for SaaS apps
5
+ Author-email: Two Solutions <dev@two-solutions.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Two-Solutions/payplus-python
8
+ Project-URL: Documentation, https://github.com/Two-Solutions/payplus-python#readme
9
+ Project-URL: Repository, https://github.com/Two-Solutions/payplus-python
10
+ Project-URL: Issues, https://github.com/Two-Solutions/payplus-python/issues
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Requires-Python: >=3.9
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: httpx>=0.25.0
23
+ Requires-Dist: email-validator>=2.0.0
24
+ Requires-Dist: pydantic>=2.0.0
25
+ Requires-Dist: sqlalchemy>=2.0.0
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
28
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
29
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
30
+ Requires-Dist: black>=23.0.0; extra == "dev"
31
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
32
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
33
+ Provides-Extra: fastapi
34
+ Requires-Dist: fastapi>=0.100.0; extra == "fastapi"
35
+ Provides-Extra: flask
36
+ Requires-Dist: flask>=2.0.0; extra == "flask"
37
+ Provides-Extra: postgres
38
+ Requires-Dist: psycopg2-binary>=2.9.0; extra == "postgres"
39
+ Requires-Dist: asyncpg>=0.28.0; extra == "postgres"
40
+ Provides-Extra: mongodb
41
+ Requires-Dist: motor>=3.0.0; extra == "mongodb"
42
+ Requires-Dist: pymongo>=4.0.0; extra == "mongodb"
43
+ Dynamic: license-file
44
+
45
+ # PayPlus Python SDK (Unofficial)
46
+
47
+ > **Note:** This is an unofficial SDK and is not affiliated with or endorsed by PayPlus.
48
+
49
+ A Python SDK for [PayPlus](https://www.payplus.co.il/) payment gateway with built-in subscription management for SaaS applications.
50
+
51
+ [![PyPI version](https://badge.fury.io/py/payplus-python.svg)](https://badge.fury.io/py/payplus-python)
52
+ [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
53
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
54
+
55
+ ## Features
56
+
57
+ - Full PayPlus API coverage — payment pages, transactions, recurring payments, customers
58
+ - Subscription management — payment-link-based recurring billing for SaaS apps
59
+ - Database integration — MongoDB and SQLAlchemy storage backends
60
+ - Webhook handling — IPN/webhook integration with HMAC signature verification
61
+ - Async support — full async/await for modern Python apps
62
+ - Type safe — Pydantic models with full type hints
63
+
64
+ ## Installation
65
+
66
+ ```bash
67
+ pip install payplus-python
68
+ ```
69
+
70
+ With optional dependencies:
71
+
72
+ ```bash
73
+ pip install payplus-python[fastapi] # FastAPI webhook integration
74
+ pip install payplus-python[postgres] # PostgreSQL storage
75
+ pip install payplus-python[mongodb] # MongoDB storage
76
+ ```
77
+
78
+ ## Implementation Steps
79
+
80
+ A step-by-step guide covering the full subscription lifecycle in your app.
81
+
82
+ ### Step 1: Initialize the SDK
83
+
84
+ ```python
85
+ from decimal import Decimal
86
+ from payplus import PayPlus, SubscriptionManager
87
+ from payplus.models.subscription import BillingCycle
88
+ from payplus.subscriptions.storage import MongoDBStorage
89
+ from payplus.webhooks import WebhookHandler
90
+ from motor.motor_asyncio import AsyncIOMotorClient
91
+
92
+ client = PayPlus(
93
+ api_key="your_api_key",
94
+ secret_key="your_secret_key",
95
+ sandbox=True,
96
+ )
97
+ mongo = AsyncIOMotorClient("mongodb://localhost:27017")
98
+ storage = MongoDBStorage(mongo.your_database)
99
+ manager = SubscriptionManager(client, storage)
100
+ webhook_handler = WebhookHandler(client)
101
+ ```
102
+
103
+ ### Step 2: Define your plans (run once on app setup)
104
+
105
+ ```python
106
+ await manager.create_tier(
107
+ tier_id="basic",
108
+ name="Basic",
109
+ price=Decimal("29"),
110
+ billing_cycle=BillingCycle.MONTHLY,
111
+ trial_days=7,
112
+ )
113
+
114
+ await manager.create_tier(
115
+ tier_id="pro",
116
+ name="Pro",
117
+ price=Decimal("79"),
118
+ billing_cycle=BillingCycle.MONTHLY,
119
+ trial_days=14,
120
+ )
121
+ ```
122
+
123
+ ### Step 3: User signs up
124
+
125
+ ```python
126
+ customer = await manager.create_customer(
127
+ email="user@example.com",
128
+ name="John Doe",
129
+ phone="050-1234567",
130
+ )
131
+ # Save customer.id in your user record
132
+ ```
133
+
134
+ ### Step 4: User subscribes to a plan
135
+
136
+ ```python
137
+ subscription = await manager.create_subscription(
138
+ customer_id=customer.id,
139
+ tier_id="pro",
140
+ callback_url="https://yourapp.com/webhooks/payplus",
141
+ success_url="https://yourapp.com/subscription/success",
142
+ failure_url="https://yourapp.com/subscription/failure",
143
+ )
144
+
145
+ # Redirect user to complete payment
146
+ redirect(subscription.payment_page_link)
147
+
148
+ # Save subscription.id in your user record
149
+ ```
150
+
151
+ Behind the scenes this:
152
+
153
+ 1. Creates the customer on PayPlus (`POST /Customers/Add`) if not already created
154
+ 2. Generates a payment link with `charge_method=3` and `recurring_settings` derived from the tier
155
+ 3. Saves the subscription locally with `status=INCOMPLETE`
156
+
157
+ The user fills in their card details on the PayPlus hosted page. You never touch card data.
158
+
159
+ ### Step 5: Set up the webhook endpoint
160
+
161
+ ```python
162
+ from fastapi import FastAPI, Request, HTTPException
163
+ from payplus.webhooks import WebhookSignatureError
164
+
165
+ app = FastAPI()
166
+
167
+ @app.post("/webhooks/payplus")
168
+ async def payplus_webhook(request: Request):
169
+ payload = await request.body()
170
+ signature = request.headers.get("X-PayPlus-Signature", "")
171
+ try:
172
+ event = await webhook_handler.handle_async(payload, signature)
173
+ await manager.handle_webhook_event(event)
174
+ return {"received": True}
175
+ except WebhookSignatureError:
176
+ raise HTTPException(status_code=400, detail="Invalid signature")
177
+ ```
178
+
179
+ This single endpoint handles every subscription event automatically:
180
+
181
+ | Webhook event | What happens |
182
+ |---|---|
183
+ | First payment succeeds | `INCOMPLETE` -> `ACTIVE`, `recurring_uid` stored |
184
+ | Recurring charge succeeds | Billing period advanced, status stays `ACTIVE` |
185
+ | Recurring charge fails | Status -> `PAST_DUE` (-> `UNPAID` after 4 failures) |
186
+ | Recurring canceled | Status -> `CANCELED` |
187
+ | Cancel at period end flagged | After last charge, cancels on PayPlus and sets `CANCELED` |
188
+
189
+ ### Step 6: Check access in your app
190
+
191
+ ```python
192
+ sub = await manager.get_subscription(subscription_id)
193
+ if sub and sub.is_active:
194
+ # User has access
195
+ ...
196
+ ```
197
+
198
+ ### Step 7: User upgrades plan
199
+
200
+ ```python
201
+ await manager.change_tier(subscription.id, new_tier_id="enterprise")
202
+ ```
203
+
204
+ This updates the recurring payment on PayPlus with the new tier's price and billing cycle. The card token is saved automatically from the first payment webhook.
205
+
206
+ ### Step 8: User pauses subscription
207
+
208
+ ```python
209
+ await manager.pause_subscription(subscription.id)
210
+
211
+ # Later, resume it
212
+ await manager.resume_subscription(subscription.id)
213
+ ```
214
+
215
+ ### Step 9: User cancels subscription
216
+
217
+ ```python
218
+ # Cancel at end of billing period (user keeps access until then)
219
+ await manager.cancel_subscription(
220
+ subscription.id,
221
+ at_period_end=True,
222
+ reason="Customer requested",
223
+ )
224
+
225
+ # Or cancel immediately
226
+ await manager.cancel_subscription(subscription.id, at_period_end=False)
227
+ ```
228
+
229
+ ### Step 10: React to lifecycle events (optional)
230
+
231
+ Register hooks to trigger your own business logic:
232
+
233
+ ```python
234
+ manager.on("subscription.activated", lambda sub: send_welcome_email(sub))
235
+ manager.on("subscription.renewed", lambda sub: log_renewal(sub))
236
+ manager.on("subscription.payment_failed", lambda sub: send_dunning_email(sub))
237
+ manager.on("subscription.canceled", lambda sub: handle_offboarding(sub))
238
+ ```
239
+
240
+ ### Trials
241
+
242
+ If a tier has `trial_days` set, the subscription flow changes:
243
+
244
+ - `create_subscription()` sets `jump_payments` in `recurring_settings`, telling PayPlus to wait N days before the first charge
245
+ - The subscription starts as `INCOMPLETE` (waiting for the user to enter card details on the payment page)
246
+ - When the user completes the payment page, PayPlus validates the card but doesn't charge yet
247
+ - The webhook activates the subscription as `TRIALING` (since `trial_end` is in the future)
248
+ - After the trial period, PayPlus charges automatically and sends a `recurring.charged` webhook
249
+ - `is_active` returns `True` for both `ACTIVE` and `TRIALING` statuses
250
+
251
+ ```python
252
+ # Tier with a 14-day trial
253
+ await manager.create_tier(
254
+ tier_id="pro",
255
+ name="Pro",
256
+ price=Decimal("79"),
257
+ trial_days=14, # 14 free days before first charge
258
+ )
259
+
260
+ # After subscription is created and user completes payment page:
261
+ # sub.status == "trialing"
262
+ # sub.is_active == True
263
+ # sub.trial_end == ~14 days from now
264
+ ```
265
+
266
+ ### How it all fits together
267
+
268
+ ```
269
+ User clicks "Subscribe to Pro"
270
+ |
271
+ v
272
+ create_subscription()
273
+ - Creates customer on PayPlus
274
+ - Generates payment link with recurring settings
275
+ - Subscription status: INCOMPLETE
276
+ |
277
+ v
278
+ User redirected to PayPlus payment page
279
+ User enters card details and pays
280
+ |
281
+ v
282
+ PayPlus sends webhook to callback_url
283
+ |
284
+ v
285
+ handle_webhook_event()
286
+ - Matches webhook to subscription via page_request_uid
287
+ - Saves card token and recurring_uid
288
+ - Sets status: ACTIVE (or TRIALING if trial_days > 0)
289
+ |
290
+ v
291
+ Every billing cycle, PayPlus charges automatically
292
+ - recurring.charged -> period advanced, still ACTIVE
293
+ - recurring.failed -> PAST_DUE (-> UNPAID after 4 failures)
294
+
295
+ Lifecycle actions (from your app):
296
+ - change_tier() -> updates amount on PayPlus
297
+ - pause/resume -> updates local status
298
+ - cancel(at_period_end=True) -> flags locally, cancels on PayPlus after last charge
299
+ - cancel(at_period_end=False) -> cancels on PayPlus immediately, status: CANCELED
300
+ ```
301
+
302
+ ## Direct API Usage
303
+
304
+ You can also use the PayPlus API directly without the subscription manager:
305
+
306
+ ### Payment Link
307
+
308
+ ```python
309
+ result = client.payment_pages.generate_link(
310
+ amount=100.00,
311
+ currency="ILS",
312
+ description="One-time payment",
313
+ customer_email="customer@example.com",
314
+ success_url="https://yourapp.com/success",
315
+ callback_url="https://yourapp.com/webhooks/payplus",
316
+ )
317
+ print(result["data"]["payment_page_link"])
318
+ ```
319
+
320
+ ### Payment Link with Recurring
321
+
322
+ ```python
323
+ from payplus.api.payment_pages import build_recurring_settings
324
+
325
+ result = client.payment_pages.generate_link(
326
+ amount=79.00,
327
+ currency="ILS",
328
+ charge_method=3, # Recurring
329
+ customer_uid="payplus-customer-uid",
330
+ callback_url="https://yourapp.com/webhooks/payplus",
331
+ recurring_settings=build_recurring_settings(
332
+ billing_cycle="monthly",
333
+ trial_days=14,
334
+ number_of_charges=0, # Unlimited
335
+ ),
336
+ )
337
+ ```
338
+
339
+ ### Create Customer
340
+
341
+ ```python
342
+ result = client.customers.add(
343
+ customer_name="John Doe",
344
+ email="john@example.com",
345
+ phone="050-1234567",
346
+ )
347
+ customer_uid = result["data"]["customer_uid"]
348
+ ```
349
+
350
+ ### Transactions
351
+
352
+ ```python
353
+ # Charge a saved card token
354
+ result = client.transactions.charge(
355
+ token="card_token",
356
+ amount=99.00,
357
+ currency="ILS",
358
+ )
359
+
360
+ # Refund
361
+ client.transactions.refund(
362
+ transaction_uid=result["data"]["transaction_uid"],
363
+ amount=99.00,
364
+ )
365
+ ```
366
+
367
+ ### Recurring Payments
368
+
369
+ ```python
370
+ # Create recurring from token
371
+ result = client.recurring.add(
372
+ token="card_token",
373
+ amount=49.00,
374
+ interval="month",
375
+ )
376
+
377
+ # Cancel
378
+ client.recurring.cancel(result["data"]["recurring_uid"])
379
+ ```
380
+
381
+ ## Storage Backends
382
+
383
+ ### MongoDB
384
+
385
+ ```python
386
+ from motor.motor_asyncio import AsyncIOMotorClient
387
+ from payplus.subscriptions.storage import MongoDBStorage
388
+
389
+ mongo = AsyncIOMotorClient("mongodb://localhost:27017")
390
+ storage = MongoDBStorage(mongo.your_database)
391
+ await storage.create_indexes() # Run once
392
+ ```
393
+
394
+ ### SQLAlchemy (PostgreSQL, MySQL, SQLite)
395
+
396
+ ```python
397
+ from sqlalchemy.ext.asyncio import create_async_engine
398
+ from payplus.subscriptions.storage import SQLAlchemyStorage
399
+
400
+ engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/db")
401
+ storage = SQLAlchemyStorage(engine)
402
+ await storage.create_tables() # Run once
403
+ ```
404
+
405
+ ### In-Memory (development/testing)
406
+
407
+ ```python
408
+ # Used automatically when no storage is provided
409
+ manager = SubscriptionManager(client)
410
+ ```
411
+
412
+ ## API Reference
413
+
414
+ ### PayPlus Client
415
+
416
+ | Module | Methods |
417
+ |--------|---------|
418
+ | `client.customers` | `add()` |
419
+ | `client.payment_pages` | `generate_link()`, `get_status()` |
420
+ | `client.transactions` | `charge()`, `get()`, `refund()`, `list()` |
421
+ | `client.recurring` | `add()`, `update()`, `charge()`, `cancel()`, `get()`, `list()` |
422
+ | `client.payments` | `check_card()`, `tokenize()`, `get_token()`, `delete_token()` |
423
+
424
+ ### Subscription Manager
425
+
426
+ | Method | Description |
427
+ |--------|-------------|
428
+ | `create_customer()` | Create a new customer |
429
+ | `get_customer()` | Get a customer by ID |
430
+ | `create_tier()` | Create a pricing tier |
431
+ | `get_tier()` | Get a tier by ID |
432
+ | `list_tiers()` | List all tiers |
433
+ | `create_subscription()` | Create subscription and generate payment link |
434
+ | `get_subscription()` | Get a subscription by ID |
435
+ | `change_tier()` | Upgrade/downgrade (updates PayPlus recurring) |
436
+ | `pause_subscription()` | Pause a subscription |
437
+ | `resume_subscription()` | Resume a paused subscription |
438
+ | `cancel_subscription()` | Cancel immediately or at period end |
439
+ | `handle_webhook_event()` | Process webhook and update subscription state |
440
+
441
+ ## Configuration
442
+
443
+ ```bash
444
+ PAYPLUS_API_KEY=your_api_key
445
+ PAYPLUS_SECRET_KEY=your_secret_key
446
+ PAYPLUS_TERMINAL_UID=your_terminal_uid # Optional
447
+ PAYPLUS_SANDBOX=true # For testing
448
+ ```
449
+
450
+ ```python
451
+ # Sandbox (restapidev.payplus.co.il)
452
+ client = PayPlus(api_key="...", secret_key="...", sandbox=True)
453
+
454
+ # Production (restapi.payplus.co.il)
455
+ client = PayPlus(api_key="...", secret_key="...", sandbox=False)
456
+ ```
457
+
458
+ ## Error Handling
459
+
460
+ ```python
461
+ from payplus.exceptions import (
462
+ PayPlusError,
463
+ PayPlusAPIError,
464
+ PayPlusAuthError,
465
+ SubscriptionError,
466
+ WebhookSignatureError,
467
+ )
468
+
469
+ try:
470
+ result = client.transactions.charge(token="...", amount=100)
471
+ except PayPlusAuthError:
472
+ print("Invalid API credentials")
473
+ except PayPlusAPIError as e:
474
+ print(f"API error [{e.status_code}]: {e.message}")
475
+ except PayPlusError as e:
476
+ print(f"General error: {e}")
477
+ ```
478
+
479
+ ## License
480
+
481
+ MIT License - see [LICENSE](LICENSE) for details.
482
+
483
+ ## Links
484
+
485
+ - [PayPlus Documentation](https://docs.payplus.co.il/)
486
+ - [GitHub Repository](https://github.com/Two-Solutions/payplus-python)
487
+ - [Issue Tracker](https://github.com/Two-Solutions/payplus-python/issues)