Skip to content

Commit b3f5326

Browse files
committed
feat(readme): updated
♬ Arion - Blasphemous Paradise
1 parent 9ea10c4 commit b3f5326

File tree

2 files changed

+119
-20
lines changed

2 files changed

+119
-20
lines changed

paypal_subscription/paypal.py

+114-19
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@ class PayPalAPI:
1515
def __init__(self):
1616
self.client_id = os.getenv("PAYPAL_CLIENT_ID")
1717
self.client_secret = os.getenv("PAYPAL_CLIENT_SECRET")
18-
self.base_url = "https://api.sandbox.paypal.com" if os.getenv("PAYPAL_SANDBOX", "True") == True else "https://api.paypal.com"
19-
self.access_token = self._get_access_token()
18+
self.base_url = "https://api.sandbox.paypal.com" if os.getenv("PAYPAL_SANDBOX", "True") == "True" else "https://api.paypal.com"
19+
if self.client_id != "" and self.client_secret != "":
20+
self.access_token = self._get_access_token()
21+
else:
22+
raise ValueError("Missing Paypal Secrets")
2023
self.headers = {"Authorization": f"Bearer {self.access_token}", "Content-Type": "application/json"}
2124
self.plan_id = ""
2225

@@ -65,42 +68,39 @@ def subscription_exists(self, subscription_id: str) -> bool:
6568
response.raise_for_status()
6669
return False
6770

68-
def verify_paypal_response(self, token: str, subscription_id: str) -> Dict[str, Any]:
71+
def verify_subscription(self, subscription_id: str, payer_id: str) -> Dict[str, Any]:
6972
"""
7073
Verify PayPal response by checking the subscription details.
7174
7275
Args:
73-
token (str): PayPal transaction token.
74-
subscription_id (str): PayPal Payer ID.
76+
subscription_id (str): PayPal Subscription ID.
77+
payer_id (str): PayPal Payer ID.
7578
7679
Returns:
7780
Dict[str, Any]: Verification result.
7881
"""
79-
if not token or not subscription_id:
80-
return {"status": "error", "message": "Token or subscription_id missing"}
82+
if not subscription_id or not payer_id:
83+
return {"status": "error", "message": "Subscription ID or Payer ID missing"}
8184

8285
try:
83-
subscription_details = self.subscription_exists(token)
86+
subscription_details = self.subscription_exists(subscription_id)
8487
if subscription_details == False:
8588
return {"status": "error", "message": "Subscription check failed"}
8689

87-
if subscription_details.get("id") != token:
88-
return {"status": "error", "message": "Token does not match subscription"}
89-
9090
subscriber_info = subscription_details.get("subscriber", {})
91-
stored_payer_id = subscriber_info.get("subscription_id")
91+
stored_payer_id = subscriber_info.get("payer_id")
9292

93-
if stored_payer_id and stored_payer_id != subscription_id:
94-
return {"status": "error", "message": "subscription_id does not match"}
93+
if stored_payer_id and stored_payer_id != payer_id:
94+
return {"status": "error", "message": "Payer ID does not match"}
9595

9696
status = subscription_details.get("status")
9797
if os.getenv("DEBUG", False):
9898
if status == "ACTIVE":
99-
print(f"Subscription {token} is active.")
99+
print(f"Subscription {subscription_id} is active.")
100100
elif status == "CANCELLED":
101-
print(f"Subscription {token} is cancelled.")
101+
print(f"Subscription {subscription_id} is cancelled.")
102102
else:
103-
print(f"Subscription {token} status: {status}.")
103+
print(f"Subscription {subscription_id} status: {status}.")
104104

105105
return {
106106
"status": "success",
@@ -110,6 +110,35 @@ def verify_paypal_response(self, token: str, subscription_id: str) -> Dict[str,
110110
except requests.exceptions.RequestException as e:
111111
return {"status": "error", "message": f"PayPal API error: {e}"}
112112

113+
def verify_payment(self, order_id: str) -> Dict[str, Any]:
114+
"""
115+
Verify the payment by checking the order details.
116+
117+
Args:
118+
order_id (str): PayPal Order ID.
119+
120+
Returns:
121+
Dict[str, Any]: Verification result.
122+
"""
123+
url = f"{self.base_url}/v2/checkout/orders/{order_id}"
124+
response = requests.get(url, headers=self.headers)
125+
response.raise_for_status()
126+
order_details = response.json()
127+
128+
if order_details['status'] == 'COMPLETED':
129+
return {
130+
"status": "success",
131+
"order_id": order_id,
132+
"payer_email": order_details['payer']['email_address'],
133+
"amount": order_details['purchase_units'][0]['amount']['value'],
134+
"currency": order_details['purchase_units'][0]['amount']['currency_code']
135+
}
136+
else:
137+
return {
138+
"status": "error",
139+
"order_details": order_details
140+
}
141+
113142
def create_product(self, name: str, description: str, type_: str = "SERVICE", category: str = "SOFTWARE") -> Dict[str, Any]:
114143
"""
115144
Create a product for subscription.
@@ -132,7 +161,7 @@ def create_product(self, name: str, description: str, type_: str = "SERVICE", ca
132161
url = f"{self.base_url}/v1/catalogs/products"
133162
return self._make_request(url=url, method="POST", json=product_data, headers=self.headers)
134163

135-
def create_plan(self, product_id: str, name: str, description: str, price: str, currency: str = "EUR") -> Dict[str, Any]:
164+
def create_plan(self, product_id: str, name: str, description: str, price: str, currency: str = "EUR", cycles: int = 1) -> Dict[str, Any]:
136165
"""
137166
Create a subscription plan.
138167
@@ -142,6 +171,7 @@ def create_plan(self, product_id: str, name: str, description: str, price: str,
142171
description (str): Plan description.
143172
price (str): Plan price.
144173
currency (str): Currency code (default is "EUR").
174+
cycles (int): Number of payment cycles (default is 1 for one-time subscription, 0 infinite).
145175
146176
Returns:
147177
Dict[str, Any]: API response with plan details.
@@ -155,7 +185,7 @@ def create_plan(self, product_id: str, name: str, description: str, price: str,
155185
"frequency": {"interval_unit": "WEEK", "interval_count": 1},
156186
"tenure_type": "REGULAR",
157187
"sequence": 1,
158-
"total_cycles": 0,
188+
"total_cycles": cycles,
159189
"pricing_scheme": {"fixed_price": {"value": price, "currency_code": currency}}
160190
}
161191
],
@@ -168,6 +198,65 @@ def create_plan(self, product_id: str, name: str, description: str, price: str,
168198
url = f"{self.base_url}/v1/billing/plans"
169199
return self._make_request(url=url, method="POST", json=data, headers=self.headers)
170200

201+
def create_order(self, amount: str, currency: str = "EUR", return_url: str, cancel_url: str) -> Dict[str, Any]:
202+
"""
203+
Create a new order for a one-time payment.
204+
205+
Args:
206+
amount (str): The amount to be paid.
207+
currency (str): The currency code (default is "EUR").
208+
return_url (str): The URL to redirect to after the payment is approved.
209+
cancel_url (str): The URL to redirect to if the payment is cancelled.
210+
211+
Returns:
212+
Dict[str, Any]: API response with order details.
213+
"""
214+
data = {
215+
"intent": "CAPTURE",
216+
"purchase_units": [
217+
{
218+
"amount": {
219+
"currency_code": currency,
220+
"value": amount,
221+
"breakdown": {
222+
"item_total": {
223+
"currency_code": currency,
224+
"value": amount
225+
}
226+
}
227+
}
228+
}
229+
],
230+
"application_context": {
231+
"return_url": return_url,
232+
"cancel_url": cancel_url
233+
}
234+
}
235+
236+
url = f"{self.base_url}/v2/checkout/orders"
237+
return self._make_request(url=url, method="POST", json=data, headers=self.headers)
238+
239+
def reactivate_subscription(self, subscription_id: str) -> Dict[str, Any]:
240+
"""
241+
Reactivate a suspended or cancelled subscription.
242+
243+
Args:
244+
subscription_id (str): The ID of the subscription to reactivate.
245+
246+
Returns:
247+
Dict[str, Any]: API response with reactivation details.
248+
"""
249+
url = f"{self.base_url}/v1/billing/subscriptions/{subscription_id}/activate"
250+
response = requests.post(url, headers=self.headers)
251+
252+
if response.status_code == 204:
253+
return {"status": "success", "message": "Subscription reactivated successfully"}
254+
elif response.status_code == 404:
255+
return {"status": "error", "message": "Subscription not found"}
256+
else:
257+
response.raise_for_status()
258+
return {"status": "error", "message": "Failed to reactivate subscription"}
259+
171260
def update_subscription_price(self, subscription_id: str, new_price: str, currency: str = "EUR", custom_id: str = '') -> Dict[str, Any]:
172261
"""
173262
Update the subscription price.
@@ -181,6 +270,12 @@ def update_subscription_price(self, subscription_id: str, new_price: str, curren
181270
Returns:
182271
Dict[str, Any]: API response with updated subscription details.
183272
"""
273+
subscription_details = self.subscription_exists(subscription_id)
274+
if subscription_details and subscription_details.get("status") in ["SUSPENDED", "CANCELLED"]:
275+
reactivation_response = self.reactivate_subscription(subscription_id)
276+
if reactivation_response["status"] == "error":
277+
return reactivation_response
278+
184279
url = f"{self.base_url}/v1/billing/subscriptions/{subscription_id}/revise"
185280
data = {
186281
"plan_id": self.plan_id,

readme.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ This Python library allows you to interact with the PayPal REST API to manage su
55

66
Note: When the subscription is approved the user receive an email from Paypal to inform that a new automatic payment is approved, and of course one about the payment itself.
77

8+
## Addendum
9+
10+
To simplify and avoid to use also the PayPal SDK this library implements methods like `create_order` and `verify_payment` so can be used also for single payments.
11+
812
## Usage Example
913

1014
This example demonstrates how to create a CLI app that creates or updates a PayPal subscription and a FastAPI server with a webhook to save the PayPal identifier in an SQLite database. This setup allows you to check if a plan exists and in case update it automatically.
@@ -152,7 +156,7 @@ def save_subscription_to_db(subscription_id: str, name: str, description: str, p
152156
async def return_url(request: Request):
153157
data = await request.json()
154158
subscription_id = data.get("subscription_id")
155-
subscription_details = paypal_api.verify_paypal_response(token=subscription_id, subscription_id=subscription_id)
159+
subscription_details = paypal_api.verify_subscription(subscription_id=subscription_id, payer_id="email")
156160

157161
if subscription_details["status"] == "success":
158162
save_subscription_to_db(

0 commit comments

Comments
 (0)