@@ -15,17 +15,19 @@ class PayPalAPI:
15
15
def __init__ (self ):
16
16
self .client_id = os .getenv ("PAYPAL_CLIENT_ID" )
17
17
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"
18
+ self .base_url = "https://api-m .sandbox.paypal.com" if os .getenv ("PAYPAL_SANDBOX" , "True" ) == "True" else "https://api.paypal.com"
19
19
if self .client_id != "" and self .client_secret != "" :
20
20
self .access_token = self ._get_access_token ()
21
21
else :
22
22
raise ValueError ("Missing Paypal Secrets" )
23
- self .headers = {"Authorization" : f"Bearer { self .access_token } " , "Content-Type" : "application/json" }
23
+ self .headers = {"Authorization" : f"Bearer { self .access_token } " , "Content-Type" : "application/json" , "Accept" : "application/json" }
24
24
self .plan_id = ""
25
25
26
26
def _make_request (self , url : str , method : str , ** kwargs ) -> Any :
27
27
response = requests .request (method , url , ** kwargs )
28
- response .raise_for_status ()
28
+ if not response .ok :
29
+ print (f"PayPal API Error: { response .status_code } - { response .text } " )
30
+ response .raise_for_status ()
29
31
return response .json ()
30
32
31
33
def _get_access_token (self ) -> str :
@@ -139,6 +141,26 @@ def verify_payment(self, order_id: str) -> Dict[str, Any]:
139
141
"order_details" : order_details
140
142
}
141
143
144
+ def get_product_by_name (self , name : str ) -> Optional [Dict [str , Any ]]:
145
+ """
146
+ Search for a product by name.
147
+
148
+ Args:
149
+ name (str): Name of the product.
150
+
151
+ Returns:
152
+ Optional[Dict[str, Any]]: Product details if found, else None.
153
+ """
154
+ url = f"{ self .base_url } /v1/catalogs/products?page_size=20"
155
+ response = requests .get (url , headers = self .headers )
156
+ response .raise_for_status ()
157
+ products = response .json ().get ("products" , [])
158
+
159
+ for product in products :
160
+ if product .get ("name" ) == name :
161
+ return product
162
+ return None
163
+
142
164
def create_product (self , name : str , description : str , type_ : str = "SERVICE" , category : str = "SOFTWARE" ) -> Dict [str , Any ]:
143
165
"""
144
166
Create a product for subscription.
@@ -161,21 +183,27 @@ def create_product(self, name: str, description: str, type_: str = "SERVICE", ca
161
183
url = f"{ self .base_url } /v1/catalogs/products"
162
184
return self ._make_request (url = url , method = "POST" , json = product_data , headers = self .headers )
163
185
164
- def create_plan (self , product_id : str , name : str , description : str , price : str , currency : str = "EUR" , cycles : int = 1 ) -> Dict [str , Any ]:
186
+ def create_plan (self , name : str , description : str , price : str , currency : str = "EUR" , cycles : int = 1 ) -> Dict [str , Any ]:
165
187
"""
166
- Create a subscription plan.
188
+ Create a subscription plan, and create the product if it doesn't exist .
167
189
168
190
Args:
169
- product_id (str): Product ID.
170
- name (str): Plan name.
171
- description (str): Plan description.
191
+ name (str): Plan and product name.
192
+ description (str): Plan and product description.
172
193
price (str): Plan price.
173
194
currency (str): Currency code (default is "EUR").
174
- cycles (int): Number of payment cycles (default is 1 for one-time subscription, 0 infinite ).
195
+ cycles (int): Number of billing cycles (default is 1).
175
196
176
197
Returns:
177
198
Dict[str, Any]: API response with plan details.
178
199
"""
200
+ product = self .get_product_by_name (name )
201
+ if product :
202
+ product_id = product ["id" ]
203
+ else :
204
+ product = self .create_product (name = name , description = description )
205
+ product_id = product ["id" ]
206
+
179
207
data = {
180
208
"product_id" : product_id ,
181
209
"name" : name ,
@@ -186,15 +214,25 @@ def create_plan(self, product_id: str, name: str, description: str, price: str,
186
214
"tenure_type" : "REGULAR" ,
187
215
"sequence" : 1 ,
188
216
"total_cycles" : cycles ,
189
- "pricing_scheme" : {"fixed_price" : {"value" : price , "currency_code" : currency }}
217
+ "pricing_scheme" : {
218
+ "fixed_price" : {
219
+ "value" : price ,
220
+ "currency_code" : currency
221
+ }
222
+ }
190
223
}
191
224
],
192
225
"payment_preferences" : {
193
226
"auto_bill_outstanding" : True ,
227
+ "setup_fee" : {
228
+ "value" : "0" ,
229
+ "currency_code" : currency
230
+ },
194
231
"setup_fee_failure_action" : "CONTINUE" ,
195
232
"payment_failure_threshold" : 3
196
233
}
197
234
}
235
+
198
236
url = f"{ self .base_url } /v1/billing/plans"
199
237
return self ._make_request (url = url , method = "POST" , json = data , headers = self .headers )
200
238
@@ -372,7 +410,6 @@ def create_or_update_subscription(self, identifier: str, name: str = "", descrip
372
410
else :
373
411
if os .getenv ("DEBUG" , False ):
374
412
print (f"Subscription { identifier } not found. Creating a new subscription." )
375
- product = self .create_product (name = name , description = description )
376
- plan = self .create_plan (product_id = product ["id" ], name = name , description = description , price = price , currency = currency )
413
+ plan = self .create_plan (name = name , description = description , price = price , currency = currency )
377
414
new_subscription = self .create_subscription (plan_id = plan ["id" ], subscriber_email = subscriber_email , return_url = return_url , cancel_url = cancel_url )
378
415
return new_subscription
0 commit comments