Initial commit
This commit is contained in:
467
skills/paypal-integration/SKILL.md
Normal file
467
skills/paypal-integration/SKILL.md
Normal file
@@ -0,0 +1,467 @@
|
||||
---
|
||||
name: paypal-integration
|
||||
description: Integrate PayPal payment processing with support for express checkout, subscriptions, and refund management. Use when implementing PayPal payments, processing online transactions, or building e-commerce checkout flows.
|
||||
---
|
||||
|
||||
# PayPal Integration
|
||||
|
||||
Master PayPal payment integration including Express Checkout, IPN handling, recurring billing, and refund workflows.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Integrating PayPal as a payment option
|
||||
- Implementing express checkout flows
|
||||
- Setting up recurring billing with PayPal
|
||||
- Processing refunds and payment disputes
|
||||
- Handling PayPal webhooks (IPN)
|
||||
- Supporting international payments
|
||||
- Implementing PayPal subscriptions
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### 1. Payment Products
|
||||
**PayPal Checkout**
|
||||
- One-time payments
|
||||
- Express checkout experience
|
||||
- Guest and PayPal account payments
|
||||
|
||||
**PayPal Subscriptions**
|
||||
- Recurring billing
|
||||
- Subscription plans
|
||||
- Automatic renewals
|
||||
|
||||
**PayPal Payouts**
|
||||
- Send money to multiple recipients
|
||||
- Marketplace and platform payments
|
||||
|
||||
### 2. Integration Methods
|
||||
**Client-Side (JavaScript SDK)**
|
||||
- Smart Payment Buttons
|
||||
- Hosted payment flow
|
||||
- Minimal backend code
|
||||
|
||||
**Server-Side (REST API)**
|
||||
- Full control over payment flow
|
||||
- Custom checkout UI
|
||||
- Advanced features
|
||||
|
||||
### 3. IPN (Instant Payment Notification)
|
||||
- Webhook-like payment notifications
|
||||
- Asynchronous payment updates
|
||||
- Verification required
|
||||
|
||||
## Quick Start
|
||||
|
||||
```javascript
|
||||
// Frontend - PayPal Smart Buttons
|
||||
<div id="paypal-button-container"></div>
|
||||
|
||||
<script src="https://www.paypal.com/sdk/js?client-id=YOUR_CLIENT_ID¤cy=USD"></script>
|
||||
<script>
|
||||
paypal.Buttons({
|
||||
createOrder: function(data, actions) {
|
||||
return actions.order.create({
|
||||
purchase_units: [{
|
||||
amount: {
|
||||
value: '25.00'
|
||||
}
|
||||
}]
|
||||
});
|
||||
},
|
||||
onApprove: function(data, actions) {
|
||||
return actions.order.capture().then(function(details) {
|
||||
// Payment successful
|
||||
console.log('Transaction completed by ' + details.payer.name.given_name);
|
||||
|
||||
// Send to backend for verification
|
||||
fetch('/api/paypal/capture', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({orderID: data.orderID})
|
||||
});
|
||||
});
|
||||
}
|
||||
}).render('#paypal-button-container');
|
||||
</script>
|
||||
```
|
||||
|
||||
```python
|
||||
# Backend - Verify and capture order
|
||||
from paypalrestsdk import Payment
|
||||
import paypalrestsdk
|
||||
|
||||
paypalrestsdk.configure({
|
||||
"mode": "sandbox", # or "live"
|
||||
"client_id": "YOUR_CLIENT_ID",
|
||||
"client_secret": "YOUR_CLIENT_SECRET"
|
||||
})
|
||||
|
||||
def capture_paypal_order(order_id):
|
||||
"""Capture a PayPal order."""
|
||||
payment = Payment.find(order_id)
|
||||
|
||||
if payment.execute({"payer_id": payment.payer.payer_info.payer_id}):
|
||||
# Payment successful
|
||||
return {
|
||||
'status': 'success',
|
||||
'transaction_id': payment.id,
|
||||
'amount': payment.transactions[0].amount.total
|
||||
}
|
||||
else:
|
||||
# Payment failed
|
||||
return {
|
||||
'status': 'failed',
|
||||
'error': payment.error
|
||||
}
|
||||
```
|
||||
|
||||
## Express Checkout Implementation
|
||||
|
||||
### Server-Side Order Creation
|
||||
```python
|
||||
import requests
|
||||
import json
|
||||
|
||||
class PayPalClient:
|
||||
def __init__(self, client_id, client_secret, mode='sandbox'):
|
||||
self.client_id = client_id
|
||||
self.client_secret = client_secret
|
||||
self.base_url = 'https://api-m.sandbox.paypal.com' if mode == 'sandbox' else 'https://api-m.paypal.com'
|
||||
self.access_token = self.get_access_token()
|
||||
|
||||
def get_access_token(self):
|
||||
"""Get OAuth access token."""
|
||||
url = f"{self.base_url}/v1/oauth2/token"
|
||||
headers = {"Accept": "application/json", "Accept-Language": "en_US"}
|
||||
|
||||
response = requests.post(
|
||||
url,
|
||||
headers=headers,
|
||||
data={"grant_type": "client_credentials"},
|
||||
auth=(self.client_id, self.client_secret)
|
||||
)
|
||||
|
||||
return response.json()['access_token']
|
||||
|
||||
def create_order(self, amount, currency='USD'):
|
||||
"""Create a PayPal order."""
|
||||
url = f"{self.base_url}/v2/checkout/orders"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {self.access_token}"
|
||||
}
|
||||
|
||||
payload = {
|
||||
"intent": "CAPTURE",
|
||||
"purchase_units": [{
|
||||
"amount": {
|
||||
"currency_code": currency,
|
||||
"value": str(amount)
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
return response.json()
|
||||
|
||||
def capture_order(self, order_id):
|
||||
"""Capture payment for an order."""
|
||||
url = f"{self.base_url}/v2/checkout/orders/{order_id}/capture"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {self.access_token}"
|
||||
}
|
||||
|
||||
response = requests.post(url, headers=headers)
|
||||
return response.json()
|
||||
|
||||
def get_order_details(self, order_id):
|
||||
"""Get order details."""
|
||||
url = f"{self.base_url}/v2/checkout/orders/{order_id}"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.access_token}"
|
||||
}
|
||||
|
||||
response = requests.get(url, headers=headers)
|
||||
return response.json()
|
||||
```
|
||||
|
||||
## IPN (Instant Payment Notification) Handling
|
||||
|
||||
### IPN Verification and Processing
|
||||
```python
|
||||
from flask import Flask, request
|
||||
import requests
|
||||
from urllib.parse import parse_qs
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route('/ipn', methods=['POST'])
|
||||
def handle_ipn():
|
||||
"""Handle PayPal IPN notifications."""
|
||||
# Get IPN message
|
||||
ipn_data = request.form.to_dict()
|
||||
|
||||
# Verify IPN with PayPal
|
||||
if not verify_ipn(ipn_data):
|
||||
return 'IPN verification failed', 400
|
||||
|
||||
# Process IPN based on transaction type
|
||||
payment_status = ipn_data.get('payment_status')
|
||||
txn_type = ipn_data.get('txn_type')
|
||||
|
||||
if payment_status == 'Completed':
|
||||
handle_payment_completed(ipn_data)
|
||||
elif payment_status == 'Refunded':
|
||||
handle_refund(ipn_data)
|
||||
elif payment_status == 'Reversed':
|
||||
handle_chargeback(ipn_data)
|
||||
|
||||
return 'IPN processed', 200
|
||||
|
||||
def verify_ipn(ipn_data):
|
||||
"""Verify IPN message authenticity."""
|
||||
# Add 'cmd' parameter
|
||||
verify_data = ipn_data.copy()
|
||||
verify_data['cmd'] = '_notify-validate'
|
||||
|
||||
# Send back to PayPal for verification
|
||||
paypal_url = 'https://ipnpb.sandbox.paypal.com/cgi-bin/webscr' # or production URL
|
||||
|
||||
response = requests.post(paypal_url, data=verify_data)
|
||||
|
||||
return response.text == 'VERIFIED'
|
||||
|
||||
def handle_payment_completed(ipn_data):
|
||||
"""Process completed payment."""
|
||||
txn_id = ipn_data.get('txn_id')
|
||||
payer_email = ipn_data.get('payer_email')
|
||||
mc_gross = ipn_data.get('mc_gross')
|
||||
item_name = ipn_data.get('item_name')
|
||||
|
||||
# Check if already processed (prevent duplicates)
|
||||
if is_transaction_processed(txn_id):
|
||||
return
|
||||
|
||||
# Update database
|
||||
# Send confirmation email
|
||||
# Fulfill order
|
||||
print(f"Payment completed: {txn_id}, Amount: ${mc_gross}")
|
||||
|
||||
def handle_refund(ipn_data):
|
||||
"""Handle refund."""
|
||||
parent_txn_id = ipn_data.get('parent_txn_id')
|
||||
mc_gross = ipn_data.get('mc_gross')
|
||||
|
||||
# Process refund in your system
|
||||
print(f"Refund processed: {parent_txn_id}, Amount: ${mc_gross}")
|
||||
|
||||
def handle_chargeback(ipn_data):
|
||||
"""Handle payment reversal/chargeback."""
|
||||
txn_id = ipn_data.get('txn_id')
|
||||
reason_code = ipn_data.get('reason_code')
|
||||
|
||||
# Handle chargeback
|
||||
print(f"Chargeback: {txn_id}, Reason: {reason_code}")
|
||||
```
|
||||
|
||||
## Subscription/Recurring Billing
|
||||
|
||||
### Create Subscription Plan
|
||||
```python
|
||||
def create_subscription_plan(name, amount, interval='MONTH'):
|
||||
"""Create a subscription plan."""
|
||||
client = PayPalClient(CLIENT_ID, CLIENT_SECRET)
|
||||
|
||||
url = f"{client.base_url}/v1/billing/plans"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {client.access_token}"
|
||||
}
|
||||
|
||||
payload = {
|
||||
"product_id": "PRODUCT_ID", # Create product first
|
||||
"name": name,
|
||||
"billing_cycles": [{
|
||||
"frequency": {
|
||||
"interval_unit": interval,
|
||||
"interval_count": 1
|
||||
},
|
||||
"tenure_type": "REGULAR",
|
||||
"sequence": 1,
|
||||
"total_cycles": 0, # Infinite
|
||||
"pricing_scheme": {
|
||||
"fixed_price": {
|
||||
"value": str(amount),
|
||||
"currency_code": "USD"
|
||||
}
|
||||
}
|
||||
}],
|
||||
"payment_preferences": {
|
||||
"auto_bill_outstanding": True,
|
||||
"setup_fee": {
|
||||
"value": "0",
|
||||
"currency_code": "USD"
|
||||
},
|
||||
"setup_fee_failure_action": "CONTINUE",
|
||||
"payment_failure_threshold": 3
|
||||
}
|
||||
}
|
||||
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
return response.json()
|
||||
|
||||
def create_subscription(plan_id, subscriber_email):
|
||||
"""Create a subscription for a customer."""
|
||||
client = PayPalClient(CLIENT_ID, CLIENT_SECRET)
|
||||
|
||||
url = f"{client.base_url}/v1/billing/subscriptions"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {client.access_token}"
|
||||
}
|
||||
|
||||
payload = {
|
||||
"plan_id": plan_id,
|
||||
"subscriber": {
|
||||
"email_address": subscriber_email
|
||||
},
|
||||
"application_context": {
|
||||
"return_url": "https://yourdomain.com/subscription/success",
|
||||
"cancel_url": "https://yourdomain.com/subscription/cancel"
|
||||
}
|
||||
}
|
||||
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
subscription = response.json()
|
||||
|
||||
# Get approval URL
|
||||
for link in subscription.get('links', []):
|
||||
if link['rel'] == 'approve':
|
||||
return {
|
||||
'subscription_id': subscription['id'],
|
||||
'approval_url': link['href']
|
||||
}
|
||||
```
|
||||
|
||||
## Refund Workflows
|
||||
|
||||
```python
|
||||
def create_refund(capture_id, amount=None, note=None):
|
||||
"""Create a refund for a captured payment."""
|
||||
client = PayPalClient(CLIENT_ID, CLIENT_SECRET)
|
||||
|
||||
url = f"{client.base_url}/v2/payments/captures/{capture_id}/refund"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {client.access_token}"
|
||||
}
|
||||
|
||||
payload = {}
|
||||
if amount:
|
||||
payload["amount"] = {
|
||||
"value": str(amount),
|
||||
"currency_code": "USD"
|
||||
}
|
||||
|
||||
if note:
|
||||
payload["note_to_payer"] = note
|
||||
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
return response.json()
|
||||
|
||||
def get_refund_details(refund_id):
|
||||
"""Get refund details."""
|
||||
client = PayPalClient(CLIENT_ID, CLIENT_SECRET)
|
||||
|
||||
url = f"{client.base_url}/v2/payments/refunds/{refund_id}"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {client.access_token}"
|
||||
}
|
||||
|
||||
response = requests.get(url, headers=headers)
|
||||
return response.json()
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```python
|
||||
class PayPalError(Exception):
|
||||
"""Custom PayPal error."""
|
||||
pass
|
||||
|
||||
def handle_paypal_api_call(api_function):
|
||||
"""Wrapper for PayPal API calls with error handling."""
|
||||
try:
|
||||
result = api_function()
|
||||
return result
|
||||
except requests.exceptions.RequestException as e:
|
||||
# Network error
|
||||
raise PayPalError(f"Network error: {str(e)}")
|
||||
except Exception as e:
|
||||
# Other errors
|
||||
raise PayPalError(f"PayPal API error: {str(e)}")
|
||||
|
||||
# Usage
|
||||
try:
|
||||
order = handle_paypal_api_call(lambda: client.create_order(25.00))
|
||||
except PayPalError as e:
|
||||
# Handle error appropriately
|
||||
log_error(e)
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```python
|
||||
# Use sandbox credentials
|
||||
SANDBOX_CLIENT_ID = "..."
|
||||
SANDBOX_SECRET = "..."
|
||||
|
||||
# Test accounts
|
||||
# Create test buyer and seller accounts at developer.paypal.com
|
||||
|
||||
def test_payment_flow():
|
||||
"""Test complete payment flow."""
|
||||
client = PayPalClient(SANDBOX_CLIENT_ID, SANDBOX_SECRET, mode='sandbox')
|
||||
|
||||
# Create order
|
||||
order = client.create_order(10.00)
|
||||
assert 'id' in order
|
||||
|
||||
# Get approval URL
|
||||
approval_url = next((link['href'] for link in order['links'] if link['rel'] == 'approve'), None)
|
||||
assert approval_url is not None
|
||||
|
||||
# After approval (manual step with test account)
|
||||
# Capture order
|
||||
# captured = client.capture_order(order['id'])
|
||||
# assert captured['status'] == 'COMPLETED'
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- **references/express-checkout.md**: Express Checkout implementation guide
|
||||
- **references/ipn-handling.md**: IPN verification and processing
|
||||
- **references/refund-workflows.md**: Refund handling patterns
|
||||
- **references/billing-agreements.md**: Recurring billing setup
|
||||
- **assets/paypal-client.py**: Production PayPal client
|
||||
- **assets/ipn-processor.py**: IPN webhook processor
|
||||
- **assets/recurring-billing.py**: Subscription management
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always Verify IPN**: Never trust IPN without verification
|
||||
2. **Idempotent Processing**: Handle duplicate IPN notifications
|
||||
3. **Error Handling**: Implement robust error handling
|
||||
4. **Logging**: Log all transactions and errors
|
||||
5. **Test Thoroughly**: Use sandbox extensively
|
||||
6. **Webhook Backup**: Don't rely solely on client-side callbacks
|
||||
7. **Currency Handling**: Always specify currency explicitly
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- **Not Verifying IPN**: Accepting IPN without verification
|
||||
- **Duplicate Processing**: Not checking for duplicate transactions
|
||||
- **Wrong Environment**: Mixing sandbox and production URLs/credentials
|
||||
- **Missing Webhooks**: Not handling all payment states
|
||||
- **Hardcoded Values**: Not making configurable for different environments
|
||||
Reference in New Issue
Block a user