@@ -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 ,
0 commit comments