payplus-python 0.1.2__py3-none-any.whl

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,29 @@
1
+ """
2
+ Basic payment example - Generate a payment link.
3
+ """
4
+
5
+ import os
6
+ from payplus import PayPlus
7
+
8
+ # Initialize client
9
+ client = PayPlus(
10
+ api_key=os.environ.get("PAYPLUS_API_KEY", "your_api_key"),
11
+ secret_key=os.environ.get("PAYPLUS_SECRET_KEY", "your_secret_key"),
12
+ sandbox=True, # Use sandbox for testing
13
+ )
14
+
15
+ # Generate a payment link
16
+ result = client.payment_pages.generate_link(
17
+ amount=100.00,
18
+ currency="ILS",
19
+ description="Test Payment",
20
+ customer_email="test@example.com",
21
+ customer_name="Test User",
22
+ success_url="https://example.com/success",
23
+ failure_url="https://example.com/failure",
24
+ create_token=True, # Save card for future use
25
+ )
26
+
27
+ print("Payment Link Generated:")
28
+ print(f"URL: {result.get('data', {}).get('payment_page_link')}")
29
+ print(f"Request UID: {result.get('data', {}).get('page_request_uid')}")
@@ -0,0 +1,130 @@
1
+ """
2
+ FastAPI webhook handling example.
3
+ """
4
+
5
+ import os
6
+ from fastapi import FastAPI, Request, HTTPException
7
+ from payplus import PayPlus
8
+ from payplus.webhooks import WebhookHandler, WebhookSignatureError
9
+
10
+ app = FastAPI(title="PayPlus Webhook Example")
11
+
12
+ # Initialize PayPlus client
13
+ client = PayPlus(
14
+ api_key=os.environ.get("PAYPLUS_API_KEY", "your_api_key"),
15
+ secret_key=os.environ.get("PAYPLUS_SECRET_KEY", "your_secret_key"),
16
+ )
17
+
18
+ # Initialize webhook handler
19
+ webhooks = WebhookHandler(client, verify_signature=True)
20
+
21
+
22
+ # Register event handlers
23
+ @webhooks.on("payment.success")
24
+ async def handle_payment_success(event):
25
+ """Handle successful payment."""
26
+ print(f"✅ Payment succeeded!")
27
+ print(f" Transaction: {event.transaction_uid}")
28
+ print(f" Amount: {event.amount} {event.currency}")
29
+ print(f" Customer: {event.customer_email}")
30
+
31
+ # Your business logic here:
32
+ # - Update order status
33
+ # - Send confirmation email
34
+ # - Provision service
35
+ # - etc.
36
+
37
+
38
+ @webhooks.on("payment.failure")
39
+ async def handle_payment_failure(event):
40
+ """Handle failed payment."""
41
+ print(f"❌ Payment failed!")
42
+ print(f" Error: {event.status_description}")
43
+ print(f" Customer: {event.customer_email}")
44
+
45
+ # Your business logic here:
46
+ # - Update order status
47
+ # - Send notification email
48
+ # - Log for analysis
49
+
50
+
51
+ @webhooks.on("recurring.charged")
52
+ async def handle_recurring_charge(event):
53
+ """Handle recurring payment charge."""
54
+ print(f"🔄 Recurring payment charged!")
55
+ print(f" Recurring UID: {event.recurring_uid}")
56
+ print(f" Amount: {event.amount} {event.currency}")
57
+
58
+ # Your business logic here:
59
+ # - Extend subscription
60
+ # - Create invoice
61
+ # - Send receipt
62
+
63
+
64
+ @webhooks.on("recurring.failed")
65
+ async def handle_recurring_failure(event):
66
+ """Handle recurring payment failure."""
67
+ print(f"⚠️ Recurring payment failed!")
68
+ print(f" Recurring UID: {event.recurring_uid}")
69
+ print(f" Error: {event.status_description}")
70
+
71
+ # Your business logic here:
72
+ # - Mark subscription as past_due
73
+ # - Send dunning email
74
+ # - Schedule retry
75
+
76
+
77
+ @webhooks.on("token.created")
78
+ async def handle_token_created(event):
79
+ """Handle new card token."""
80
+ print(f"💳 Card tokenized!")
81
+ print(f" Token: {event.card_uid}")
82
+ print(f" Brand: {event.card_brand}")
83
+ print(f" Last 4: {event.card_last_four}")
84
+
85
+ # Your business logic here:
86
+ # - Save token to customer profile
87
+ # - Update payment method
88
+
89
+
90
+ @webhooks.on("*")
91
+ async def handle_all_events(event):
92
+ """Catch-all handler for logging."""
93
+ print(f"📨 Webhook received: {event.type.value}")
94
+
95
+
96
+ # Webhook endpoint
97
+ @app.post("/webhooks/payplus")
98
+ async def payplus_webhook(request: Request):
99
+ """
100
+ PayPlus IPN/Webhook endpoint.
101
+
102
+ Configure this URL in your PayPlus dashboard:
103
+ https://your-domain.com/webhooks/payplus
104
+ """
105
+ payload = await request.body()
106
+ signature = request.headers.get("X-PayPlus-Signature", "")
107
+
108
+ try:
109
+ event = await webhooks.handle_async(payload, signature)
110
+ return {
111
+ "received": True,
112
+ "event_id": event.id,
113
+ "event_type": event.type.value,
114
+ }
115
+ except WebhookSignatureError:
116
+ raise HTTPException(status_code=400, detail="Invalid signature")
117
+ except Exception as e:
118
+ print(f"Webhook error: {e}")
119
+ raise HTTPException(status_code=500, detail="Webhook processing failed")
120
+
121
+
122
+ # Health check
123
+ @app.get("/health")
124
+ async def health():
125
+ return {"status": "healthy"}
126
+
127
+
128
+ if __name__ == "__main__":
129
+ import uvicorn
130
+ uvicorn.run(app, host="0.0.0.0", port=8000)
@@ -0,0 +1,206 @@
1
+ """
2
+ Full SaaS subscription example with MongoDB storage.
3
+ """
4
+
5
+ import asyncio
6
+ import os
7
+ from decimal import Decimal
8
+
9
+ from payplus import PayPlus, SubscriptionManager
10
+ from payplus.subscriptions.storage import MongoDBStorage
11
+ from payplus.subscriptions import BillingService
12
+
13
+
14
+ async def main():
15
+ # Initialize PayPlus client
16
+ client = PayPlus(
17
+ api_key=os.environ.get("PAYPLUS_API_KEY", "your_api_key"),
18
+ secret_key=os.environ.get("PAYPLUS_SECRET_KEY", "your_secret_key"),
19
+ sandbox=True,
20
+ )
21
+
22
+ # Setup MongoDB storage
23
+ try:
24
+ from motor.motor_asyncio import AsyncIOMotorClient
25
+ mongo = AsyncIOMotorClient(os.environ.get("MONGODB_URI", "mongodb://localhost:27017"))
26
+ storage = MongoDBStorage(mongo.payplus_demo)
27
+ await storage.create_indexes()
28
+ except ImportError:
29
+ print("MongoDB not available, using in-memory storage")
30
+ storage = None
31
+
32
+ # Create subscription manager
33
+ manager = SubscriptionManager(client, storage)
34
+
35
+ # Register event handlers
36
+ manager.on("subscription.created", lambda s: print(f"✅ Subscription created: {s.id}"))
37
+ manager.on("subscription.activated", lambda s: print(f"🎉 Subscription activated: {s.id}"))
38
+ manager.on("payment.succeeded", lambda p: print(f"💰 Payment received: {p.amount} {p.currency}"))
39
+ manager.on("payment.failed", lambda p: print(f"❌ Payment failed: {p.failure_message}"))
40
+
41
+ # ==================== Setup Pricing Tiers ====================
42
+ print("\n📦 Creating pricing tiers...")
43
+
44
+ # Free tier
45
+ free_tier = await manager.create_tier(
46
+ tier_id="free",
47
+ name="Free",
48
+ price=Decimal("0"),
49
+ features=[
50
+ {"feature_id": "projects", "name": "Projects", "included_quantity": 3},
51
+ {"feature_id": "storage_gb", "name": "Storage (GB)", "included_quantity": 1},
52
+ {"feature_id": "team_members", "name": "Team Members", "included_quantity": 1},
53
+ ],
54
+ )
55
+ print(f" - {free_tier.name}: ₪{free_tier.price}/month")
56
+
57
+ # Basic tier
58
+ basic_tier = await manager.create_tier(
59
+ tier_id="basic",
60
+ name="Basic",
61
+ price=Decimal("29"),
62
+ trial_days=7,
63
+ features=[
64
+ {"feature_id": "projects", "name": "Projects", "included_quantity": 10},
65
+ {"feature_id": "storage_gb", "name": "Storage (GB)", "included_quantity": 10},
66
+ {"feature_id": "team_members", "name": "Team Members", "included_quantity": 5},
67
+ {"feature_id": "api_access", "name": "API Access"},
68
+ ],
69
+ )
70
+ print(f" - {basic_tier.name}: ₪{basic_tier.price}/month (7-day trial)")
71
+
72
+ # Pro tier
73
+ pro_tier = await manager.create_tier(
74
+ tier_id="pro",
75
+ name="Pro",
76
+ price=Decimal("79"),
77
+ trial_days=14,
78
+ is_popular=True,
79
+ annual_discount_percent=Decimal("20"),
80
+ features=[
81
+ {"feature_id": "projects", "name": "Projects", "included_quantity": None}, # Unlimited
82
+ {"feature_id": "storage_gb", "name": "Storage (GB)", "included_quantity": 100},
83
+ {"feature_id": "team_members", "name": "Team Members", "included_quantity": 20},
84
+ {"feature_id": "api_access", "name": "API Access"},
85
+ {"feature_id": "priority_support", "name": "Priority Support"},
86
+ {"feature_id": "custom_domain", "name": "Custom Domain"},
87
+ ],
88
+ )
89
+ print(f" - {pro_tier.name}: ₪{pro_tier.price}/month (14-day trial, 20% annual discount)")
90
+
91
+ # Enterprise tier
92
+ enterprise_tier = await manager.create_tier(
93
+ tier_id="enterprise",
94
+ name="Enterprise",
95
+ price=Decimal("199"),
96
+ annual_discount_percent=Decimal("25"),
97
+ features=[
98
+ {"feature_id": "projects", "name": "Projects", "included_quantity": None},
99
+ {"feature_id": "storage_gb", "name": "Storage (GB)", "included_quantity": None},
100
+ {"feature_id": "team_members", "name": "Team Members", "included_quantity": None},
101
+ {"feature_id": "api_access", "name": "API Access"},
102
+ {"feature_id": "priority_support", "name": "Priority Support"},
103
+ {"feature_id": "custom_domain", "name": "Custom Domain"},
104
+ {"feature_id": "sso", "name": "SSO Integration"},
105
+ {"feature_id": "dedicated_support", "name": "Dedicated Account Manager"},
106
+ ],
107
+ )
108
+ print(f" - {enterprise_tier.name}: ₪{enterprise_tier.price}/month (25% annual discount)")
109
+
110
+ # ==================== Create Customer ====================
111
+ print("\n👤 Creating customer...")
112
+
113
+ customer = await manager.create_customer(
114
+ email="demo@example.com",
115
+ name="Demo User",
116
+ metadata={"source": "signup_form", "campaign": "launch_2024"},
117
+ )
118
+ print(f" Customer ID: {customer.id}")
119
+ print(f" Email: {customer.email}")
120
+
121
+ # ==================== Add Payment Method ====================
122
+ print("\n💳 Adding payment method...")
123
+
124
+ # In production, this token comes from a PayPlus payment page
125
+ # For demo, we'll simulate with a fake token
126
+ demo_token = "demo_card_token_xxx"
127
+
128
+ payment_method = await manager.add_payment_method(
129
+ customer_id=customer.id,
130
+ token=demo_token,
131
+ card_brand="Visa",
132
+ last_four="4242",
133
+ expiry_month="12",
134
+ expiry_year="2028",
135
+ )
136
+ print(f" Payment Method: {payment_method.card_brand} ****{payment_method.last_four}")
137
+
138
+ # ==================== Create Subscription ====================
139
+ print("\n📋 Creating Pro subscription...")
140
+
141
+ subscription = await manager.create_subscription(
142
+ customer_id=customer.id,
143
+ tier_id="pro",
144
+ )
145
+
146
+ print(f" Subscription ID: {subscription.id}")
147
+ print(f" Status: {subscription.status}")
148
+ print(f" Amount: ₪{subscription.amount}/month")
149
+ print(f" Trial ends: {subscription.trial_end}")
150
+ print(f" Current period: {subscription.current_period_start} - {subscription.current_period_end}")
151
+
152
+ # ==================== Subscription Operations ====================
153
+ print("\n⚙️ Subscription operations...")
154
+
155
+ # Check if active
156
+ print(f" Is active: {subscription.is_active}")
157
+ print(f" Is trialing: {subscription.is_trialing}")
158
+
159
+ # Upgrade to enterprise
160
+ print("\n 📈 Upgrading to Enterprise...")
161
+ subscription = await manager.change_tier(subscription.id, "enterprise")
162
+ print(f" New tier: {subscription.tier_id}")
163
+ print(f" New amount: ₪{subscription.amount}/month")
164
+
165
+ # Pause subscription
166
+ print("\n ⏸️ Pausing subscription...")
167
+ subscription = await manager.pause_subscription(subscription.id)
168
+ print(f" Status: {subscription.status}")
169
+
170
+ # Resume subscription
171
+ print("\n ▶️ Resuming subscription...")
172
+ subscription = await manager.resume_subscription(subscription.id)
173
+ print(f" Status: {subscription.status}")
174
+
175
+ # Cancel at period end
176
+ print("\n 🚫 Scheduling cancellation...")
177
+ subscription = await manager.cancel_subscription(
178
+ subscription.id,
179
+ at_period_end=True,
180
+ reason="Demo completed",
181
+ )
182
+ print(f" Will cancel at: {subscription.current_period_end}")
183
+ print(f" Cancellation reason: {subscription.cancellation_reason}")
184
+
185
+ # ==================== Billing Service ====================
186
+ print("\n💵 Billing service...")
187
+
188
+ billing = BillingService(manager)
189
+
190
+ # In production, these would run on a schedule
191
+ print(" Processing due renewals...")
192
+ renewed = await billing.process_due_renewals()
193
+ print(f" Renewed: {len(renewed)} subscriptions")
194
+
195
+ print(" Processing trial endings...")
196
+ converted = await billing.process_trial_endings()
197
+ print(f" Converted: {len(converted)} trials")
198
+
199
+ print("\n✅ Demo completed!")
200
+
201
+ # Cleanup
202
+ client.close()
203
+
204
+
205
+ if __name__ == "__main__":
206
+ asyncio.run(main())
payplus/__init__.py ADDED
@@ -0,0 +1,30 @@
1
+ """
2
+ PayPlus Python SDK - Payment gateway integration with subscription management for SaaS apps.
3
+ """
4
+
5
+ from payplus.client import PayPlus
6
+ from payplus.subscriptions.manager import SubscriptionManager
7
+ from payplus.models.subscription import (
8
+ Subscription,
9
+ SubscriptionStatus,
10
+ BillingCycle,
11
+ )
12
+ from payplus.models.customer import Customer
13
+ from payplus.models.payment import Payment, PaymentStatus
14
+ from payplus.models.invoice import Invoice, InvoiceStatus
15
+ from payplus.models.tier import Tier
16
+
17
+ __version__ = "0.1.0"
18
+ __all__ = [
19
+ "PayPlus",
20
+ "SubscriptionManager",
21
+ "Subscription",
22
+ "SubscriptionStatus",
23
+ "BillingCycle",
24
+ "Customer",
25
+ "Payment",
26
+ "PaymentStatus",
27
+ "Invoice",
28
+ "InvoiceStatus",
29
+ "Tier",
30
+ ]
@@ -0,0 +1,15 @@
1
+ """
2
+ PayPlus API modules.
3
+ """
4
+
5
+ from payplus.api.payments import PaymentsAPI
6
+ from payplus.api.recurring import RecurringAPI
7
+ from payplus.api.transactions import TransactionsAPI
8
+ from payplus.api.payment_pages import PaymentPagesAPI
9
+
10
+ __all__ = [
11
+ "PaymentsAPI",
12
+ "RecurringAPI",
13
+ "TransactionsAPI",
14
+ "PaymentPagesAPI",
15
+ ]
payplus/api/base.py ADDED
@@ -0,0 +1,37 @@
1
+ """
2
+ Base API class for PayPlus endpoints.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import TYPE_CHECKING, Any, Optional
8
+
9
+ if TYPE_CHECKING:
10
+ from payplus.client import PayPlus
11
+
12
+
13
+ class BaseAPI:
14
+ """Base class for API endpoints."""
15
+
16
+ def __init__(self, client: "PayPlus"):
17
+ self._client = client
18
+
19
+ def _request(
20
+ self,
21
+ method: str,
22
+ endpoint: str,
23
+ data: Optional[dict[str, Any]] = None,
24
+ params: Optional[dict[str, Any]] = None,
25
+ ) -> dict[str, Any]:
26
+ """Make a synchronous API request."""
27
+ return self._client._request(method, endpoint, data, params)
28
+
29
+ async def _async_request(
30
+ self,
31
+ method: str,
32
+ endpoint: str,
33
+ data: Optional[dict[str, Any]] = None,
34
+ params: Optional[dict[str, Any]] = None,
35
+ ) -> dict[str, Any]:
36
+ """Make an asynchronous API request."""
37
+ return await self._client._async_request(method, endpoint, data, params)
@@ -0,0 +1,176 @@
1
+ """
2
+ PayPlus Payment Pages API.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Any, Optional
8
+ from decimal import Decimal
9
+
10
+ from payplus.api.base import BaseAPI
11
+
12
+
13
+ class PaymentPagesAPI(BaseAPI):
14
+ """
15
+ Payment Pages API for generating payment links and handling hosted checkout.
16
+ """
17
+
18
+ def generate_link(
19
+ self,
20
+ amount: float | Decimal,
21
+ currency: str = "ILS",
22
+ description: Optional[str] = None,
23
+ customer_uid: Optional[str] = None,
24
+ customer_email: Optional[str] = None,
25
+ customer_name: Optional[str] = None,
26
+ customer_phone: Optional[str] = None,
27
+ charge_method: int = 1,
28
+ payments: int = 1,
29
+ min_payments: int = 1,
30
+ max_payments: int = 12,
31
+ success_url: Optional[str] = None,
32
+ failure_url: Optional[str] = None,
33
+ cancel_url: Optional[str] = None,
34
+ callback_url: Optional[str] = None,
35
+ create_token: bool = False,
36
+ refurl_address: Optional[str] = None,
37
+ language: str = "he",
38
+ expiration_time: int = 60,
39
+ more_info: Optional[str] = None,
40
+ more_info_1: Optional[str] = None,
41
+ more_info_2: Optional[str] = None,
42
+ items: Optional[list[dict[str, Any]]] = None,
43
+ **kwargs: Any,
44
+ ) -> dict[str, Any]:
45
+ """
46
+ Generate a payment link for hosted checkout.
47
+
48
+ Args:
49
+ amount: Payment amount
50
+ currency: Currency code (default: ILS)
51
+ description: Payment description
52
+ customer_uid: Existing customer UID
53
+ customer_email: Customer email
54
+ customer_name: Customer name
55
+ customer_phone: Customer phone
56
+ charge_method: Charge method (1=regular, 2=recurring, 3=token only)
57
+ payments: Number of installments
58
+ min_payments: Minimum installments
59
+ max_payments: Maximum installments
60
+ success_url: URL to redirect on success
61
+ failure_url: URL to redirect on failure
62
+ cancel_url: URL to redirect on cancel
63
+ callback_url: IPN/webhook callback URL
64
+ create_token: Whether to tokenize the card
65
+ refurl_address: Refund URL
66
+ language: Page language (he/en)
67
+ expiration_time: Link expiration in minutes
68
+ more_info: Custom field 1
69
+ more_info_1: Custom field 2
70
+ more_info_2: Custom field 3
71
+ items: List of items with name, price, quantity
72
+
73
+ Returns:
74
+ API response with payment link URL
75
+ """
76
+ data: dict[str, Any] = {
77
+ "payment_page_uid": kwargs.get("payment_page_uid"),
78
+ "charge_method": charge_method,
79
+ "amount": float(amount),
80
+ "currency_code": currency,
81
+ "sendEmailApproval": kwargs.get("send_email_approval", True),
82
+ "sendEmailFailure": kwargs.get("send_email_failure", False),
83
+ "create_token": create_token,
84
+ "payments": payments,
85
+ "min_payments": min_payments,
86
+ "max_payments": max_payments,
87
+ "language_code": language,
88
+ "expiry_datetime": expiration_time,
89
+ }
90
+
91
+ if description:
92
+ data["more_info"] = description
93
+ if more_info:
94
+ data["more_info"] = more_info
95
+ if more_info_1:
96
+ data["more_info_1"] = more_info_1
97
+ if more_info_2:
98
+ data["more_info_2"] = more_info_2
99
+
100
+ # Customer info
101
+ customer = {}
102
+ if customer_uid:
103
+ customer["customer_uid"] = customer_uid
104
+ if customer_email:
105
+ customer["email"] = customer_email
106
+ if customer_name:
107
+ customer["customer_name"] = customer_name
108
+ if customer_phone:
109
+ customer["phone"] = customer_phone
110
+ if customer:
111
+ data["customer"] = customer
112
+
113
+ # URLs
114
+ if success_url:
115
+ data["success_page_url"] = success_url
116
+ if failure_url:
117
+ data["failure_page_url"] = failure_url
118
+ if cancel_url:
119
+ data["cancel_page_url"] = cancel_url
120
+ if callback_url:
121
+ data["callback_url"] = callback_url
122
+ if refurl_address:
123
+ data["refurl_address"] = refurl_address
124
+
125
+ # Items
126
+ if items:
127
+ data["items"] = items
128
+
129
+ # Add terminal if set
130
+ if self._client.terminal_uid:
131
+ data["terminal_uid"] = self._client.terminal_uid
132
+
133
+ # Add any extra kwargs
134
+ for key, value in kwargs.items():
135
+ if key not in data and value is not None:
136
+ data[key] = value
137
+
138
+ return self._request("POST", "PaymentPages/generateLink", data)
139
+
140
+ async def async_generate_link(
141
+ self,
142
+ amount: float | Decimal,
143
+ **kwargs: Any,
144
+ ) -> dict[str, Any]:
145
+ """Async version of generate_link."""
146
+ # Build data same as sync version
147
+ data: dict[str, Any] = {
148
+ "charge_method": kwargs.get("charge_method", 1),
149
+ "amount": float(amount),
150
+ "currency_code": kwargs.get("currency", "ILS"),
151
+ "sendEmailApproval": kwargs.get("send_email_approval", True),
152
+ "sendEmailFailure": kwargs.get("send_email_failure", False),
153
+ "create_token": kwargs.get("create_token", False),
154
+ "payments": kwargs.get("payments", 1),
155
+ "language_code": kwargs.get("language", "he"),
156
+ }
157
+
158
+ if kwargs.get("description"):
159
+ data["more_info"] = kwargs["description"]
160
+
161
+ if self._client.terminal_uid:
162
+ data["terminal_uid"] = self._client.terminal_uid
163
+
164
+ return await self._async_request("POST", "PaymentPages/generateLink", data)
165
+
166
+ def get_status(self, page_request_uid: str) -> dict[str, Any]:
167
+ """
168
+ Get payment page status.
169
+
170
+ Args:
171
+ page_request_uid: Payment page request UID
172
+
173
+ Returns:
174
+ Payment page status
175
+ """
176
+ return self._request("GET", f"PaymentPages/{page_request_uid}")