hardik8588 commited on
Commit
699775a
·
verified ·
1 Parent(s): cdc873f

Update paypal_integration.py

Browse files
Files changed (1) hide show
  1. paypal_integration.py +1008 -1004
paypal_integration.py CHANGED
@@ -1,1004 +1,1008 @@
1
- import requests
2
- import json
3
- import sqlite3
4
- from datetime import datetime, timedelta
5
- import uuid
6
- import os
7
- import logging
8
- from requests.adapters import HTTPAdapter
9
- from requests.packages.urllib3.util.retry import Retry
10
- from auth import get_db_connection
11
- from dotenv import load_dotenv
12
-
13
- # PayPal API Configuration - Remove default values for production
14
- PAYPAL_CLIENT_ID = os.getenv("PAYPAL_CLIENT_ID")
15
- PAYPAL_SECRET = os.getenv("PAYPAL_SECRET")
16
- PAYPAL_BASE_URL = os.getenv("PAYPAL_BASE_URL", "https://api-m.sandbox.paypal.com")
17
-
18
- # Add validation to ensure credentials are provided
19
- # Set up logging
20
- logging.basicConfig(
21
- level=logging.INFO,
22
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
23
- handlers=[
24
- logging.FileHandler(os.path.join(os.path.dirname(__file__), "../logs/paypal.log")),
25
- logging.StreamHandler()
26
- ]
27
- )
28
- logger = logging.getLogger("paypal_integration")
29
-
30
- # Then replace print statements with logger calls
31
- # For example:
32
- if not PAYPAL_CLIENT_ID or not PAYPAL_SECRET:
33
- logger.warning("PayPal credentials not found in environment variables")
34
-
35
-
36
- # Get PayPal access token
37
- # Add better error handling for production
38
- # Create a session with retry capability
39
- def create_retry_session(retries=3, backoff_factor=0.3):
40
- session = requests.Session()
41
- retry = Retry(
42
- total=retries,
43
- read=retries,
44
- connect=retries,
45
- backoff_factor=backoff_factor,
46
- status_forcelist=[500, 502, 503, 504],
47
- )
48
- adapter = HTTPAdapter(max_retries=retry)
49
- session.mount('http://', adapter)
50
- session.mount('https://', adapter)
51
- return session
52
-
53
- # Then use this session for API calls
54
- # Replace get_access_token with logger instead of print
55
- def get_access_token():
56
- url = f"{PAYPAL_BASE_URL}/v1/oauth2/token"
57
- headers = {
58
- "Accept": "application/json",
59
- "Accept-Language": "en_US"
60
- }
61
- data = "grant_type=client_credentials"
62
-
63
- try:
64
- session = create_retry_session()
65
- response = session.post(
66
- url,
67
- auth=(PAYPAL_CLIENT_ID, PAYPAL_SECRET),
68
- headers=headers,
69
- data=data
70
- )
71
-
72
- if response.status_code == 200:
73
- return response.json()["access_token"]
74
- else:
75
- logger.error(f"Error getting access token: {response.status_code}")
76
- return None
77
- except Exception as e:
78
- logger.error(f"Exception in get_access_token: {str(e)}")
79
- return None
80
-
81
- def call_paypal_api(endpoint, method="GET", data=None, token=None):
82
- """
83
- Helper function to make PayPal API calls
84
-
85
- Args:
86
- endpoint: API endpoint (without base URL)
87
- method: HTTP method (GET, POST, etc.)
88
- data: Request payload (for POST/PUT)
89
- token: PayPal access token (will be fetched if None)
90
-
91
- Returns:
92
- tuple: (success, response_data or error_message)
93
- """
94
- try:
95
- if not token:
96
- token = get_access_token()
97
- if not token:
98
- return False, "Failed to get PayPal access token"
99
-
100
- url = f"{PAYPAL_BASE_URL}{endpoint}"
101
- headers = {
102
- "Content-Type": "application/json",
103
- "Authorization": f"Bearer {token}"
104
- }
105
-
106
- session = create_retry_session()
107
-
108
- if method.upper() == "GET":
109
- response = session.get(url, headers=headers)
110
- elif method.upper() == "POST":
111
- response = session.post(url, headers=headers, data=json.dumps(data) if data else None)
112
- elif method.upper() == "PUT":
113
- response = session.put(url, headers=headers, data=json.dumps(data) if data else None)
114
- else:
115
- return False, f"Unsupported HTTP method: {method}"
116
-
117
- if response.status_code in [200, 201, 204]:
118
- if response.status_code == 204: # No content
119
- return True, {}
120
- return True, response.json() if response.text else {}
121
- else:
122
- logger.error(f"PayPal API error: {response.status_code} - {response.text}")
123
- return False, f"PayPal API error: {response.status_code} - {response.text}"
124
-
125
- except Exception as e:
126
- logger.error(f"Error calling PayPal API: {str(e)}")
127
- return False, f"Error calling PayPal API: {str(e)}"
128
-
129
- def create_paypal_subscription(user_id, tier):
130
- """Create a PayPal subscription for a user"""
131
- try:
132
- # Get the price from the subscription tier
133
- from auth import SUBSCRIPTION_TIERS
134
-
135
- if tier not in SUBSCRIPTION_TIERS:
136
- return False, f"Invalid tier: {tier}"
137
-
138
- price = SUBSCRIPTION_TIERS[tier]["price"]
139
- currency = SUBSCRIPTION_TIERS[tier]["currency"]
140
-
141
- # Create a PayPal subscription (implement PayPal API calls here)
142
- # For now, just return a success response
143
- return True, {
144
- "subscription_id": f"test_sub_{uuid.uuid4()}",
145
- "status": "ACTIVE",
146
- "tier": tier,
147
- "price": price,
148
- "currency": currency
149
- }
150
- except Exception as e:
151
- logger.error(f"Error creating PayPal subscription: {str(e)}")
152
- return False, f"Failed to create PayPal subscription: {str(e)}"
153
-
154
-
155
- # Create a product in PayPal
156
- def create_product(name, description):
157
- """Create a product in PayPal"""
158
- payload = {
159
- "name": name,
160
- "description": description,
161
- "type": "SERVICE",
162
- "category": "SOFTWARE"
163
- }
164
-
165
- success, result = call_paypal_api("/v1/catalogs/products", "POST", payload)
166
- if success:
167
- return result["id"]
168
- else:
169
- logger.error(f"Failed to create product: {result}")
170
- return None
171
-
172
- # Create a subscription plan in PayPal
173
- # Update create_plan to use INR instead of USD
174
- def create_plan(product_id, name, price, interval="MONTH", interval_count=1):
175
- """Create a subscription plan in PayPal"""
176
- payload = {
177
- "product_id": product_id,
178
- "name": name,
179
- "billing_cycles": [
180
- {
181
- "frequency": {
182
- "interval_unit": interval,
183
- "interval_count": interval_count
184
- },
185
- "tenure_type": "REGULAR",
186
- "sequence": 1,
187
- "total_cycles": 0, # Infinite cycles
188
- "pricing_scheme": {
189
- "fixed_price": {
190
- "value": str(price),
191
- "currency_code": "USD"
192
- }
193
- }
194
- }
195
- ],
196
- "payment_preferences": {
197
- "auto_bill_outstanding": True,
198
- "setup_fee": {
199
- "value": "0",
200
- "currency_code": "USD"
201
- },
202
- "setup_fee_failure_action": "CONTINUE",
203
- "payment_failure_threshold": 3
204
- }
205
- }
206
-
207
- success, result = call_paypal_api("/v1/billing/plans", "POST", payload)
208
- if success:
209
- return result["id"]
210
- else:
211
- logger.error(f"Failed to create plan: {result}")
212
- return None
213
-
214
- # Update initialize_subscription_plans to use INR pricing
215
- def initialize_subscription_plans():
216
- """
217
- Initialize PayPal subscription plans for the application.
218
- This should be called once to set up the plans in PayPal.
219
- """
220
- try:
221
- # Check if plans already exist
222
- existing_plans = get_subscription_plans()
223
- if existing_plans and len(existing_plans) >= 2:
224
- logger.info("PayPal plans already initialized")
225
- return existing_plans
226
-
227
- # First, create products for each tier
228
- products = {
229
- "standard_tier": {
230
- "name": "Standard Legal Document Analysis",
231
- "description": "Standard subscription with document analysis features",
232
- "type": "SERVICE",
233
- "category": "SOFTWARE"
234
- },
235
- "premium_tier": {
236
- "name": "Premium Legal Document Analysis",
237
- "description": "Premium subscription with all document analysis features",
238
- "type": "SERVICE",
239
- "category": "SOFTWARE"
240
- }
241
- }
242
-
243
- product_ids = {}
244
- for tier, product_data in products.items():
245
- success, result = call_paypal_api("/v1/catalogs/products", "POST", product_data)
246
- if success:
247
- product_ids[tier] = result["id"]
248
- logger.info(f"Created PayPal product for {tier}: {result['id']}")
249
- else:
250
- logger.error(f"Failed to create product for {tier}: {result}")
251
- return None
252
-
253
- # Define the plans with product IDs - Changed currency to USD
254
- plans = {
255
- "standard_tier": {
256
- "product_id": product_ids["standard_tier"],
257
- "name": "Standard Plan",
258
- "description": "Standard subscription with basic features",
259
- "billing_cycles": [
260
- {
261
- "frequency": {
262
- "interval_unit": "MONTH",
263
- "interval_count": 1
264
- },
265
- "tenure_type": "REGULAR",
266
- "sequence": 1,
267
- "total_cycles": 0,
268
- "pricing_scheme": {
269
- "fixed_price": {
270
- "value": "9.99",
271
- "currency_code": "USD"
272
- }
273
- }
274
- }
275
- ],
276
- "payment_preferences": {
277
- "auto_bill_outstanding": True,
278
- "setup_fee": {
279
- "value": "0",
280
- "currency_code": "USD"
281
- },
282
- "setup_fee_failure_action": "CONTINUE",
283
- "payment_failure_threshold": 3
284
- }
285
- },
286
- "premium_tier": {
287
- "product_id": product_ids["premium_tier"],
288
- "name": "Premium Plan",
289
- "description": "Premium subscription with all features",
290
- "billing_cycles": [
291
- {
292
- "frequency": {
293
- "interval_unit": "MONTH",
294
- "interval_count": 1
295
- },
296
- "tenure_type": "REGULAR",
297
- "sequence": 1,
298
- "total_cycles": 0,
299
- "pricing_scheme": {
300
- "fixed_price": {
301
- "value": "19.99",
302
- "currency_code": "USD"
303
- }
304
- }
305
- }
306
- ],
307
- "payment_preferences": {
308
- "auto_bill_outstanding": True,
309
- "setup_fee": {
310
- "value": "0",
311
- "currency_code": "USD"
312
- },
313
- "setup_fee_failure_action": "CONTINUE",
314
- "payment_failure_threshold": 3
315
- }
316
- }
317
- }
318
-
319
- # Create the plans in PayPal
320
- created_plans = {}
321
- for tier, plan_data in plans.items():
322
- success, result = call_paypal_api("/v1/billing/plans", "POST", plan_data)
323
- if success:
324
- created_plans[tier] = result["id"]
325
- logger.info(f"Created PayPal plan for {tier}: {result['id']}")
326
- else:
327
- logger.error(f"Failed to create plan for {tier}: {result}")
328
-
329
- # Save the plan IDs to a file
330
- if created_plans:
331
- save_subscription_plans(created_plans)
332
- return created_plans
333
- else:
334
- logger.error("Failed to create any PayPal plans")
335
- return None
336
- except Exception as e:
337
- logger.error(f"Error initializing subscription plans: {str(e)}")
338
- return None
339
-
340
- # Update create_subscription_link to use call_paypal_api helper
341
- def create_subscription_link(plan_id):
342
- # Get the plan IDs
343
- plans = get_subscription_plans()
344
- if not plans:
345
- return None
346
-
347
- # Use environment variable for the app URL to make it work in different environments
348
- app_url = os.getenv("APP_URL", "http://localhost:8501")
349
-
350
- payload = {
351
- "plan_id": plans[plan_id],
352
- "application_context": {
353
- "brand_name": "Legal Document Analyzer",
354
- "locale": "en_US",
355
- "shipping_preference": "NO_SHIPPING",
356
- "user_action": "SUBSCRIBE_NOW",
357
- "return_url": f"{app_url}?status=success&subscription_id={{id}}",
358
- "cancel_url": f"{app_url}?status=cancel"
359
- }
360
- }
361
-
362
- success, data = call_paypal_api("/v1/billing/subscriptions", "POST", payload)
363
- if not success:
364
- logger.error(f"Error creating subscription: {data}")
365
- return None
366
-
367
- try:
368
- return {
369
- "subscription_id": data["id"],
370
- "approval_url": next(link["href"] for link in data["links"] if link["rel"] == "approve")
371
- }
372
- except Exception as e:
373
- logger.error(f"Exception processing subscription response: {str(e)}")
374
- return None
375
-
376
- # Fix the webhook handler function signature to match how it's called in app.py
377
- def handle_subscription_webhook(payload):
378
- """
379
- Handle PayPal subscription webhooks
380
-
381
- Args:
382
- payload: The full webhook payload
383
-
384
- Returns:
385
- tuple: (success, result)
386
- - success: True if successful, False otherwise
387
- - result: Success message or error message
388
- """
389
- try:
390
- event_type = payload.get("event_type")
391
- resource = payload.get("resource", {})
392
-
393
- logger.info(f"Received PayPal webhook: {event_type}")
394
-
395
- # Handle different event types
396
- if event_type == "BILLING.SUBSCRIPTION.CREATED":
397
- # A subscription was created
398
- subscription_id = resource.get("id")
399
- if not subscription_id:
400
- return False, "Missing subscription ID in webhook"
401
-
402
- # Update subscription status in database
403
- conn = get_db_connection()
404
- cursor = conn.cursor()
405
- cursor.execute(
406
- "UPDATE subscriptions SET status = 'pending' WHERE paypal_subscription_id = ?",
407
- (subscription_id,)
408
- )
409
- conn.commit()
410
- conn.close()
411
-
412
- return True, "Subscription created successfully"
413
-
414
- elif event_type == "BILLING.SUBSCRIPTION.ACTIVATED":
415
- # A subscription was activated
416
- subscription_id = resource.get("id")
417
- if not subscription_id:
418
- return False, "Missing subscription ID in webhook"
419
-
420
- # Update subscription status in database
421
- conn = get_db_connection()
422
- cursor = conn.cursor()
423
- cursor.execute(
424
- "UPDATE subscriptions SET status = 'active' WHERE paypal_subscription_id = ?",
425
- (subscription_id,)
426
- )
427
- conn.commit()
428
- conn.close()
429
-
430
- return True, "Subscription activated successfully"
431
-
432
- elif event_type == "BILLING.SUBSCRIPTION.CANCELLED":
433
- # A subscription was cancelled
434
- subscription_id = resource.get("id")
435
- if not subscription_id:
436
- return False, "Missing subscription ID in webhook"
437
-
438
- # Update subscription status in database
439
- conn = get_db_connection()
440
- cursor = conn.cursor()
441
- cursor.execute(
442
- "UPDATE subscriptions SET status = 'cancelled' WHERE paypal_subscription_id = ?",
443
- (subscription_id,)
444
- )
445
- conn.commit()
446
- conn.close()
447
-
448
- return True, "Subscription cancelled successfully"
449
-
450
- elif event_type == "BILLING.SUBSCRIPTION.SUSPENDED":
451
- # A subscription was suspended
452
- subscription_id = resource.get("id")
453
- if not subscription_id:
454
- return False, "Missing subscription ID in webhook"
455
-
456
- # Update subscription status in database
457
- conn = get_db_connection()
458
- cursor = conn.cursor()
459
- cursor.execute(
460
- "UPDATE subscriptions SET status = 'suspended' WHERE paypal_subscription_id = ?",
461
- (subscription_id,)
462
- )
463
- conn.commit()
464
- conn.close()
465
-
466
- return True, "Subscription suspended successfully"
467
-
468
- else:
469
- # Unhandled event type
470
- logger.info(f"Unhandled webhook event type: {event_type}")
471
- return True, f"Unhandled event type: {event_type}"
472
-
473
- except Exception as e:
474
- logger.error(f"Error handling webhook: {str(e)}")
475
- return False, f"Error handling webhook: {str(e)}"
476
- # Add this function to update user subscription
477
- def update_user_subscription(user_email, subscription_id, tier):
478
- """
479
- Update a user's subscription status
480
-
481
- Args:
482
- user_email: The email of the user
483
- subscription_id: The PayPal subscription ID
484
- tier: The subscription tier
485
-
486
- Returns:
487
- tuple: (success, result)
488
- - success: True if successful, False otherwise
489
- - result: Success message or error message
490
- """
491
- try:
492
- # Get user ID from email
493
- conn = get_db_connection()
494
- cursor = conn.cursor()
495
- cursor.execute("SELECT id FROM users WHERE email = ?", (user_email,))
496
- user_result = cursor.fetchone()
497
-
498
- if not user_result:
499
- conn.close()
500
- return False, f"User not found: {user_email}"
501
-
502
- user_id = user_result[0]
503
-
504
- # Update the subscription status
505
- cursor.execute(
506
- "UPDATE subscriptions SET status = 'active' WHERE user_id = ? AND paypal_subscription_id = ?",
507
- (user_id, subscription_id)
508
- )
509
-
510
- # Deactivate any other active subscriptions for this user
511
- cursor.execute(
512
- "UPDATE subscriptions SET status = 'inactive' WHERE user_id = ? AND paypal_subscription_id != ? AND status = 'active'",
513
- (user_id, subscription_id)
514
- )
515
-
516
- # Update the user's subscription tier
517
- cursor.execute(
518
- "UPDATE users SET subscription_tier = ? WHERE email = ?",
519
- (tier, user_email)
520
- )
521
-
522
- conn.commit()
523
- conn.close()
524
-
525
- return True, f"Subscription updated to {tier} tier"
526
-
527
- except Exception as e:
528
- logger.error(f"Error updating user subscription: {str(e)}")
529
- return False, f"Error updating subscription: {str(e)}"
530
-
531
- # Add this near the top with other path definitions
532
- # Update the PLAN_IDS_PATH definition to use the correct path
533
- PLAN_IDS_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "data", "plan_ids.json"))
534
-
535
- # Make sure the data directory exists
536
- os.makedirs(os.path.dirname(PLAN_IDS_PATH), exist_ok=True)
537
-
538
- # Add this debug log to see where the file is expected
539
- logger.info(f"PayPal plans will be stored at: {PLAN_IDS_PATH}")
540
-
541
- # Add this function if it's not defined elsewhere
542
- def get_db_connection():
543
- """Get a connection to the SQLite database"""
544
- DB_PATH = os.getenv("DB_PATH", os.path.join(os.path.dirname(__file__), "../data/user_data.db"))
545
- # Make sure the data directory exists
546
- os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
547
- return sqlite3.connect(DB_PATH)
548
-
549
- # Add this function to create subscription tables if needed
550
- def initialize_database():
551
- """Initialize the database tables needed for subscriptions"""
552
- conn = get_db_connection()
553
- cursor = conn.cursor()
554
-
555
- # Check if subscriptions table exists
556
- cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='subscriptions'")
557
- if cursor.fetchone():
558
- # Table exists, check if required columns exist
559
- cursor.execute("PRAGMA table_info(subscriptions)")
560
- columns = [column[1] for column in cursor.fetchall()]
561
-
562
- # Check for missing columns and add them if needed
563
- if "user_id" not in columns:
564
- logger.info("Adding 'user_id' column to subscriptions table")
565
- cursor.execute("ALTER TABLE subscriptions ADD COLUMN user_id TEXT NOT NULL DEFAULT ''")
566
-
567
- if "created_at" not in columns:
568
- logger.info("Adding 'created_at' column to subscriptions table")
569
- cursor.execute("ALTER TABLE subscriptions ADD COLUMN created_at TIMESTAMP")
570
-
571
- if "expires_at" not in columns:
572
- logger.info("Adding 'expires_at' column to subscriptions table")
573
- cursor.execute("ALTER TABLE subscriptions ADD COLUMN expires_at TIMESTAMP")
574
-
575
- if "paypal_subscription_id" not in columns:
576
- logger.info("Adding 'paypal_subscription_id' column to subscriptions table")
577
- cursor.execute("ALTER TABLE subscriptions ADD COLUMN paypal_subscription_id TEXT")
578
- else:
579
- # Create subscriptions table with all required columns
580
- cursor.execute('''
581
- CREATE TABLE IF NOT EXISTS subscriptions (
582
- id TEXT PRIMARY KEY,
583
- user_id TEXT NOT NULL,
584
- tier TEXT NOT NULL,
585
- status TEXT NOT NULL,
586
- created_at TIMESTAMP NOT NULL,
587
- expires_at TIMESTAMP,
588
- paypal_subscription_id TEXT
589
- )
590
- ''')
591
- logger.info("Created subscriptions table with all required columns")
592
-
593
- # Create PayPal plans table if it doesn't exist
594
- cursor.execute('''
595
- CREATE TABLE IF NOT EXISTS paypal_plans (
596
- plan_id TEXT PRIMARY KEY,
597
- tier TEXT NOT NULL,
598
- price REAL NOT NULL,
599
- currency TEXT NOT NULL,
600
- created_at TIMESTAMP NOT NULL
601
- )
602
- ''')
603
-
604
- conn.commit()
605
- conn.close()
606
- logger.info("Database initialization completed")
607
-
608
-
609
- def create_user_subscription_mock(user_email, tier):
610
- """
611
- Create a mock subscription for testing
612
-
613
- Args:
614
- user_email: The email of the user
615
- tier: The subscription tier
616
-
617
- Returns:
618
- tuple: (success, result)
619
- """
620
- try:
621
- logger.info(f"Creating mock subscription for {user_email} at tier {tier}")
622
-
623
- # Get user ID from email
624
- conn = get_db_connection()
625
- cursor = conn.cursor()
626
- cursor.execute("SELECT id FROM users WHERE email = ?", (user_email,))
627
- user_result = cursor.fetchone()
628
-
629
- if not user_result:
630
- conn.close()
631
- return False, f"User not found: {user_email}"
632
-
633
- user_id = user_result[0]
634
-
635
- # Create a mock subscription ID
636
- subscription_id = f"mock_sub_{uuid.uuid4()}"
637
-
638
- # Store the subscription in database
639
- sub_id = str(uuid.uuid4())
640
- start_date = datetime.now()
641
-
642
- cursor.execute(
643
- "INSERT INTO subscriptions (id, user_id, tier, status, created_at, expires_at, paypal_subscription_id) VALUES (?, ?, ?, ?, ?, ?, ?)",
644
- (sub_id, user_id, tier, "active", start_date, start_date + timedelta(days=30), subscription_id)
645
- )
646
-
647
- # Update user's subscription tier
648
- cursor.execute(
649
- "UPDATE users SET subscription_tier = ? WHERE id = ?",
650
- (tier, user_id)
651
- )
652
-
653
- conn.commit()
654
- conn.close()
655
-
656
- # Use environment variable for the app URL
657
- app_url = os.getenv("APP_URL", "http://localhost:3000")
658
-
659
- # Return success with mock approval URL that matches the real PayPal URL pattern
660
- return True, {
661
- "subscription_id": subscription_id,
662
- "approval_url": f"{app_url}/subscription/callback?status=success&subscription_id={subscription_id}",
663
- "tier": tier
664
- }
665
-
666
- except Exception as e:
667
- logger.error(f"Error creating mock subscription: {str(e)}")
668
- return False, f"Error creating subscription: {str(e)}"
669
-
670
- # Add this at the end of the file
671
- def initialize():
672
- """Initialize the PayPal integration module"""
673
- try:
674
- # Create necessary directories
675
- os.makedirs(os.path.dirname(PLAN_IDS_PATH), exist_ok=True)
676
-
677
- # Initialize database
678
- initialize_database()
679
-
680
- # Initialize subscription plans
681
- plans = get_subscription_plans()
682
- if plans:
683
- logger.info(f"Subscription plans initialized: {plans}")
684
- else:
685
- logger.warning("Failed to initialize subscription plans")
686
-
687
- return True
688
- except Exception as e:
689
- logger.error(f"Error initializing PayPal integration: {str(e)}")
690
- return False
691
-
692
- # Call initialize when the module is imported
693
- initialize()
694
-
695
- # Add this function to get subscription plans
696
- def get_subscription_plans():
697
- """
698
- Get all available subscription plans with correct pricing
699
- """
700
- try:
701
- # Check if we have plan IDs saved in a file
702
- if os.path.exists(PLAN_IDS_PATH):
703
- try:
704
- with open(PLAN_IDS_PATH, 'r') as f:
705
- plans = json.load(f)
706
- logger.info(f"Loaded subscription plans from {PLAN_IDS_PATH}: {plans}")
707
- return plans
708
- except Exception as e:
709
- logger.error(f"Error reading plan IDs file: {str(e)}")
710
- return {}
711
-
712
- # If no file exists, return empty dict
713
- logger.warning(f"No plan IDs file found at {PLAN_IDS_PATH}. Please initialize subscription plans.")
714
- return {}
715
-
716
- except Exception as e:
717
- logger.error(f"Error getting subscription plans: {str(e)}")
718
- return {}
719
-
720
- # Add this function to create subscription tables if needed
721
- def initialize_database():
722
- """Initialize the database tables needed for subscriptions"""
723
- conn = get_db_connection()
724
- cursor = conn.cursor()
725
-
726
- # Create subscriptions table if it doesn't exist
727
- cursor.execute('''
728
- CREATE TABLE IF NOT EXISTS subscriptions (
729
- id TEXT PRIMARY KEY,
730
- user_id TEXT NOT NULL,
731
- tier TEXT NOT NULL,
732
- status TEXT NOT NULL,
733
- created_at TIMESTAMP NOT NULL,
734
- expires_at TIMESTAMP,
735
- paypal_subscription_id TEXT
736
- )
737
- ''')
738
-
739
- # Create PayPal plans table if it doesn't exist
740
- cursor.execute('''
741
- CREATE TABLE IF NOT EXISTS paypal_plans (
742
- plan_id TEXT PRIMARY KEY,
743
- tier TEXT NOT NULL,
744
- price REAL NOT NULL,
745
- currency TEXT NOT NULL,
746
- created_at TIMESTAMP NOT NULL
747
- )
748
- ''')
749
-
750
- conn.commit()
751
- conn.close()
752
-
753
-
754
- def create_user_subscription(user_email, tier):
755
- """
756
- Create a real PayPal subscription for a user
757
-
758
- Args:
759
- user_email: The email of the user
760
- tier: The subscription tier (standard_tier or premium_tier)
761
-
762
- Returns:
763
- tuple: (success, result)
764
- - success: True if successful, False otherwise
765
- - result: Dictionary with subscription details or error message
766
- """
767
- try:
768
- # Validate tier
769
- valid_tiers = ["standard_tier", "premium_tier"]
770
- if tier not in valid_tiers:
771
- return False, f"Invalid tier: {tier}. Must be one of {valid_tiers}"
772
-
773
- # Get the plan IDs
774
- plans = get_subscription_plans()
775
-
776
- # Log the plans for debugging
777
- logger.info(f"Available subscription plans: {plans}")
778
-
779
- # If no plans found, check if the file exists and try to load it directly
780
- if not plans:
781
- if os.path.exists(PLAN_IDS_PATH):
782
- logger.info(f"Plan IDs file exists at {PLAN_IDS_PATH}, but couldn't load plans. Trying direct load.")
783
- try:
784
- with open(PLAN_IDS_PATH, 'r') as f:
785
- plans = json.load(f)
786
- logger.info(f"Directly loaded plans: {plans}")
787
- except Exception as e:
788
- logger.error(f"Error directly loading plans: {str(e)}")
789
- else:
790
- logger.error(f"Plan IDs file does not exist at {PLAN_IDS_PATH}")
791
-
792
- # If still no plans, return error
793
- if not plans:
794
- logger.error("No PayPal plans found. Please initialize plans first.")
795
- return False, "PayPal plans not configured. Please contact support."
796
-
797
- # Check if the tier exists in plans
798
- if tier not in plans:
799
- return False, f"No plan found for tier: {tier}"
800
-
801
- # Use environment variable for the app URL
802
- app_url = os.getenv("APP_URL", "http://localhost:3000")
803
-
804
- # Create the subscription with PayPal
805
- payload = {
806
- "plan_id": plans[tier],
807
- "subscriber": {
808
- "email_address": user_email
809
- },
810
- "application_context": {
811
- "brand_name": "Legal Document Analyzer",
812
- "locale": "en-US", # Changed from en_US to en-US
813
- "shipping_preference": "NO_SHIPPING",
814
- "user_action": "SUBSCRIBE_NOW",
815
- "return_url": f"{app_url}/subscription/callback?status=success",
816
- "cancel_url": f"{app_url}/subscription/callback?status=cancel"
817
- }
818
- }
819
-
820
- # Make the API call to PayPal
821
- success, subscription_data = call_paypal_api("/v1/billing/subscriptions", "POST", payload)
822
- if not success:
823
- return False, subscription_data # This is already an error message
824
-
825
- # Extract the approval URL
826
- approval_url = next((link["href"] for link in subscription_data["links"]
827
- if link["rel"] == "approve"), None)
828
-
829
- if not approval_url:
830
- return False, "No approval URL found in PayPal response"
831
-
832
- # Get user ID from email
833
- conn = get_db_connection()
834
- cursor = conn.cursor()
835
- cursor.execute("SELECT id FROM users WHERE email = ?", (user_email,))
836
- user_result = cursor.fetchone()
837
-
838
- if not user_result:
839
- conn.close()
840
- return False, f"User not found: {user_email}"
841
-
842
- user_id = user_result[0]
843
-
844
- # Store pending subscription in database
845
- sub_id = str(uuid.uuid4())
846
- start_date = datetime.now()
847
-
848
- cursor.execute(
849
- "INSERT INTO subscriptions (id, user_id, tier, status, created_at, expires_at, paypal_subscription_id) VALUES (?, ?, ?, ?, ?, ?, ?)",
850
- (sub_id, user_id, tier, "pending", start_date, None, subscription_data["id"])
851
- )
852
-
853
- conn.commit()
854
- conn.close()
855
-
856
- # Return success with approval URL
857
- return True, {
858
- "subscription_id": subscription_data["id"],
859
- "approval_url": approval_url,
860
- "tier": tier
861
- }
862
-
863
- except Exception as e:
864
- logger.error(f"Error creating user subscription: {str(e)}")
865
- return False, f"Error creating subscription: {str(e)}"
866
-
867
- # Add a function to cancel a subscription
868
- def cancel_subscription(subscription_id, reason="Customer requested cancellation"):
869
- """
870
- Cancel a PayPal subscription
871
-
872
- Args:
873
- subscription_id: The PayPal subscription ID
874
- reason: The reason for cancellation
875
-
876
- Returns:
877
- tuple: (success, result)
878
- - success: True if successful, False otherwise
879
- - result: Success message or error message
880
- """
881
- try:
882
- # Cancel the subscription with PayPal
883
- payload = {
884
- "reason": reason
885
- }
886
-
887
- success, result = call_paypal_api(
888
- f"/v1/billing/subscriptions/{subscription_id}/cancel",
889
- "POST",
890
- payload
891
- )
892
-
893
- if not success:
894
- return False, result
895
-
896
- # Update subscription status in database
897
- conn = get_db_connection()
898
- cursor = conn.cursor()
899
- cursor.execute(
900
- "UPDATE subscriptions SET status = 'cancelled' WHERE paypal_subscription_id = ?",
901
- (subscription_id,)
902
- )
903
-
904
- # Get the user ID for this subscription
905
- cursor.execute(
906
- "SELECT user_id FROM subscriptions WHERE paypal_subscription_id = ?",
907
- (subscription_id,)
908
- )
909
- user_result = cursor.fetchone()
910
-
911
- if user_result:
912
- # Update user to free tier
913
- cursor.execute(
914
- "UPDATE users SET subscription_tier = 'free_tier' WHERE id = ?",
915
- (user_result[0],)
916
- )
917
-
918
- conn.commit()
919
- conn.close()
920
-
921
- return True, "Subscription cancelled successfully"
922
-
923
- except Exception as e:
924
- logger.error(f"Error cancelling subscription: {str(e)}")
925
- return False, f"Error cancelling subscription: {str(e)}"
926
-
927
- def verify_subscription_payment(subscription_id):
928
- """
929
- Verify a subscription payment with PayPal
930
-
931
- Args:
932
- subscription_id: The PayPal subscription ID
933
-
934
- Returns:
935
- tuple: (success, result)
936
- - success: True if successful, False otherwise
937
- - result: Dictionary with subscription details or error message
938
- """
939
- try:
940
- # Get subscription details from PayPal using our helper
941
- success, subscription_data = call_paypal_api(f"/v1/billing/subscriptions/{subscription_id}")
942
- if not success:
943
- return False, subscription_data # This is already an error message
944
-
945
- # Check subscription status
946
- status = subscription_data.get("status", "").upper()
947
-
948
- if status not in ["ACTIVE", "APPROVED"]:
949
- return False, f"Subscription is not active: {status}"
950
-
951
- # Return success with subscription data
952
- return True, subscription_data
953
-
954
- except Exception as e:
955
- logger.error(f"Error verifying subscription: {str(e)}")
956
- return False, f"Error verifying subscription: {str(e)}"
957
-
958
- def verify_paypal_subscription(subscription_id):
959
- """
960
- Verify a PayPal subscription
961
-
962
- Args:
963
- subscription_id: The PayPal subscription ID
964
-
965
- Returns:
966
- tuple: (success, result)
967
- """
968
- try:
969
- # Skip verification for mock subscriptions
970
- if subscription_id.startswith("mock_sub_"):
971
- return True, {"status": "ACTIVE"}
972
-
973
- # For real subscriptions, call PayPal API
974
- success, result = call_paypal_api(f"/v1/billing/subscriptions/{subscription_id}", "GET")
975
-
976
- if success:
977
- # Check subscription status
978
- if result.get("status") == "ACTIVE":
979
- return True, result
980
- else:
981
- return False, f"Subscription is not active: {result.get('status')}"
982
- else:
983
- logger.error(f"PayPal API error: {result}")
984
- return False, f"Failed to verify subscription: {result}"
985
- except Exception as e:
986
- logger.error(f"Error verifying PayPal subscription: {str(e)}")
987
- return False, f"Error verifying subscription: {str(e)}"
988
-
989
- # Add this function to save subscription plans
990
- def save_subscription_plans(plans):
991
- """
992
- Save subscription plans to a file
993
-
994
- Args:
995
- plans: Dictionary of plan IDs by tier
996
- """
997
- try:
998
- with open(PLAN_IDS_PATH, 'w') as f:
999
- json.dump(plans, f)
1000
- logger.info(f"Saved subscription plans to {PLAN_IDS_PATH}")
1001
- return True
1002
- except Exception as e:
1003
- logger.error(f"Error saving subscription plans: {str(e)}")
1004
- return False
 
 
 
 
 
1
+ import requests
2
+ import json
3
+ import sqlite3
4
+ from datetime import datetime, timedelta
5
+ import uuid
6
+ import os
7
+ import logging
8
+ from requests.adapters import HTTPAdapter
9
+ from requests.packages.urllib3.util.retry import Retry
10
+ from auth import get_db_connection
11
+ from dotenv import load_dotenv
12
+
13
+ # PayPal API Configuration - Remove default values for production
14
+ PAYPAL_CLIENT_ID = os.getenv("PAYPAL_CLIENT_ID")
15
+ PAYPAL_SECRET = os.getenv("PAYPAL_SECRET")
16
+ PAYPAL_BASE_URL = os.getenv("PAYPAL_BASE_URL", "https://api-m.sandbox.paypal.com")
17
+
18
+ # Add validation to ensure credentials are provided
19
+ # Set up logging
20
+ LOG_DIR = os.path.abspath("/tmp/logs")
21
+ os.makedirs(LOG_DIR, exist_ok=True)
22
+ LOG_FILE = os.path.join(LOG_DIR, "paypal.log")
23
+
24
+ logging.basicConfig(
25
+ level=logging.INFO,
26
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
27
+ handlers=[
28
+ logging.FileHandler(LOG_FILE),
29
+ logging.StreamHandler()
30
+ ]
31
+ )
32
+ logger = logging.getLogger("paypal_integration")
33
+
34
+ # Then replace print statements with logger calls
35
+ # For example:
36
+ if not PAYPAL_CLIENT_ID or not PAYPAL_SECRET:
37
+ logger.warning("PayPal credentials not found in environment variables")
38
+
39
+
40
+ # Get PayPal access token
41
+ # Add better error handling for production
42
+ # Create a session with retry capability
43
+ def create_retry_session(retries=3, backoff_factor=0.3):
44
+ session = requests.Session()
45
+ retry = Retry(
46
+ total=retries,
47
+ read=retries,
48
+ connect=retries,
49
+ backoff_factor=backoff_factor,
50
+ status_forcelist=[500, 502, 503, 504],
51
+ )
52
+ adapter = HTTPAdapter(max_retries=retry)
53
+ session.mount('http://', adapter)
54
+ session.mount('https://', adapter)
55
+ return session
56
+
57
+ # Then use this session for API calls
58
+ # Replace get_access_token with logger instead of print
59
+ def get_access_token():
60
+ url = f"{PAYPAL_BASE_URL}/v1/oauth2/token"
61
+ headers = {
62
+ "Accept": "application/json",
63
+ "Accept-Language": "en_US"
64
+ }
65
+ data = "grant_type=client_credentials"
66
+
67
+ try:
68
+ session = create_retry_session()
69
+ response = session.post(
70
+ url,
71
+ auth=(PAYPAL_CLIENT_ID, PAYPAL_SECRET),
72
+ headers=headers,
73
+ data=data
74
+ )
75
+
76
+ if response.status_code == 200:
77
+ return response.json()["access_token"]
78
+ else:
79
+ logger.error(f"Error getting access token: {response.status_code}")
80
+ return None
81
+ except Exception as e:
82
+ logger.error(f"Exception in get_access_token: {str(e)}")
83
+ return None
84
+
85
+ def call_paypal_api(endpoint, method="GET", data=None, token=None):
86
+ """
87
+ Helper function to make PayPal API calls
88
+
89
+ Args:
90
+ endpoint: API endpoint (without base URL)
91
+ method: HTTP method (GET, POST, etc.)
92
+ data: Request payload (for POST/PUT)
93
+ token: PayPal access token (will be fetched if None)
94
+
95
+ Returns:
96
+ tuple: (success, response_data or error_message)
97
+ """
98
+ try:
99
+ if not token:
100
+ token = get_access_token()
101
+ if not token:
102
+ return False, "Failed to get PayPal access token"
103
+
104
+ url = f"{PAYPAL_BASE_URL}{endpoint}"
105
+ headers = {
106
+ "Content-Type": "application/json",
107
+ "Authorization": f"Bearer {token}"
108
+ }
109
+
110
+ session = create_retry_session()
111
+
112
+ if method.upper() == "GET":
113
+ response = session.get(url, headers=headers)
114
+ elif method.upper() == "POST":
115
+ response = session.post(url, headers=headers, data=json.dumps(data) if data else None)
116
+ elif method.upper() == "PUT":
117
+ response = session.put(url, headers=headers, data=json.dumps(data) if data else None)
118
+ else:
119
+ return False, f"Unsupported HTTP method: {method}"
120
+
121
+ if response.status_code in [200, 201, 204]:
122
+ if response.status_code == 204: # No content
123
+ return True, {}
124
+ return True, response.json() if response.text else {}
125
+ else:
126
+ logger.error(f"PayPal API error: {response.status_code} - {response.text}")
127
+ return False, f"PayPal API error: {response.status_code} - {response.text}"
128
+
129
+ except Exception as e:
130
+ logger.error(f"Error calling PayPal API: {str(e)}")
131
+ return False, f"Error calling PayPal API: {str(e)}"
132
+
133
+ def create_paypal_subscription(user_id, tier):
134
+ """Create a PayPal subscription for a user"""
135
+ try:
136
+ # Get the price from the subscription tier
137
+ from auth import SUBSCRIPTION_TIERS
138
+
139
+ if tier not in SUBSCRIPTION_TIERS:
140
+ return False, f"Invalid tier: {tier}"
141
+
142
+ price = SUBSCRIPTION_TIERS[tier]["price"]
143
+ currency = SUBSCRIPTION_TIERS[tier]["currency"]
144
+
145
+ # Create a PayPal subscription (implement PayPal API calls here)
146
+ # For now, just return a success response
147
+ return True, {
148
+ "subscription_id": f"test_sub_{uuid.uuid4()}",
149
+ "status": "ACTIVE",
150
+ "tier": tier,
151
+ "price": price,
152
+ "currency": currency
153
+ }
154
+ except Exception as e:
155
+ logger.error(f"Error creating PayPal subscription: {str(e)}")
156
+ return False, f"Failed to create PayPal subscription: {str(e)}"
157
+
158
+
159
+ # Create a product in PayPal
160
+ def create_product(name, description):
161
+ """Create a product in PayPal"""
162
+ payload = {
163
+ "name": name,
164
+ "description": description,
165
+ "type": "SERVICE",
166
+ "category": "SOFTWARE"
167
+ }
168
+
169
+ success, result = call_paypal_api("/v1/catalogs/products", "POST", payload)
170
+ if success:
171
+ return result["id"]
172
+ else:
173
+ logger.error(f"Failed to create product: {result}")
174
+ return None
175
+
176
+ # Create a subscription plan in PayPal
177
+ # Update create_plan to use INR instead of USD
178
+ def create_plan(product_id, name, price, interval="MONTH", interval_count=1):
179
+ """Create a subscription plan in PayPal"""
180
+ payload = {
181
+ "product_id": product_id,
182
+ "name": name,
183
+ "billing_cycles": [
184
+ {
185
+ "frequency": {
186
+ "interval_unit": interval,
187
+ "interval_count": interval_count
188
+ },
189
+ "tenure_type": "REGULAR",
190
+ "sequence": 1,
191
+ "total_cycles": 0, # Infinite cycles
192
+ "pricing_scheme": {
193
+ "fixed_price": {
194
+ "value": str(price),
195
+ "currency_code": "USD"
196
+ }
197
+ }
198
+ }
199
+ ],
200
+ "payment_preferences": {
201
+ "auto_bill_outstanding": True,
202
+ "setup_fee": {
203
+ "value": "0",
204
+ "currency_code": "USD"
205
+ },
206
+ "setup_fee_failure_action": "CONTINUE",
207
+ "payment_failure_threshold": 3
208
+ }
209
+ }
210
+
211
+ success, result = call_paypal_api("/v1/billing/plans", "POST", payload)
212
+ if success:
213
+ return result["id"]
214
+ else:
215
+ logger.error(f"Failed to create plan: {result}")
216
+ return None
217
+
218
+ # Update initialize_subscription_plans to use INR pricing
219
+ def initialize_subscription_plans():
220
+ """
221
+ Initialize PayPal subscription plans for the application.
222
+ This should be called once to set up the plans in PayPal.
223
+ """
224
+ try:
225
+ # Check if plans already exist
226
+ existing_plans = get_subscription_plans()
227
+ if existing_plans and len(existing_plans) >= 2:
228
+ logger.info("PayPal plans already initialized")
229
+ return existing_plans
230
+
231
+ # First, create products for each tier
232
+ products = {
233
+ "standard_tier": {
234
+ "name": "Standard Legal Document Analysis",
235
+ "description": "Standard subscription with document analysis features",
236
+ "type": "SERVICE",
237
+ "category": "SOFTWARE"
238
+ },
239
+ "premium_tier": {
240
+ "name": "Premium Legal Document Analysis",
241
+ "description": "Premium subscription with all document analysis features",
242
+ "type": "SERVICE",
243
+ "category": "SOFTWARE"
244
+ }
245
+ }
246
+
247
+ product_ids = {}
248
+ for tier, product_data in products.items():
249
+ success, result = call_paypal_api("/v1/catalogs/products", "POST", product_data)
250
+ if success:
251
+ product_ids[tier] = result["id"]
252
+ logger.info(f"Created PayPal product for {tier}: {result['id']}")
253
+ else:
254
+ logger.error(f"Failed to create product for {tier}: {result}")
255
+ return None
256
+
257
+ # Define the plans with product IDs - Changed currency to USD
258
+ plans = {
259
+ "standard_tier": {
260
+ "product_id": product_ids["standard_tier"],
261
+ "name": "Standard Plan",
262
+ "description": "Standard subscription with basic features",
263
+ "billing_cycles": [
264
+ {
265
+ "frequency": {
266
+ "interval_unit": "MONTH",
267
+ "interval_count": 1
268
+ },
269
+ "tenure_type": "REGULAR",
270
+ "sequence": 1,
271
+ "total_cycles": 0,
272
+ "pricing_scheme": {
273
+ "fixed_price": {
274
+ "value": "9.99",
275
+ "currency_code": "USD"
276
+ }
277
+ }
278
+ }
279
+ ],
280
+ "payment_preferences": {
281
+ "auto_bill_outstanding": True,
282
+ "setup_fee": {
283
+ "value": "0",
284
+ "currency_code": "USD"
285
+ },
286
+ "setup_fee_failure_action": "CONTINUE",
287
+ "payment_failure_threshold": 3
288
+ }
289
+ },
290
+ "premium_tier": {
291
+ "product_id": product_ids["premium_tier"],
292
+ "name": "Premium Plan",
293
+ "description": "Premium subscription with all features",
294
+ "billing_cycles": [
295
+ {
296
+ "frequency": {
297
+ "interval_unit": "MONTH",
298
+ "interval_count": 1
299
+ },
300
+ "tenure_type": "REGULAR",
301
+ "sequence": 1,
302
+ "total_cycles": 0,
303
+ "pricing_scheme": {
304
+ "fixed_price": {
305
+ "value": "19.99",
306
+ "currency_code": "USD"
307
+ }
308
+ }
309
+ }
310
+ ],
311
+ "payment_preferences": {
312
+ "auto_bill_outstanding": True,
313
+ "setup_fee": {
314
+ "value": "0",
315
+ "currency_code": "USD"
316
+ },
317
+ "setup_fee_failure_action": "CONTINUE",
318
+ "payment_failure_threshold": 3
319
+ }
320
+ }
321
+ }
322
+
323
+ # Create the plans in PayPal
324
+ created_plans = {}
325
+ for tier, plan_data in plans.items():
326
+ success, result = call_paypal_api("/v1/billing/plans", "POST", plan_data)
327
+ if success:
328
+ created_plans[tier] = result["id"]
329
+ logger.info(f"Created PayPal plan for {tier}: {result['id']}")
330
+ else:
331
+ logger.error(f"Failed to create plan for {tier}: {result}")
332
+
333
+ # Save the plan IDs to a file
334
+ if created_plans:
335
+ save_subscription_plans(created_plans)
336
+ return created_plans
337
+ else:
338
+ logger.error("Failed to create any PayPal plans")
339
+ return None
340
+ except Exception as e:
341
+ logger.error(f"Error initializing subscription plans: {str(e)}")
342
+ return None
343
+
344
+ # Update create_subscription_link to use call_paypal_api helper
345
+ def create_subscription_link(plan_id):
346
+ # Get the plan IDs
347
+ plans = get_subscription_plans()
348
+ if not plans:
349
+ return None
350
+
351
+ # Use environment variable for the app URL to make it work in different environments
352
+ app_url = os.getenv("APP_URL", "http://localhost:8501")
353
+
354
+ payload = {
355
+ "plan_id": plans[plan_id],
356
+ "application_context": {
357
+ "brand_name": "Legal Document Analyzer",
358
+ "locale": "en_US",
359
+ "shipping_preference": "NO_SHIPPING",
360
+ "user_action": "SUBSCRIBE_NOW",
361
+ "return_url": f"{app_url}?status=success&subscription_id={{id}}",
362
+ "cancel_url": f"{app_url}?status=cancel"
363
+ }
364
+ }
365
+
366
+ success, data = call_paypal_api("/v1/billing/subscriptions", "POST", payload)
367
+ if not success:
368
+ logger.error(f"Error creating subscription: {data}")
369
+ return None
370
+
371
+ try:
372
+ return {
373
+ "subscription_id": data["id"],
374
+ "approval_url": next(link["href"] for link in data["links"] if link["rel"] == "approve")
375
+ }
376
+ except Exception as e:
377
+ logger.error(f"Exception processing subscription response: {str(e)}")
378
+ return None
379
+
380
+ # Fix the webhook handler function signature to match how it's called in app.py
381
+ def handle_subscription_webhook(payload):
382
+ """
383
+ Handle PayPal subscription webhooks
384
+
385
+ Args:
386
+ payload: The full webhook payload
387
+
388
+ Returns:
389
+ tuple: (success, result)
390
+ - success: True if successful, False otherwise
391
+ - result: Success message or error message
392
+ """
393
+ try:
394
+ event_type = payload.get("event_type")
395
+ resource = payload.get("resource", {})
396
+
397
+ logger.info(f"Received PayPal webhook: {event_type}")
398
+
399
+ # Handle different event types
400
+ if event_type == "BILLING.SUBSCRIPTION.CREATED":
401
+ # A subscription was created
402
+ subscription_id = resource.get("id")
403
+ if not subscription_id:
404
+ return False, "Missing subscription ID in webhook"
405
+
406
+ # Update subscription status in database
407
+ conn = get_db_connection()
408
+ cursor = conn.cursor()
409
+ cursor.execute(
410
+ "UPDATE subscriptions SET status = 'pending' WHERE paypal_subscription_id = ?",
411
+ (subscription_id,)
412
+ )
413
+ conn.commit()
414
+ conn.close()
415
+
416
+ return True, "Subscription created successfully"
417
+
418
+ elif event_type == "BILLING.SUBSCRIPTION.ACTIVATED":
419
+ # A subscription was activated
420
+ subscription_id = resource.get("id")
421
+ if not subscription_id:
422
+ return False, "Missing subscription ID in webhook"
423
+
424
+ # Update subscription status in database
425
+ conn = get_db_connection()
426
+ cursor = conn.cursor()
427
+ cursor.execute(
428
+ "UPDATE subscriptions SET status = 'active' WHERE paypal_subscription_id = ?",
429
+ (subscription_id,)
430
+ )
431
+ conn.commit()
432
+ conn.close()
433
+
434
+ return True, "Subscription activated successfully"
435
+
436
+ elif event_type == "BILLING.SUBSCRIPTION.CANCELLED":
437
+ # A subscription was cancelled
438
+ subscription_id = resource.get("id")
439
+ if not subscription_id:
440
+ return False, "Missing subscription ID in webhook"
441
+
442
+ # Update subscription status in database
443
+ conn = get_db_connection()
444
+ cursor = conn.cursor()
445
+ cursor.execute(
446
+ "UPDATE subscriptions SET status = 'cancelled' WHERE paypal_subscription_id = ?",
447
+ (subscription_id,)
448
+ )
449
+ conn.commit()
450
+ conn.close()
451
+
452
+ return True, "Subscription cancelled successfully"
453
+
454
+ elif event_type == "BILLING.SUBSCRIPTION.SUSPENDED":
455
+ # A subscription was suspended
456
+ subscription_id = resource.get("id")
457
+ if not subscription_id:
458
+ return False, "Missing subscription ID in webhook"
459
+
460
+ # Update subscription status in database
461
+ conn = get_db_connection()
462
+ cursor = conn.cursor()
463
+ cursor.execute(
464
+ "UPDATE subscriptions SET status = 'suspended' WHERE paypal_subscription_id = ?",
465
+ (subscription_id,)
466
+ )
467
+ conn.commit()
468
+ conn.close()
469
+
470
+ return True, "Subscription suspended successfully"
471
+
472
+ else:
473
+ # Unhandled event type
474
+ logger.info(f"Unhandled webhook event type: {event_type}")
475
+ return True, f"Unhandled event type: {event_type}"
476
+
477
+ except Exception as e:
478
+ logger.error(f"Error handling webhook: {str(e)}")
479
+ return False, f"Error handling webhook: {str(e)}"
480
+ # Add this function to update user subscription
481
+ def update_user_subscription(user_email, subscription_id, tier):
482
+ """
483
+ Update a user's subscription status
484
+
485
+ Args:
486
+ user_email: The email of the user
487
+ subscription_id: The PayPal subscription ID
488
+ tier: The subscription tier
489
+
490
+ Returns:
491
+ tuple: (success, result)
492
+ - success: True if successful, False otherwise
493
+ - result: Success message or error message
494
+ """
495
+ try:
496
+ # Get user ID from email
497
+ conn = get_db_connection()
498
+ cursor = conn.cursor()
499
+ cursor.execute("SELECT id FROM users WHERE email = ?", (user_email,))
500
+ user_result = cursor.fetchone()
501
+
502
+ if not user_result:
503
+ conn.close()
504
+ return False, f"User not found: {user_email}"
505
+
506
+ user_id = user_result[0]
507
+
508
+ # Update the subscription status
509
+ cursor.execute(
510
+ "UPDATE subscriptions SET status = 'active' WHERE user_id = ? AND paypal_subscription_id = ?",
511
+ (user_id, subscription_id)
512
+ )
513
+
514
+ # Deactivate any other active subscriptions for this user
515
+ cursor.execute(
516
+ "UPDATE subscriptions SET status = 'inactive' WHERE user_id = ? AND paypal_subscription_id != ? AND status = 'active'",
517
+ (user_id, subscription_id)
518
+ )
519
+
520
+ # Update the user's subscription tier
521
+ cursor.execute(
522
+ "UPDATE users SET subscription_tier = ? WHERE email = ?",
523
+ (tier, user_email)
524
+ )
525
+
526
+ conn.commit()
527
+ conn.close()
528
+
529
+ return True, f"Subscription updated to {tier} tier"
530
+
531
+ except Exception as e:
532
+ logger.error(f"Error updating user subscription: {str(e)}")
533
+ return False, f"Error updating subscription: {str(e)}"
534
+
535
+ # Add this near the top with other path definitions
536
+ # Update the PLAN_IDS_PATH definition to use the correct path
537
+ PLAN_IDS_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "data", "plan_ids.json"))
538
+
539
+ # Make sure the data directory exists
540
+ os.makedirs(os.path.dirname(PLAN_IDS_PATH), exist_ok=True)
541
+
542
+ # Add this debug log to see where the file is expected
543
+ logger.info(f"PayPal plans will be stored at: {PLAN_IDS_PATH}")
544
+
545
+ # Add this function if it's not defined elsewhere
546
+ def get_db_connection():
547
+ """Get a connection to the SQLite database"""
548
+ DB_PATH = os.getenv("DB_PATH", os.path.join(os.path.dirname(__file__), "../data/user_data.db"))
549
+ # Make sure the data directory exists
550
+ os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
551
+ return sqlite3.connect(DB_PATH)
552
+
553
+ # Add this function to create subscription tables if needed
554
+ def initialize_database():
555
+ """Initialize the database tables needed for subscriptions"""
556
+ conn = get_db_connection()
557
+ cursor = conn.cursor()
558
+
559
+ # Check if subscriptions table exists
560
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='subscriptions'")
561
+ if cursor.fetchone():
562
+ # Table exists, check if required columns exist
563
+ cursor.execute("PRAGMA table_info(subscriptions)")
564
+ columns = [column[1] for column in cursor.fetchall()]
565
+
566
+ # Check for missing columns and add them if needed
567
+ if "user_id" not in columns:
568
+ logger.info("Adding 'user_id' column to subscriptions table")
569
+ cursor.execute("ALTER TABLE subscriptions ADD COLUMN user_id TEXT NOT NULL DEFAULT ''")
570
+
571
+ if "created_at" not in columns:
572
+ logger.info("Adding 'created_at' column to subscriptions table")
573
+ cursor.execute("ALTER TABLE subscriptions ADD COLUMN created_at TIMESTAMP")
574
+
575
+ if "expires_at" not in columns:
576
+ logger.info("Adding 'expires_at' column to subscriptions table")
577
+ cursor.execute("ALTER TABLE subscriptions ADD COLUMN expires_at TIMESTAMP")
578
+
579
+ if "paypal_subscription_id" not in columns:
580
+ logger.info("Adding 'paypal_subscription_id' column to subscriptions table")
581
+ cursor.execute("ALTER TABLE subscriptions ADD COLUMN paypal_subscription_id TEXT")
582
+ else:
583
+ # Create subscriptions table with all required columns
584
+ cursor.execute('''
585
+ CREATE TABLE IF NOT EXISTS subscriptions (
586
+ id TEXT PRIMARY KEY,
587
+ user_id TEXT NOT NULL,
588
+ tier TEXT NOT NULL,
589
+ status TEXT NOT NULL,
590
+ created_at TIMESTAMP NOT NULL,
591
+ expires_at TIMESTAMP,
592
+ paypal_subscription_id TEXT
593
+ )
594
+ ''')
595
+ logger.info("Created subscriptions table with all required columns")
596
+
597
+ # Create PayPal plans table if it doesn't exist
598
+ cursor.execute('''
599
+ CREATE TABLE IF NOT EXISTS paypal_plans (
600
+ plan_id TEXT PRIMARY KEY,
601
+ tier TEXT NOT NULL,
602
+ price REAL NOT NULL,
603
+ currency TEXT NOT NULL,
604
+ created_at TIMESTAMP NOT NULL
605
+ )
606
+ ''')
607
+
608
+ conn.commit()
609
+ conn.close()
610
+ logger.info("Database initialization completed")
611
+
612
+
613
+ def create_user_subscription_mock(user_email, tier):
614
+ """
615
+ Create a mock subscription for testing
616
+
617
+ Args:
618
+ user_email: The email of the user
619
+ tier: The subscription tier
620
+
621
+ Returns:
622
+ tuple: (success, result)
623
+ """
624
+ try:
625
+ logger.info(f"Creating mock subscription for {user_email} at tier {tier}")
626
+
627
+ # Get user ID from email
628
+ conn = get_db_connection()
629
+ cursor = conn.cursor()
630
+ cursor.execute("SELECT id FROM users WHERE email = ?", (user_email,))
631
+ user_result = cursor.fetchone()
632
+
633
+ if not user_result:
634
+ conn.close()
635
+ return False, f"User not found: {user_email}"
636
+
637
+ user_id = user_result[0]
638
+
639
+ # Create a mock subscription ID
640
+ subscription_id = f"mock_sub_{uuid.uuid4()}"
641
+
642
+ # Store the subscription in database
643
+ sub_id = str(uuid.uuid4())
644
+ start_date = datetime.now()
645
+
646
+ cursor.execute(
647
+ "INSERT INTO subscriptions (id, user_id, tier, status, created_at, expires_at, paypal_subscription_id) VALUES (?, ?, ?, ?, ?, ?, ?)",
648
+ (sub_id, user_id, tier, "active", start_date, start_date + timedelta(days=30), subscription_id)
649
+ )
650
+
651
+ # Update user's subscription tier
652
+ cursor.execute(
653
+ "UPDATE users SET subscription_tier = ? WHERE id = ?",
654
+ (tier, user_id)
655
+ )
656
+
657
+ conn.commit()
658
+ conn.close()
659
+
660
+ # Use environment variable for the app URL
661
+ app_url = os.getenv("APP_URL", "http://localhost:3000")
662
+
663
+ # Return success with mock approval URL that matches the real PayPal URL pattern
664
+ return True, {
665
+ "subscription_id": subscription_id,
666
+ "approval_url": f"{app_url}/subscription/callback?status=success&subscription_id={subscription_id}",
667
+ "tier": tier
668
+ }
669
+
670
+ except Exception as e:
671
+ logger.error(f"Error creating mock subscription: {str(e)}")
672
+ return False, f"Error creating subscription: {str(e)}"
673
+
674
+ # Add this at the end of the file
675
+ def initialize():
676
+ """Initialize the PayPal integration module"""
677
+ try:
678
+ # Create necessary directories
679
+ os.makedirs(os.path.dirname(PLAN_IDS_PATH), exist_ok=True)
680
+
681
+ # Initialize database
682
+ initialize_database()
683
+
684
+ # Initialize subscription plans
685
+ plans = get_subscription_plans()
686
+ if plans:
687
+ logger.info(f"Subscription plans initialized: {plans}")
688
+ else:
689
+ logger.warning("Failed to initialize subscription plans")
690
+
691
+ return True
692
+ except Exception as e:
693
+ logger.error(f"Error initializing PayPal integration: {str(e)}")
694
+ return False
695
+
696
+ # Call initialize when the module is imported
697
+ initialize()
698
+
699
+ # Add this function to get subscription plans
700
+ def get_subscription_plans():
701
+ """
702
+ Get all available subscription plans with correct pricing
703
+ """
704
+ try:
705
+ # Check if we have plan IDs saved in a file
706
+ if os.path.exists(PLAN_IDS_PATH):
707
+ try:
708
+ with open(PLAN_IDS_PATH, 'r') as f:
709
+ plans = json.load(f)
710
+ logger.info(f"Loaded subscription plans from {PLAN_IDS_PATH}: {plans}")
711
+ return plans
712
+ except Exception as e:
713
+ logger.error(f"Error reading plan IDs file: {str(e)}")
714
+ return {}
715
+
716
+ # If no file exists, return empty dict
717
+ logger.warning(f"No plan IDs file found at {PLAN_IDS_PATH}. Please initialize subscription plans.")
718
+ return {}
719
+
720
+ except Exception as e:
721
+ logger.error(f"Error getting subscription plans: {str(e)}")
722
+ return {}
723
+
724
+ # Add this function to create subscription tables if needed
725
+ def initialize_database():
726
+ """Initialize the database tables needed for subscriptions"""
727
+ conn = get_db_connection()
728
+ cursor = conn.cursor()
729
+
730
+ # Create subscriptions table if it doesn't exist
731
+ cursor.execute('''
732
+ CREATE TABLE IF NOT EXISTS subscriptions (
733
+ id TEXT PRIMARY KEY,
734
+ user_id TEXT NOT NULL,
735
+ tier TEXT NOT NULL,
736
+ status TEXT NOT NULL,
737
+ created_at TIMESTAMP NOT NULL,
738
+ expires_at TIMESTAMP,
739
+ paypal_subscription_id TEXT
740
+ )
741
+ ''')
742
+
743
+ # Create PayPal plans table if it doesn't exist
744
+ cursor.execute('''
745
+ CREATE TABLE IF NOT EXISTS paypal_plans (
746
+ plan_id TEXT PRIMARY KEY,
747
+ tier TEXT NOT NULL,
748
+ price REAL NOT NULL,
749
+ currency TEXT NOT NULL,
750
+ created_at TIMESTAMP NOT NULL
751
+ )
752
+ ''')
753
+
754
+ conn.commit()
755
+ conn.close()
756
+
757
+
758
+ def create_user_subscription(user_email, tier):
759
+ """
760
+ Create a real PayPal subscription for a user
761
+
762
+ Args:
763
+ user_email: The email of the user
764
+ tier: The subscription tier (standard_tier or premium_tier)
765
+
766
+ Returns:
767
+ tuple: (success, result)
768
+ - success: True if successful, False otherwise
769
+ - result: Dictionary with subscription details or error message
770
+ """
771
+ try:
772
+ # Validate tier
773
+ valid_tiers = ["standard_tier", "premium_tier"]
774
+ if tier not in valid_tiers:
775
+ return False, f"Invalid tier: {tier}. Must be one of {valid_tiers}"
776
+
777
+ # Get the plan IDs
778
+ plans = get_subscription_plans()
779
+
780
+ # Log the plans for debugging
781
+ logger.info(f"Available subscription plans: {plans}")
782
+
783
+ # If no plans found, check if the file exists and try to load it directly
784
+ if not plans:
785
+ if os.path.exists(PLAN_IDS_PATH):
786
+ logger.info(f"Plan IDs file exists at {PLAN_IDS_PATH}, but couldn't load plans. Trying direct load.")
787
+ try:
788
+ with open(PLAN_IDS_PATH, 'r') as f:
789
+ plans = json.load(f)
790
+ logger.info(f"Directly loaded plans: {plans}")
791
+ except Exception as e:
792
+ logger.error(f"Error directly loading plans: {str(e)}")
793
+ else:
794
+ logger.error(f"Plan IDs file does not exist at {PLAN_IDS_PATH}")
795
+
796
+ # If still no plans, return error
797
+ if not plans:
798
+ logger.error("No PayPal plans found. Please initialize plans first.")
799
+ return False, "PayPal plans not configured. Please contact support."
800
+
801
+ # Check if the tier exists in plans
802
+ if tier not in plans:
803
+ return False, f"No plan found for tier: {tier}"
804
+
805
+ # Use environment variable for the app URL
806
+ app_url = os.getenv("APP_URL", "http://localhost:3000")
807
+
808
+ # Create the subscription with PayPal
809
+ payload = {
810
+ "plan_id": plans[tier],
811
+ "subscriber": {
812
+ "email_address": user_email
813
+ },
814
+ "application_context": {
815
+ "brand_name": "Legal Document Analyzer",
816
+ "locale": "en-US", # Changed from en_US to en-US
817
+ "shipping_preference": "NO_SHIPPING",
818
+ "user_action": "SUBSCRIBE_NOW",
819
+ "return_url": f"{app_url}/subscription/callback?status=success",
820
+ "cancel_url": f"{app_url}/subscription/callback?status=cancel"
821
+ }
822
+ }
823
+
824
+ # Make the API call to PayPal
825
+ success, subscription_data = call_paypal_api("/v1/billing/subscriptions", "POST", payload)
826
+ if not success:
827
+ return False, subscription_data # This is already an error message
828
+
829
+ # Extract the approval URL
830
+ approval_url = next((link["href"] for link in subscription_data["links"]
831
+ if link["rel"] == "approve"), None)
832
+
833
+ if not approval_url:
834
+ return False, "No approval URL found in PayPal response"
835
+
836
+ # Get user ID from email
837
+ conn = get_db_connection()
838
+ cursor = conn.cursor()
839
+ cursor.execute("SELECT id FROM users WHERE email = ?", (user_email,))
840
+ user_result = cursor.fetchone()
841
+
842
+ if not user_result:
843
+ conn.close()
844
+ return False, f"User not found: {user_email}"
845
+
846
+ user_id = user_result[0]
847
+
848
+ # Store pending subscription in database
849
+ sub_id = str(uuid.uuid4())
850
+ start_date = datetime.now()
851
+
852
+ cursor.execute(
853
+ "INSERT INTO subscriptions (id, user_id, tier, status, created_at, expires_at, paypal_subscription_id) VALUES (?, ?, ?, ?, ?, ?, ?)",
854
+ (sub_id, user_id, tier, "pending", start_date, None, subscription_data["id"])
855
+ )
856
+
857
+ conn.commit()
858
+ conn.close()
859
+
860
+ # Return success with approval URL
861
+ return True, {
862
+ "subscription_id": subscription_data["id"],
863
+ "approval_url": approval_url,
864
+ "tier": tier
865
+ }
866
+
867
+ except Exception as e:
868
+ logger.error(f"Error creating user subscription: {str(e)}")
869
+ return False, f"Error creating subscription: {str(e)}"
870
+
871
+ # Add a function to cancel a subscription
872
+ def cancel_subscription(subscription_id, reason="Customer requested cancellation"):
873
+ """
874
+ Cancel a PayPal subscription
875
+
876
+ Args:
877
+ subscription_id: The PayPal subscription ID
878
+ reason: The reason for cancellation
879
+
880
+ Returns:
881
+ tuple: (success, result)
882
+ - success: True if successful, False otherwise
883
+ - result: Success message or error message
884
+ """
885
+ try:
886
+ # Cancel the subscription with PayPal
887
+ payload = {
888
+ "reason": reason
889
+ }
890
+
891
+ success, result = call_paypal_api(
892
+ f"/v1/billing/subscriptions/{subscription_id}/cancel",
893
+ "POST",
894
+ payload
895
+ )
896
+
897
+ if not success:
898
+ return False, result
899
+
900
+ # Update subscription status in database
901
+ conn = get_db_connection()
902
+ cursor = conn.cursor()
903
+ cursor.execute(
904
+ "UPDATE subscriptions SET status = 'cancelled' WHERE paypal_subscription_id = ?",
905
+ (subscription_id,)
906
+ )
907
+
908
+ # Get the user ID for this subscription
909
+ cursor.execute(
910
+ "SELECT user_id FROM subscriptions WHERE paypal_subscription_id = ?",
911
+ (subscription_id,)
912
+ )
913
+ user_result = cursor.fetchone()
914
+
915
+ if user_result:
916
+ # Update user to free tier
917
+ cursor.execute(
918
+ "UPDATE users SET subscription_tier = 'free_tier' WHERE id = ?",
919
+ (user_result[0],)
920
+ )
921
+
922
+ conn.commit()
923
+ conn.close()
924
+
925
+ return True, "Subscription cancelled successfully"
926
+
927
+ except Exception as e:
928
+ logger.error(f"Error cancelling subscription: {str(e)}")
929
+ return False, f"Error cancelling subscription: {str(e)}"
930
+
931
+ def verify_subscription_payment(subscription_id):
932
+ """
933
+ Verify a subscription payment with PayPal
934
+
935
+ Args:
936
+ subscription_id: The PayPal subscription ID
937
+
938
+ Returns:
939
+ tuple: (success, result)
940
+ - success: True if successful, False otherwise
941
+ - result: Dictionary with subscription details or error message
942
+ """
943
+ try:
944
+ # Get subscription details from PayPal using our helper
945
+ success, subscription_data = call_paypal_api(f"/v1/billing/subscriptions/{subscription_id}")
946
+ if not success:
947
+ return False, subscription_data # This is already an error message
948
+
949
+ # Check subscription status
950
+ status = subscription_data.get("status", "").upper()
951
+
952
+ if status not in ["ACTIVE", "APPROVED"]:
953
+ return False, f"Subscription is not active: {status}"
954
+
955
+ # Return success with subscription data
956
+ return True, subscription_data
957
+
958
+ except Exception as e:
959
+ logger.error(f"Error verifying subscription: {str(e)}")
960
+ return False, f"Error verifying subscription: {str(e)}"
961
+
962
+ def verify_paypal_subscription(subscription_id):
963
+ """
964
+ Verify a PayPal subscription
965
+
966
+ Args:
967
+ subscription_id: The PayPal subscription ID
968
+
969
+ Returns:
970
+ tuple: (success, result)
971
+ """
972
+ try:
973
+ # Skip verification for mock subscriptions
974
+ if subscription_id.startswith("mock_sub_"):
975
+ return True, {"status": "ACTIVE"}
976
+
977
+ # For real subscriptions, call PayPal API
978
+ success, result = call_paypal_api(f"/v1/billing/subscriptions/{subscription_id}", "GET")
979
+
980
+ if success:
981
+ # Check subscription status
982
+ if result.get("status") == "ACTIVE":
983
+ return True, result
984
+ else:
985
+ return False, f"Subscription is not active: {result.get('status')}"
986
+ else:
987
+ logger.error(f"PayPal API error: {result}")
988
+ return False, f"Failed to verify subscription: {result}"
989
+ except Exception as e:
990
+ logger.error(f"Error verifying PayPal subscription: {str(e)}")
991
+ return False, f"Error verifying subscription: {str(e)}"
992
+
993
+ # Add this function to save subscription plans
994
+ def save_subscription_plans(plans):
995
+ """
996
+ Save subscription plans to a file
997
+
998
+ Args:
999
+ plans: Dictionary of plan IDs by tier
1000
+ """
1001
+ try:
1002
+ with open(PLAN_IDS_PATH, 'w') as f:
1003
+ json.dump(plans, f)
1004
+ logger.info(f"Saved subscription plans to {PLAN_IDS_PATH}")
1005
+ return True
1006
+ except Exception as e:
1007
+ logger.error(f"Error saving subscription plans: {str(e)}")
1008
+ return False