@@ -15,8 +15,11 @@ 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"
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" )
20
23
self .headers = {"Authorization" : f"Bearer { self .access_token } " , "Content-Type" : "application/json" }
21
24
self .plan_id = ""
22
25
@@ -65,42 +68,39 @@ def subscription_exists(self, subscription_id: str) -> bool:
65
68
response .raise_for_status ()
66
69
return False
67
70
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 ]:
69
72
"""
70
73
Verify PayPal response by checking the subscription details.
71
74
72
75
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.
75
78
76
79
Returns:
77
80
Dict[str, Any]: Verification result.
78
81
"""
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" }
81
84
82
85
try :
83
- subscription_details = self .subscription_exists (token )
86
+ subscription_details = self .subscription_exists (subscription_id )
84
87
if subscription_details == False :
85
88
return {"status" : "error" , "message" : "Subscription check failed" }
86
89
87
- if subscription_details .get ("id" ) != token :
88
- return {"status" : "error" , "message" : "Token does not match subscription" }
89
-
90
90
subscriber_info = subscription_details .get ("subscriber" , {})
91
- stored_payer_id = subscriber_info .get ("subscription_id " )
91
+ stored_payer_id = subscriber_info .get ("payer_id " )
92
92
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" }
95
95
96
96
status = subscription_details .get ("status" )
97
97
if os .getenv ("DEBUG" , False ):
98
98
if status == "ACTIVE" :
99
- print (f"Subscription { token } is active." )
99
+ print (f"Subscription { subscription_id } is active." )
100
100
elif status == "CANCELLED" :
101
- print (f"Subscription { token } is cancelled." )
101
+ print (f"Subscription { subscription_id } is cancelled." )
102
102
else :
103
- print (f"Subscription { token } status: { status } ." )
103
+ print (f"Subscription { subscription_id } status: { status } ." )
104
104
105
105
return {
106
106
"status" : "success" ,
@@ -110,6 +110,35 @@ def verify_paypal_response(self, token: str, subscription_id: str) -> Dict[str,
110
110
except requests .exceptions .RequestException as e :
111
111
return {"status" : "error" , "message" : f"PayPal API error: { e } " }
112
112
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
+
113
142
def create_product (self , name : str , description : str , type_ : str = "SERVICE" , category : str = "SOFTWARE" ) -> Dict [str , Any ]:
114
143
"""
115
144
Create a product for subscription.
@@ -132,7 +161,7 @@ def create_product(self, name: str, description: str, type_: str = "SERVICE", ca
132
161
url = f"{ self .base_url } /v1/catalogs/products"
133
162
return self ._make_request (url = url , method = "POST" , json = product_data , headers = self .headers )
134
163
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 ]:
136
165
"""
137
166
Create a subscription plan.
138
167
@@ -142,6 +171,7 @@ def create_plan(self, product_id: str, name: str, description: str, price: str,
142
171
description (str): Plan description.
143
172
price (str): Plan price.
144
173
currency (str): Currency code (default is "EUR").
174
+ cycles (int): Number of payment cycles (default is 1 for one-time subscription, 0 infinite).
145
175
146
176
Returns:
147
177
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,
155
185
"frequency" : {"interval_unit" : "WEEK" , "interval_count" : 1 },
156
186
"tenure_type" : "REGULAR" ,
157
187
"sequence" : 1 ,
158
- "total_cycles" : 0 ,
188
+ "total_cycles" : cycles ,
159
189
"pricing_scheme" : {"fixed_price" : {"value" : price , "currency_code" : currency }}
160
190
}
161
191
],
@@ -168,6 +198,65 @@ def create_plan(self, product_id: str, name: str, description: str, price: str,
168
198
url = f"{ self .base_url } /v1/billing/plans"
169
199
return self ._make_request (url = url , method = "POST" , json = data , headers = self .headers )
170
200
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
+
171
260
def update_subscription_price (self , subscription_id : str , new_price : str , currency : str = "EUR" , custom_id : str = '' ) -> Dict [str , Any ]:
172
261
"""
173
262
Update the subscription price.
@@ -181,6 +270,12 @@ def update_subscription_price(self, subscription_id: str, new_price: str, curren
181
270
Returns:
182
271
Dict[str, Any]: API response with updated subscription details.
183
272
"""
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
+
184
279
url = f"{ self .base_url } /v1/billing/subscriptions/{ subscription_id } /revise"
185
280
data = {
186
281
"plan_id" : self .plan_id ,
0 commit comments