@@ -15,17 +15,19 @@ 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"
18+ self .base_url = "https://api-m .sandbox.paypal.com" if os .getenv ("PAYPAL_SANDBOX" , "True" ) == "True" else "https://api.paypal.com"
1919 if self .client_id != "" and self .client_secret != "" :
2020 self .access_token = self ._get_access_token ()
2121 else :
2222 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" }
2424 self .plan_id = ""
2525
2626 def _make_request (self , url : str , method : str , ** kwargs ) -> Any :
2727 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 ()
2931 return response .json ()
3032
3133 def _get_access_token (self ) -> str :
@@ -139,6 +141,26 @@ def verify_payment(self, order_id: str) -> Dict[str, Any]:
139141 "order_details" : order_details
140142 }
141143
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+
142164 def create_product (self , name : str , description : str , type_ : str = "SERVICE" , category : str = "SOFTWARE" ) -> Dict [str , Any ]:
143165 """
144166 Create a product for subscription.
@@ -161,21 +183,27 @@ def create_product(self, name: str, description: str, type_: str = "SERVICE", ca
161183 url = f"{ self .base_url } /v1/catalogs/products"
162184 return self ._make_request (url = url , method = "POST" , json = product_data , headers = self .headers )
163185
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 ]:
165187 """
166- Create a subscription plan.
188+ Create a subscription plan, and create the product if it doesn't exist .
167189
168190 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.
172193 price (str): Plan price.
173194 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).
175196
176197 Returns:
177198 Dict[str, Any]: API response with plan details.
178199 """
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+
179207 data = {
180208 "product_id" : product_id ,
181209 "name" : name ,
@@ -186,15 +214,25 @@ def create_plan(self, product_id: str, name: str, description: str, price: str,
186214 "tenure_type" : "REGULAR" ,
187215 "sequence" : 1 ,
188216 "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+ }
190223 }
191224 ],
192225 "payment_preferences" : {
193226 "auto_bill_outstanding" : True ,
227+ "setup_fee" : {
228+ "value" : "0" ,
229+ "currency_code" : currency
230+ },
194231 "setup_fee_failure_action" : "CONTINUE" ,
195232 "payment_failure_threshold" : 3
196233 }
197234 }
235+
198236 url = f"{ self .base_url } /v1/billing/plans"
199237 return self ._make_request (url = url , method = "POST" , json = data , headers = self .headers )
200238
@@ -372,7 +410,6 @@ def create_or_update_subscription(self, identifier: str, name: str = "", descrip
372410 else :
373411 if os .getenv ("DEBUG" , False ):
374412 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 )
377414 new_subscription = self .create_subscription (plan_id = plan ["id" ], subscriber_email = subscriber_email , return_url = return_url , cancel_url = cancel_url )
378415 return new_subscription
0 commit comments