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.
- examples/basic_payment.py +29 -0
- examples/fastapi_webhooks.py +130 -0
- examples/subscription_saas.py +206 -0
- payplus/__init__.py +30 -0
- payplus/api/__init__.py +15 -0
- payplus/api/base.py +37 -0
- payplus/api/payment_pages.py +176 -0
- payplus/api/payments.py +117 -0
- payplus/api/recurring.py +216 -0
- payplus/api/transactions.py +203 -0
- payplus/client.py +211 -0
- payplus/exceptions.py +57 -0
- payplus/models/__init__.py +23 -0
- payplus/models/customer.py +136 -0
- payplus/models/invoice.py +242 -0
- payplus/models/payment.py +179 -0
- payplus/models/subscription.py +193 -0
- payplus/models/tier.py +226 -0
- payplus/subscriptions/__init__.py +11 -0
- payplus/subscriptions/billing.py +231 -0
- payplus/subscriptions/manager.py +600 -0
- payplus/subscriptions/storage.py +571 -0
- payplus/webhooks/__init__.py +10 -0
- payplus/webhooks/handler.py +370 -0
- payplus_python-0.1.2.dist-info/METADATA +446 -0
- payplus_python-0.1.2.dist-info/RECORD +31 -0
- payplus_python-0.1.2.dist-info/WHEEL +5 -0
- payplus_python-0.1.2.dist-info/licenses/LICENSE +21 -0
- payplus_python-0.1.2.dist-info/top_level.txt +3 -0
- tests/__init__.py +1 -0
- tests/test_models.py +348 -0
|
@@ -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
|
+
]
|
payplus/api/__init__.py
ADDED
|
@@ -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}")
|