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