Files changed (10) hide show
  1. .env +0 -27
  2. Dockerfile +0 -36
  3. README.md +10 -40
  4. app.py +0 -1531
  5. auth.py +0 -633
  6. check_routes.py +0 -61
  7. fix_users_table.py +0 -180
  8. initialize_plans.py +0 -25
  9. paypal_integration.py +0 -1004
  10. requirements.txt +0 -21
.env DELETED
@@ -1,27 +0,0 @@
1
- # Backend API URL (will be updated to your Hugging Face Space URL)
2
- BACKEND_URL=https://huggingface.co/spaces/tejash300/testing
3
-
4
- # Frontend App URL (if you're deploying the frontend separately)
5
- APP_URL=http://localhost:3000
6
-
7
- # PayPal Configuration
8
- PAYPAL_BASE_URL=https://api-m.sandbox.paypal.com
9
- PAYPAL_CLIENT_ID=ASOzKSgawVTyJpK_1vOKap4TTJ3OsHQ9syDvEX43O2Vi7ZVoto1z6zYWWm20LrrJ-dA9wqD33jrT5qLu
10
- PAYPAL_SECRET=ED0YJoSvOq6sUjOfQvz88Z-NsFhDyK2Dv2TUI3LOoEZ11rkNev92Cp6O8d2mBLw7fdunKRfHhcMyNuXN
11
-
12
- # JWT Secret
13
- JWT_SECRET=13105030e5bfb231ebf59b8cdf91a39571e51a51fe62a4e1c323079f9945cb7d
14
-
15
- # Database Path (adjusted for Hugging Face container structure)
16
- DB_PATH=/app/data/user_data.db
17
-
18
- # Plan IDs Path (adjusted for Hugging Face container structure)
19
- PLAN_IDS_PATH=/app/data/plan_ids.json
20
-
21
- # Model Loading Configuration
22
- LOAD_MODELS=True
23
- MODELS_CACHE_DIR=/app/models_cache
24
-
25
- # Hugging Face Spaces Configuration
26
- PORT=7860
27
- HOST=0.0.0.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Dockerfile DELETED
@@ -1,36 +0,0 @@
1
- FROM python:3.9-slim
2
-
3
- WORKDIR /app
4
-
5
- # Install system dependencies
6
- RUN apt-get update && apt-get install -y \
7
- build-essential \
8
- libffi-dev \
9
- git \
10
- ffmpeg \
11
- && rm -rf /var/lib/apt/lists/*
12
-
13
- # Copy requirements first for better caching
14
- COPY requirements.txt .
15
- RUN pip install --no-cache-dir -r requirements.txt
16
-
17
- # Create necessary directories
18
- RUN mkdir -p static uploads temp data models_cache
19
-
20
- # Copy only the necessary files
21
- COPY app.py .
22
- COPY auth.py .
23
- COPY paypal_integration.py .
24
- COPY .env .
25
-
26
- # Set environment variables
27
- ENV LOAD_MODELS=True
28
- ENV MODELS_CACHE_DIR=/app/models_cache
29
- ENV PORT=7860
30
- ENV HOST=0.0.0.0
31
-
32
- # Expose the port
33
- EXPOSE 7860
34
-
35
- # Command to run the application
36
- CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -1,40 +1,10 @@
1
- ---
2
- title: Legal Document Analysis API
3
- emoji: 📄
4
- colorFrom: blue
5
- colorTo: indigo
6
- sdk: docker
7
- pinned: false
8
- license: mit
9
- ---
10
-
11
- # Legal Document Analysis API
12
-
13
- This API provides tools for analyzing legal documents, videos, and audio files. It uses NLP models to extract insights, summarize content, and answer legal questions.
14
-
15
- ## Features
16
-
17
- - Document analysis (PDF)
18
- - Video and audio transcription and analysis
19
- - Legal question answering
20
- - Risk assessment and visualization
21
- - Contract clause analysis
22
-
23
- ## Deployment
24
-
25
- This API is deployed on Hugging Face Spaces.
26
-
27
- ## API Endpoints
28
-
29
- - `/analyze_document` - Analyze legal documents
30
- - `/analyze_legal_video` - Analyze legal videos
31
- - `/analyze_legal_audio` - Analyze legal audio
32
- - `/ask_legal_question` - Ask questions about legal documents
33
-
34
- ## Technologies
35
-
36
- - FastAPI
37
- - Hugging Face Transformers
38
- - SpaCy
39
- - PyTorch
40
- - MoviePy
 
1
+ ---
2
+ title: Testing
3
+ emoji: 📚
4
+ colorFrom: gray
5
+ colorTo: blue
6
+ sdk: docker
7
+ pinned: false
8
+ ---
9
+
10
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.py DELETED
@@ -1,1531 +0,0 @@
1
- from huggingface_hub import snapshot_download
2
-
3
- import os
4
- import io
5
- import time
6
- import uuid
7
- import tempfile
8
- import numpy as np
9
- import matplotlib.pyplot as plt
10
- import pdfplumber
11
- import spacy
12
- import torch
13
- import sqlite3
14
- import uvicorn
15
- import moviepy.editor as mp
16
- from threading import Thread
17
- from datetime import datetime, timedelta
18
- from typing import List, Dict, Optional
19
- from fastapi import FastAPI, File, UploadFile, Form, Depends, HTTPException, status, Header
20
- from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
21
- from fastapi.staticfiles import StaticFiles
22
- from fastapi.middleware.cors import CORSMiddleware
23
- import logging
24
- from pydantic import BaseModel
25
- from transformers import (
26
- AutoTokenizer,
27
- AutoModelForQuestionAnswering,
28
- pipeline,
29
- TrainingArguments,
30
- Trainer
31
- )
32
- from sentence_transformers import SentenceTransformer
33
- from passlib.context import CryptContext
34
- from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
35
- import jwt
36
- from dotenv import load_dotenv
37
- # Import get_db_connection from auth
38
- from auth import (
39
- User, UserCreate, Token, get_current_active_user, authenticate_user,
40
- create_access_token, hash_password, register_user, check_subscription_access,
41
- SUBSCRIPTION_TIERS, JWT_EXPIRATION_DELTA, get_db_connection, update_auth_db_schema
42
- )
43
- # Add this import near the top with your other imports
44
- from paypal_integration import (
45
- create_user_subscription, verify_subscription_payment,
46
- update_user_subscription, handle_subscription_webhook, initialize_database
47
- )
48
- from fastapi import Request # Add this if not already imported
49
-
50
- logging.basicConfig(
51
- level=logging.INFO,
52
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
53
- )
54
- logger = logging.getLogger("app")
55
-
56
- # Initialize the database
57
- # Initialize FastAPI app
58
- app = FastAPI(
59
- title="Legal Document Analysis API",
60
- description="API for analyzing legal documents, videos, and audio",
61
- version="1.0.0"
62
- )
63
-
64
- # Set up CORS middleware
65
- app.add_middleware(
66
- CORSMiddleware,
67
- allow_origins=["https://testing-78wtxfqt0-hardikkandpals-projects.vercel.app",
68
- "http://localhost:3000",
69
- # Add the actual Hugging Face Space URL
70
- "https://huggingface.co/spaces/tejash300/testing",
71
- # Add the direct Space URL format
72
- "https://tejash300-testing.hf.space"], # Frontend URL
73
- allow_credentials=True,
74
- allow_methods=["*"],
75
- allow_headers=["*"],
76
- )
77
- initialize_database()
78
- try:
79
- update_auth_db_schema()
80
- logger.info("Database schema updated successfully")
81
- except Exception as e:
82
- logger.error(f"Database schema update error: {e}")
83
-
84
- # Create static directory for file storage
85
- os.makedirs("static", exist_ok=True)
86
- os.makedirs("uploads", exist_ok=True)
87
- os.makedirs("temp", exist_ok=True)
88
- app.mount("/static", StaticFiles(directory="static"), name="static")
89
-
90
- # Set device for model inference
91
- device = "cuda" if torch.cuda.is_available() else "cpu"
92
- print(f"Using device: {device}")
93
-
94
- # Initialize chat history
95
- chat_history = []
96
-
97
- # Document context storage
98
- document_contexts = {}
99
-
100
- def store_document_context(task_id, text):
101
- """Store document text for later retrieval."""
102
- document_contexts[task_id] = text
103
-
104
- def load_document_context(task_id):
105
- """Load document text for a given task ID."""
106
- return document_contexts.get(task_id, "")
107
-
108
- def get_db_connection():
109
- """Get a connection to the SQLite database."""
110
- db_path = os.path.join(os.path.dirname(__file__), "legal_analysis.db")
111
- conn = sqlite3.connect(db_path)
112
- conn.row_factory = sqlite3.Row
113
- return conn
114
-
115
- load_dotenv()
116
- DB_PATH = os.getenv("DB_PATH", os.path.join(os.path.dirname(__file__), "data/user_data.db"))
117
- os.makedirs(os.path.join(os.path.dirname(__file__), "data"), exist_ok=True)
118
-
119
- def fine_tune_qa_model():
120
- """Fine-tunes a QA model on the CUAD dataset."""
121
- print("Loading base model for fine-tuning...")
122
- tokenizer = AutoTokenizer.from_pretrained("deepset/roberta-base-squad2")
123
- model = AutoModelForQuestionAnswering.from_pretrained("deepset/roberta-base-squad2")
124
-
125
- # Load and preprocess CUAD dataset
126
- print("Loading CUAD dataset...")
127
- from datasets import load_dataset
128
-
129
- try:
130
- dataset = load_dataset("cuad")
131
- except Exception as e:
132
- print(f"Error loading CUAD dataset: {str(e)}")
133
- print("Downloading CUAD dataset from alternative source...")
134
- # Implement alternative dataset loading here
135
- return tokenizer, model
136
-
137
- print(f"Dataset loaded with {len(dataset['train'])} training examples")
138
-
139
- # Preprocess the dataset
140
- def preprocess_function(examples):
141
- questions = [q.strip() for q in examples["question"]]
142
- contexts = [c.strip() for c in examples["context"]]
143
-
144
- inputs = tokenizer(
145
- questions,
146
- contexts,
147
- max_length=384,
148
- truncation="only_second",
149
- stride=128,
150
- return_overflowing_tokens=True,
151
- return_offsets_mapping=True,
152
- padding="max_length",
153
- )
154
-
155
- offset_mapping = inputs.pop("offset_mapping")
156
- sample_map = inputs.pop("overflow_to_sample_mapping")
157
-
158
- answers = examples["answers"]
159
- start_positions = []
160
- end_positions = []
161
-
162
- for i, offset in enumerate(offset_mapping):
163
- sample_idx = sample_map[i]
164
- answer = answers[sample_idx]
165
-
166
- start_char = answer["answer_start"][0] if len(answer["answer_start"]) > 0 else 0
167
- end_char = start_char + len(answer["text"][0]) if len(answer["text"]) > 0 else 0
168
-
169
- sequence_ids = inputs.sequence_ids(i)
170
-
171
- # Find the start and end of the context
172
- idx = 0
173
- while sequence_ids[idx] != 1:
174
- idx += 1
175
- context_start = idx
176
-
177
- while idx < len(sequence_ids) and sequence_ids[idx] == 1:
178
- idx += 1
179
- context_end = idx - 1
180
-
181
- # If the answer is not fully inside the context, label is (0, 0)
182
- if offset[context_start][0] > start_char or offset[context_end][1] < end_char:
183
- start_positions.append(0)
184
- end_positions.append(0)
185
- else:
186
- # Otherwise it's the start and end token positions
187
- idx = context_start
188
- while idx <= context_end and offset[idx][0] <= start_char:
189
- idx += 1
190
- start_positions.append(idx - 1)
191
-
192
- idx = context_end
193
- while idx >= context_start and offset[idx][1] >= end_char:
194
- idx -= 1
195
- end_positions.append(idx + 1)
196
-
197
- inputs["start_positions"] = start_positions
198
- inputs["end_positions"] = end_positions
199
- return inputs
200
-
201
- print("Preprocessing dataset...")
202
- processed_dataset = dataset.map(
203
- preprocess_function,
204
- batched=True,
205
- remove_columns=dataset["train"].column_names,
206
- )
207
-
208
- print("Splitting dataset...")
209
- train_dataset = processed_dataset["train"]
210
- val_dataset = processed_dataset["validation"]
211
-
212
- train_dataset.set_format(type="torch", columns=["input_ids", "attention_mask", "start_positions", "end_positions"])
213
- val_dataset.set_format(type="torch", columns=["input_ids", "attention_mask", "start_positions", "end_positions"])
214
-
215
- training_args = TrainingArguments(
216
- output_dir="./fine_tuned_legal_qa",
217
- evaluation_strategy="steps",
218
- eval_steps=100,
219
- learning_rate=2e-5,
220
- per_device_train_batch_size=16,
221
- per_device_eval_batch_size=16,
222
- num_train_epochs=1,
223
- weight_decay=0.01,
224
- logging_steps=50,
225
- save_steps=100,
226
- load_best_model_at_end=True,
227
- report_to=[]
228
- )
229
-
230
- print("✅ Starting fine tuning on CUAD QA dataset...")
231
- trainer = Trainer(
232
- model=model,
233
- args=training_args,
234
- train_dataset=train_dataset,
235
- eval_dataset=val_dataset,
236
- tokenizer=tokenizer,
237
- )
238
-
239
- trainer.train()
240
- print("✅ Fine tuning completed. Saving model...")
241
-
242
- model.save_pretrained("./fine_tuned_legal_qa")
243
- tokenizer.save_pretrained("./fine_tuned_legal_qa")
244
-
245
- return tokenizer, model
246
-
247
- #############################
248
- # Load NLP Models #
249
- #############################
250
-
251
- # Initialize model variables
252
- nlp = None
253
- summarizer = None
254
- embedding_model = None
255
- ner_model = None
256
- speech_to_text = None
257
- cuad_model = None
258
- cuad_tokenizer = None
259
- qa_model = None
260
-
261
- # Add model caching functionality
262
- import pickle
263
- import os.path
264
-
265
- #MODELS_CACHE_DIR = "c:\\Users\\hardi\\OneDrive\\Desktop\\New folder (7)\\doc-vid-analyze-main\\models_cache"
266
- MODELS_CACHE_DIR = os.getenv("MODELS_CACHE_DIR", "models_cache")
267
- os.makedirs(MODELS_CACHE_DIR, exist_ok=True)
268
-
269
- def download_model_from_hub(model_id, subfolder=None):
270
- """Download a model from Hugging Face Hub"""
271
- try:
272
- local_dir = snapshot_download(
273
- repo_id=model_id,
274
- subfolder=subfolder,
275
- local_dir=os.path.join(MODELS_CACHE_DIR, model_id.replace("/", "_"))
276
- )
277
- print(f"✅ Downloaded model {model_id} to {local_dir}")
278
- return local_dir
279
- except Exception as e:
280
- print(f"⚠️ Error downloading model {model_id}: {str(e)}")
281
- return None
282
-
283
-
284
- def save_model_to_cache(model, model_name):
285
- """Save a model to the cache directory"""
286
- try:
287
- cache_path = os.path.join(MODELS_CACHE_DIR, f"{model_name}.pkl")
288
- with open(cache_path, 'wb') as f:
289
- pickle.dump(model, f)
290
- print(f"✅ Saved {model_name} to cache")
291
- return True
292
- except Exception as e:
293
- print(f"⚠️ Failed to save {model_name} to cache: {str(e)}")
294
- return False
295
-
296
- def load_model_from_cache(model_name):
297
- """Load a model from the cache directory"""
298
- try:
299
- cache_path = os.path.join(MODELS_CACHE_DIR, f"{model_name}.pkl")
300
- if os.path.exists(cache_path):
301
- with open(cache_path, 'rb') as f:
302
- model = pickle.load(f)
303
- print(f"✅ Loaded {model_name} from cache")
304
- return model
305
- return None
306
- except Exception as e:
307
- print(f"⚠️ Failed to load {model_name} from cache: {str(e)}")
308
- return None
309
-
310
- # Add a flag to control model loading
311
- LOAD_MODELS = os.getenv("LOAD_MODELS", "True").lower() in ("true", "1", "t")
312
-
313
- try:
314
- if LOAD_MODELS:
315
- # Try to load SpaCy from cache first
316
- nlp = load_model_from_cache("spacy_model")
317
- if nlp is None:
318
- try:
319
- nlp = spacy.load("en_core_web_sm")
320
- save_model_to_cache(nlp, "spacy_model")
321
- except:
322
- print("⚠️ SpaCy model not found, downloading...")
323
- spacy.cli.download("en_core_web_sm")
324
- nlp = spacy.load("en_core_web_sm")
325
- save_model_to_cache(nlp, "spacy_model")
326
-
327
- print("✅ Loading NLP models...")
328
-
329
- # Load the summarizer with caching
330
- print("Loading summarizer model...")
331
- summarizer = load_model_from_cache("summarizer_model")
332
- if summarizer is None:
333
- try:
334
- summarizer = pipeline("summarization", model="facebook/bart-large-cnn",
335
- device=0 if torch.cuda.is_available() else -1)
336
- save_model_to_cache(summarizer, "summarizer_model")
337
- print("✅ Summarizer loaded successfully")
338
- except Exception as e:
339
- print(f"⚠️ Error loading summarizer: {str(e)}")
340
- try:
341
- print("Trying alternative summarizer model...")
342
- summarizer = pipeline("summarization", model="sshleifer/distilbart-cnn-12-6",
343
- device=0 if torch.cuda.is_available() else -1)
344
- save_model_to_cache(summarizer, "summarizer_model")
345
- print("✅ Alternative summarizer loaded successfully")
346
- except Exception as e2:
347
- print(f"⚠️ Error loading alternative summarizer: {str(e2)}")
348
- summarizer = None
349
-
350
- # Load the embedding model with caching
351
- print("Loading embedding model...")
352
- embedding_model = load_model_from_cache("embedding_model")
353
- if embedding_model is None:
354
- try:
355
- embedding_model = SentenceTransformer("all-mpnet-base-v2", device=device)
356
- save_model_to_cache(embedding_model, "embedding_model")
357
- print("✅ Embedding model loaded successfully")
358
- except Exception as e:
359
- print(f"⚠️ Error loading embedding model: {str(e)}")
360
- embedding_model = None
361
-
362
- # Load the NER model with caching
363
- print("Loading NER model...")
364
- ner_model = load_model_from_cache("ner_model")
365
- if ner_model is None:
366
- try:
367
- ner_model = pipeline("ner", model="dslim/bert-base-NER",
368
- device=0 if torch.cuda.is_available() else -1)
369
- save_model_to_cache(ner_model, "ner_model")
370
- print("✅ NER model loaded successfully")
371
- except Exception as e:
372
- print(f"⚠️ Error loading NER model: {str(e)}")
373
- ner_model = None
374
-
375
- # Speech to text model with caching
376
- print("Loading speech to text model...")
377
- speech_to_text = load_model_from_cache("speech_to_text_model")
378
- if speech_to_text is None:
379
- try:
380
- speech_to_text = pipeline("automatic-speech-recognition",
381
- model="openai/whisper-medium",
382
- chunk_length_s=30,
383
- device_map="auto" if torch.cuda.is_available() else "cpu")
384
- save_model_to_cache(speech_to_text, "speech_to_text_model")
385
- print("✅ Speech to text model loaded successfully")
386
- except Exception as e:
387
- print(f"⚠️ Error loading speech to text model: {str(e)}")
388
- speech_to_text = None
389
-
390
- # Load the fine-tuned model with caching
391
- print("Loading fine-tuned CUAD QA model...")
392
- cuad_model = load_model_from_cache("cuad_model")
393
- cuad_tokenizer = load_model_from_cache("cuad_tokenizer")
394
-
395
- if cuad_model is None or cuad_tokenizer is None:
396
- try:
397
- cuad_tokenizer = AutoTokenizer.from_pretrained("hardik8588/fine-tuned-legal-qa")
398
- from transformers import AutoModelForQuestionAnswering
399
- cuad_model = AutoModelForQuestionAnswering.from_pretrained("hardik8588/fine-tuned-legal-qa")
400
- cuad_model.to(device)
401
- save_model_to_cache(cuad_tokenizer, "cuad_tokenizer")
402
- save_model_to_cache(cuad_model, "cuad_model")
403
- print("✅ Successfully loaded fine-tuned model")
404
- except Exception as e:
405
- print(f"⚠️ Error loading fine-tuned model: {str(e)}")
406
- print("⚠️ Falling back to pre-trained model...")
407
- try:
408
- cuad_tokenizer = AutoTokenizer.from_pretrained("deepset/roberta-base-squad2")
409
- from transformers import AutoModelForQuestionAnswering
410
- cuad_model = AutoModelForQuestionAnswering.from_pretrained("deepset/roberta-base-squad2")
411
- cuad_model.to(device)
412
- save_model_to_cache(cuad_tokenizer, "cuad_tokenizer")
413
- save_model_to_cache(cuad_model, "cuad_model")
414
- print("✅ Pre-trained model loaded successfully")
415
- except Exception as e2:
416
- print(f"⚠️ Error loading pre-trained model: {str(e2)}")
417
- cuad_model = None
418
- cuad_tokenizer = None
419
-
420
- # Load a general QA model with caching
421
- print("Loading general QA model...")
422
- qa_model = load_model_from_cache("qa_model")
423
- if qa_model is None:
424
- try:
425
- qa_model = pipeline("question-answering", model="deepset/roberta-base-squad2")
426
- save_model_to_cache(qa_model, "qa_model")
427
- print("✅ QA model loaded successfully")
428
- except Exception as e:
429
- print(f"⚠️ Error loading QA model: {str(e)}")
430
- qa_model = None
431
-
432
- print("✅ All models loaded successfully")
433
- else:
434
- print("⚠️ Model loading skipped (LOAD_MODELS=False)")
435
-
436
- except Exception as e:
437
- print(f"⚠️ Error loading models: {str(e)}")
438
- # Instead of raising an error, set fallback behavior
439
- nlp = None
440
- summarizer = None
441
- embedding_model = None
442
- ner_model = None
443
- speech_to_text = None
444
- cuad_model = None
445
- cuad_tokenizer = None
446
- qa_model = None
447
- print("⚠️ Running with limited functionality due to model loading errors")
448
-
449
- def legal_chatbot(user_input, context):
450
- """Uses a real NLP model for legal Q&A."""
451
- global chat_history
452
- chat_history.append({"role": "user", "content": user_input})
453
- response = qa_model(question=user_input, context=context)["answer"]
454
- chat_history.append({"role": "assistant", "content": response})
455
- return response
456
-
457
- def extract_text_from_pdf(pdf_file):
458
- """Extracts text from a PDF file using pdfplumber."""
459
- try:
460
- # Suppress pdfplumber warnings about CropBox
461
- import logging
462
- logging.getLogger("pdfminer").setLevel(logging.ERROR)
463
-
464
- with pdfplumber.open(pdf_file) as pdf:
465
- print(f"Processing PDF with {len(pdf.pages)} pages")
466
- text = ""
467
- for i, page in enumerate(pdf.pages):
468
- page_text = page.extract_text() or ""
469
- text += page_text + "\n"
470
- if (i + 1) % 10 == 0: # Log progress every 10 pages
471
- print(f"Processed {i + 1} pages...")
472
-
473
- print(f"✅ PDF text extraction complete: {len(text)} characters extracted")
474
- return text.strip() if text else None
475
- except Exception as e:
476
- print(f"❌ PDF extraction error: {str(e)}")
477
- raise HTTPException(status_code=400, detail=f"PDF extraction failed: {str(e)}")
478
-
479
- def process_video_to_text(video_file_path):
480
- """Extract audio from video and convert to text."""
481
- try:
482
- print(f"Processing video file at {video_file_path}")
483
- temp_audio_path = os.path.join("temp", "extracted_audio.wav")
484
- video = mp.VideoFileClip(video_file_path)
485
- video.audio.write_audiofile(temp_audio_path, codec='pcm_s16le')
486
- print(f"Audio extracted to {temp_audio_path}")
487
- result = speech_to_text(temp_audio_path)
488
- transcript = result["text"]
489
- print(f"Transcription completed: {len(transcript)} characters")
490
- if os.path.exists(temp_audio_path):
491
- os.remove(temp_audio_path)
492
- return transcript
493
- except Exception as e:
494
- print(f"Error in video processing: {str(e)}")
495
- raise HTTPException(status_code=400, detail=f"Video processing failed: {str(e)}")
496
-
497
- def process_audio_to_text(audio_file_path):
498
- """Process audio file and convert to text."""
499
- try:
500
- print(f"Processing audio file at {audio_file_path}")
501
- result = speech_to_text(audio_file_path)
502
- transcript = result["text"]
503
- print(f"Transcription completed: {len(transcript)} characters")
504
- return transcript
505
- except Exception as e:
506
- print(f"Error in audio processing: {str(e)}")
507
- raise HTTPException(status_code=400, detail=f"Audio processing failed: {str(e)}")
508
-
509
- def extract_named_entities(text):
510
- """Extracts named entities from legal text."""
511
- max_length = 10000
512
- entities = []
513
- for i in range(0, len(text), max_length):
514
- chunk = text[i:i+max_length]
515
- doc = nlp(chunk)
516
- entities.extend([{"entity": ent.text, "label": ent.label_} for ent in doc.ents])
517
- return entities
518
-
519
- def analyze_risk(text):
520
- """Analyzes legal risk in the document using keyword-based analysis."""
521
- risk_keywords = {
522
- "Liability": ["liability", "responsible", "responsibility", "legal obligation"],
523
- "Termination": ["termination", "breach", "contract end", "default"],
524
- "Indemnification": ["indemnification", "indemnify", "hold harmless", "compensate", "compensation"],
525
- "Payment Risk": ["payment", "terms", "reimbursement", "fee", "schedule", "invoice", "money"],
526
- "Insurance": ["insurance", "coverage", "policy", "claims"],
527
- }
528
- risk_scores = {category: 0 for category in risk_keywords}
529
- lower_text = text.lower()
530
- for category, keywords in risk_keywords.items():
531
- for keyword in keywords:
532
- risk_scores[category] += lower_text.count(keyword.lower())
533
- return risk_scores
534
-
535
- def extract_context_for_risk_terms(text, risk_keywords, window=1):
536
- """
537
- Extracts and summarizes the context around risk terms.
538
- """
539
- doc = nlp(text)
540
- sentences = list(doc.sents)
541
- risk_contexts = {category: [] for category in risk_keywords}
542
- for i, sent in enumerate(sentences):
543
- sent_text_lower = sent.text.lower()
544
- for category, details in risk_keywords.items():
545
- for keyword in details["keywords"]:
546
- if keyword.lower() in sent_text_lower:
547
- start_idx = max(0, i - window)
548
- end_idx = min(len(sentences), i + window + 1)
549
- context_chunk = " ".join([s.text for s in sentences[start_idx:end_idx]])
550
- risk_contexts[category].append(context_chunk)
551
- summarized_contexts = {}
552
- for category, contexts in risk_contexts.items():
553
- if contexts:
554
- combined_context = " ".join(contexts)
555
- try:
556
- summary_result = summarizer(combined_context, max_length=100, min_length=30, do_sample=False)
557
- summary = summary_result[0]['summary_text']
558
- except Exception as e:
559
- summary = "Context summarization failed."
560
- summarized_contexts[category] = summary
561
- else:
562
- summarized_contexts[category] = "No contextual details found."
563
- return summarized_contexts
564
-
565
- def get_detailed_risk_info(text):
566
- """
567
- Returns detailed risk information by merging risk scores with descriptive details
568
- and contextual summaries from the document.
569
- """
570
- risk_details = {
571
- "Liability": {
572
- "description": "Liability refers to the legal responsibility for losses or damages.",
573
- "common_concerns": "Broad liability clauses may expose parties to unforeseen risks.",
574
- "recommendations": "Review and negotiate clear limits on liability.",
575
- "example": "E.g., 'The party shall be liable for direct damages due to negligence.'"
576
- },
577
- "Termination": {
578
- "description": "Termination involves conditions under which a contract can be ended.",
579
- "common_concerns": "Unilateral termination rights or ambiguous conditions can be risky.",
580
- "recommendations": "Ensure termination clauses are balanced and include notice periods.",
581
- "example": "E.g., 'Either party may terminate the agreement with 30 days notice.'"
582
- },
583
- "Indemnification": {
584
- "description": "Indemnification requires one party to compensate for losses incurred by the other.",
585
- "common_concerns": "Overly broad indemnification can shift significant risk.",
586
- "recommendations": "Negotiate clear limits and carve-outs where necessary.",
587
- "example": "E.g., 'The seller shall indemnify the buyer against claims from product defects.'"
588
- },
589
- "Payment Risk": {
590
- "description": "Payment risk pertains to terms regarding fees, schedules, and reimbursements.",
591
- "common_concerns": "Vague payment terms or hidden charges increase risk.",
592
- "recommendations": "Clarify payment conditions and include penalties for delays.",
593
- "example": "E.g., 'Payments must be made within 30 days, with a 2% late fee thereafter.'"
594
- },
595
- "Insurance": {
596
- "description": "Insurance risk covers the adequacy and scope of required coverage.",
597
- "common_concerns": "Insufficient insurance can leave parties exposed in unexpected events.",
598
- "recommendations": "Review insurance requirements to ensure they meet the risk profile.",
599
- "example": "E.g., 'The contractor must maintain liability insurance with at least $1M coverage.'"
600
- }
601
- }
602
- risk_scores = analyze_risk(text)
603
- risk_keywords_context = {
604
- "Liability": {"keywords": ["liability", "responsible", "responsibility", "legal obligation"]},
605
- "Termination": {"keywords": ["termination", "breach", "contract end", "default"]},
606
- "Indemnification": {"keywords": ["indemnification", "indemnify", "hold harmless", "compensate", "compensation"]},
607
- "Payment Risk": {"keywords": ["payment", "terms", "reimbursement", "fee", "schedule", "invoice", "money"]},
608
- "Insurance": {"keywords": ["insurance", "coverage", "policy", "claims"]}
609
- }
610
- risk_contexts = extract_context_for_risk_terms(text, risk_keywords_context, window=1)
611
- detailed_info = {}
612
- for risk_term, score in risk_scores.items():
613
- if score > 0:
614
- info = risk_details.get(risk_term, {"description": "No details available."})
615
- detailed_info[risk_term] = {
616
- "score": score,
617
- "description": info.get("description", ""),
618
- "common_concerns": info.get("common_concerns", ""),
619
- "recommendations": info.get("recommendations", ""),
620
- "example": info.get("example", ""),
621
- "context_summary": risk_contexts.get(risk_term, "No context available.")
622
- }
623
- return detailed_info
624
-
625
- def analyze_contract_clauses(text):
626
- """Analyzes contract clauses using the fine-tuned CUAD QA model."""
627
- max_length = 512
628
- step = 256
629
- clauses_detected = []
630
- try:
631
- clause_types = list(cuad_model.config.id2label.values())
632
- except Exception as e:
633
- clause_types = [
634
- "Obligations of Seller", "Governing Law", "Termination", "Indemnification",
635
- "Confidentiality", "Insurance", "Non-Compete", "Change of Control",
636
- "Assignment", "Warranty", "Limitation of Liability", "Arbitration",
637
- "IP Rights", "Force Majeure", "Revenue/Profit Sharing", "Audit Rights"
638
- ]
639
- chunks = [text[i:i+max_length] for i in range(0, len(text), step) if i+step < len(text)]
640
- for chunk in chunks:
641
- inputs = cuad_tokenizer(chunk, return_tensors="pt", truncation=True, max_length=512).to(device)
642
- with torch.no_grad():
643
- outputs = cuad_model(**inputs)
644
- predictions = torch.sigmoid(outputs.start_logits).cpu().numpy()[0]
645
- for idx, confidence in enumerate(predictions):
646
- if confidence > 0.5 and idx < len(clause_types):
647
- clauses_detected.append({"type": clause_types[idx], "confidence": float(confidence)})
648
- aggregated_clauses = {}
649
- for clause in clauses_detected:
650
- clause_type = clause["type"]
651
- if clause_type not in aggregated_clauses or clause["confidence"] > aggregated_clauses[clause_type]["confidence"]:
652
- aggregated_clauses[clause_type] = clause
653
- return list(aggregated_clauses.values())
654
-
655
- def summarize_text(text):
656
- """Summarizes legal text using the summarizer model."""
657
- try:
658
- if summarizer is None:
659
- return "Basic analysis (NLP models not available)"
660
-
661
- # Split text into chunks if it's too long
662
- max_chunk_size = 1024
663
- if len(text) > max_chunk_size:
664
- chunks = [text[i:i+max_chunk_size] for i in range(0, len(text), max_chunk_size)]
665
- summaries = []
666
- for chunk in chunks:
667
- summary = summarizer(chunk, max_length=100, min_length=30, do_sample=False)
668
- summaries.append(summary[0]['summary_text'])
669
- return " ".join(summaries)
670
- else:
671
- summary = summarizer(text, max_length=100, min_length=30, do_sample=False)
672
- return summary[0]['summary_text']
673
- except Exception as e:
674
- print(f"Error in summarization: {str(e)}")
675
- return "Summarization failed. Please try again later."
676
-
677
- @app.post("/analyze_legal_document")
678
- async def analyze_legal_document(
679
- file: UploadFile = File(...),
680
- current_user: User = Depends(get_current_active_user)
681
- ):
682
- """Analyzes a legal document (PDF) and returns insights based on subscription tier."""
683
- try:
684
- # Calculate file size in MB
685
- file_content = await file.read()
686
- file_size_mb = len(file_content) / (1024 * 1024)
687
-
688
- # Check subscription access for document analysis
689
- check_subscription_access(current_user, "document_analysis", file_size_mb)
690
-
691
- print(f"Processing file: {file.filename}")
692
-
693
- # Create a temporary file to store the uploaded PDF
694
- with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as tmp:
695
- tmp.write(file_content)
696
- tmp_path = tmp.name
697
-
698
- # Extract text from PDF
699
- text = extract_text_from_pdf(tmp_path)
700
-
701
- # Clean up the temporary file
702
- os.unlink(tmp_path)
703
-
704
- if not text:
705
- raise HTTPException(status_code=400, detail="Could not extract text from PDF")
706
-
707
- # Generate a task ID
708
- task_id = str(uuid.uuid4())
709
-
710
- # Store document context for later retrieval
711
- store_document_context(task_id, text)
712
-
713
- # Basic analysis available to all tiers
714
- summary = summarize_text(text)
715
- entities = extract_named_entities(text)
716
- risk_scores = analyze_risk(text)
717
-
718
- # Prepare response based on subscription tier
719
- response = {
720
- "task_id": task_id,
721
- "summary": summary,
722
- "entities": entities,
723
- "risk_assessment": risk_scores,
724
- "subscription_tier": current_user.subscription_tier
725
- }
726
-
727
- # Add premium features if user has access
728
- if current_user.subscription_tier == "premium_tier":
729
- # Add detailed risk assessment
730
- if "detailed_risk_assessment" in SUBSCRIPTION_TIERS[current_user.subscription_tier]["features"]:
731
- detailed_risk = get_detailed_risk_info(text)
732
- response["detailed_risk_assessment"] = detailed_risk
733
-
734
- # Add contract clause analysis
735
- if "contract_clause_analysis" in SUBSCRIPTION_TIERS[current_user.subscription_tier]["features"]:
736
- clauses = analyze_contract_clauses(text)
737
- response["contract_clauses"] = clauses
738
-
739
- return response
740
-
741
- except Exception as e:
742
- print(f"Error analyzing document: {str(e)}")
743
- raise HTTPException(status_code=500, detail=f"Error analyzing document: {str(e)}")
744
-
745
- # Add this function to check resource limits based on subscription tier
746
- def check_resource_limits(user: User, resource_type: str, size_mb: float = None, count: int = 1):
747
- """
748
- Check if the user has exceeded their subscription limits for a specific resource
749
-
750
- Args:
751
- user: The user making the request
752
- resource_type: Type of resource (document, video, audio)
753
- size_mb: Size of the resource in MB
754
- count: Number of resources being used (default 1)
755
-
756
- Returns:
757
- bool: True if within limits, raises HTTPException otherwise
758
- """
759
- # Get the user's subscription tier limits
760
- tier = user.subscription_tier
761
- tier_limits = SUBSCRIPTION_TIERS.get(tier, SUBSCRIPTION_TIERS["free_tier"])["limits"]
762
-
763
- # Check size limits
764
- if size_mb is not None:
765
- if resource_type == "document" and size_mb > tier_limits["document_size_mb"]:
766
- raise HTTPException(
767
- status_code=status.HTTP_403_FORBIDDEN,
768
- detail=f"Document size exceeds the {tier_limits['document_size_mb']}MB limit for your {tier} subscription"
769
- )
770
- elif resource_type == "video" and size_mb > tier_limits["video_size_mb"]:
771
- raise HTTPException(
772
- status_code=status.HTTP_403_FORBIDDEN,
773
- detail=f"Video size exceeds the {tier_limits['video_size_mb']}MB limit for your {tier} subscription"
774
- )
775
- elif resource_type == "audio" and size_mb > tier_limits["audio_size_mb"]:
776
- raise HTTPException(
777
- status_code=status.HTTP_403_FORBIDDEN,
778
- detail=f"Audio size exceeds the {tier_limits['audio_size_mb']}MB limit for your {tier} subscription"
779
- )
780
-
781
- # Check monthly document count
782
- if resource_type == "document":
783
- # Get current month and year
784
- now = datetime.now()
785
- month, year = now.month, now.year
786
-
787
- # Check usage stats for current month
788
- conn = get_db_connection()
789
- cursor = conn.cursor()
790
- cursor.execute(
791
- "SELECT analyses_used FROM usage_stats WHERE user_id = ? AND month = ? AND year = ?",
792
- (user.id, month, year)
793
- )
794
- result = cursor.fetchone()
795
-
796
- current_usage = result[0] if result else 0
797
-
798
- # Check if adding this usage would exceed the limit
799
- if current_usage + count > tier_limits["documents_per_month"]:
800
- conn.close()
801
- raise HTTPException(
802
- status_code=status.HTTP_403_FORBIDDEN,
803
- detail=f"You have reached your monthly limit of {tier_limits['documents_per_month']} document analyses for your {tier} subscription"
804
- )
805
-
806
- # Update usage stats
807
- if result:
808
- cursor.execute(
809
- "UPDATE usage_stats SET analyses_used = ? WHERE user_id = ? AND month = ? AND year = ?",
810
- (current_usage + count, user.id, month, year)
811
- )
812
- else:
813
- usage_id = str(uuid.uuid4())
814
- cursor.execute(
815
- "INSERT INTO usage_stats (id, user_id, month, year, analyses_used) VALUES (?, ?, ?, ?, ?)",
816
- (usage_id, user.id, month, year, count)
817
- )
818
-
819
- conn.commit()
820
- conn.close()
821
-
822
- # Check if feature is available in the tier
823
- if resource_type == "video" and tier_limits["video_size_mb"] == 0:
824
- raise HTTPException(
825
- status_code=status.HTTP_403_FORBIDDEN,
826
- detail=f"Video analysis is not available in your {tier} subscription"
827
- )
828
-
829
- if resource_type == "audio" and tier_limits["audio_size_mb"] == 0:
830
- raise HTTPException(
831
- status_code=status.HTTP_403_FORBIDDEN,
832
- detail=f"Audio analysis is not available in your {tier} subscription"
833
- )
834
-
835
- return True
836
-
837
- @app.post("/analyze_legal_video")
838
- async def analyze_legal_video(
839
- file: UploadFile = File(...),
840
- current_user: User = Depends(get_current_active_user)
841
- ):
842
- """Analyzes legal video by transcribing and analyzing the transcript."""
843
- try:
844
- # Calculate file size in MB
845
- file_content = await file.read()
846
- file_size_mb = len(file_content) / (1024 * 1024)
847
-
848
- # Check subscription access for video analysis
849
- check_subscription_access(current_user, "video_analysis", file_size_mb)
850
-
851
- print(f"Processing video file: {file.filename}")
852
-
853
- # Create a temporary file to store the uploaded video
854
- with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') as tmp:
855
- tmp.write(file_content)
856
- tmp_path = tmp.name
857
-
858
- # Process video to extract transcript
859
- transcript = process_video_to_text(tmp_path)
860
-
861
- # Clean up the temporary file
862
- os.unlink(tmp_path)
863
-
864
- if not transcript:
865
- raise HTTPException(status_code=400, detail="Could not extract transcript from video")
866
-
867
- # Generate a task ID
868
- task_id = str(uuid.uuid4())
869
-
870
- # Store document context for later retrieval
871
- store_document_context(task_id, transcript)
872
-
873
- # Basic analysis
874
- summary = summarize_text(transcript)
875
- entities = extract_named_entities(transcript)
876
- risk_scores = analyze_risk(transcript)
877
-
878
- # Prepare response
879
- response = {
880
- "task_id": task_id,
881
- "transcript": transcript,
882
- "summary": summary,
883
- "entities": entities,
884
- "risk_assessment": risk_scores,
885
- "subscription_tier": current_user.subscription_tier
886
- }
887
-
888
- # Add premium features if user has access
889
- if current_user.subscription_tier == "premium_tier":
890
- # Add detailed risk assessment
891
- if "detailed_risk_assessment" in SUBSCRIPTION_TIERS[current_user.subscription_tier]["features"]:
892
- detailed_risk = get_detailed_risk_info(transcript)
893
- response["detailed_risk_assessment"] = detailed_risk
894
-
895
- return response
896
-
897
- except Exception as e:
898
- print(f"Error analyzing video: {str(e)}")
899
- raise HTTPException(status_code=500, detail=f"Error analyzing video: {str(e)}")
900
-
901
-
902
- @app.post("/legal_chatbot/{task_id}")
903
- async def chat_with_document(
904
- task_id: str,
905
- question: str = Form(...),
906
- current_user: User = Depends(get_current_active_user)
907
- ):
908
- """Chat with a document using the legal chatbot."""
909
- try:
910
- # Check if user has access to chatbot feature
911
- if "chatbot" not in SUBSCRIPTION_TIERS[current_user.subscription_tier]["features"]:
912
- raise HTTPException(
913
- status_code=403,
914
- detail=f"The chatbot feature is not available in your {current_user.subscription_tier} subscription. Please upgrade to access this feature."
915
- )
916
-
917
- # Check if document context exists
918
- context = load_document_context(task_id)
919
- if not context:
920
- raise HTTPException(status_code=404, detail="Document context not found. Please analyze a document first.")
921
-
922
- # Use the chatbot to answer the question
923
- answer = legal_chatbot(question, context)
924
-
925
- return {"answer": answer, "chat_history": chat_history}
926
-
927
- except Exception as e:
928
- print(f"Error in chatbot: {str(e)}")
929
- raise HTTPException(status_code=500, detail=f"Error in chatbot: {str(e)}")
930
-
931
- @app.get("/")
932
- async def root():
933
- """Root endpoint that returns a welcome message."""
934
- return HTMLResponse(content="""
935
- <html>
936
- <head>
937
- <title>Legal Document Analysis API</title>
938
- <style>
939
- body {
940
- font-family: Arial, sans-serif;
941
- max-width: 800px;
942
- margin: 0 auto;
943
- padding: 20px;
944
- }
945
- h1 {
946
- color: #2c3e50;
947
- }
948
- .endpoint {
949
- background-color: #f8f9fa;
950
- padding: 15px;
951
- margin-bottom: 10px;
952
- border-radius: 5px;
953
- }
954
- .method {
955
- font-weight: bold;
956
- color: #e74c3c;
957
- }
958
- </style>
959
- </head>
960
- <body>
961
- <h1>Legal Document Analysis API</h1>
962
- <p>Welcome to the Legal Document Analysis API. This API provides tools for analyzing legal documents, videos, and audio.</p>
963
- <h2>Available Endpoints:</h2>
964
- <div class="endpoint">
965
- <p><span class="method">POST</span> /analyze_legal_document - Analyze a legal document (PDF)</p>
966
- </div>
967
- <div class="endpoint">
968
- <p><span class="method">POST</span> /analyze_legal_video - Analyze a legal video</p>
969
- </div>
970
- <div class="endpoint">
971
- <p><span class="method">POST</span> /analyze_legal_audio - Analyze legal audio</p>
972
- </div>
973
- <div class="endpoint">
974
- <p><span class="method">POST</span> /legal_chatbot/{task_id} - Chat with a document</p>
975
- </div>
976
- <div class="endpoint">
977
- <p><span class="method">POST</span> /register - Register a new user</p>
978
- </div>
979
- <div class="endpoint">
980
- <p><span class="method">POST</span> /token - Login to get an access token</p>
981
- </div>
982
- <div class="endpoint">
983
- <p><span class="method">GET</span> /users/me - Get current user information</p>
984
- </div>
985
- <div class="endpoint">
986
- <p><span class="method">POST</span> /subscribe/{tier} - Subscribe to a plan</p>
987
- </div>
988
- <p>For more details, visit the <a href="/docs">API documentation</a>.</p>
989
- </body>
990
- </html>
991
- """)
992
-
993
- @app.post("/register", response_model=Token)
994
- async def register_new_user(user_data: UserCreate):
995
- """Register a new user with a free subscription"""
996
- try:
997
- success, result = register_user(user_data.email, user_data.password)
998
-
999
- if not success:
1000
- raise HTTPException(status_code=400, detail=result)
1001
-
1002
- return {"access_token": result["access_token"], "token_type": "bearer"}
1003
-
1004
- except HTTPException:
1005
- # Re-raise HTTP exceptions
1006
- raise
1007
- except Exception as e:
1008
- print(f"Registration error: {str(e)}")
1009
- raise HTTPException(status_code=500, detail=f"Registration failed: {str(e)}")
1010
-
1011
- @app.post("/token", response_model=Token)
1012
- async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
1013
- """Endpoint for OAuth2 token generation"""
1014
- try:
1015
- # Add debug logging
1016
- logger.info(f"Token request for username: {form_data.username}")
1017
-
1018
- user = authenticate_user(form_data.username, form_data.password)
1019
- if not user:
1020
- logger.warning(f"Authentication failed for: {form_data.username}")
1021
- raise HTTPException(
1022
- status_code=status.HTTP_401_UNAUTHORIZED,
1023
- detail="Incorrect username or password",
1024
- headers={"WWW-Authenticate": "Bearer"},
1025
- )
1026
-
1027
- access_token = create_access_token(user.id)
1028
- if not access_token:
1029
- logger.error(f"Failed to create access token for user: {user.id}")
1030
- raise HTTPException(
1031
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
1032
- detail="Could not create access token",
1033
- )
1034
-
1035
- logger.info(f"Login successful for: {form_data.username}")
1036
- return {"access_token": access_token, "token_type": "bearer"}
1037
- except Exception as e:
1038
- logger.error(f"Token endpoint error: {e}")
1039
- raise HTTPException(
1040
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
1041
- detail=f"Login error: {str(e)}",
1042
- )
1043
-
1044
-
1045
- @app.get("/debug/token")
1046
- async def debug_token(authorization: str = Header(None)):
1047
- """Debug endpoint to check token validity"""
1048
- try:
1049
- if not authorization:
1050
- return {"valid": False, "error": "No authorization header provided"}
1051
-
1052
- # Extract token from Authorization header
1053
- scheme, token = authorization.split()
1054
- if scheme.lower() != 'bearer':
1055
- return {"valid": False, "error": "Not a bearer token"}
1056
-
1057
- # Log the token for debugging
1058
- logger.info(f"Debugging token: {token[:10]}...")
1059
-
1060
- # Try to validate the token
1061
- try:
1062
- user = await get_current_active_user(token)
1063
- return {"valid": True, "user_id": user.id, "email": user.email}
1064
- except Exception as e:
1065
- return {"valid": False, "error": str(e)}
1066
- except Exception as e:
1067
- return {"valid": False, "error": f"Token debug error: {str(e)}"}
1068
-
1069
-
1070
- @app.post("/login")
1071
- async def api_login(email: str, password: str):
1072
- success, result = login_user(email, password)
1073
- if not success:
1074
- raise HTTPException(
1075
- status_code=status.HTTP_401_UNAUTHORIZED,
1076
- detail=result
1077
- )
1078
- return result
1079
-
1080
- @app.get("/health")
1081
- def health_check():
1082
- """Simple health check endpoint to verify the API is running"""
1083
- return {"status": "ok", "message": "API is running"}
1084
-
1085
- @app.get("/users/me", response_model=User)
1086
- async def read_users_me(current_user: User = Depends(get_current_active_user)):
1087
- return current_user
1088
-
1089
- @app.post("/analyze_legal_audio")
1090
- async def analyze_legal_audio(
1091
- file: UploadFile = File(...),
1092
- current_user: User = Depends(get_current_active_user)
1093
- ):
1094
- """Analyzes legal audio by transcribing and analyzing the transcript."""
1095
- try:
1096
- # Calculate file size in MB
1097
- file_content = await file.read()
1098
- file_size_mb = len(file_content) / (1024 * 1024)
1099
-
1100
- # Check subscription access for audio analysis
1101
- check_subscription_access(current_user, "audio_analysis", file_size_mb)
1102
-
1103
- print(f"Processing audio file: {file.filename}")
1104
-
1105
- # Create a temporary file to store the uploaded audio
1106
- with tempfile.NamedTemporaryFile(delete=False, suffix='.wav') as tmp:
1107
- tmp.write(file_content)
1108
- tmp_path = tmp.name
1109
-
1110
- # Process audio to extract transcript
1111
- transcript = process_audio_to_text(tmp_path)
1112
-
1113
- # Clean up the temporary file
1114
- os.unlink(tmp_path)
1115
-
1116
- if not transcript:
1117
- raise HTTPException(status_code=400, detail="Could not extract transcript from audio")
1118
-
1119
- # Generate a task ID
1120
- task_id = str(uuid.uuid4())
1121
-
1122
- # Store document context for later retrieval
1123
- store_document_context(task_id, transcript)
1124
-
1125
- # Basic analysis
1126
- summary = summarize_text(transcript)
1127
- entities = extract_named_entities(transcript)
1128
- risk_scores = analyze_risk(transcript)
1129
-
1130
- # Prepare response
1131
- response = {
1132
- "task_id": task_id,
1133
- "transcript": transcript,
1134
- "summary": summary,
1135
- "entities": entities,
1136
- "risk_assessment": risk_scores,
1137
- "subscription_tier": current_user.subscription_tier
1138
- }
1139
-
1140
- # Add premium features if user has access
1141
- if current_user.subscription_tier == "premium_tier": # Change from premium_tier to premium
1142
- # Add detailed risk assessment
1143
- if "detailed_risk_assessment" in SUBSCRIPTION_TIERS[current_user.subscription_tier]["features"]:
1144
- detailed_risk = get_detailed_risk_info(transcript)
1145
- response["detailed_risk_assessment"] = detailed_risk
1146
-
1147
- return response
1148
-
1149
- except Exception as e:
1150
- print(f"Error analyzing audio: {str(e)}")
1151
- raise HTTPException(status_code=500, detail=f"Error analyzing audio: {str(e)}")
1152
-
1153
-
1154
-
1155
- # Add these new endpoints before the if __name__ == "__main__" line
1156
- @app.get("/users/me/subscription")
1157
- async def get_user_subscription(current_user: User = Depends(get_current_active_user)):
1158
- """Get the current user's subscription details"""
1159
- try:
1160
- # Get subscription details from database
1161
- conn = get_db_connection()
1162
- cursor = conn.cursor()
1163
-
1164
- # Get the most recent active subscription
1165
- try:
1166
- cursor.execute(
1167
- "SELECT id, tier, status, created_at, expires_at, paypal_subscription_id FROM subscriptions "
1168
- "WHERE user_id = ? AND status = 'active' ORDER BY created_at DESC LIMIT 1",
1169
- (current_user.id,)
1170
- )
1171
- subscription = cursor.fetchone()
1172
- except sqlite3.OperationalError as e:
1173
- # Handle missing tier column
1174
- if "no such column: tier" in str(e):
1175
- logger.warning("Subscriptions table missing 'tier' column. Returning default subscription.")
1176
- subscription = None
1177
- else:
1178
- raise
1179
-
1180
- # Get subscription tiers with pricing directly from SUBSCRIPTION_TIERS
1181
- subscription_tiers = {
1182
- "free_tier": {
1183
- "price": SUBSCRIPTION_TIERS["free_tier"]["price"],
1184
- "currency": SUBSCRIPTION_TIERS["free_tier"]["currency"],
1185
- "features": SUBSCRIPTION_TIERS["free_tier"]["features"]
1186
- },
1187
- "standard_tier": {
1188
- "price": SUBSCRIPTION_TIERS["standard_tier"]["price"],
1189
- "currency": SUBSCRIPTION_TIERS["standard_tier"]["currency"],
1190
- "features": SUBSCRIPTION_TIERS["standard_tier"]["features"]
1191
- },
1192
- "premium_tier": {
1193
- "price": SUBSCRIPTION_TIERS["premium_tier"]["price"],
1194
- "currency": SUBSCRIPTION_TIERS["premium_tier"]["currency"],
1195
- "features": SUBSCRIPTION_TIERS["premium_tier"]["features"]
1196
- }
1197
- }
1198
-
1199
- if subscription:
1200
- sub_id, tier, status, created_at, expires_at, paypal_id = subscription
1201
- result = {
1202
- "id": sub_id,
1203
- "tier": tier,
1204
- "status": status,
1205
- "created_at": created_at,
1206
- "expires_at": expires_at,
1207
- "paypal_subscription_id": paypal_id,
1208
- "current_tier": current_user.subscription_tier,
1209
- "subscription_tiers": subscription_tiers
1210
- }
1211
- else:
1212
- result = {
1213
- "tier": "free_tier",
1214
- "status": "active",
1215
- "current_tier": current_user.subscription_tier,
1216
- "subscription_tiers": subscription_tiers
1217
- }
1218
-
1219
- conn.close()
1220
- return result
1221
- except Exception as e:
1222
- logger.error(f"Error getting subscription: {str(e)}")
1223
- raise HTTPException(status_code=500, detail=f"Error getting subscription: {str(e)}")
1224
- # Add this model definition before your endpoints
1225
- class SubscriptionCreate(BaseModel):
1226
- tier: str
1227
-
1228
- @app.post("/create_subscription")
1229
- async def create_subscription(
1230
- subscription: SubscriptionCreate,
1231
- current_user: User = Depends(get_current_active_user)
1232
- ):
1233
- """Create a subscription for the current user"""
1234
- try:
1235
- # Log the request for debugging
1236
- logger.info(f"Creating subscription for user {current_user.email} with tier {subscription.tier}")
1237
- logger.info(f"Available tiers: {list(SUBSCRIPTION_TIERS.keys())}")
1238
-
1239
- # Validate tier
1240
- valid_tiers = ["standard_tier", "premium_tier"]
1241
- if subscription.tier not in valid_tiers:
1242
- logger.warning(f"Invalid tier requested: {subscription.tier}")
1243
- raise HTTPException(status_code=400, detail=f"Invalid tier: {subscription.tier}. Must be one of {valid_tiers}")
1244
-
1245
- # Create subscription
1246
- logger.info(f"Calling create_user_subscription with email: {current_user.email}, tier: {subscription.tier}")
1247
- success, result = create_user_subscription(current_user.email, subscription.tier)
1248
-
1249
- if not success:
1250
- logger.error(f"Failed to create subscription: {result}")
1251
- raise HTTPException(status_code=400, detail=result)
1252
-
1253
- logger.info(f"Subscription created successfully: {result}")
1254
- return result
1255
- except Exception as e:
1256
- logger.error(f"Error creating subscription: {str(e)}")
1257
- # Include the full traceback for better debugging
1258
- import traceback
1259
- logger.error(f"Traceback: {traceback.format_exc()}")
1260
- raise HTTPException(status_code=500, detail=f"Error creating subscription: {str(e)}")
1261
-
1262
- @app.post("/subscribe/{tier}")
1263
- async def subscribe_to_tier(
1264
- tier: str,
1265
- current_user: User = Depends(get_current_active_user)
1266
- ):
1267
- """Subscribe to a specific tier"""
1268
- try:
1269
- # Validate tier
1270
- valid_tiers = ["standard_tier", "premium_tier"]
1271
- if tier not in valid_tiers:
1272
- raise HTTPException(status_code=400, detail=f"Invalid tier: {tier}. Must be one of {valid_tiers}")
1273
-
1274
- # Create subscription
1275
- success, result = create_user_subscription(current_user.email, tier)
1276
-
1277
- if not success:
1278
- raise HTTPException(status_code=400, detail=result)
1279
-
1280
- return result
1281
- except Exception as e:
1282
- logger.error(f"Error creating subscription: {str(e)}")
1283
- raise HTTPException(status_code=500, detail=f"Error creating subscription: {str(e)}")
1284
-
1285
- @app.post("/subscription/create")
1286
- async def create_subscription(request: Request, current_user: User = Depends(get_current_active_user)):
1287
- """Create a subscription for the current user"""
1288
- try:
1289
- data = await request.json()
1290
- tier = data.get("tier")
1291
-
1292
- if not tier:
1293
- return JSONResponse(
1294
- status_code=400,
1295
- content={"detail": "Tier is required"}
1296
- )
1297
-
1298
- # Log the request for debugging
1299
- logger.info(f"Creating subscription for user {current_user.email} with tier {tier}")
1300
-
1301
- # Create the subscription using the imported function directly
1302
- success, result = create_user_subscription(current_user.email, tier)
1303
-
1304
- if success:
1305
- # Make sure we're returning the approval_url in the response
1306
- logger.info(f"Subscription created successfully: {result}")
1307
- logger.info(f"Approval URL: {result.get('approval_url')}")
1308
-
1309
- return {
1310
- "success": True,
1311
- "data": {
1312
- "approval_url": result["approval_url"],
1313
- "subscription_id": result["subscription_id"],
1314
- "tier": result["tier"]
1315
- }
1316
- }
1317
- else:
1318
- logger.error(f"Failed to create subscription: {result}")
1319
- return JSONResponse(
1320
- status_code=400,
1321
- content={"success": False, "detail": result}
1322
- )
1323
- except Exception as e:
1324
- logger.error(f"Error creating subscription: {str(e)}")
1325
- import traceback
1326
- logger.error(f"Traceback: {traceback.format_exc()}")
1327
- return JSONResponse(
1328
- status_code=500,
1329
- content={"success": False, "detail": f"Error creating subscription: {str(e)}"}
1330
- )
1331
-
1332
- @app.post("/admin/initialize-paypal-plans")
1333
- async def initialize_paypal_plans(request: Request):
1334
- """Initialize PayPal subscription plans"""
1335
- try:
1336
- # This should be protected with admin authentication in production
1337
- plans = initialize_subscription_plans()
1338
-
1339
- if plans:
1340
- return JSONResponse(
1341
- status_code=200,
1342
- content={"success": True, "plans": plans}
1343
- )
1344
- else:
1345
- return JSONResponse(
1346
- status_code=500,
1347
- content={"success": False, "detail": "Failed to initialize plans"}
1348
- )
1349
- except Exception as e:
1350
- logger.error(f"Error initializing PayPal plans: {str(e)}")
1351
- return JSONResponse(
1352
- status_code=500,
1353
- content={"success": False, "detail": f"Error initializing plans: {str(e)}"}
1354
- )
1355
-
1356
-
1357
- @app.post("/subscription/verify")
1358
- async def verify_subscription(request: Request, current_user: User = Depends(get_current_active_user)):
1359
- """Verify a subscription after payment"""
1360
- try:
1361
- data = await request.json()
1362
- subscription_id = data.get("subscription_id")
1363
-
1364
- if not subscription_id:
1365
- return JSONResponse(
1366
- status_code=400,
1367
- content={"success": False, "detail": "Subscription ID is required"}
1368
- )
1369
-
1370
- logger.info(f"Verifying subscription: {subscription_id}")
1371
-
1372
- # Verify the subscription with PayPal
1373
- success, result = verify_paypal_subscription(subscription_id)
1374
-
1375
- if not success:
1376
- logger.error(f"Subscription verification failed: {result}")
1377
- return JSONResponse(
1378
- status_code=400,
1379
- content={"success": False, "detail": str(result)}
1380
- )
1381
-
1382
- # Update the user's subscription in the database
1383
- conn = get_db_connection()
1384
- cursor = conn.cursor()
1385
-
1386
- # Get the subscription details
1387
- cursor.execute(
1388
- "SELECT tier FROM subscriptions WHERE paypal_subscription_id = ?",
1389
- (subscription_id,)
1390
- )
1391
- subscription = cursor.fetchone()
1392
-
1393
- if not subscription:
1394
- # This is a new subscription, get the tier from the PayPal response
1395
- tier = "standard_tier" # Default to standard tier
1396
- # You could extract the tier from the PayPal plan ID if needed
1397
-
1398
- # Create a new subscription record
1399
- sub_id = str(uuid.uuid4())
1400
- start_date = datetime.now()
1401
- expires_at = start_date + timedelta(days=30)
1402
-
1403
- cursor.execute(
1404
- "INSERT INTO subscriptions (id, user_id, tier, status, created_at, expires_at, paypal_subscription_id) VALUES (?, ?, ?, ?, ?, ?, ?)",
1405
- (sub_id, current_user.id, tier, "active", start_date, expires_at, subscription_id)
1406
- )
1407
- else:
1408
- # Update existing subscription
1409
- tier = subscription[0]
1410
- cursor.execute(
1411
- "UPDATE subscriptions SET status = 'active' WHERE paypal_subscription_id = ?",
1412
- (subscription_id,)
1413
- )
1414
-
1415
- # Update user's subscription tier
1416
- cursor.execute(
1417
- "UPDATE users SET subscription_tier = ? WHERE id = ?",
1418
- (tier, current_user.id)
1419
- )
1420
-
1421
- conn.commit()
1422
- conn.close()
1423
-
1424
- return JSONResponse(
1425
- status_code=200,
1426
- content={"success": True, "detail": "Subscription verified successfully"}
1427
- )
1428
-
1429
- except Exception as e:
1430
- logger.error(f"Error verifying subscription: {str(e)}")
1431
- return JSONResponse(
1432
- status_code=500,
1433
- content={"success": False, "detail": f"Error verifying subscription: {str(e)}"}
1434
- )
1435
-
1436
- @app.post("/subscription/webhook")
1437
- async def subscription_webhook(request: Request):
1438
- """Handle PayPal subscription webhooks"""
1439
- try:
1440
- payload = await request.json()
1441
- success, result = handle_subscription_webhook(payload)
1442
-
1443
- if not success:
1444
- logger.error(f"Webhook processing failed: {result}")
1445
- return {"status": "error", "message": result}
1446
-
1447
- return {"status": "success", "message": result}
1448
- except Exception as e:
1449
- logger.error(f"Error processing webhook: {str(e)}")
1450
- return {"status": "error", "message": f"Error processing webhook: {str(e)}"}
1451
-
1452
- @app.get("/subscription/verify/{subscription_id}")
1453
- async def verify_subscription(
1454
- subscription_id: str,
1455
- current_user: User = Depends(get_current_active_user)
1456
- ):
1457
- """Verify a subscription payment and update user tier"""
1458
- try:
1459
- # Verify the subscription
1460
- success, result = verify_subscription_payment(subscription_id)
1461
-
1462
- if not success:
1463
- raise HTTPException(status_code=400, detail=f"Subscription verification failed: {result}")
1464
-
1465
- # Get the plan ID from the subscription to determine tier
1466
- plan_id = result.get("plan_id", "")
1467
-
1468
- # Connect to DB to get the tier for this plan
1469
- conn = get_db_connection()
1470
- cursor = conn.cursor()
1471
- cursor.execute("SELECT tier FROM paypal_plans WHERE plan_id = ?", (plan_id,))
1472
- tier_result = cursor.fetchone()
1473
- conn.close()
1474
-
1475
- if not tier_result:
1476
- raise HTTPException(status_code=400, detail="Could not determine subscription tier")
1477
-
1478
- tier = tier_result[0]
1479
-
1480
- # Update the user's subscription
1481
- success, update_result = update_user_subscription(current_user.email, subscription_id, tier)
1482
-
1483
- if not success:
1484
- raise HTTPException(status_code=500, detail=f"Failed to update subscription: {update_result}")
1485
-
1486
- return {
1487
- "message": f"Successfully subscribed to {tier} tier",
1488
- "subscription_id": subscription_id,
1489
- "status": result.get("status", ""),
1490
- "next_billing_time": result.get("billing_info", {}).get("next_billing_time", "")
1491
- }
1492
-
1493
- except HTTPException:
1494
- raise
1495
- except Exception as e:
1496
- print(f"Subscription verification error: {str(e)}")
1497
- raise HTTPException(status_code=500, detail=f"Subscription verification failed: {str(e)}")
1498
-
1499
- @app.post("/webhook/paypal")
1500
- async def paypal_webhook(request: Request):
1501
- """Handle PayPal subscription webhooks"""
1502
- try:
1503
- payload = await request.json()
1504
- logger.info(f"Received PayPal webhook: {payload.get('event_type', 'unknown event')}")
1505
-
1506
- # Process the webhook
1507
- result = handle_subscription_webhook(payload)
1508
-
1509
- return {"status": "success", "message": "Webhook processed"}
1510
- except Exception as e:
1511
- logger.error(f"Webhook processing error: {str(e)}")
1512
- # Return 200 even on error to acknowledge receipt to PayPal
1513
- return {"status": "error", "message": str(e)}
1514
-
1515
- # Add this to your startup code
1516
- @app.on_event("startup")
1517
- async def startup_event():
1518
- """Initialize subscription plans on startup"""
1519
- try:
1520
- # Initialize PayPal subscription plans if needed
1521
- # If you have an initialize_subscription_plans function in your paypal_integration.py,
1522
- # you can call it here
1523
- print("Application started successfully")
1524
- except Exception as e:
1525
- print(f"Error during startup: {str(e)}")
1526
-
1527
- if __name__ == "__main__":
1528
- import uvicorn
1529
- port = int(os.environ.get("PORT", 7860))
1530
- host = os.environ.get("HOST", "0.0.0.0")
1531
- uvicorn.run("app:app", host=host, port=port, reload=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
auth.py DELETED
@@ -1,633 +0,0 @@
1
- import sqlite3
2
- import uuid
3
- import os
4
- import logging
5
- from datetime import datetime, timedelta
6
- import hashlib # Use hashlib instead of jwt
7
- from passlib.hash import bcrypt
8
- from dotenv import load_dotenv
9
- from fastapi import Depends, HTTPException
10
- from fastapi.security import OAuth2PasswordBearer
11
- from pydantic import BaseModel
12
- from typing import Optional
13
- from fastapi import HTTPException, status
14
- import jwt
15
- from jwt.exceptions import PyJWTError
16
- import sqlite3
17
-
18
- # Load environment variables
19
- load_dotenv()
20
-
21
- # Configure logging
22
- logging.basicConfig(
23
- level=logging.INFO,
24
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
25
- )
26
- logger = logging.getLogger('auth')
27
-
28
- # Security configuration
29
- SECRET_KEY = os.getenv("JWT_SECRET", "your-secret-key-for-development-only")
30
- ALGORITHM = "HS256"
31
- JWT_EXPIRATION_DELTA = timedelta(days=1) # Token valid for 1 day
32
- # Database path from environment variable or default
33
- # Fix the incorrect DB_PATH
34
- DB_PATH = os.getenv("DB_PATH", os.path.join(os.path.dirname(__file__), "data/user_data.db"))
35
-
36
- # FastAPI OAuth2 scheme
37
- oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
38
-
39
- # Pydantic models for FastAPI
40
- class User(BaseModel):
41
- id: str
42
- email: str
43
- subscription_tier: str = "free_tier"
44
- subscription_expiry: Optional[datetime] = None
45
- api_calls_remaining: int = 5
46
- last_reset_date: Optional[datetime] = None
47
-
48
- class UserCreate(BaseModel):
49
- email: str
50
- password: str
51
-
52
- class Token(BaseModel):
53
- access_token: str
54
- token_type: str
55
-
56
- class TokenData(BaseModel):
57
- user_id: Optional[str] = None
58
-
59
- # Subscription tiers and limits
60
- # Update the SUBSCRIPTION_TIERS dictionary
61
- SUBSCRIPTION_TIERS = {
62
- "free_tier": {
63
- "price": 0,
64
- "currency": "INR",
65
- "features": ["basic_document_analysis", "basic_risk_assessment"],
66
- "limits": {
67
- "document_size_mb": 5,
68
- "documents_per_month": 3,
69
- "video_size_mb": 0,
70
- "audio_size_mb": 0
71
- }
72
- },
73
- "standard_tier": {
74
- "price": 799,
75
- "currency": "INR",
76
- "features": ["basic_document_analysis", "basic_risk_assessment", "video_analysis", "audio_analysis", "chatbot"],
77
- "limits": {
78
- "document_size_mb": 20,
79
- "documents_per_month": 20,
80
- "video_size_mb": 100,
81
- "audio_size_mb": 50
82
- }
83
- },
84
- "premium_tier": {
85
- "price": 1499,
86
- "currency": "INR",
87
- "features": ["basic_document_analysis", "basic_risk_assessment", "video_analysis", "audio_analysis", "chatbot", "detailed_risk_assessment", "contract_clause_analysis"],
88
- "limits": {
89
- "document_size_mb": 50,
90
- "documents_per_month": 999999, # Unlimited
91
- "video_size_mb": 500,
92
- "audio_size_mb": 200
93
- }
94
- }
95
- }
96
-
97
- # Database connection management
98
- def get_db_connection():
99
- """Create and return a database connection with proper error handling"""
100
- try:
101
- # Ensure the directory exists
102
- db_dir = os.path.dirname(DB_PATH)
103
- os.makedirs(db_dir, exist_ok=True)
104
-
105
- conn = sqlite3.connect(DB_PATH)
106
- conn.row_factory = sqlite3.Row # Return rows as dictionaries
107
- return conn
108
- except sqlite3.Error as e:
109
- logger.error(f"Database connection error: {e}")
110
- raise Exception(f"Database connection failed: {e}")
111
-
112
- # Database setup
113
- # In the init_auth_db function, update the CREATE TABLE statement to match our schema
114
- def init_auth_db():
115
- """Initialize the authentication database with required tables"""
116
- try:
117
- conn = get_db_connection()
118
- c = conn.cursor()
119
-
120
- # Create users table with the correct schema
121
- c.execute('''
122
- CREATE TABLE IF NOT EXISTS users (
123
- id TEXT PRIMARY KEY,
124
- email TEXT UNIQUE NOT NULL,
125
- hashed_password TEXT NOT NULL,
126
- password TEXT,
127
- subscription_tier TEXT DEFAULT 'free_tier',
128
- is_active BOOLEAN DEFAULT 1,
129
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
130
- api_calls_remaining INTEGER DEFAULT 10,
131
- last_reset_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
132
- )
133
- ''')
134
-
135
- # Create subscriptions table
136
- c.execute('''
137
- CREATE TABLE IF NOT EXISTS subscriptions (
138
- id TEXT PRIMARY KEY,
139
- user_id TEXT,
140
- tier TEXT,
141
- plan_id TEXT,
142
- status TEXT,
143
- created_at TIMESTAMP,
144
- expires_at TIMESTAMP,
145
- paypal_subscription_id TEXT,
146
- FOREIGN KEY (user_id) REFERENCES users (id)
147
- )
148
- ''')
149
-
150
- # Create usage stats table
151
- c.execute('''
152
- CREATE TABLE IF NOT EXISTS usage_stats (
153
- id TEXT PRIMARY KEY,
154
- user_id TEXT,
155
- month INTEGER,
156
- year INTEGER,
157
- analyses_used INTEGER,
158
- FOREIGN KEY (user_id) REFERENCES users (id)
159
- )
160
- ''')
161
-
162
- # Create tokens table for refresh tokens
163
- c.execute('''
164
- CREATE TABLE IF NOT EXISTS refresh_tokens (
165
- user_id TEXT,
166
- token TEXT,
167
- expires_at TIMESTAMP,
168
- FOREIGN KEY (user_id) REFERENCES users (id)
169
- )
170
- ''')
171
-
172
- conn.commit()
173
- logger.info("Database initialized successfully")
174
- except Exception as e:
175
- logger.error(f"Database initialization error: {e}")
176
- raise
177
- finally:
178
- if conn:
179
- conn.close()
180
-
181
- # Initialize the database
182
- init_auth_db()
183
-
184
- # Password hashing with bcrypt
185
- # Update the password hashing and verification functions to use a more reliable method
186
-
187
- # Replace these functions
188
- # Remove these conflicting functions
189
- # def hash_password(password):
190
- # """Hash a password using bcrypt"""
191
- # return bcrypt.hash(password)
192
- #
193
- # def verify_password(plain_password, hashed_password):
194
- # """Verify a password against its hash"""
195
- # return bcrypt.verify(plain_password, hashed_password)
196
-
197
- # Keep only these improved functions
198
- def hash_password(password):
199
- """Hash a password using bcrypt"""
200
- # Use a more direct approach to avoid bcrypt version issues
201
- import bcrypt
202
- # Convert password to bytes if it's not already
203
- if isinstance(password, str):
204
- password = password.encode('utf-8')
205
- # Generate salt and hash
206
- salt = bcrypt.gensalt()
207
- hashed = bcrypt.hashpw(password, salt)
208
- # Return as string for storage
209
- return hashed.decode('utf-8')
210
-
211
- def verify_password(plain_password, hashed_password):
212
- """Verify a password against its hash"""
213
- import bcrypt
214
- # Convert inputs to bytes if they're not already
215
- if isinstance(plain_password, str):
216
- plain_password = plain_password.encode('utf-8')
217
- if isinstance(hashed_password, str):
218
- hashed_password = hashed_password.encode('utf-8')
219
-
220
- try:
221
- # Use direct bcrypt verification
222
- return bcrypt.checkpw(plain_password, hashed_password)
223
- except Exception as e:
224
- logger.error(f"Password verification error: {e}")
225
- return False
226
-
227
- # User registration
228
- def register_user(email, password):
229
- try:
230
- conn = get_db_connection()
231
- c = conn.cursor()
232
-
233
- # Check if user already exists
234
- c.execute("SELECT * FROM users WHERE email = ?", (email,))
235
- if c.fetchone():
236
- return False, "Email already registered"
237
-
238
- # Create new user
239
- user_id = str(uuid.uuid4())
240
-
241
- # Add more detailed logging
242
- logger.info(f"Registering new user with email: {email}")
243
- hashed_pw = hash_password(password)
244
- logger.info(f"Password hashed successfully: {bool(hashed_pw)}")
245
-
246
- c.execute("""
247
- INSERT INTO users
248
- (id, email, hashed_password, subscription_tier, api_calls_remaining, last_reset_date)
249
- VALUES (?, ?, ?, ?, ?, ?)
250
- """, (user_id, email, hashed_pw, "free_tier", 5, datetime.now()))
251
-
252
- conn.commit()
253
- logger.info(f"User registered successfully: {email}")
254
-
255
- # Verify the user was actually stored
256
- c.execute("SELECT * FROM users WHERE email = ?", (email,))
257
- stored_user = c.fetchone()
258
- logger.info(f"User verification after registration: {bool(stored_user)}")
259
-
260
- access_token = create_access_token(user_id)
261
- return True, {
262
- "user_id": user_id,
263
- "access_token": access_token,
264
- "token_type": "bearer"
265
- }
266
- except Exception as e:
267
- logger.error(f"User registration error: {e}")
268
- return False, f"Registration failed: {str(e)}"
269
- finally:
270
- if conn:
271
- conn.close()
272
-
273
- # User login
274
- # Fix the authenticate_user function
275
- # In the authenticate_user function, update the password verification to use hashed_password
276
- def authenticate_user(email, password):
277
- """Authenticate a user and return user data with tokens"""
278
- try:
279
- conn = get_db_connection()
280
- c = conn.cursor()
281
-
282
- # Get user by email
283
- c.execute("SELECT * FROM users WHERE email = ? AND is_active = 1", (email,))
284
- user = c.fetchone()
285
-
286
- if not user:
287
- logger.warning(f"User not found: {email}")
288
- return None
289
-
290
- # Add debug logging for password verification
291
- logger.info(f"Verifying password for user: {email}")
292
- logger.info(f"Stored hashed password: {user['hashed_password'][:20]}...")
293
-
294
- try:
295
- # Check if password verification works
296
- is_valid = verify_password(password, user['hashed_password'])
297
- logger.info(f"Password verification result: {is_valid}")
298
-
299
- if not is_valid:
300
- logger.warning(f"Password verification failed for user: {email}")
301
- return None
302
- except Exception as e:
303
- logger.error(f"Password verification error: {e}")
304
- return None
305
-
306
- # Update last login time if column exists
307
- try:
308
- c.execute("UPDATE users SET last_login = ? WHERE id = ?",
309
- (datetime.now(), user['id']))
310
- conn.commit()
311
- except sqlite3.OperationalError:
312
- # last_login column might not exist
313
- pass
314
-
315
- # Convert sqlite3.Row to dict to use get() method
316
- user_dict = dict(user)
317
-
318
- # Create and return a User object
319
- return User(
320
- id=user_dict['id'],
321
- email=user_dict['email'],
322
- subscription_tier=user_dict.get('subscription_tier', 'free_tier'),
323
- subscription_expiry=None, # Handle this properly if needed
324
- api_calls_remaining=user_dict.get('api_calls_remaining', 5),
325
- last_reset_date=user_dict.get('last_reset_date')
326
- )
327
- except Exception as e:
328
- logger.error(f"Login error: {e}")
329
- return None
330
- finally:
331
- if conn:
332
- conn.close()
333
-
334
- # Token generation and validation - completely replaced
335
- def create_access_token(user_id):
336
- """Create a new access token for a user"""
337
- try:
338
- # Create a JWT token with user_id and expiration
339
- expiration = datetime.now() + JWT_EXPIRATION_DELTA
340
-
341
- # Create a token payload
342
- payload = {
343
- "sub": user_id,
344
- "exp": expiration.timestamp()
345
- }
346
-
347
- # Generate the JWT token
348
- token = jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)
349
-
350
- logger.info(f"Created access token for user: {user_id}")
351
- return token
352
- except Exception as e:
353
- logger.error(f"Token creation error: {e}")
354
- return None
355
-
356
-
357
- def update_auth_db_schema():
358
- """Update the authentication database schema with any missing columns"""
359
- try:
360
- conn = get_db_connection()
361
- c = conn.cursor()
362
-
363
- # Check if tier column exists in subscriptions table
364
- c.execute("PRAGMA table_info(subscriptions)")
365
- columns = [column[1] for column in c.fetchall()]
366
-
367
- # Add tier column if it doesn't exist
368
- if "tier" not in columns:
369
- logger.info("Adding 'tier' column to subscriptions table")
370
- c.execute("ALTER TABLE subscriptions ADD COLUMN tier TEXT")
371
- conn.commit()
372
- logger.info("Database schema updated successfully")
373
-
374
- conn.close()
375
- except Exception as e:
376
- logger.error(f"Database schema update error: {e}")
377
- raise HTTPException(
378
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
379
- detail=f"Database schema update error: {str(e)}"
380
- )
381
-
382
- # Add this to your get_current_user function
383
- async def get_current_user(token: str = Depends(oauth2_scheme)):
384
- credentials_exception = HTTPException(
385
- status_code=status.HTTP_401_UNAUTHORIZED,
386
- detail="Could not validate credentials",
387
- headers={"WWW-Authenticate": "Bearer"},
388
- )
389
- try:
390
- # Decode the JWT token
391
- payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
392
- user_id: str = payload.get("sub")
393
- if user_id is None:
394
- logger.error("Token missing 'sub' field")
395
- raise credentials_exception
396
- except Exception as e:
397
- logger.error(f"Token validation error: {str(e)}")
398
- raise credentials_exception
399
-
400
- # Get user from database
401
- conn = get_db_connection()
402
- cursor = conn.cursor()
403
- cursor.execute("SELECT id, email, subscription_tier, is_active FROM users WHERE id = ?", (user_id,))
404
- user_data = cursor.fetchone()
405
- conn.close()
406
-
407
- if user_data is None:
408
- logger.error(f"User not found: {user_id}")
409
- raise credentials_exception
410
-
411
- user = User(
412
- id=user_data[0],
413
- email=user_data[1],
414
- subscription_tier=user_data[2],
415
- is_active=bool(user_data[3])
416
- )
417
-
418
- return user
419
-
420
- async def get_current_active_user(current_user: User = Depends(get_current_user)):
421
- """Get the current active user"""
422
- return current_user
423
-
424
- def create_user_subscription(email, tier):
425
- """Create a subscription for a user"""
426
- try:
427
- # Get user by email
428
- conn = get_db_connection()
429
- c = conn.cursor()
430
-
431
- # Get user ID
432
- c.execute("SELECT id FROM users WHERE email = ?", (email,))
433
- user_data = c.fetchone()
434
-
435
- if not user_data:
436
- return False, "User not found"
437
-
438
- user_id = user_data['id']
439
-
440
- # Check if tier is valid
441
- valid_tiers = ["standard_tier", "premium_tier"]
442
- if tier not in valid_tiers:
443
- return False, f"Invalid tier: {tier}. Must be one of {valid_tiers}"
444
-
445
- # Create subscription
446
- subscription_id = str(uuid.uuid4())
447
- created_at = datetime.now()
448
- expires_at = created_at + timedelta(days=30) # 30-day subscription
449
-
450
- # Insert subscription
451
- c.execute("""
452
- INSERT INTO subscriptions
453
- (id, user_id, tier, status, created_at, expires_at)
454
- VALUES (?, ?, ?, ?, ?, ?)
455
- """, (subscription_id, user_id, tier, "active", created_at, expires_at))
456
-
457
- # Update user's subscription tier
458
- c.execute("""
459
- UPDATE users
460
- SET subscription_tier = ?
461
- WHERE id = ?
462
- """, (tier, user_id))
463
-
464
- conn.commit()
465
-
466
- return True, {
467
- "id": subscription_id,
468
- "user_id": user_id,
469
- "tier": tier,
470
- "status": "active",
471
- "created_at": created_at.isoformat(),
472
- "expires_at": expires_at.isoformat()
473
- }
474
- except Exception as e:
475
- logger.error(f"Subscription creation error: {e}")
476
- return False, f"Failed to create subscription: {str(e)}"
477
- finally:
478
- if conn:
479
- conn.close()
480
-
481
- def get_user(user_id: str):
482
- """Get user by ID"""
483
- try:
484
- conn = get_db_connection()
485
- c = conn.cursor()
486
-
487
- # Get user
488
- c.execute("SELECT * FROM users WHERE id = ? AND is_active = 1", (user_id,))
489
- user_data = c.fetchone()
490
-
491
- if not user_data:
492
- return None
493
-
494
- # Convert to User model
495
- user_dict = dict(user_data)
496
-
497
- # Handle datetime conversions if needed
498
- if user_dict.get("subscription_expiry") and isinstance(user_dict["subscription_expiry"], str):
499
- user_dict["subscription_expiry"] = datetime.fromisoformat(user_dict["subscription_expiry"])
500
- if user_dict.get("last_reset_date") and isinstance(user_dict["last_reset_date"], str):
501
- user_dict["last_reset_date"] = datetime.fromisoformat(user_dict["last_reset_date"])
502
-
503
- return User(
504
- id=user_dict['id'],
505
- email=user_dict['email'],
506
- subscription_tier=user_dict['subscription_tier'],
507
- subscription_expiry=user_dict.get('subscription_expiry'),
508
- api_calls_remaining=user_dict.get('api_calls_remaining', 5),
509
- last_reset_date=user_dict.get('last_reset_date')
510
- )
511
- except Exception as e:
512
- logger.error(f"Get user error: {e}")
513
- return None
514
- finally:
515
- if conn:
516
- conn.close()
517
-
518
- def check_subscription_access(user: User, feature: str, file_size_mb: Optional[float] = None):
519
- """Check if the user has access to the requested feature and file size"""
520
- # Check if subscription is expired
521
- if user.subscription_tier != "free_tier" and user.subscription_expiry and user.subscription_expiry < datetime.now():
522
- # Downgrade to free tier if subscription expired
523
- user.subscription_tier = "free_tier"
524
- user.api_calls_remaining = SUBSCRIPTION_TIERS["free_tier"]["daily_api_calls"]
525
- with get_db_connection() as conn:
526
- c = conn.cursor()
527
- c.execute("""
528
- UPDATE users
529
- SET subscription_tier = ?, api_calls_remaining = ?
530
- WHERE id = ?
531
- """, (user.subscription_tier, user.api_calls_remaining, user.id))
532
- conn.commit()
533
-
534
- # Reset API calls if needed
535
- user = reset_api_calls_if_needed(user)
536
-
537
- # Check if user has API calls remaining
538
- if user.api_calls_remaining <= 0:
539
- raise HTTPException(
540
- status_code=429,
541
- detail="API call limit reached for today. Please upgrade your subscription or try again tomorrow."
542
- )
543
-
544
- # Check if feature is available in user's subscription tier
545
- tier_features = SUBSCRIPTION_TIERS[user.subscription_tier]["features"]
546
- if feature not in tier_features:
547
- raise HTTPException(
548
- status_code=403,
549
- detail=f"The {feature} feature is not available in your {user.subscription_tier} subscription. Please upgrade to access this feature."
550
- )
551
-
552
- # Check file size limit if applicable
553
- if file_size_mb:
554
- max_size = SUBSCRIPTION_TIERS[user.subscription_tier]["max_document_size_mb"]
555
- if file_size_mb > max_size:
556
- raise HTTPException(
557
- status_code=413,
558
- detail=f"File size exceeds the {max_size}MB limit for your {user.subscription_tier} subscription. Please upgrade or use a smaller file."
559
- )
560
-
561
- # Decrement API calls remaining
562
- user.api_calls_remaining -= 1
563
- with get_db_connection() as conn:
564
- c = conn.cursor()
565
- c.execute("""
566
- UPDATE users
567
- SET api_calls_remaining = ?
568
- WHERE id = ?
569
- """, (user.api_calls_remaining, user.id))
570
- conn.commit()
571
-
572
- return True
573
-
574
- def reset_api_calls_if_needed(user: User):
575
- """Reset API call counter if it's a new day"""
576
- today = datetime.now().date()
577
- if user.last_reset_date is None or user.last_reset_date.date() < today:
578
- tier_limits = SUBSCRIPTION_TIERS[user.subscription_tier]
579
- user.api_calls_remaining = tier_limits["daily_api_calls"]
580
- user.last_reset_date = datetime.now()
581
- # Update the user in the database
582
- with get_db_connection() as conn:
583
- c = conn.cursor()
584
- c.execute("""
585
- UPDATE users
586
- SET api_calls_remaining = ?, last_reset_date = ?
587
- WHERE id = ?
588
- """, (user.api_calls_remaining, user.last_reset_date, user.id))
589
- conn.commit()
590
-
591
- return user
592
-
593
- def login_user(email, password):
594
- """Login a user with email and password"""
595
- try:
596
- # Authenticate user
597
- user = authenticate_user(email, password)
598
- if not user:
599
- return False, "Incorrect username or password"
600
-
601
- # Create access token
602
- access_token = create_access_token(user.id)
603
-
604
- # Create refresh token
605
- refresh_token = str(uuid.uuid4())
606
- expires_at = datetime.now() + timedelta(days=30)
607
-
608
- # Store refresh token
609
- conn = get_db_connection()
610
- c = conn.cursor()
611
- c.execute("INSERT INTO refresh_tokens VALUES (?, ?, ?)",
612
- (user.id, refresh_token, expires_at))
613
- conn.commit()
614
-
615
- # Get subscription info
616
- c.execute("SELECT * FROM subscriptions WHERE user_id = ? AND status = 'active'", (user.id,))
617
- subscription = c.fetchone()
618
-
619
- # Convert subscription to dict if it exists, otherwise set to None
620
- subscription_dict = dict(subscription) if subscription else None
621
-
622
- conn.close()
623
-
624
- return True, {
625
- "user_id": user.id,
626
- "email": user.email,
627
- "access_token": access_token,
628
- "refresh_token": refresh_token,
629
- "subscription": subscription_dict
630
- }
631
- except Exception as e:
632
- logger.error(f"Login error: {e}")
633
- return False, f"Login failed: {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
check_routes.py DELETED
@@ -1,61 +0,0 @@
1
- import os
2
- import sys
3
- import importlib.util
4
-
5
- def check_fastapi_routes():
6
- """Check the FastAPI routes in the app.py file."""
7
- print("Checking FastAPI routes...")
8
-
9
- # Store current directory
10
- current_dir = os.getcwd()
11
-
12
- try:
13
- # Change to the backend directory
14
- os.chdir("doc-vid-analyze-main")
15
- print(f"Current directory: {os.getcwd()}")
16
-
17
- # Check if app.py exists
18
- if not os.path.exists("app.py"):
19
- print(f"❌ Error: app.py not found in {os.getcwd()}")
20
- return False
21
-
22
- # Load the app.py module
23
- print("Loading app.py module...")
24
- spec = importlib.util.spec_from_file_location("app", "app.py")
25
- app_module = importlib.util.module_from_spec(spec)
26
- sys.modules["app"] = app_module
27
- spec.loader.exec_module(app_module)
28
-
29
- # Check if app is defined
30
- if not hasattr(app_module, "app"):
31
- print("❌ Error: 'app' object not found in app.py")
32
- return False
33
-
34
- app = app_module.app
35
-
36
- # Print app information
37
- print("\n📋 FastAPI App Information:")
38
- print(f"App title: {app.title}")
39
- print(f"App version: {app.version}")
40
- print(f"App description: {app.description}")
41
-
42
- # Print routes
43
- print("\n📋 FastAPI Routes:")
44
- for route in app.routes:
45
- print(f"Route: {route.path}")
46
- print(f" Methods: {route.methods}")
47
- print(f" Name: {route.name}")
48
- print(f" Endpoint: {route.endpoint.__name__ if hasattr(route.endpoint, '__name__') else route.endpoint}")
49
- print()
50
-
51
- return True
52
-
53
- except Exception as e:
54
- print(f"❌ Error checking routes: {e}")
55
- return False
56
- finally:
57
- # Return to original directory
58
- os.chdir(current_dir)
59
-
60
- if __name__ == "__main__":
61
- check_fastapi_routes()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fix_users_table.py DELETED
@@ -1,180 +0,0 @@
1
- import sqlite3
2
- import os
3
- import uuid
4
- import datetime
5
-
6
- # Define both database paths
7
- DB_PATH_1 = os.path.join(os.path.dirname(__file__), "../data/user_data.db")
8
- DB_PATH_2 = os.path.join(os.path.dirname(__file__), "data/user_data.db")
9
-
10
- # Define the function to create users table
11
- # Make sure the create_users_table function allows NULL for hashed_password temporarily
12
- def create_users_table(cursor):
13
- """Create the users table with all required columns"""
14
- cursor.execute('''
15
- CREATE TABLE users (
16
- id TEXT PRIMARY KEY,
17
- email TEXT UNIQUE NOT NULL,
18
- hashed_password TEXT DEFAULT 'temp_hash_for_migration',
19
- password TEXT,
20
- subscription_tier TEXT DEFAULT 'free',
21
- is_active BOOLEAN DEFAULT 1,
22
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
23
- api_calls_remaining INTEGER DEFAULT 10,
24
- last_reset_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
25
- )
26
- ''')
27
-
28
- # Update the CREATE TABLE statement to include all necessary columns
29
- def fix_users_table(db_path):
30
- # Make sure the data directory exists
31
- data_dir = os.path.dirname(db_path)
32
- if not os.path.exists(data_dir):
33
- print(f"Creating data directory: {data_dir}")
34
- os.makedirs(data_dir, exist_ok=True)
35
-
36
- if not os.path.exists(db_path):
37
- print(f"Database does not exist at: {os.path.abspath(db_path)}")
38
- return False
39
-
40
- print(f"Using database path: {os.path.abspath(db_path)}")
41
-
42
- # Connect to the database
43
- conn = sqlite3.connect(db_path)
44
- cursor = conn.cursor()
45
-
46
- # Check if users table exists
47
- cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='users'")
48
- if cursor.fetchone():
49
- print("Users table exists, checking schema...")
50
-
51
- # Check columns
52
- cursor.execute("PRAGMA table_info(users)")
53
- columns_info = cursor.fetchall()
54
- columns = [column[1] for column in columns_info]
55
-
56
- # List of all required columns
57
- required_columns = ['id', 'email', 'hashed_password', 'password', 'subscription_tier',
58
- 'is_active', 'created_at', 'api_calls_remaining', 'last_reset_date']
59
-
60
- # Check if any required column is missing
61
- missing_columns = [col for col in required_columns if col not in columns]
62
-
63
- if missing_columns:
64
- print(f"Schema needs fixing. Missing columns: {', '.join(missing_columns)}")
65
-
66
- # Dynamically build the SELECT query based on available columns
67
- available_columns = [col for col in columns if col != 'id'] # Exclude id as we'll generate new ones
68
-
69
- if not available_columns:
70
- print("No usable columns found in users table, creating new table...")
71
- cursor.execute("DROP TABLE users")
72
- create_users_table(cursor)
73
- print("Created new empty users table with correct schema")
74
- else:
75
- # Backup existing users with available columns
76
- select_query = f"SELECT {', '.join(available_columns)} FROM users"
77
- print(f"Backing up users with query: {select_query}")
78
- cursor.execute(select_query)
79
- existing_users = cursor.fetchall()
80
-
81
- # Drop the existing table
82
- cursor.execute("DROP TABLE users")
83
-
84
- # Create the table with the correct schema
85
- create_users_table(cursor)
86
-
87
- # Restore the users with new UUIDs for IDs
88
- if existing_users:
89
- current_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
90
- for user in existing_users:
91
- user_id = str(uuid.uuid4())
92
-
93
- # Create a dictionary to map column names to values
94
- user_data = {'id': user_id}
95
- for i, col in enumerate(available_columns):
96
- user_data[col] = user[i]
97
-
98
- # Set default values for missing columns
99
- # Add a default value for hashed_password in the Set default values section
100
- if 'hashed_password' not in user_data:
101
- user_data['hashed_password'] = 'temp_hash_for_migration' # Temporary hash for migration
102
- if 'subscription_tier' not in user_data:
103
- user_data['subscription_tier'] = 'free'
104
- if 'is_active' not in user_data:
105
- user_data['is_active'] = 1
106
- if 'created_at' not in user_data:
107
- user_data['created_at'] = current_time
108
- if 'api_calls_remaining' not in user_data:
109
- user_data['api_calls_remaining'] = 10
110
- if 'last_reset_date' not in user_data:
111
- user_data['last_reset_date'] = current_time
112
-
113
- # Build INSERT query with all required columns
114
- insert_columns = ['id']
115
- insert_values = [user_id]
116
-
117
- # Add values for columns that exist in the old table
118
- for col in available_columns:
119
- insert_columns.append(col)
120
- insert_values.append(user_data[col])
121
-
122
- # Add default values for columns that don't exist in the old table
123
- for col in required_columns:
124
- # Add hashed_password to the column default values section
125
- if col not in ['id'] + available_columns:
126
- insert_columns.append(col)
127
- if col == 'subscription_tier':
128
- insert_values.append('free')
129
- elif col == 'is_active':
130
- insert_values.append(1)
131
- elif col == 'created_at':
132
- insert_values.append(current_time)
133
- elif col == 'api_calls_remaining':
134
- insert_values.append(10)
135
- elif col == 'last_reset_date':
136
- insert_values.append(current_time)
137
- elif col == 'hashed_password':
138
- insert_values.append('temp_hash_for_migration') # Temporary hash for migration
139
- else:
140
- insert_values.append(None) # Default to NULL for other columns
141
-
142
- placeholders = ', '.join(['?'] * len(insert_columns))
143
- insert_query = f"INSERT INTO users ({', '.join(insert_columns)}) VALUES ({placeholders})"
144
-
145
- cursor.execute(insert_query, insert_values)
146
-
147
- print(f"Fixed users table, restored {len(existing_users)} users")
148
- else:
149
- print("Users table schema is correct")
150
- else:
151
- print("Users table doesn't exist, creating it now...")
152
- create_users_table(cursor)
153
- print("Users table created successfully")
154
-
155
- # Commit changes and close connection
156
- conn.commit()
157
- conn.close()
158
- return True
159
-
160
- if __name__ == "__main__":
161
- print("Checking first database location...")
162
- success1 = fix_users_table(DB_PATH_1)
163
-
164
- print("\nChecking second database location...")
165
- success2 = fix_users_table(DB_PATH_2)
166
-
167
- if not (success1 or success2):
168
- print("\nWarning: Could not find any existing database files.")
169
- print("Creating a new database at the primary location...")
170
- # Create a new database at the primary location
171
- data_dir = os.path.dirname(DB_PATH_1)
172
- if not os.path.exists(data_dir):
173
- os.makedirs(data_dir, exist_ok=True)
174
-
175
- conn = sqlite3.connect(DB_PATH_1)
176
- cursor = conn.cursor()
177
- create_users_table(cursor)
178
- conn.commit()
179
- conn.close()
180
- print(f"Created new database at: {os.path.abspath(DB_PATH_1)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
initialize_plans.py DELETED
@@ -1,25 +0,0 @@
1
- import os
2
- import sys
3
- from dotenv import load_dotenv
4
- from paypal_integration import initialize_subscription_plans
5
-
6
- # Load environment variables
7
- load_dotenv()
8
-
9
- def main():
10
- """Initialize PayPal subscription plans"""
11
- print("Initializing PayPal subscription plans...")
12
- plans = initialize_subscription_plans()
13
-
14
- if plans:
15
- print("✅ Plans initialized successfully:")
16
- for tier, plan_id in plans.items():
17
- print(f" - {tier}: {plan_id}")
18
- return True
19
- else:
20
- print("❌ Failed to initialize plans. Check the logs for details.")
21
- return False
22
-
23
- if __name__ == "__main__":
24
- success = main()
25
- sys.exit(0 if success else 1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
paypal_integration.py DELETED
@@ -1,1004 +0,0 @@
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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements.txt DELETED
@@ -1,21 +0,0 @@
1
- fastapi>=0.95.0
2
- uvicorn>=0.21.1
3
- pydantic>=1.10.7
4
- python-multipart>=0.0.6
5
- python-dotenv>=1.0.0
6
- pdfplumber>=0.9.0
7
- spacy>=3.5.2
8
- torch>=2.0.0
9
- transformers>=4.28.1
10
- sentence-transformers>=2.2.2
11
- moviepy>=1.0.3
12
- matplotlib>=3.7.1
13
- numpy>=1.24.2
14
- passlib>=1.7.4
15
- python-jose[cryptography]>=3.3.0
16
- bcrypt>=4.0.1
17
- requests>=2.28.2
18
- SQLAlchemy>=2.0.9
19
- aiofiles>=23.1.0
20
- huggingface_hub>=0.16.4
21
- en-core-web-sm @ https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.5.0/en_core_web_sm-3.5.0-py3-none-any.whl