Mavithya commited on
Commit
121b808
·
verified ·
1 Parent(s): e193833

Upload backend code and PyTorch weights

Browse files
Files changed (47) hide show
  1. .gitattributes +1 -0
  2. backend/__pycache__/api_routes.cpython-313.pyc +3 -0
  3. backend/__pycache__/config.cpython-313.pyc +0 -0
  4. backend/__pycache__/main.cpython-313.pyc +0 -0
  5. backend/core/__pycache__/config.cpython-313.pyc +0 -0
  6. backend/core/__pycache__/dependencies.cpython-313.pyc +0 -0
  7. backend/core/__pycache__/security.cpython-313.pyc +0 -0
  8. backend/core/config.py +29 -0
  9. backend/core/dependencies.py +66 -0
  10. backend/core/security.py +41 -0
  11. backend/main.py +210 -0
  12. backend/models/__pycache__/cat_skin.cpython-313.pyc +0 -0
  13. backend/models/__pycache__/dog_eye.cpython-313.pyc +0 -0
  14. backend/models/__pycache__/dog_skin.cpython-313.pyc +0 -0
  15. backend/models/cat_skin.py +18 -0
  16. backend/models/dog_eye.py +24 -0
  17. backend/models/dog_skin.py +25 -0
  18. backend/routers/__pycache__/admin.cpython-313.pyc +0 -0
  19. backend/routers/__pycache__/appointments.cpython-313.pyc +0 -0
  20. backend/routers/__pycache__/auth.cpython-313.pyc +0 -0
  21. backend/routers/__pycache__/clinics.cpython-313.pyc +0 -0
  22. backend/routers/__pycache__/pets.cpython-313.pyc +0 -0
  23. backend/routers/admin.py +104 -0
  24. backend/routers/appointments.py +171 -0
  25. backend/routers/auth.py +254 -0
  26. backend/routers/clinics.py +173 -0
  27. backend/routers/pets.py +463 -0
  28. backend/schemas/__pycache__/schemas.cpython-313.pyc +0 -0
  29. backend/schemas/schemas.py +56 -0
  30. backend/services/__pycache__/appointment_service.cpython-313.pyc +0 -0
  31. backend/services/__pycache__/auth_service.cpython-313.pyc +0 -0
  32. backend/services/__pycache__/clinic_service.cpython-313.pyc +0 -0
  33. backend/services/__pycache__/pet_service.cpython-313.pyc +0 -0
  34. backend/services/__pycache__/predictor.cpython-313.pyc +0 -0
  35. backend/services/__pycache__/router.cpython-313.pyc +0 -0
  36. backend/services/appointment_service.py +289 -0
  37. backend/services/auth_service.py +203 -0
  38. backend/services/clinic_service.py +250 -0
  39. backend/services/pet_service.py +165 -0
  40. backend/services/predictor.py +15 -0
  41. backend/services/router.py +13 -0
  42. backend/utils/__pycache__/image.cpython-313.pyc +0 -0
  43. backend/utils/image.py +13 -0
  44. weights/cat_skin_model.pth +3 -0
  45. weights/dog_eye_model.pth +3 -0
  46. weights/dog_eye_model_ResNet_NEW.pth +3 -0
  47. weights/dog_skin_model.pth +3 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ backend/__pycache__/api_routes.cpython-313.pyc filter=lfs diff=lfs merge=lfs -text
backend/__pycache__/api_routes.cpython-313.pyc ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ba4cd72be3aca9ed555b38531910b78128c75c9bd7e9c027ff0ec714c55f5a10
3
+ size 100417
backend/__pycache__/config.cpython-313.pyc ADDED
Binary file (1.45 kB). View file
 
backend/__pycache__/main.cpython-313.pyc ADDED
Binary file (9.69 kB). View file
 
backend/core/__pycache__/config.cpython-313.pyc ADDED
Binary file (1.94 kB). View file
 
backend/core/__pycache__/dependencies.cpython-313.pyc ADDED
Binary file (3.42 kB). View file
 
backend/core/__pycache__/security.cpython-313.pyc ADDED
Binary file (2.21 kB). View file
 
backend/core/config.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+
4
+ # Load environment variables from .env file
5
+ load_dotenv()
6
+
7
+ # Base directory points to the backend/ folder
8
+ BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
9
+
10
+ DOG_SKIN_MODEL = os.path.join(BASE_DIR, "weights/dog_skin_model.pth")
11
+ DOG_EYE_MODEL = os.path.join(BASE_DIR, "weights/dog_eye_model_ResNet_NEW.pth")
12
+ CAT_SKIN_MODEL = os.path.join(BASE_DIR, "weights/cat_skin_model.pth")
13
+
14
+ DEVICE = "cuda" if __import__("torch").cuda.is_available() else "cpu"
15
+
16
+ # Supabase Configurations
17
+ SUPABASE_URL = os.getenv("SUPABASE_URL", "")
18
+ SUPABASE_KEY = os.getenv("SUPABASE_KEY", "")
19
+ SUPABASE_SERVICE_KEY = os.getenv("SUPABASE_SERVICE_KEY", "")
20
+
21
+ # Admin Credentials
22
+ ADMIN_EMAIL = os.getenv("ADMIN_EMAIL", "admin@petai.com")
23
+ ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "AdminPass!234")
24
+
25
+ # LangSmith Configuration (for tracing and debugging)
26
+ LANGCHAIN_TRACING_V2 = os.getenv("LANGCHAIN_TRACING_V2", "false").lower() == "true"
27
+ LANGCHAIN_API_KEY = os.getenv("LANGCHAIN_API_KEY", None)
28
+ LANGCHAIN_ENDPOINT = os.getenv("LANGCHAIN_ENDPOINT", "https://api.smith.langchain.com")
29
+ LANGCHAIN_PROJECT = os.getenv("LANGCHAIN_PROJECT", "Pet_AI")
backend/core/dependencies.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import Request, Depends, HTTPException, status
2
+ from fastapi.security import HTTPAuthorizationCredentials
3
+ from backend.core.security import verify_supabase_jwt, security_scheme
4
+ from chatbot.supabase_config import supabase
5
+ from typing import Dict, List, Optional
6
+
7
+ async def get_current_user(
8
+ credentials: Optional[HTTPAuthorizationCredentials] = Depends(security_scheme)
9
+ ) -> Dict:
10
+ """
11
+ Dependency to get the currently authenticated user.
12
+ Verifies the JWT and fetches their profile from public.users.
13
+ """
14
+ if not credentials:
15
+ raise HTTPException(
16
+ status_code=status.HTTP_401_UNAUTHORIZED,
17
+ detail="Authentication credentials missing"
18
+ )
19
+
20
+ token = credentials.credentials
21
+ # Verify JWT via Supabase Auth
22
+ user_data = verify_supabase_jwt(token)
23
+ user_id = user_data["id"]
24
+
25
+ # Query the public.users table to get the latest role and status
26
+ try:
27
+ user_response = supabase.table("users").select("*").eq("id", user_id).execute()
28
+ if not user_response.data:
29
+ # Fallback to metadata if user doesn't exist yet (e.g., during signup)
30
+ return user_data
31
+
32
+ db_user = user_response.data[0]
33
+
34
+ # Return merged user data
35
+ return {
36
+ "id": user_id,
37
+ "email": user_data["email"],
38
+ "role": db_user.get("role", "owner"),
39
+ "phone": db_user.get("phone_number", ""),
40
+ "full_name": db_user.get("full_name", ""),
41
+ "avatar_url": db_user.get("avatar_url", "")
42
+ }
43
+ except HTTPException:
44
+ raise
45
+ except Exception as e:
46
+ # Fallback to verified JWT data if DB query fails
47
+ return user_data
48
+
49
+ class RoleChecker:
50
+ def __init__(self, allowed_roles: List[str]):
51
+ self.allowed_roles = allowed_roles
52
+
53
+ def __call__(self, current_user: Dict = Depends(get_current_user)) -> Dict:
54
+ if current_user["role"] not in self.allowed_roles:
55
+ raise HTTPException(
56
+ status_code=status.HTTP_403_FORBIDDEN,
57
+ detail=f"Access denied. Required role(s): {', '.join(self.allowed_roles)}"
58
+ )
59
+ return current_user
60
+
61
+ # Helper dependency builders
62
+ def require_role(role: str):
63
+ return Depends(RoleChecker([role]))
64
+
65
+ def require_any_role(roles: List[str]):
66
+ return Depends(RoleChecker(roles))
backend/core/security.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import Request, HTTPException, status
2
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
3
+ from typing import Optional, Dict
4
+ import logging
5
+ from chatbot.supabase_config import supabase
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ security_scheme = HTTPBearer(auto_error=False)
10
+
11
+ def verify_supabase_jwt(token: str) -> Dict:
12
+ """
13
+ Verify Supabase JWT token using the Supabase auth client.
14
+ Returns the user data dict if valid.
15
+ Raises HTTPException 401 if invalid/expired.
16
+ """
17
+ try:
18
+ # get_user verifies the token against the Supabase Auth server
19
+ response = supabase.auth.get_user(token)
20
+ if not response or not response.user:
21
+ raise HTTPException(
22
+ status_code=status.HTTP_401_UNAUTHORIZED,
23
+ detail="Invalid or expired authentication token"
24
+ )
25
+
26
+ user = response.user
27
+ # Convert user object to dictionary
28
+ user_data = {
29
+ "id": user.id,
30
+ "email": user.email,
31
+ "role": user.user_metadata.get("role", "owner") if user.user_metadata else "owner",
32
+ "phone": user.user_metadata.get("phone", "") if user.user_metadata else "",
33
+ "is_active": user.user_metadata.get("is_active", True) if user.user_metadata else True
34
+ }
35
+ return user_data
36
+ except Exception as e:
37
+ logger.error(f"JWT verification failed: {str(e)}")
38
+ raise HTTPException(
39
+ status_code=status.HTTP_401_UNAUTHORIZED,
40
+ detail=f"Authentication failed: {str(e)}"
41
+ )
backend/main.py ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, UploadFile, File, Form, HTTPException
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ import logging
4
+ import tempfile
5
+ import os
6
+
7
+ # Import models & services with updated backend package prefix
8
+ from backend.models import dog_skin, dog_eye, cat_skin
9
+ from backend.utils.image import preprocess
10
+ from backend.services.router import route_prediction
11
+
12
+ # Import refactored routers
13
+ from backend.routers.auth import router as auth_router
14
+ from backend.routers.pets import router as pets_router
15
+ from backend.routers.appointments import router as appointments_router
16
+ from backend.routers.clinics import router as clinics_router
17
+ from backend.routers.admin import router as admin_router
18
+
19
+ from chatbot.langsmith_config import setup_langsmith
20
+ from chatbot.tools import _analyze_pet_image_impl
21
+ from chatbot.rag.agentic_rag import query_agentic_rag
22
+
23
+ # Configure logging
24
+ logging.basicConfig(level=logging.INFO)
25
+ logger = logging.getLogger(__name__)
26
+
27
+ app = FastAPI(title="Pet PULSE Disease Detection API")
28
+
29
+ # Enable CORS for frontend
30
+ app.add_middleware(
31
+ CORSMiddleware,
32
+ allow_origins=[
33
+ "http://localhost:3000",
34
+ "http://localhost:3001",
35
+ "http://127.0.0.1:3000",
36
+ "http://127.0.0.1:3001",
37
+ "*",
38
+ ],
39
+ allow_credentials=True,
40
+ allow_methods=["*"],
41
+ allow_headers=["*"],
42
+ )
43
+
44
+ # Include refactored routers under the /api prefix
45
+ app.include_router(auth_router, prefix="/api")
46
+ app.include_router(pets_router, prefix="/api")
47
+ app.include_router(appointments_router, prefix="/api")
48
+ app.include_router(clinics_router, prefix="/api")
49
+ app.include_router(admin_router, prefix="/api")
50
+
51
+ # 🏠 Root endpoint
52
+ @app.get("/")
53
+ async def root():
54
+ """Root endpoint with API information"""
55
+ return {
56
+ "name": "Pet PULSE Disease Detection API",
57
+ "version": "1.0.0",
58
+ "docs": "http://localhost:8000/docs",
59
+ "status": "running",
60
+ "endpoints": {
61
+ "prediction": "/predict",
62
+ "image_analysis": "/analyze-image",
63
+ "pets": "/api/pets",
64
+ "clinics": "/api/clinics"
65
+ }
66
+ }
67
+
68
+ # 🏥 Health check endpoint
69
+ @app.get("/health")
70
+ async def health():
71
+ """Health check endpoint"""
72
+ return {
73
+ "status": "healthy",
74
+ "service": "Pet PULSE API",
75
+ "models_loaded": {
76
+ "dog_skin": hasattr(app.state, 'dog_skin') and app.state.dog_skin is not None,
77
+ "dog_eye": hasattr(app.state, 'dog_eye') and app.state.dog_eye is not None,
78
+ "cat_skin": hasattr(app.state, 'cat_skin') and app.state.cat_skin is not None
79
+ }
80
+ }
81
+
82
+ # 🔥 Load models ONCE on startup
83
+ @app.on_event("startup")
84
+ def load_models():
85
+ try:
86
+ # Initialize LangSmith tracing (optional)
87
+ setup_langsmith()
88
+ logger.info("LangSmith tracing initialized")
89
+ except Exception as e:
90
+ logger.warning(f"LangSmith initialization failed (optional): {e}")
91
+
92
+ try:
93
+ app.state.dog_skin = dog_skin.load_model()
94
+ logger.info("Dog skin model loaded")
95
+ except Exception as e:
96
+ logger.warning(f"Dog skin model loading failed: {e}")
97
+ app.state.dog_skin = None
98
+
99
+ try:
100
+ app.state.dog_eye = dog_eye.load_model()
101
+ logger.info("Dog eye model loaded")
102
+ except Exception as e:
103
+ logger.warning(f"Dog eye model loading failed: {e}")
104
+ app.state.dog_eye = None
105
+
106
+ try:
107
+ app.state.cat_skin = cat_skin.load_model()
108
+ logger.info("Cat skin model loaded")
109
+ except Exception as e:
110
+ logger.warning(f"Cat skin model loading failed: {e}")
111
+ app.state.cat_skin = None
112
+
113
+ logger.info("✅ Backend startup complete")
114
+
115
+ # 📸 Prediction endpoint
116
+ @app.post("/predict")
117
+ async def predict(
118
+ file: UploadFile = File(...),
119
+ animal: str = Form(...),
120
+ disease_type: str = Form(...)
121
+ ):
122
+ image = preprocess(file.file)
123
+ result = route_prediction(app, animal, disease_type, image)
124
+ return result
125
+
126
+ # 🔍 Analyze image endpoint (for chatbot integration)
127
+ @app.post("/analyze-image")
128
+ async def analyze_image(
129
+ file: UploadFile = File(...),
130
+ disease_type: str = Form(...),
131
+ animal: str = Form("dog"),
132
+ user_id: str = Form("demo")
133
+ ):
134
+ """
135
+ Analyze pet image for disease detection.
136
+ Used by chatbot for skin/eye disease diagnosis.
137
+ """
138
+ image = preprocess(file.file)
139
+ result = route_prediction(app, animal, disease_type, image)
140
+ return result
141
+
142
+ # 📸 Chatbot image upload endpoint
143
+ @app.post("/api/chat/upload-image")
144
+ async def upload_image(
145
+ session_id: str = Form(...),
146
+ disease_type: str = Form(...),
147
+ file: UploadFile = File(...)
148
+ ):
149
+ """
150
+ Upload and analyze a pet image for chatbot.
151
+ """
152
+ try:
153
+ animal = "dog" # Default - in production, retrieve from session
154
+
155
+ if disease_type not in ["skin", "eye"]:
156
+ raise HTTPException(status_code=400, detail="disease_type must be 'skin' or 'eye'")
157
+
158
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".jpg") as tmp:
159
+ content = await file.read()
160
+ tmp.write(content)
161
+ tmp_path = tmp.name
162
+
163
+ try:
164
+ tool_result = _analyze_pet_image_impl(
165
+ image_path=tmp_path,
166
+ animal=animal,
167
+ disease_type=disease_type
168
+ )
169
+
170
+ if isinstance(tool_result, dict) and "error" in tool_result:
171
+ raise HTTPException(status_code=400, detail=tool_result['error'])
172
+
173
+ disease_class = tool_result.get('class', 'Unknown')
174
+ confidence = tool_result.get('confidence', 0.0)
175
+
176
+ explanation_query = f"""The computer vision model detected {disease_class} (confidence: {confidence:.1%}) from a {animal}'s {disease_type} image.
177
+
178
+ Provide a detailed veterinary explanation covering:
179
+ 1. What is {disease_class}?
180
+ 2. Common causes and risk factors for this condition
181
+ 3. Treatment options and recommendations
182
+ 4. When to seek professional veterinary care
183
+ 5. Prevention and management tips
184
+
185
+ Be thorough and informative. Use formatting with headers and bullet points for clarity."""
186
+
187
+ explanation_text = query_agentic_rag(
188
+ question=explanation_query,
189
+ chat_history="",
190
+ force_rag=True
191
+ )
192
+
193
+ logger.info(f"Image analyzed for {disease_type}: {disease_class}")
194
+
195
+ return {
196
+ "session_id": session_id,
197
+ "disease_class": disease_class,
198
+ "confidence": confidence,
199
+ "explanation": explanation_text
200
+ }
201
+
202
+ finally:
203
+ if os.path.exists(tmp_path):
204
+ os.remove(tmp_path)
205
+
206
+ except HTTPException:
207
+ raise
208
+ except Exception as e:
209
+ logger.error(f"Error uploading image: {e}")
210
+ raise HTTPException(status_code=500, detail=str(e))
backend/models/__pycache__/cat_skin.cpython-313.pyc ADDED
Binary file (1.12 kB). View file
 
backend/models/__pycache__/dog_eye.cpython-313.pyc ADDED
Binary file (1.15 kB). View file
 
backend/models/__pycache__/dog_skin.cpython-313.pyc ADDED
Binary file (1.16 kB). View file
 
backend/models/cat_skin.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ from torchvision import models
4
+ from backend.core.config import CAT_SKIN_MODEL, DEVICE
5
+
6
+ class_names = ["Flea_Allergy", "Health", "Ringworm", "Scabies"]
7
+
8
+ def load_model():
9
+ model = models.resnet50(pretrained=False)
10
+ model.fc = nn.Linear(model.fc.in_features, len(class_names))
11
+
12
+ checkpoint = torch.load(CAT_SKIN_MODEL, map_location=DEVICE)
13
+ model.load_state_dict(checkpoint['model_state_dict'])
14
+
15
+ model.to(DEVICE)
16
+ model.eval()
17
+
18
+ return model, class_names
backend/models/dog_eye.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ from torchvision import models
4
+ from backend.core.config import DOG_EYE_MODEL, DEVICE
5
+
6
+ class_names = [
7
+ "Pigmented keratitis",
8
+ "blepharitis",
9
+ "entropion",
10
+ "eyelid_tumor",
11
+ "mastopathy"
12
+ ]
13
+
14
+ def load_model():
15
+ model = models.resnet50(pretrained=False)
16
+ model.fc = nn.Linear(model.fc.in_features, len(class_names))
17
+
18
+ checkpoint = torch.load(DOG_EYE_MODEL, map_location=DEVICE)
19
+ model.load_state_dict(checkpoint['model_state_dict'])
20
+
21
+ model.to(DEVICE)
22
+ model.eval()
23
+
24
+ return model, class_names
backend/models/dog_skin.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ from torchvision import models
4
+ from backend.core.config import DOG_SKIN_MODEL, DEVICE
5
+
6
+ class_names = [
7
+ "Dermatitis",
8
+ "Fungal_infections",
9
+ "Healthy",
10
+ "Hypersensitivity",
11
+ "demodicosis",
12
+ "ringworm"
13
+ ]
14
+
15
+ def load_model():
16
+ model = models.resnet50(pretrained=False)
17
+ model.fc = nn.Linear(model.fc.in_features, len(class_names))
18
+
19
+ checkpoint = torch.load(DOG_SKIN_MODEL, map_location=DEVICE)
20
+ model.load_state_dict(checkpoint['model_state_dict'])
21
+
22
+ model.to(DEVICE)
23
+ model.eval()
24
+
25
+ return model, class_names
backend/routers/__pycache__/admin.cpython-313.pyc ADDED
Binary file (7.26 kB). View file
 
backend/routers/__pycache__/appointments.cpython-313.pyc ADDED
Binary file (8.76 kB). View file
 
backend/routers/__pycache__/auth.cpython-313.pyc ADDED
Binary file (13.7 kB). View file
 
backend/routers/__pycache__/clinics.cpython-313.pyc ADDED
Binary file (9.43 kB). View file
 
backend/routers/__pycache__/pets.cpython-313.pyc ADDED
Binary file (23.2 kB). View file
 
backend/routers/admin.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException, Request
2
+ from typing import Optional
3
+ import logging
4
+ from backend.core.dependencies import require_role
5
+ from backend.services.clinic_service import ClinicService
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ # Protect the entire router with require_role("admin")
10
+ router = APIRouter(prefix="/admin", tags=["Admin Operations"], dependencies=[require_role("admin")])
11
+
12
+ @router.get("/clinics/pending")
13
+ async def get_pending_clinics():
14
+ """List all pending clinics (unverified and not rejected)"""
15
+ logger.info("Admin: Fetching pending clinics")
16
+ result = ClinicService.get_pending_clinics()
17
+ if not result["success"]:
18
+ raise HTTPException(status_code=400, detail=result.get("error"))
19
+ return result
20
+
21
+ @router.get("/clinics")
22
+ async def get_all_clinics():
23
+ """List all clinics with their derived verification status"""
24
+ logger.info("Admin: Fetching all clinics")
25
+ result = ClinicService.get_public_clinics() # We can fetch all or public
26
+ # Let's query all clinics from Supabase directly for admin
27
+ from chatbot.supabase_config import supabase
28
+ try:
29
+ resp = supabase.table("clinics").select("*").order("created_at", desc=True).execute()
30
+ clinics = [ClinicService._parse_clinic_status(c) for c in (resp.data or [])]
31
+ return {"success": True, "clinics": clinics, "count": len(clinics)}
32
+ except Exception as e:
33
+ raise HTTPException(status_code=400, detail=str(e))
34
+
35
+ @router.get("/stats")
36
+ async def get_admin_stats():
37
+ """Return admin dashboard counts and recent clinic activity"""
38
+ logger.info("Admin: Fetching dashboard stats")
39
+
40
+ from chatbot.supabase_config import supabase
41
+ try:
42
+ all_resp = supabase.table("clinics").select("id", "is_verified", "clinic_name", "created_at", "description").execute()
43
+ clinics = [ClinicService._parse_clinic_status(c) for c in (all_resp.data or [])]
44
+
45
+ total_clinics = len(clinics)
46
+ approved_clinics = sum(1 for c in clinics if c.get("verification_status") == "approved")
47
+ pending_clinics = sum(1 for c in clinics if c.get("verification_status") == "pending")
48
+ rejected_clinics = sum(1 for c in clinics if c.get("verification_status") == "rejected")
49
+
50
+ recent = sorted(clinics, key=lambda c: c.get("created_at") or "", reverse=True)[:3]
51
+ recent_verifications = []
52
+ for clinic in recent:
53
+ recent_verifications.append({
54
+ "clinic": clinic.get("clinic_name", "Unknown Clinic"),
55
+ "action": clinic.get("verification_status", "pending").title(),
56
+ "time": clinic.get("created_at"),
57
+ "badge": (
58
+ "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400"
59
+ if clinic.get("verification_status") == "approved"
60
+ else "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400"
61
+ if clinic.get("verification_status") == "rejected"
62
+ else "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400"
63
+ ),
64
+ })
65
+
66
+ return {
67
+ "success": True,
68
+ "stats": {
69
+ "total_clinics": total_clinics,
70
+ "pending_verifications": pending_clinics,
71
+ "approved_clinics": approved_clinics,
72
+ "rejected": rejected_clinics,
73
+ },
74
+ "recent_verifications": recent_verifications,
75
+ }
76
+ except Exception as e:
77
+ logger.error(f"Error fetching admin stats: {str(e)}")
78
+ raise HTTPException(status_code=400, detail=str(e))
79
+
80
+ @router.post("/clinics/{clinic_id}/approve")
81
+ async def approve_clinic(clinic_id: str):
82
+ """Approve a clinic (verify it and activate its login profile)"""
83
+ logger.info(f"Admin: Approving clinic {clinic_id}")
84
+ result = ClinicService.approve_clinic(clinic_id=clinic_id)
85
+ if not result["success"]:
86
+ raise HTTPException(status_code=400, detail=result.get("error"))
87
+ return result
88
+
89
+ @router.post("/clinics/{clinic_id}/reject")
90
+ async def reject_clinic(clinic_id: str, request: Request):
91
+ """Reject a clinic registration with an optional reason"""
92
+ logger.info(f"Admin: Rejecting clinic {clinic_id}")
93
+
94
+ # Read optional reason from request body
95
+ try:
96
+ body = await request.json()
97
+ except Exception:
98
+ body = {}
99
+ reason = (body or {}).get("reason")
100
+
101
+ result = ClinicService.reject_clinic(clinic_id=clinic_id, reason=reason)
102
+ if not result["success"]:
103
+ raise HTTPException(status_code=400, detail=result.get("error"))
104
+ return result
backend/routers/appointments.py ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException, status, Form
2
+ from typing import Optional
3
+ import logging
4
+ from backend.core.dependencies import get_current_user
5
+ from backend.schemas.schemas import CreateAppointmentRequest, CreateReviewRequest
6
+ from backend.services.appointment_service import AppointmentService
7
+ from chatbot.supabase_config import supabase
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ router = APIRouter(tags=["Appointments & Reviews"])
12
+
13
+ @router.post("/appointments")
14
+ async def create_appointment(
15
+ request: CreateAppointmentRequest,
16
+ current_user: dict = Depends(get_current_user)
17
+ ):
18
+ """Create a new appointment"""
19
+ logger.info(f"Creating appointment for pet {request.pet_id} by user: {current_user['id']}")
20
+
21
+ # Check authorization: If owner, must be the owner of the pet
22
+ if current_user["role"] == "owner":
23
+ pet_resp = supabase.table("pets").select("user_id").eq("id", request.pet_id).execute()
24
+ if not pet_resp.data or pet_resp.data[0]["user_id"] != current_user["id"]:
25
+ raise HTTPException(status_code=403, detail="You can only schedule appointments for your own pets")
26
+
27
+ result = AppointmentService.create_appointment(
28
+ pet_id=request.pet_id,
29
+ clinic_id=request.clinic_id,
30
+ owner_id=current_user["id"] if current_user["role"] == "owner" else request.owner_id,
31
+ appointment_date=request.appointment_date,
32
+ appointment_time=request.appointment_time,
33
+ reason=request.reason,
34
+ notes=request.notes,
35
+ )
36
+
37
+ if not result["success"]:
38
+ raise HTTPException(status_code=400, detail=result.get("error"))
39
+
40
+ return result
41
+
42
+ @router.get("/appointments/owner")
43
+ async def get_owner_appointments(
44
+ owner_id: Optional[str] = None,
45
+ current_user: dict = Depends(get_current_user)
46
+ ):
47
+ """Get appointments for a pet owner"""
48
+ target_owner_id = owner_id or current_user["id"]
49
+
50
+ # Check authorization: Owners can only view their own appointments
51
+ if current_user["role"] == "owner" and target_owner_id != current_user["id"]:
52
+ raise HTTPException(status_code=403, detail="Access denied")
53
+
54
+ result = AppointmentService.get_owner_appointments(owner_id=target_owner_id)
55
+ if not result["success"]:
56
+ raise HTTPException(status_code=400, detail=result.get("error"))
57
+
58
+ return result
59
+
60
+ @router.get("/appointments/clinic")
61
+ async def get_clinic_appointments(
62
+ clinic_id: str,
63
+ current_user: dict = Depends(get_current_user)
64
+ ):
65
+ """Get appointments for a clinic"""
66
+ # Check authorization: Clinics can only view their own appointments
67
+ if current_user["role"] == "clinic":
68
+ # Get the clinic's ID from user_id
69
+ clinic_resp = supabase.table("clinics").select("id").eq("user_id", current_user["id"]).execute()
70
+ if not clinic_resp.data or clinic_resp.data[0]["id"] != clinic_id:
71
+ raise HTTPException(status_code=403, detail="Access denied")
72
+
73
+ result = AppointmentService.get_clinic_appointments(clinic_id=clinic_id)
74
+ if not result["success"]:
75
+ raise HTTPException(status_code=400, detail=result.get("error"))
76
+
77
+ return result
78
+
79
+ @router.get("/appointments/pet")
80
+ async def get_pet_appointments(
81
+ pet_id: str,
82
+ current_user: dict = Depends(get_current_user)
83
+ ):
84
+ """Get all appointments for a pet"""
85
+ # Check authorization: Owners can only view appointments for their own pets
86
+ if current_user["role"] == "owner":
87
+ pet_resp = supabase.table("pets").select("user_id").eq("id", pet_id).execute()
88
+ if not pet_resp.data or pet_resp.data[0]["user_id"] != current_user["id"]:
89
+ raise HTTPException(status_code=403, detail="Access denied")
90
+
91
+ result = AppointmentService.get_pet_appointments(pet_id=pet_id)
92
+ if not result["success"]:
93
+ raise HTTPException(status_code=400, detail=result.get("error"))
94
+
95
+ return result
96
+
97
+ @router.post("/appointments/{appointment_id}/status")
98
+ async def update_appointment_status(
99
+ appointment_id: str,
100
+ status: str = Form(...),
101
+ current_user: dict = Depends(get_current_user)
102
+ ):
103
+ """Update status of an appointment"""
104
+ # Validate status
105
+ valid_statuses = {"scheduled", "completed", "cancelled", "in_progress"}
106
+ if status not in valid_statuses:
107
+ raise HTTPException(status_code=400, detail="Invalid status value")
108
+
109
+ # Check authorization
110
+ appt_resp = supabase.table("appointments").select("owner_id", "clinic_id").eq("id", appointment_id).execute()
111
+ if not appt_resp.data:
112
+ raise HTTPException(status_code=404, detail="Appointment not found")
113
+
114
+ appt = appt_resp.data[0]
115
+
116
+ if current_user["role"] == "owner":
117
+ if appt["owner_id"] != current_user["id"]:
118
+ raise HTTPException(status_code=403, detail="Access denied")
119
+ if status != "cancelled":
120
+ raise HTTPException(status_code=403, detail="Owners can only cancel appointments")
121
+ elif current_user["role"] == "clinic":
122
+ clinic_resp = supabase.table("clinics").select("id").eq("user_id", current_user["id"]).execute()
123
+ if not clinic_resp.data or clinic_resp.data[0]["id"] != appt["clinic_id"]:
124
+ raise HTTPException(status_code=403, detail="Access denied")
125
+
126
+ result = AppointmentService.update_appointment_status(appointment_id=appointment_id, status=status)
127
+ if not result["success"]:
128
+ raise HTTPException(status_code=400, detail=result.get("error"))
129
+
130
+ return result
131
+
132
+ @router.post("/reviews")
133
+ async def create_review(
134
+ request: CreateReviewRequest,
135
+ current_user: dict = Depends(get_current_user)
136
+ ):
137
+ """Create a clinic review for a completed appointment"""
138
+ if current_user["role"] != "owner":
139
+ raise HTTPException(status_code=403, detail="Only pet owners can review clinics")
140
+
141
+ # Check authorization: Must be the owner of the appointment
142
+ appt_resp = supabase.table("appointments").select("owner_id").eq("id", request.appointment_id).execute()
143
+ if not appt_resp.data:
144
+ raise HTTPException(status_code=404, detail="Appointment not found")
145
+
146
+ if appt_resp.data[0]["owner_id"] != current_user["id"]:
147
+ raise HTTPException(status_code=403, detail="Access denied")
148
+
149
+ result = AppointmentService.create_review(
150
+ appointment_id=request.appointment_id,
151
+ rating=request.rating,
152
+ treatment=request.treatment,
153
+ comment=request.comment
154
+ )
155
+
156
+ if not result["success"]:
157
+ raise HTTPException(status_code=400, detail=result.get("error"))
158
+
159
+ return result
160
+
161
+ @router.get("/reviews/clinic")
162
+ async def get_reviews_for_clinic(
163
+ clinic_id: str,
164
+ current_user: dict = Depends(get_current_user)
165
+ ):
166
+ """Get all reviews for a clinic"""
167
+ result = AppointmentService.get_reviews_for_clinic(clinic_id=clinic_id)
168
+ if not result["success"]:
169
+ raise HTTPException(status_code=400, detail=result.get("error"))
170
+
171
+ return result
backend/routers/auth.py ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File, Form
2
+ from typing import Optional
3
+ import logging
4
+ import time
5
+ import os
6
+ from datetime import datetime
7
+ from backend.core.dependencies import get_current_user
8
+ from backend.schemas.schemas import RegisterOwnerRequest, RegisterClinicRequest, NotificationReadRequest
9
+ from backend.services.auth_service import AuthService
10
+ from chatbot.supabase_config import supabase, SupabaseStorage
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ router = APIRouter(prefix="/auth", tags=["Authentication & Notifications"])
15
+
16
+ @router.post("/register/owner")
17
+ async def register_owner(
18
+ request: RegisterOwnerRequest,
19
+ current_user: dict = Depends(get_current_user)
20
+ ):
21
+ """Complete registration of a new pet owner profile"""
22
+ logger.info(f"Registering pet owner profile for user: {current_user['id']}")
23
+
24
+ result = AuthService.register_pet_owner(
25
+ user_id=current_user["id"],
26
+ email=current_user["email"],
27
+ first_name=request.first_name,
28
+ last_name=request.last_name,
29
+ phone=request.phone,
30
+ address=request.address,
31
+ state=request.state,
32
+ zip_code=request.zip_code,
33
+ country=request.country,
34
+ bio=request.bio,
35
+ )
36
+
37
+ if not result["success"]:
38
+ raise HTTPException(status_code=400, detail=result.get("error"))
39
+
40
+ return result
41
+
42
+ @router.post("/register/clinic")
43
+ async def register_clinic(
44
+ clinic_name: str = Form(...),
45
+ phone: str = Form(...),
46
+ address: str = Form(...),
47
+ city: Optional[str] = Form(None),
48
+ state: Optional[str] = Form(None),
49
+ zip_code: Optional[str] = Form(None),
50
+ country: Optional[str] = Form(None),
51
+ website: Optional[str] = Form(None),
52
+ opening_hours: Optional[str] = Form(None),
53
+ description: Optional[str] = Form(None),
54
+ clinic_photo: Optional[UploadFile] = File(None),
55
+ clinic_license: Optional[UploadFile] = File(None),
56
+ latitude: Optional[float] = Form(None),
57
+ longitude: Optional[float] = Form(None),
58
+ current_user: dict = Depends(get_current_user)
59
+ ):
60
+ """Complete registration of a new clinic profile"""
61
+ logger.info(f"Registering clinic profile for user: {current_user['id']}")
62
+
63
+ clinic_logo_url = None
64
+ license_document_url = None
65
+
66
+ # Handle image upload
67
+ if clinic_photo and clinic_photo.filename:
68
+ if not clinic_photo.content_type or not clinic_photo.content_type.startswith("image/"):
69
+ raise HTTPException(status_code=400, detail="Scale photo must be an image")
70
+
71
+ photo_bytes = await clinic_photo.read()
72
+ photo_ext = os.path.splitext(clinic_photo.filename)[1] or ".jpg"
73
+ photo_name = f"{clinic_name.replace(' ', '_')}-{int(time.time())}{photo_ext}"
74
+ photo_path = SupabaseStorage.upload_clinic_image(
75
+ user_id=current_user["id"],
76
+ file_data=photo_bytes,
77
+ filename=photo_name,
78
+ content_type=clinic_photo.content_type or "image/jpeg"
79
+ )
80
+ clinic_logo_url = supabase.storage.from_("clinic-images").get_public_url(photo_path)
81
+
82
+ # Handle license upload
83
+ if clinic_license and clinic_license.filename:
84
+ valid_types = ["image/jpeg", "image/png", "image/jpg", "application/pdf"]
85
+ if clinic_license.content_type and clinic_license.content_type not in valid_types:
86
+ raise HTTPException(status_code=400, detail="License must be a PDF or image (JPG/PNG)")
87
+
88
+ license_bytes = await clinic_license.read()
89
+ license_ext = os.path.splitext(clinic_license.filename)[1] or ".pdf"
90
+ license_name = f"license-{clinic_name.replace(' ', '_')}-{int(time.time())}{license_ext}"
91
+ license_path = SupabaseStorage.upload_clinic_document(
92
+ user_id=current_user["id"],
93
+ file_data=license_bytes,
94
+ filename=license_name,
95
+ content_type=clinic_license.content_type or "application/pdf"
96
+ )
97
+ license_document_url = supabase.storage.from_("clinic-documents").get_public_url(license_path)
98
+
99
+ result = AuthService.register_clinic(
100
+ user_id=current_user["id"],
101
+ email=current_user["email"],
102
+ clinic_name=clinic_name,
103
+ phone=phone,
104
+ address=address,
105
+ city=city,
106
+ state=state,
107
+ zip_code=zip_code,
108
+ country=country,
109
+ website=website,
110
+ opening_hours=opening_hours,
111
+ description=description,
112
+ clinic_logo_url=clinic_logo_url,
113
+ license_document_url=license_document_url,
114
+ latitude=latitude,
115
+ longitude=longitude,
116
+ )
117
+
118
+ if not result["success"]:
119
+ raise HTTPException(status_code=400, detail=result.get("error"))
120
+
121
+ return result
122
+
123
+ @router.get("/profile")
124
+ async def get_profile(current_user: dict = Depends(get_current_user)):
125
+ """Get the current authenticated user's profile"""
126
+ result = AuthService.get_user_profile(current_user["id"])
127
+ if not result["success"]:
128
+ raise HTTPException(status_code=404, detail=result.get("error"))
129
+ return result
130
+
131
+ @router.put("/profile")
132
+ async def update_profile(
133
+ full_name: Optional[str] = Form(None),
134
+ phone: Optional[str] = Form(None),
135
+ address: Optional[str] = Form(None),
136
+ state: Optional[str] = Form(None),
137
+ zip_code: Optional[str] = Form(None),
138
+ country: Optional[str] = Form(None),
139
+ bio: Optional[str] = Form(None),
140
+ emergency_contact_name: Optional[str] = Form(None),
141
+ emergency_contact_phone: Optional[str] = Form(None),
142
+ photo: Optional[UploadFile] = File(None),
143
+ latitude: Optional[float] = Form(None),
144
+ longitude: Optional[float] = Form(None),
145
+ current_user: dict = Depends(get_current_user)
146
+ ):
147
+ """Update pet owner profile details"""
148
+ profile_image_url = None
149
+
150
+ if photo and photo.filename:
151
+ if not photo.content_type or not photo.content_type.startswith("image/"):
152
+ raise HTTPException(status_code=400, detail="Profile photo must be an image")
153
+
154
+ photo_bytes = await photo.read()
155
+ photo_ext = os.path.splitext(photo.filename)[1] or ".jpg"
156
+ photo_name = f"avatar-{int(time.time())}{photo_ext}"
157
+ photo_path = SupabaseStorage.upload_user_avatar(
158
+ user_id=current_user["id"],
159
+ file_data=photo_bytes,
160
+ filename=photo_name,
161
+ content_type=photo.content_type or "image/jpeg"
162
+ )
163
+ profile_image_url = supabase.storage.from_("user-avatars").get_public_url(photo_path)
164
+
165
+ updates = {}
166
+ if full_name is not None:
167
+ updates["full_name"] = full_name
168
+ if phone is not None:
169
+ updates["phone"] = phone
170
+ if address is not None:
171
+ updates["address"] = address
172
+ if state is not None:
173
+ updates["state"] = state
174
+ if zip_code is not None:
175
+ updates["zip_code"] = zip_code
176
+ if country is not None:
177
+ updates["country"] = country
178
+ if bio is not None:
179
+ updates["bio"] = bio
180
+ if emergency_contact_name is not None:
181
+ updates["emergency_contact_name"] = emergency_contact_name
182
+ if emergency_contact_phone is not None:
183
+ updates["emergency_contact_phone"] = emergency_contact_phone
184
+ if profile_image_url is not None:
185
+ updates["profile_image_url"] = profile_image_url
186
+ if latitude is not None:
187
+ updates["latitude"] = latitude
188
+ if longitude is not None:
189
+ updates["longitude"] = longitude
190
+
191
+ result = AuthService.update_user_profile(user_id=current_user["id"], updates=updates)
192
+ if not result["success"]:
193
+ raise HTTPException(status_code=400, detail=result.get("error"))
194
+
195
+ return result
196
+
197
+ # ============================================
198
+ # Notification Endpoints
199
+ # ============================================
200
+
201
+ @router.get("/notifications")
202
+ async def get_notifications(limit: int = 20, current_user: dict = Depends(get_current_user)):
203
+ """Get notifications for the current authenticated user"""
204
+ logger.info(f"Fetching notifications for user: {current_user['id']}")
205
+ try:
206
+ response = (
207
+ supabase.table("notifications")
208
+ .select("*")
209
+ .eq("user_id", current_user["id"])
210
+ .order("created_at", desc=True)
211
+ .limit(limit)
212
+ .execute()
213
+ )
214
+ notifications = response.data or []
215
+ unread_count = sum(1 for item in notifications if not item.get("is_read"))
216
+ return {"success": True, "notifications": notifications, "count": len(notifications), "unread_count": unread_count}
217
+ except Exception as e:
218
+ logger.error(f"Error fetching notifications: {str(e)}")
219
+ raise HTTPException(status_code=400, detail=str(e))
220
+
221
+ @router.post("/notifications/{notification_id}/read")
222
+ async def mark_notification_read(notification_id: str, current_user: dict = Depends(get_current_user)):
223
+ """Mark a single notification as read"""
224
+ try:
225
+ # Check authorization
226
+ notif_resp = supabase.table("notifications").select("user_id").eq("id", notification_id).execute()
227
+ if not notif_resp.data or notif_resp.data[0]["user_id"] != current_user["id"]:
228
+ raise HTTPException(status_code=403, detail="Access denied")
229
+
230
+ supabase.table("notifications").update({
231
+ "is_read": True,
232
+ "read_at": datetime.utcnow().isoformat()
233
+ }).eq("id", notification_id).execute()
234
+
235
+ return {"success": True, "notification_id": notification_id}
236
+ except HTTPException:
237
+ raise
238
+ except Exception as e:
239
+ logger.error(f"Error marking notification read: {str(e)}")
240
+ raise HTTPException(status_code=400, detail=str(e))
241
+
242
+ @router.post("/notifications/read-all")
243
+ async def mark_all_notifications_read(current_user: dict = Depends(get_current_user)):
244
+ """Mark all notifications for the current user as read"""
245
+ try:
246
+ supabase.table("notifications").update({
247
+ "is_read": True,
248
+ "read_at": datetime.utcnow().isoformat()
249
+ }).eq("user_id", current_user["id"]).eq("is_read", False).execute()
250
+
251
+ return {"success": True, "user_id": current_user["id"]}
252
+ except Exception as e:
253
+ logger.error(f"Error marking all notifications read: {str(e)}")
254
+ raise HTTPException(status_code=400, detail=str(e))
backend/routers/clinics.py ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException, status, Form, UploadFile, File
2
+ from typing import Optional, List
3
+ import logging
4
+ import time
5
+ import os
6
+ from backend.core.dependencies import get_current_user, require_role
7
+ from backend.services.clinic_service import ClinicService
8
+ from chatbot.supabase_config import supabase, SupabaseStorage
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ router = APIRouter(tags=["Clinics"])
13
+
14
+ @router.get("/clinic/profile")
15
+ async def get_clinic_profile(current_user: dict = Depends(get_current_user)):
16
+ """Get the current logged-in clinic's profile details"""
17
+ if current_user["role"] != "clinic":
18
+ raise HTTPException(status_code=403, detail="Access denied. Clinic role required.")
19
+
20
+ result = ClinicService.get_clinic_profile(user_id=current_user["id"])
21
+ if not result["success"]:
22
+ raise HTTPException(status_code=404, detail=result.get("error"))
23
+ return result
24
+
25
+ @router.put("/clinic/profile")
26
+ async def update_clinic_profile(
27
+ clinic_name: Optional[str] = Form(None),
28
+ phone: Optional[str] = Form(None),
29
+ address: Optional[str] = Form(None),
30
+ city: Optional[str] = Form(None),
31
+ state: Optional[str] = Form(None),
32
+ zip_code: Optional[str] = Form(None),
33
+ country: Optional[str] = Form(None),
34
+ website: Optional[str] = Form(None),
35
+ opening_hours: Optional[str] = Form(None),
36
+ description: Optional[str] = Form(None),
37
+ photo: Optional[UploadFile] = File(None),
38
+ photos: Optional[List[UploadFile]] = File(None),
39
+ latitude: Optional[float] = Form(None),
40
+ longitude: Optional[float] = Form(None),
41
+ current_user: dict = Depends(get_current_user)
42
+ ):
43
+ """Update the current logged-in clinic's profile details"""
44
+ if current_user["role"] != "clinic":
45
+ raise HTTPException(status_code=403, detail="Access denied. Clinic role required.")
46
+
47
+ updates = {}
48
+ if clinic_name is not None:
49
+ updates["clinic_name"] = clinic_name
50
+ if phone is not None:
51
+ updates["phone"] = phone
52
+ if address is not None:
53
+ updates["address"] = address
54
+ if city is not None:
55
+ updates["city"] = city
56
+ if state is not None:
57
+ updates["state"] = state
58
+ if zip_code is not None:
59
+ updates["zip_code"] = zip_code
60
+ if country is not None:
61
+ updates["country"] = country
62
+ if website is not None:
63
+ updates["website"] = website
64
+ if opening_hours is not None:
65
+ updates["opening_hours"] = opening_hours
66
+ if description is not None:
67
+ updates["description"] = description
68
+ if latitude is not None:
69
+ updates["latitude"] = latitude
70
+ if longitude is not None:
71
+ updates["longitude"] = longitude
72
+
73
+ # Upload main photo
74
+ if photo and photo.filename:
75
+ if not photo.content_type or not photo.content_type.startswith("image/"):
76
+ raise HTTPException(status_code=400, detail="Clinic photo must be an image")
77
+
78
+ photo_bytes = await photo.read()
79
+ photo_ext = os.path.splitext(photo.filename)[1] or ".jpg"
80
+ photo_name = f"{clinic_name or 'logo'}-{int(time.time())}{photo_ext}"
81
+ photo_path = SupabaseStorage.upload_clinic_image(
82
+ user_id=current_user["id"],
83
+ file_data=photo_bytes,
84
+ filename=photo_name,
85
+ content_type=photo.content_type or "image/jpeg"
86
+ )
87
+ updates["clinic_logo_url"] = supabase.storage.from_("clinic-images").get_public_url(photo_path)
88
+
89
+ # Upload gallery photos
90
+ for gallery_photo in photos or []:
91
+ if not gallery_photo or not gallery_photo.filename:
92
+ continue
93
+ if not gallery_photo.content_type or not gallery_photo.content_type.startswith("image/"):
94
+ raise HTTPException(status_code=400, detail="Clinic gallery images must be images")
95
+
96
+ image_bytes = await gallery_photo.read()
97
+ image_ext = os.path.splitext(gallery_photo.filename)[1] or ".jpg"
98
+ image_name = f"gallery-{int(time.time())}-{gallery_photo.filename.replace(' ', '_')}"
99
+ if not image_name.endswith(image_ext):
100
+ image_name += image_ext
101
+
102
+ # We upload it but we don't save to the main table, as SupabaseStorage lists them
103
+ SupabaseStorage.upload_clinic_image(
104
+ user_id=current_user["id"],
105
+ file_data=image_bytes,
106
+ filename=image_name,
107
+ content_type=gallery_photo.content_type or "image/jpeg",
108
+ )
109
+
110
+ result = ClinicService.update_clinic_profile(user_id=current_user["id"], updates=updates)
111
+ if not result["success"]:
112
+ raise HTTPException(status_code=400, detail=result.get("error"))
113
+
114
+ return result
115
+
116
+ @router.get("/clinics")
117
+ async def get_public_clinics():
118
+ """Public endpoint: Get all verified clinics"""
119
+ result = ClinicService.get_public_clinics()
120
+ if not result["success"]:
121
+ raise HTTPException(status_code=400, detail=result.get("error"))
122
+ return result
123
+
124
+ @router.get("/clinics/{clinic_id}")
125
+ async def get_clinic_by_id(clinic_id: str):
126
+ """Public endpoint: Get clinic details by clinic ID"""
127
+ result = ClinicService.get_clinic_by_id(clinic_id=clinic_id)
128
+ if not result["success"]:
129
+ raise HTTPException(status_code=404, detail=result.get("error"))
130
+ return result
131
+
132
+ @router.get("/clinic/patients")
133
+ async def get_clinic_patients(current_user: dict = Depends(get_current_user)):
134
+ """Get all appointments / patients for the current clinic"""
135
+ if current_user["role"] != "clinic":
136
+ raise HTTPException(status_code=403, detail="Access denied. Clinic role required.")
137
+
138
+ # Get the clinic's ID from user_id
139
+ clinic_resp = supabase.table("clinics").select("id").eq("user_id", current_user["id"]).execute()
140
+ if not clinic_resp.data:
141
+ raise HTTPException(status_code=404, detail="Clinic profile not found")
142
+
143
+ clinic_id = clinic_resp.data[0]["id"]
144
+
145
+ try:
146
+ resp = supabase.table("appointments").select("*").eq("clinic_id", clinic_id).order("appointment_date", desc=False).execute()
147
+ appts = resp.data or []
148
+
149
+ # Enrich each appointment with pet name and owner full name
150
+ enriched = []
151
+ for a in appts:
152
+ pet_name = None
153
+ owner_name = None
154
+ try:
155
+ if a.get("pet_id"):
156
+ pet_resp = supabase.table("pets").select("name").eq("id", a.get("pet_id")).execute()
157
+ if pet_resp.data:
158
+ pet_name = pet_resp.data[0].get("name")
159
+ if a.get("owner_id"):
160
+ owner_resp = supabase.table("pet_owners").select("full_name").eq("user_id", a.get("owner_id")).execute()
161
+ if owner_resp.data:
162
+ owner_name = owner_resp.data[0].get("full_name")
163
+ except Exception:
164
+ pass
165
+
166
+ item = dict(a)
167
+ item["pet_name"] = pet_name or a.get("pet_id")
168
+ item["owner_name"] = owner_name or a.get("owner_id")
169
+ enriched.append(item)
170
+
171
+ return {"success": True, "appointments": enriched, "count": len(enriched)}
172
+ except Exception as e:
173
+ raise HTTPException(status_code=400, detail=str(e))
backend/routers/pets.py ADDED
@@ -0,0 +1,463 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File, Form
2
+ from typing import Optional, List
3
+ import logging
4
+ import time
5
+ import os
6
+ import tempfile
7
+ from backend.core.dependencies import get_current_user
8
+ from backend.services.pet_service import PetService
9
+ from chatbot.supabase_config import supabase, SupabaseStorage
10
+ from chatbot.vaccine_service import VaccineService
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ router = APIRouter(tags=["Pets, Vaccines & Medical Records"])
15
+
16
+ # ============================================
17
+ # Pet Endpoints
18
+ # ============================================
19
+
20
+ @router.post("/pets")
21
+ async def create_pet(
22
+ name: str = Form(...),
23
+ pet_type: str = Form(...),
24
+ breed: str = Form(...),
25
+ date_of_birth: str = Form(...),
26
+ weight: float = Form(...),
27
+ weight_unit: str = Form("kg"),
28
+ gender: Optional[str] = Form(None),
29
+ blood_type: Optional[str] = Form(None),
30
+ allergies: Optional[str] = Form(None),
31
+ medical_conditions: Optional[str] = Form(None),
32
+ notes: Optional[str] = Form(None),
33
+ photo: Optional[UploadFile] = File(None),
34
+ current_user: dict = Depends(get_current_user)
35
+ ):
36
+ """Add a new pet (Owner only)"""
37
+ if current_user["role"] != "owner":
38
+ raise HTTPException(status_code=403, detail="Only pet owners can add pets")
39
+
40
+ profile_image_url = None
41
+ if photo and photo.filename:
42
+ if not photo.content_type or not photo.content_type.startswith("image/"):
43
+ raise HTTPException(status_code=400, detail="Pet photo must be an image")
44
+
45
+ file_data = await photo.read()
46
+ file_ext = os.path.splitext(photo.filename)[1] or ".jpg"
47
+ storage_path = f"{current_user['id']}/{int(time.time())}-{name.replace(' ', '_')}{file_ext}"
48
+ SupabaseStorage.ensure_bucket("pet-images", public=True, allowed_mime_types=["image/*"])
49
+ supabase.storage.from_("pet-images").upload(
50
+ file=file_data,
51
+ path=storage_path,
52
+ file_options={
53
+ "content-type": photo.content_type or "image/jpeg",
54
+ "upsert": "false",
55
+ },
56
+ )
57
+ profile_image_url = supabase.storage.from_("pet-images").get_public_url(storage_path)
58
+
59
+ result = PetService.add_pet(
60
+ user_id=current_user["id"],
61
+ name=name,
62
+ pet_type=pet_type,
63
+ breed=breed,
64
+ date_of_birth=date_of_birth,
65
+ weight=weight,
66
+ weight_unit=weight_unit,
67
+ gender=gender,
68
+ blood_type=blood_type,
69
+ allergies=allergies,
70
+ medical_conditions=medical_conditions,
71
+ notes=notes,
72
+ profile_image_url=profile_image_url
73
+ )
74
+
75
+ if not result["success"]:
76
+ raise HTTPException(status_code=400, detail=result.get("error"))
77
+
78
+ return result
79
+
80
+ @router.get("/pets")
81
+ async def get_pets(current_user: dict = Depends(get_current_user)):
82
+ """Get all pets for the currently logged-in user (Owner) or all pets if Clinic/Admin"""
83
+ if current_user["role"] == "owner":
84
+ result = PetService.get_user_pets(user_id=current_user["id"])
85
+ else:
86
+ try:
87
+ response = supabase.table("pets").select("*").execute()
88
+ result = {
89
+ "success": True,
90
+ "pets": response.data or [],
91
+ "count": len(response.data or [])
92
+ }
93
+ except Exception as e:
94
+ raise HTTPException(status_code=400, detail=str(e))
95
+
96
+ if not result["success"]:
97
+ raise HTTPException(status_code=400, detail=result.get("error"))
98
+
99
+ return result
100
+
101
+ @router.get("/pets/{pet_id}")
102
+ async def get_pet_detail(pet_id: str, current_user: dict = Depends(get_current_user)):
103
+ """Get detailed profile of a specific pet"""
104
+ try:
105
+ response = supabase.table("pets").select("*").eq("id", pet_id).execute()
106
+ if not response.data:
107
+ raise HTTPException(status_code=404, detail="Pet not found")
108
+
109
+ pet = response.data[0]
110
+ if current_user["role"] == "owner" and pet["user_id"] != current_user["id"]:
111
+ raise HTTPException(status_code=403, detail="Access denied")
112
+
113
+ return {"success": True, "pet": pet}
114
+ except HTTPException:
115
+ raise
116
+ except Exception as e:
117
+ raise HTTPException(status_code=400, detail=str(e))
118
+
119
+ @router.put("/pets/{pet_id}")
120
+ async def update_pet_detail(
121
+ pet_id: str,
122
+ name: Optional[str] = Form(None),
123
+ pet_type: Optional[str] = Form(None),
124
+ breed: Optional[str] = Form(None),
125
+ date_of_birth: Optional[str] = Form(None),
126
+ weight: Optional[float] = Form(None),
127
+ weight_unit: Optional[str] = Form(None),
128
+ gender: Optional[str] = Form(None),
129
+ blood_type: Optional[str] = Form(None),
130
+ allergies: Optional[str] = Form(None),
131
+ medical_conditions: Optional[str] = Form(None),
132
+ notes: Optional[str] = Form(None),
133
+ photo: Optional[UploadFile] = File(None),
134
+ current_user: dict = Depends(get_current_user)
135
+ ):
136
+ """Update details of a specific pet"""
137
+ try:
138
+ current = supabase.table("pets").select("*").eq("id", pet_id).execute()
139
+ if not current.data:
140
+ raise HTTPException(status_code=404, detail="Pet not found")
141
+
142
+ pet = current.data[0]
143
+ if current_user["role"] == "owner" and pet["user_id"] != current_user["id"]:
144
+ raise HTTPException(status_code=403, detail="Access denied")
145
+
146
+ updates = {}
147
+ if name is not None:
148
+ updates["name"] = name
149
+ if pet_type is not None:
150
+ updates["type"] = pet_type.lower()
151
+ if breed is not None:
152
+ updates["breed"] = breed
153
+ if date_of_birth is not None:
154
+ updates["date_of_birth"] = date_of_birth
155
+ if weight is not None:
156
+ updates["weight"] = weight
157
+ if weight_unit is not None:
158
+ updates["weight_unit"] = weight_unit
159
+ if gender is not None:
160
+ updates["gender"] = gender
161
+ if blood_type is not None:
162
+ updates["blood_type"] = blood_type
163
+ if allergies is not None:
164
+ updates["allergies"] = allergies
165
+ if medical_conditions is not None:
166
+ updates["medical_conditions"] = medical_conditions
167
+ if notes is not None:
168
+ updates["notes"] = notes
169
+
170
+ if photo and photo.filename:
171
+ if not photo.content_type or not photo.content_type.startswith("image/"):
172
+ raise HTTPException(status_code=400, detail="Pet photo must be an image")
173
+
174
+ file_data = await photo.read()
175
+ file_ext = os.path.splitext(photo.filename)[1] or ".jpg"
176
+ storage_path = f"{pet['user_id']}/{int(time.time())}-{name or pet['name']}{file_ext}".replace(' ', '_')
177
+ SupabaseStorage.ensure_bucket("pet-images", public=True, allowed_mime_types=["image/*"])
178
+ supabase.storage.from_("pet-images").upload(
179
+ file=file_data,
180
+ path=storage_path,
181
+ file_options={
182
+ "content-type": photo.content_type or "image/jpeg",
183
+ "upsert": "false",
184
+ },
185
+ )
186
+ updates["profile_image_url"] = supabase.storage.from_("pet-images").get_public_url(storage_path)
187
+
188
+ result = PetService.update_pet(pet_id=pet_id, updates=updates)
189
+ if not result["success"]:
190
+ raise HTTPException(status_code=400, detail=result.get("error"))
191
+
192
+ return result
193
+ except HTTPException:
194
+ raise
195
+ except Exception as e:
196
+ raise HTTPException(status_code=500, detail=str(e))
197
+
198
+ # ============================================
199
+ # Vaccine Records Endpoints
200
+ # ============================================
201
+
202
+ @router.post("/vaccine-records")
203
+ async def upload_vaccine_record(
204
+ pet_id: str = Form(...),
205
+ file: UploadFile = File(...),
206
+ upload_date: Optional[str] = Form(None),
207
+ current_user: dict = Depends(get_current_user)
208
+ ):
209
+ """Upload a vaccination record for a pet"""
210
+ try:
211
+ pet_resp = supabase.table("pets").select("user_id").eq("id", pet_id).execute()
212
+ if not pet_resp.data:
213
+ raise HTTPException(status_code=404, detail="Pet not found")
214
+
215
+ if current_user["role"] == "owner" and pet_resp.data[0]["user_id"] != current_user["id"]:
216
+ raise HTTPException(status_code=403, detail="Access denied")
217
+
218
+ file_data = await file.read()
219
+ file_type = "pdf" if file.content_type == "application/pdf" else "image"
220
+
221
+ result = PetService.upload_vaccine_record(
222
+ pet_id=pet_id,
223
+ file_data=file_data,
224
+ file_name=file.filename,
225
+ file_type=file_type,
226
+ uploaded_by=current_user["id"],
227
+ upload_date=upload_date
228
+ )
229
+
230
+ if not result["success"]:
231
+ raise HTTPException(status_code=400, detail=result.get("error"))
232
+
233
+ return result
234
+ except HTTPException:
235
+ raise
236
+ except Exception as e:
237
+ raise HTTPException(status_code=400, detail=str(e))
238
+
239
+ @router.get("/vaccine-records")
240
+ async def get_vaccine_records(pet_id: str, current_user: dict = Depends(get_current_user)):
241
+ """Get all vaccine records for a pet"""
242
+ try:
243
+ pet_resp = supabase.table("pets").select("user_id").eq("id", pet_id).execute()
244
+ if not pet_resp.data:
245
+ raise HTTPException(status_code=404, detail="Pet not found")
246
+
247
+ if current_user["role"] == "owner" and pet_resp.data[0]["user_id"] != current_user["id"]:
248
+ raise HTTPException(status_code=403, detail="Access denied")
249
+
250
+ result = PetService.get_pet_vaccine_records(pet_id=pet_id)
251
+ if not result["success"]:
252
+ raise HTTPException(status_code=400, detail=result.get("error"))
253
+
254
+ return result
255
+ except HTTPException:
256
+ raise
257
+ except Exception as e:
258
+ raise HTTPException(status_code=400, detail=str(e))
259
+
260
+ # ============================================
261
+ # Advanced Vaccine Processing Endpoints
262
+ # ============================================
263
+
264
+ @router.post("/vaccines/upload-document")
265
+ async def upload_vaccine_document(
266
+ pet_id: str = Form(...),
267
+ file: UploadFile = File(...),
268
+ current_user: dict = Depends(get_current_user)
269
+ ):
270
+ """Upload vaccine booklet/card image, extract data via VLM, and store records"""
271
+ logger.info(f"Uploading vaccine document for pet: {pet_id} by user: {current_user['id']}")
272
+
273
+ # Check authorization
274
+ pet_resp = supabase.table("pets").select("user_id").eq("id", pet_id).execute()
275
+ if not pet_resp.data:
276
+ raise HTTPException(status_code=404, detail="Pet not found")
277
+ if current_user["role"] == "owner" and pet_resp.data[0]["user_id"] != current_user["id"]:
278
+ raise HTTPException(status_code=403, detail="Access denied")
279
+
280
+ with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(file.filename or ".jpg")[1]) as tmp:
281
+ content = await file.read()
282
+ tmp.write(content)
283
+ tmp_path = tmp.name
284
+
285
+ try:
286
+ file_ext = os.path.splitext(file.filename or ".jpg")[1]
287
+ storage_path = f"vaccine-documents/{pet_id}/{int(time.time())}{file_ext}"
288
+
289
+ SupabaseStorage.ensure_bucket("vaccine-documents", public=True, allowed_mime_types=["image/*", "application/pdf"])
290
+
291
+ with open(tmp_path, "rb") as f:
292
+ file_data = f.read()
293
+
294
+ supabase.storage.from_("vaccine-documents").upload(
295
+ file=file_data,
296
+ path=storage_path,
297
+ file_options={
298
+ "content-type": file.content_type or "image/jpeg",
299
+ "upsert": "false",
300
+ },
301
+ )
302
+ image_url = supabase.storage.from_("vaccine-documents").get_public_url(storage_path)
303
+
304
+ result = VaccineService.upload_vaccine_document(
305
+ pet_id=pet_id,
306
+ image_url=image_url,
307
+ image_path=tmp_path
308
+ )
309
+
310
+ if not result.get("success"):
311
+ raise HTTPException(status_code=400, detail=result.get("error", "Failed to process vaccine document"))
312
+
313
+ return {
314
+ "success": True,
315
+ "document_id": result.get("document_id"),
316
+ "records_count": result.get("records_count"),
317
+ "records": result.get("records"),
318
+ "message": f"Successfully extracted {result.get('records_count')} vaccine records!"
319
+ }
320
+ finally:
321
+ if os.path.exists(tmp_path):
322
+ os.remove(tmp_path)
323
+
324
+ @router.post("/vaccines/manual-entry")
325
+ async def add_manual_vaccine(
326
+ pet_id: str = Form(...),
327
+ vaccine_name: str = Form(...),
328
+ vaccination_date: str = Form(...),
329
+ next_due_date: Optional[str] = Form(None),
330
+ batch_number: Optional[str] = Form(None),
331
+ veterinarian_name: Optional[str] = Form(None),
332
+ clinic_name: Optional[str] = Form(None),
333
+ clinic_id: Optional[str] = Form(None),
334
+ notes: Optional[str] = Form(None),
335
+ source: str = Form("vet_entry"),
336
+ current_user: dict = Depends(get_current_user)
337
+ ):
338
+ """Add a vaccine record manually (Clinics or Vets)"""
339
+ # Verify clinic role or admin
340
+ if current_user["role"] not in ["clinic", "admin"]:
341
+ raise HTTPException(status_code=403, detail="Only clinics and admins can make manual vaccine entries")
342
+
343
+ result = VaccineService.add_manual_vaccine_entry(
344
+ pet_id=pet_id,
345
+ vaccine_name=vaccine_name,
346
+ vaccination_date=vaccination_date,
347
+ next_due_date=next_due_date,
348
+ batch_number=batch_number,
349
+ veterinarian_name=veterinarian_name,
350
+ clinic_name=clinic_name,
351
+ clinic_id=clinic_id,
352
+ notes=notes,
353
+ source=source
354
+ )
355
+
356
+ if not result.get("success"):
357
+ raise HTTPException(status_code=400, detail=result.get("error"))
358
+
359
+ return result
360
+
361
+ @router.get("/vaccines/{pet_id}")
362
+ async def get_pet_vaccines(pet_id: str, current_user: dict = Depends(get_current_user)):
363
+ """Get all vaccination records for a pet"""
364
+ pet_resp = supabase.table("pets").select("user_id").eq("id", pet_id).execute()
365
+ if not pet_resp.data:
366
+ raise HTTPException(status_code=404, detail="Pet not found")
367
+ if current_user["role"] == "owner" and pet_resp.data[0]["user_id"] != current_user["id"]:
368
+ raise HTTPException(status_code=403, detail="Access denied")
369
+
370
+ result = VaccineService.get_pet_vaccines(pet_id=pet_id)
371
+ if not result.get("success"):
372
+ raise HTTPException(status_code=400, detail=result.get("error"))
373
+
374
+ return result
375
+
376
+ @router.get("/vaccines/{pet_id}/documents")
377
+ async def get_pet_vaccine_documents(pet_id: str, current_user: dict = Depends(get_current_user)):
378
+ """Get uploaded vaccine documents for a pet"""
379
+ pet_resp = supabase.table("pets").select("user_id").eq("id", pet_id).execute()
380
+ if not pet_resp.data:
381
+ raise HTTPException(status_code=404, detail="Pet not found")
382
+ if current_user["role"] == "owner" and pet_resp.data[0]["user_id"] != current_user["id"]:
383
+ raise HTTPException(status_code=403, detail="Access denied")
384
+
385
+ result = VaccineService.get_pet_vaccine_documents(pet_id=pet_id)
386
+ if not result.get("success"):
387
+ raise HTTPException(status_code=400, detail=result.get("error"))
388
+
389
+ return result
390
+
391
+ @router.post("/vaccines/check-reminders")
392
+ async def check_vaccine_reminders(current_user: dict = Depends(get_current_user)):
393
+ """Trigger reminder check for all vaccines (Admin only)"""
394
+ if current_user["role"] != "admin":
395
+ raise HTTPException(status_code=403, detail="Admin access required")
396
+
397
+ result = VaccineService.check_and_send_reminders()
398
+ return result
399
+
400
+ # ============================================
401
+ # Medical Records Endpoints
402
+ # ============================================
403
+
404
+ @router.post("/medical-records")
405
+ async def create_medical_record(
406
+ clinic_id: str,
407
+ pet_id: str,
408
+ record_type: str,
409
+ visit_date: str,
410
+ diagnosis: Optional[str] = None,
411
+ treatment: Optional[str] = None,
412
+ notes: Optional[str] = None,
413
+ current_user: dict = Depends(get_current_user)
414
+ ):
415
+ """Create medical record (Clinics only)"""
416
+ if current_user["role"] != "clinic":
417
+ raise HTTPException(status_code=403, detail="Only clinics can create medical records")
418
+
419
+ # Check if the clinic owns this clinic_id
420
+ clinic_resp = supabase.table("clinics").select("id").eq("user_id", current_user["id"]).execute()
421
+ if not clinic_resp.data or clinic_resp.data[0]["id"] != clinic_id:
422
+ raise HTTPException(status_code=403, detail="Access denied. Invalid clinic identity.")
423
+
424
+ try:
425
+ medical_data = {
426
+ "pet_id": pet_id,
427
+ "clinic_id": clinic_id,
428
+ "record_type": record_type,
429
+ "visit_date": visit_date,
430
+ "diagnosis": diagnosis,
431
+ "treatment": treatment,
432
+ "notes": notes
433
+ }
434
+ response = supabase.table("medical_records").insert(medical_data).execute()
435
+ if not response.data:
436
+ raise HTTPException(status_code=400, detail="Failed to create medical record")
437
+
438
+ return {
439
+ "success": True,
440
+ "record_id": response.data[0]["id"],
441
+ "message": "Medical record created successfully!"
442
+ }
443
+ except Exception as e:
444
+ raise HTTPException(status_code=400, detail=str(e))
445
+
446
+ @router.get("/pet/medical-records")
447
+ async def get_pet_medical_records(pet_id: str, current_user: dict = Depends(get_current_user)):
448
+ """Get all medical records for a pet"""
449
+ pet_resp = supabase.table("pets").select("user_id").eq("id", pet_id).execute()
450
+ if not pet_resp.data:
451
+ raise HTTPException(status_code=404, detail="Pet not found")
452
+ if current_user["role"] == "owner" and pet_resp.data[0]["user_id"] != current_user["id"]:
453
+ raise HTTPException(status_code=403, detail="Access denied")
454
+
455
+ try:
456
+ response = supabase.table("medical_records").select("*").eq("pet_id", pet_id).execute()
457
+ return {
458
+ "success": True,
459
+ "records": response.data or [],
460
+ "count": len(response.data or [])
461
+ }
462
+ except Exception as e:
463
+ raise HTTPException(status_code=400, detail=str(e))
backend/schemas/__pycache__/schemas.cpython-313.pyc ADDED
Binary file (3.2 kB). View file
 
backend/schemas/schemas.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+ from typing import Optional, List
3
+
4
+ class RegisterOwnerRequest(BaseModel):
5
+ first_name: str
6
+ last_name: str
7
+ phone: str
8
+ address: Optional[str] = None
9
+ state: Optional[str] = None
10
+ zip_code: Optional[str] = None
11
+ country: Optional[str] = None
12
+ bio: Optional[str] = None
13
+
14
+ class RegisterClinicRequest(BaseModel):
15
+ clinic_name: str
16
+ phone: str
17
+ address: str
18
+ city: Optional[str] = None
19
+ state: Optional[str] = None
20
+ zip_code: Optional[str] = None
21
+ country: Optional[str] = None
22
+ website: Optional[str] = None
23
+ opening_hours: Optional[str] = None
24
+ description: Optional[str] = None
25
+ clinic_logo_url: Optional[str] = None
26
+ license_document_url: Optional[str] = None
27
+
28
+ class NotificationReadRequest(BaseModel):
29
+ user_id: str
30
+
31
+ class AddPetRequest(BaseModel):
32
+ name: str
33
+ pet_type: str
34
+ breed: str
35
+ date_of_birth: str
36
+ weight: float
37
+ weight_unit: Optional[str] = "kg"
38
+ gender: Optional[str] = None
39
+ blood_type: Optional[str] = None
40
+ allergies: Optional[str] = None
41
+ medical_conditions: Optional[str] = None
42
+ notes: Optional[str] = None
43
+
44
+ class CreateAppointmentRequest(BaseModel):
45
+ pet_id: str
46
+ clinic_id: str
47
+ appointment_date: str
48
+ appointment_time: str
49
+ reason: Optional[str] = None
50
+ notes: Optional[str] = None
51
+
52
+ class CreateReviewRequest(BaseModel):
53
+ appointment_id: str
54
+ rating: int
55
+ treatment: str
56
+ comment: Optional[str] = None
backend/services/__pycache__/appointment_service.cpython-313.pyc ADDED
Binary file (14.7 kB). View file
 
backend/services/__pycache__/auth_service.cpython-313.pyc ADDED
Binary file (8.87 kB). View file
 
backend/services/__pycache__/clinic_service.cpython-313.pyc ADDED
Binary file (13.5 kB). View file
 
backend/services/__pycache__/pet_service.cpython-313.pyc ADDED
Binary file (6.59 kB). View file
 
backend/services/__pycache__/predictor.cpython-313.pyc ADDED
Binary file (980 Bytes). View file
 
backend/services/__pycache__/router.cpython-313.pyc ADDED
Binary file (822 Bytes). View file
 
backend/services/appointment_service.py ADDED
@@ -0,0 +1,289 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, List, Optional
2
+ from datetime import datetime
3
+ import time
4
+ import logging
5
+ from chatbot.supabase_config import supabase
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ class AppointmentService:
10
+ """Service for managing appointments and reviews"""
11
+
12
+ @staticmethod
13
+ def create_appointment(
14
+ pet_id: str,
15
+ clinic_id: str,
16
+ owner_id: str,
17
+ appointment_date: str,
18
+ appointment_time: str,
19
+ reason: Optional[str] = None,
20
+ notes: Optional[str] = None,
21
+ ) -> Dict:
22
+ """Create appointment"""
23
+ try:
24
+ appointment_data = {
25
+ "pet_id": pet_id,
26
+ "clinic_id": clinic_id,
27
+ "owner_id": owner_id,
28
+ "appointment_date": appointment_date,
29
+ "appointment_time": appointment_time,
30
+ "reason": reason,
31
+ "notes": notes,
32
+ "status": "scheduled"
33
+ }
34
+
35
+ response = supabase.table("appointments").insert(appointment_data).execute()
36
+ if not response.data:
37
+ return {"success": False, "error": "Failed to create appointment"}
38
+
39
+ created = response.data[0]
40
+
41
+ # Send notifications
42
+ try:
43
+ AppointmentService._send_appointment_notifications(created)
44
+ except Exception as notif_err:
45
+ logger.warning(f"Failed to send appointment notifications: {notif_err}")
46
+
47
+ return {
48
+ "success": True,
49
+ "appointment_id": created["id"],
50
+ "appointment": created,
51
+ "message": "Appointment created successfully!"
52
+ }
53
+ except Exception as e:
54
+ return {
55
+ "success": False,
56
+ "error": f"Error creating appointment: {str(e)}"
57
+ }
58
+
59
+ @staticmethod
60
+ def get_owner_appointments(owner_id: str) -> Dict:
61
+ """Get all appointments for pet owner"""
62
+ try:
63
+ response = supabase.table("appointments").select("*").eq("owner_id", owner_id).execute()
64
+ appointments = response.data or []
65
+ # We can enrich reviews here if needed
66
+ return {
67
+ "success": True,
68
+ "appointments": appointments,
69
+ "count": len(appointments)
70
+ }
71
+ except Exception as e:
72
+ return {
73
+ "success": False,
74
+ "error": f"Error fetching owner appointments: {str(e)}"
75
+ }
76
+
77
+ @staticmethod
78
+ def get_clinic_appointments(clinic_id: str) -> Dict:
79
+ """Get all appointments for clinic"""
80
+ try:
81
+ response = supabase.table("appointments").select("*").eq("clinic_id", clinic_id).execute()
82
+ appointments = response.data or []
83
+ return {
84
+ "success": True,
85
+ "appointments": appointments,
86
+ "count": len(appointments)
87
+ }
88
+ except Exception as e:
89
+ return {
90
+ "success": False,
91
+ "error": f"Error fetching clinic appointments: {str(e)}"
92
+ }
93
+
94
+ @staticmethod
95
+ def get_pet_appointments(pet_id: str) -> Dict:
96
+ """Get all appointments for a pet"""
97
+ try:
98
+ response = supabase.table("appointments").select("*").eq("pet_id", pet_id).execute()
99
+ appointments = response.data or []
100
+ return {
101
+ "success": True,
102
+ "appointments": appointments,
103
+ "count": len(appointments)
104
+ }
105
+ except Exception as e:
106
+ return {
107
+ "success": False,
108
+ "error": f"Error fetching pet appointments: {str(e)}"
109
+ }
110
+
111
+ @staticmethod
112
+ def update_appointment_status(appointment_id: str, status: str) -> Dict:
113
+ """Update appointment status (scheduled, completed, cancelled, etc.)"""
114
+ try:
115
+ # Fetch appointment
116
+ appt_resp = supabase.table("appointments").select("*").eq("id", appointment_id).execute()
117
+ if not appt_resp.data:
118
+ return {"success": False, "error": "Appointment not found"}
119
+
120
+ appt = appt_resp.data[0]
121
+
122
+ # Update status
123
+ timestamp = datetime.utcnow().isoformat()
124
+ supabase.table("appointments").update({"status": status, "updated_at": timestamp}).eq("id", appointment_id).execute()
125
+
126
+ # Send status update notifications
127
+ try:
128
+ AppointmentService._send_status_notifications(appt, status)
129
+ except Exception as notif_err:
130
+ logger.warning(f"Failed to send status notifications: {notif_err}")
131
+
132
+ return {
133
+ "success": True,
134
+ "appointment_id": appointment_id,
135
+ "status": status
136
+ }
137
+ except Exception as e:
138
+ return {
139
+ "success": False,
140
+ "error": f"Error updating appointment status: {str(e)}"
141
+ }
142
+
143
+ @staticmethod
144
+ def create_review(
145
+ appointment_id: str,
146
+ rating: int,
147
+ treatment: str,
148
+ comment: Optional[str] = None,
149
+ ) -> Dict:
150
+ """Create review for a completed appointment"""
151
+ try:
152
+ # Fetch appointment
153
+ appt_resp = supabase.table("appointments").select("*").eq("id", appointment_id).execute()
154
+ if not appt_resp.data:
155
+ return {"success": False, "error": "Appointment not found"}
156
+
157
+ appt = appt_resp.data[0]
158
+ if appt.get("status") != "completed":
159
+ return {"success": False, "error": "Only completed appointments can be reviewed"}
160
+
161
+ review_data = {
162
+ "appointment_id": appointment_id,
163
+ "clinic_id": appt.get("clinic_id"),
164
+ "pet_id": appt.get("pet_id"),
165
+ "owner_id": appt.get("owner_id"),
166
+ "rating": rating,
167
+ "treatment": treatment[:100],
168
+ "comment": comment,
169
+ }
170
+
171
+ response = supabase.table("clinic_reviews").insert(review_data).execute()
172
+ if not response.data:
173
+ return {"success": False, "error": "Failed to create review"}
174
+
175
+ created_review = response.data[0]
176
+
177
+ # Notify clinic
178
+ try:
179
+ AppointmentService._notify_clinic_about_review(created_review)
180
+ except Exception as notif_err:
181
+ logger.warning(f"Failed to notify clinic about review: {notif_err}")
182
+
183
+ return {
184
+ "success": True,
185
+ "review": created_review,
186
+ "message": "Review submitted successfully"
187
+ }
188
+ except Exception as e:
189
+ return {
190
+ "success": False,
191
+ "error": f"Error creating review: {str(e)}"
192
+ }
193
+
194
+ @staticmethod
195
+ def get_reviews_for_clinic(clinic_id: str) -> Dict:
196
+ """Get reviews for a clinic"""
197
+ try:
198
+ response = supabase.table("clinic_reviews").select("*").eq("clinic_id", clinic_id).order("created_at", desc=True).execute()
199
+ reviews = response.data or []
200
+ avg_rating = round(sum(r.get("rating", 0) for r in reviews) / len(reviews), 1) if reviews else 0.0
201
+ return {
202
+ "success": True,
203
+ "reviews": reviews,
204
+ "count": len(reviews),
205
+ "average_rating": avg_rating
206
+ }
207
+ except Exception as e:
208
+ return {
209
+ "success": False,
210
+ "error": f"Error fetching clinic reviews: {str(e)}"
211
+ }
212
+
213
+ # Private helper methods for notifications
214
+ @staticmethod
215
+ def _create_notification(user_id: str, type_: str, title: str, message: str, role: str, entity_type: str, entity_id: str):
216
+ payload = {
217
+ "user_id": user_id,
218
+ "user_role": role,
219
+ "type": type_,
220
+ "title": title,
221
+ "message": message,
222
+ "entity_type": entity_type,
223
+ "entity_id": entity_id,
224
+ "is_read": False,
225
+ "created_at": datetime.utcnow().isoformat(),
226
+ }
227
+ supabase.table("notifications").insert(payload).execute()
228
+
229
+ @staticmethod
230
+ def _send_appointment_notifications(appt: Dict):
231
+ pet_resp = supabase.table("pets").select("name").eq("id", appt["pet_id"]).execute()
232
+ pet_name = pet_resp.data[0]["name"] if pet_resp.data else "your pet"
233
+
234
+ clinic_resp = supabase.table("clinics").select("clinic_name", "user_id").eq("id", appt["clinic_id"]).execute()
235
+ clinic_name = "Clinic"
236
+ clinic_user_id = None
237
+ if clinic_resp.data:
238
+ clinic_name = clinic_resp.data[0]["clinic_name"]
239
+ clinic_user_id = clinic_resp.data[0]["user_id"]
240
+
241
+ title = "Appointment Scheduled"
242
+ message = f"{pet_name} has a new appointment with {clinic_name} on {appt['appointment_date']} at {appt['appointment_time']}."
243
+
244
+ # Notify owner
245
+ AppointmentService._create_notification(appt["owner_id"], "appointment", title, message, "owner", "appointment", appt["id"])
246
+
247
+ # Notify clinic
248
+ if clinic_user_id:
249
+ AppointmentService._create_notification(clinic_user_id, "appointment", title, message, "clinic", "appointment", appt["id"])
250
+
251
+ @staticmethod
252
+ def _send_status_notifications(appt: Dict, status: str):
253
+ pet_resp = supabase.table("pets").select("name").eq("id", appt["pet_id"]).execute()
254
+ pet_name = pet_resp.data[0]["name"] if pet_resp.data else "your pet"
255
+
256
+ clinic_resp = supabase.table("clinics").select("clinic_name", "user_id").eq("id", appt["clinic_id"]).execute()
257
+ clinic_name = "Clinic"
258
+ clinic_user_id = None
259
+ if clinic_resp.data:
260
+ clinic_name = clinic_resp.data[0]["clinic_name"]
261
+ clinic_user_id = clinic_resp.data[0]["user_id"]
262
+
263
+ status_label = status.replace("_", " ").title()
264
+ title = f"Appointment {status_label}"
265
+ message = f"{pet_name}'s appointment with {clinic_name} on {appt['appointment_date']} at {appt['appointment_time']} was updated to {status_label}."
266
+
267
+ # Notify owner
268
+ AppointmentService._create_notification(appt["owner_id"], "appointment_status", title, message, "owner", "appointment", appt["id"])
269
+
270
+ # Notify clinic
271
+ if clinic_user_id:
272
+ AppointmentService._create_notification(clinic_user_id, "appointment_status", title, message, "clinic", "appointment", appt["id"])
273
+
274
+ @staticmethod
275
+ def _notify_clinic_about_review(review: Dict):
276
+ clinic_resp = supabase.table("clinics").select("user_id", "clinic_name").eq("id", review["clinic_id"]).execute()
277
+ if clinic_resp.data and clinic_resp.data[0].get("user_id"):
278
+ clinic_user_id = clinic_resp.data[0]["user_id"]
279
+ clinic_name = clinic_resp.data[0]["clinic_name"]
280
+ AppointmentService._create_notification(
281
+ clinic_user_id,
282
+ "clinic_review",
283
+ "New Client Review",
284
+ f"A pet owner left a {review['rating']}-star review for {clinic_name}.",
285
+ "clinic",
286
+ "review",
287
+ review["id"]
288
+ )
289
+
backend/services/auth_service.py ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, Optional
2
+ from chatbot.supabase_config import supabase
3
+
4
+ class AuthService:
5
+ """Authentication and Profile Service"""
6
+
7
+ @staticmethod
8
+ def register_pet_owner(
9
+ user_id: str,
10
+ email: str,
11
+ first_name: str,
12
+ last_name: str,
13
+ phone: str,
14
+ address: Optional[str] = None,
15
+ state: Optional[str] = None,
16
+ zip_code: Optional[str] = None,
17
+ country: Optional[str] = None,
18
+ bio: Optional[str] = None,
19
+ ) -> Dict:
20
+ """Complete pet owner profile registration"""
21
+ try:
22
+ # 1. Update phone and role in users
23
+ supabase.table("users").update({"phone_number": phone, "role": "owner"}).eq("id", user_id).execute()
24
+
25
+ # 2. Insert or update pet_owners (idempotent registration)
26
+ owner_data = {
27
+ "user_id": user_id,
28
+ "full_name": f"{first_name} {last_name}",
29
+ "email": email,
30
+ "phone": phone,
31
+ "address": address,
32
+ "state": state,
33
+ "zip_code": zip_code,
34
+ "country": country,
35
+ "bio": bio,
36
+ }
37
+
38
+ existing = supabase.table("pet_owners").select("*").eq("user_id", user_id).execute()
39
+ if existing.data:
40
+ response = supabase.table("pet_owners").update(owner_data).eq("user_id", user_id).execute()
41
+ else:
42
+ response = supabase.table("pet_owners").insert(owner_data).execute()
43
+
44
+ return {
45
+ "success": True,
46
+ "user_id": user_id,
47
+ "role": "owner",
48
+ "profile": response.data[0] if response.data else {},
49
+ "message": "Pet owner profile registered successfully!"
50
+ }
51
+ except Exception as e:
52
+ return {
53
+ "success": False,
54
+ "error": f"Registration failed: {str(e)}"
55
+ }
56
+
57
+ @staticmethod
58
+ def register_clinic(
59
+ user_id: str,
60
+ email: str,
61
+ clinic_name: str,
62
+ phone: str,
63
+ address: str,
64
+ city: Optional[str] = None,
65
+ state: Optional[str] = None,
66
+ zip_code: Optional[str] = None,
67
+ country: Optional[str] = None,
68
+ website: Optional[str] = None,
69
+ opening_hours: Optional[str] = None,
70
+ description: Optional[str] = None,
71
+ clinic_logo_url: Optional[str] = None,
72
+ license_document_url: Optional[str] = None,
73
+ latitude: Optional[float] = None,
74
+ longitude: Optional[float] = None,
75
+ ) -> Dict:
76
+ """Complete clinic profile registration"""
77
+ try:
78
+ # 1. Update phone and role in users
79
+ supabase.table("users").update({"phone_number": phone, "role": "clinic"}).eq("id", user_id).execute()
80
+
81
+ # 2. Insert or update clinics (idempotent registration)
82
+ clinic_data = {
83
+ "user_id": user_id,
84
+ "clinic_name": clinic_name,
85
+ "email": email,
86
+ "phone": phone,
87
+ "address": address,
88
+ "city": city,
89
+ "state": state,
90
+ "zip_code": zip_code,
91
+ "country": country,
92
+ "website": website,
93
+ "opening_hours": opening_hours,
94
+ "description": description,
95
+ "clinic_logo_url": clinic_logo_url,
96
+ "license_document_url": license_document_url,
97
+ "latitude": latitude,
98
+ "longitude": longitude,
99
+ }
100
+
101
+ existing = supabase.table("clinics").select("*").eq("user_id", user_id).execute()
102
+ if existing.data:
103
+ # Preserve existing verification status
104
+ clinic_data["is_verified"] = existing.data[0].get("is_verified", False)
105
+ response = supabase.table("clinics").update(clinic_data).eq("user_id", user_id).execute()
106
+ else:
107
+ clinic_data["is_verified"] = False # Starts as pending
108
+ response = supabase.table("clinics").insert(clinic_data).execute()
109
+
110
+ return {
111
+ "success": True,
112
+ "user_id": user_id,
113
+ "role": "clinic",
114
+ "profile": response.data[0] if response.data else {},
115
+ "message": "Clinic profile registered successfully! Pending admin approval."
116
+ }
117
+ except Exception as e:
118
+ return {
119
+ "success": False,
120
+ "error": f"Clinic registration failed: {str(e)}"
121
+ }
122
+
123
+ @staticmethod
124
+ def get_user_profile(user_id: str) -> Dict:
125
+ """Get user profile based on their role in the users table"""
126
+ try:
127
+ # Get profile
128
+ profile_response = supabase.table("users").select("*").eq("id", user_id).execute()
129
+ if not profile_response.data:
130
+ return {"success": False, "error": "User profile not found"}
131
+
132
+ profile = profile_response.data[0]
133
+ role = profile.get("role")
134
+
135
+ # Get detail profile based on role
136
+ if role == "owner":
137
+ detail_response = supabase.table("pet_owners").select("*").eq("user_id", user_id).execute()
138
+ elif role == "clinic":
139
+ detail_response = supabase.table("clinics").select("*").eq("user_id", user_id).execute()
140
+ else:
141
+ return {
142
+ "success": True,
143
+ "user_id": user_id,
144
+ "role": role,
145
+ "profile": {
146
+ "id": user_id,
147
+ "role": role,
148
+ "email": "admin@petai.com" if role == "admin" else ""
149
+ }
150
+ }
151
+
152
+ if not detail_response.data:
153
+ return {"success": False, "error": f"{role.title()} details not found"}
154
+
155
+ return {
156
+ "success": True,
157
+ "user_id": user_id,
158
+ "role": role,
159
+ "profile": detail_response.data[0]
160
+ }
161
+ except Exception as e:
162
+ return {
163
+ "success": False,
164
+ "error": f"Error fetching profile: {str(e)}"
165
+ }
166
+
167
+ @staticmethod
168
+ def update_user_profile(user_id: str, updates: Dict) -> Dict:
169
+ """Update pet owner user profile"""
170
+ try:
171
+ # Check role
172
+ profile_resp = supabase.table("users").select("role").eq("id", user_id).execute()
173
+ if not profile_resp.data or profile_resp.data[0].get("role") != "owner":
174
+ return {"success": False, "error": "Only pet owner profiles can be updated here"}
175
+
176
+ # Update pet_owners
177
+ if updates:
178
+ supabase.table("pet_owners").update(updates).eq("user_id", user_id).execute()
179
+
180
+ # Also update the users table to keep them in sync
181
+ user_updates = {}
182
+ if "full_name" in updates:
183
+ user_updates["full_name"] = updates["full_name"]
184
+ if "phone" in updates:
185
+ user_updates["phone_number"] = updates["phone"]
186
+ if "profile_image_url" in updates:
187
+ user_updates["avatar_url"] = updates["profile_image_url"]
188
+
189
+ if user_updates:
190
+ supabase.table("users").update(user_updates).eq("id", user_id).execute()
191
+
192
+ # Retrieve updated profile
193
+ refreshed = supabase.table("pet_owners").select("*").eq("user_id", user_id).execute()
194
+
195
+ return {
196
+ "success": True,
197
+ "profile": refreshed.data[0] if refreshed.data else {}
198
+ }
199
+ except Exception as e:
200
+ return {
201
+ "success": False,
202
+ "error": f"Error updating profile: {str(e)}"
203
+ }
backend/services/clinic_service.py ADDED
@@ -0,0 +1,250 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, List, Optional
2
+ from datetime import datetime
3
+ import logging
4
+ from chatbot.supabase_config import supabase, SupabaseStorage
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ REJECTION_MARKER = "__ADMIN_REJECTION__::"
9
+
10
+ class ClinicService:
11
+ """Service for managing clinic profiles, approvals, and public listings"""
12
+
13
+ @staticmethod
14
+ def get_clinic_profile(user_id: str) -> Dict:
15
+ """Get clinic profile by owner's user_id"""
16
+ try:
17
+ response = supabase.table("clinics").select("*").eq("user_id", user_id).execute()
18
+ if not response.data:
19
+ return {"success": False, "error": "Clinic not found"}
20
+
21
+ clinic = ClinicService._parse_clinic_status(response.data[0])
22
+ clinic["gallery_urls"] = SupabaseStorage.list_clinic_images(user_id)
23
+
24
+ if not clinic.get("clinic_logo_url") and clinic["gallery_urls"]:
25
+ clinic["clinic_logo_url"] = clinic["gallery_urls"][0]
26
+
27
+ return {"success": True, "clinic": clinic}
28
+ except Exception as e:
29
+ return {"success": False, "error": f"Error fetching clinic: {str(e)}"}
30
+
31
+ @staticmethod
32
+ def update_clinic_profile(user_id: str, updates: Dict) -> Dict:
33
+ """Update clinic profile details"""
34
+ try:
35
+ if updates:
36
+ supabase.table("clinics").update(updates).eq("user_id", user_id).execute()
37
+
38
+ # Refresh and return
39
+ return ClinicService.get_clinic_profile(user_id)
40
+ except Exception as e:
41
+ return {"success": False, "error": f"Error updating clinic: {str(e)}"}
42
+
43
+ @staticmethod
44
+ def get_public_clinics() -> Dict:
45
+ """List verified clinics for the public frontend"""
46
+ try:
47
+ resp = supabase.table("clinics").select("*").eq("is_verified", True).order("created_at", desc=True).execute()
48
+ clinics = []
49
+ for clinic_data in (resp.data or []):
50
+ parsed = ClinicService._parse_clinic_status(clinic_data)
51
+ uid = parsed.get("user_id")
52
+ parsed["gallery_urls"] = SupabaseStorage.list_clinic_images(uid) if uid else []
53
+ if not parsed.get("clinic_logo_url") and parsed["gallery_urls"]:
54
+ parsed["clinic_logo_url"] = parsed["gallery_urls"][0]
55
+ clinics.append(parsed)
56
+
57
+ return {"success": True, "clinics": clinics, "count": len(clinics)}
58
+ except Exception as e:
59
+ return {"success": False, "error": f"Error fetching public clinics: {str(e)}"}
60
+
61
+ @staticmethod
62
+ def get_clinic_by_id(clinic_id: str) -> Dict:
63
+ """Get public details of a clinic by its clinic ID"""
64
+ try:
65
+ resp = supabase.table("clinics").select("*").eq("id", clinic_id).execute()
66
+ if not resp.data:
67
+ return {"success": False, "error": "Clinic not found"}
68
+
69
+ clinic = ClinicService._parse_clinic_status(resp.data[0])
70
+ uid = clinic.get("user_id")
71
+ clinic["gallery_urls"] = SupabaseStorage.list_clinic_images(uid) if uid else []
72
+ if not clinic.get("clinic_logo_url") and clinic["gallery_urls"]:
73
+ clinic["clinic_logo_url"] = clinic["gallery_urls"][0]
74
+
75
+ return {"success": True, "clinic": clinic}
76
+ except Exception as e:
77
+ return {"success": False, "error": f"Error fetching clinic: {str(e)}"}
78
+
79
+ @staticmethod
80
+ def get_pending_clinics() -> Dict:
81
+ """List all pending clinics (unverified and not rejected)"""
82
+ try:
83
+ resp = supabase.table("clinics").select("*").eq("is_verified", False).execute()
84
+ pending = []
85
+ for clinic in (resp.data or []):
86
+ parsed = ClinicService._parse_clinic_status(clinic)
87
+ if parsed["verification_status"] == "pending":
88
+ pending.append(parsed)
89
+ return {"success": True, "clinics": pending, "count": len(pending)}
90
+ except Exception as e:
91
+ return {"success": False, "error": f"Error fetching pending clinics: {str(e)}"}
92
+
93
+ @staticmethod
94
+ def approve_clinic(clinic_id: str) -> Dict:
95
+ """Approve clinic (admin action)"""
96
+ try:
97
+ clinic_resp = supabase.table("clinics").select("id", "user_id", "clinic_name").eq("id", clinic_id).execute()
98
+ if not clinic_resp.data:
99
+ return {"success": False, "error": "Clinic not found"}
100
+
101
+ clinic = clinic_resp.data[0]
102
+ user_id = clinic.get("user_id")
103
+ clinic_name = clinic.get("clinic_name") or "Your clinic"
104
+
105
+ # Fetch current description and strip rejection marker if any
106
+ current = supabase.table("clinics").select("description").eq("id", clinic_id).execute()
107
+ current_desc = current.data[0].get("description") if current.data else ""
108
+ clean_desc = ClinicService._strip_rejection_marker(current_desc)
109
+
110
+ # Update clinic to verified
111
+ supabase.table("clinics").update({"is_verified": True, "description": clean_desc}).eq("id", clinic_id).execute()
112
+
113
+ # Enable corresponding profile (profile is already active via auth, no-op)
114
+ if user_id:
115
+ # Create notification
116
+ ClinicService._create_notification(
117
+ user_id,
118
+ "clinic_approval",
119
+ "Clinic Approved ✅",
120
+ f"Congratulations! {clinic_name} has been approved by the admin. You can now access all features.",
121
+ "clinic",
122
+ "clinic",
123
+ clinic_id
124
+ )
125
+
126
+ return {"success": True, "clinic_id": clinic_id, "message": "Clinic approved successfully!"}
127
+ except Exception as e:
128
+ return {"success": False, "error": f"Error approving clinic: {str(e)}"}
129
+
130
+ @staticmethod
131
+ def reject_clinic(clinic_id: str, reason: Optional[str] = None) -> Dict:
132
+ """Reject clinic (admin action)"""
133
+ try:
134
+ clinic_resp = supabase.table("clinics").select("id", "user_id", "clinic_name").eq("id", clinic_id).execute()
135
+ if not clinic_resp.data:
136
+ return {"success": False, "error": "Clinic not found"}
137
+
138
+ clinic = clinic_resp.data[0]
139
+ user_id = clinic.get("user_id")
140
+ clinic_name = clinic.get("clinic_name") or "Your clinic"
141
+
142
+ # Fetch current description to append rejection marker
143
+ current = supabase.table("clinics").select("description").eq("id", clinic_id).execute()
144
+ current_desc = ""
145
+ if current.data:
146
+ current_desc = ClinicService._strip_rejection_marker(current.data[0].get("description") or "")
147
+
148
+ timestamp = datetime.utcnow().isoformat()
149
+ marker = REJECTION_MARKER
150
+ if reason:
151
+ safe_reason = str(reason).replace("::", "--")
152
+ marker_payload = f"reason={safe_reason}::time={timestamp}"
153
+ new_desc = current_desc + ("\n\n" if current_desc else "") + marker + marker_payload
154
+ else:
155
+ marker_payload = f"time={timestamp}"
156
+ new_desc = current_desc + ("\n\n" if current_desc else "") + marker + marker_payload
157
+
158
+ # Update clinic: set is_verified false and append description marker
159
+ supabase.table("clinics").update({"is_verified": False, "description": new_desc}).eq("id", clinic_id).execute()
160
+
161
+ # Keep associated auth profile active so they can log in, view status, and edit/resubmit profile
162
+ if user_id:
163
+ # Create notification
164
+ rejection_message = f"Your registration for {clinic_name} was rejected by the admin."
165
+ if reason:
166
+ rejection_message += f" Reason: {reason}"
167
+ else:
168
+ rejection_message += " Please check your documents and resubmit."
169
+
170
+ ClinicService._create_notification(
171
+ user_id,
172
+ "clinic_rejection",
173
+ "Clinic Verification Rejected ❌",
174
+ rejection_message,
175
+ "clinic",
176
+ "clinic",
177
+ clinic_id
178
+ )
179
+
180
+ return {
181
+ "success": True,
182
+ "clinic_id": clinic_id,
183
+ "message": "Clinic rejected successfully",
184
+ "reason": reason,
185
+ "rejected_at": timestamp
186
+ }
187
+ except Exception as e:
188
+ return {"success": False, "error": f"Error rejecting clinic: {str(e)}"}
189
+
190
+ # Private helper methods
191
+ @staticmethod
192
+ def _parse_clinic_status(clinic: dict) -> dict:
193
+ clinic_copy = dict(clinic)
194
+ desc = clinic_copy.get("description") or ""
195
+ is_rejected = False
196
+ rejection_reason = None
197
+ rejected_at = None
198
+
199
+ if REJECTION_MARKER in desc:
200
+ try:
201
+ marker_payload = desc.split(REJECTION_MARKER, 1)[1].split("::")
202
+ kv = {}
203
+ for part in marker_payload:
204
+ if "=" in part:
205
+ key, value = part.split("=", 1)
206
+ kv[key] = value
207
+ rejection_reason = kv.get("reason")
208
+ rejected_at = kv.get("time")
209
+ is_rejected = True
210
+ except Exception:
211
+ is_rejected = False
212
+
213
+ if clinic_copy.get("is_verified"):
214
+ verification_status = "approved"
215
+ is_rejected = False
216
+ rejection_reason = None
217
+ rejected_at = None
218
+ elif is_rejected:
219
+ verification_status = "rejected"
220
+ else:
221
+ verification_status = "pending"
222
+
223
+ clinic_copy["is_rejected"] = is_rejected
224
+ clinic_copy["rejection_reason"] = rejection_reason
225
+ clinic_copy["rejected_at"] = rejected_at
226
+ clinic_copy["verification_status"] = verification_status
227
+ return clinic_copy
228
+
229
+ @staticmethod
230
+ def _strip_rejection_marker(description: Optional[str]) -> str:
231
+ if not description:
232
+ return ""
233
+ if REJECTION_MARKER not in description:
234
+ return description.strip()
235
+ return description.split(REJECTION_MARKER, 1)[0].rstrip()
236
+
237
+ @staticmethod
238
+ def _create_notification(user_id: str, type_: str, title: str, message: str, role: str, entity_type: str, entity_id: str):
239
+ payload = {
240
+ "user_id": user_id,
241
+ "user_role": role,
242
+ "type": type_,
243
+ "title": title,
244
+ "message": message,
245
+ "entity_type": entity_type,
246
+ "entity_id": entity_id,
247
+ "is_read": False,
248
+ "created_at": datetime.utcnow().isoformat(),
249
+ }
250
+ supabase.table("notifications").insert(payload).execute()
backend/services/pet_service.py ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, Optional
2
+ import time
3
+ from chatbot.supabase_config import supabase, SupabaseStorage
4
+
5
+ class PetService:
6
+ """Service for managing pets and their vaccine records"""
7
+
8
+ @staticmethod
9
+ def add_pet(
10
+ user_id: str,
11
+ name: str,
12
+ pet_type: str,
13
+ breed: str,
14
+ date_of_birth: str,
15
+ weight: float,
16
+ weight_unit: str = "kg",
17
+ gender: Optional[str] = None,
18
+ blood_type: Optional[str] = None,
19
+ allergies: Optional[str] = None,
20
+ medical_conditions: Optional[str] = None,
21
+ notes: Optional[str] = None,
22
+ profile_image_url: Optional[str] = None,
23
+ ) -> Dict:
24
+ """Add new pet"""
25
+ try:
26
+ pet_data = {
27
+ "user_id": user_id,
28
+ "name": name,
29
+ "type": pet_type,
30
+ "breed": breed,
31
+ "date_of_birth": date_of_birth,
32
+ "weight": weight,
33
+ "weight_unit": weight_unit,
34
+ "gender": gender,
35
+ "blood_type": blood_type,
36
+ "allergies": allergies,
37
+ "medical_conditions": medical_conditions,
38
+ "notes": notes,
39
+ "profile_image_url": profile_image_url,
40
+ }
41
+
42
+ response = supabase.table("pets").insert(pet_data).execute()
43
+ if not response.data:
44
+ return {"success": False, "error": "Failed to create pet"}
45
+
46
+ pet = response.data[0]
47
+ return {
48
+ "success": True,
49
+ "pet_id": pet["id"],
50
+ "pet_name": pet["name"],
51
+ "message": "Pet added successfully!"
52
+ }
53
+ except Exception as e:
54
+ return {
55
+ "success": False,
56
+ "error": f"Error adding pet: {str(e)}"
57
+ }
58
+
59
+ @staticmethod
60
+ def get_user_pets(user_id: str) -> Dict:
61
+ """Get all pets for a user"""
62
+ try:
63
+ response = supabase.table("pets").select("*").eq("user_id", user_id).execute()
64
+ return {
65
+ "success": True,
66
+ "pets": response.data or [],
67
+ "count": len(response.data or [])
68
+ }
69
+ except Exception as e:
70
+ return {
71
+ "success": False,
72
+ "error": f"Error fetching pets: {str(e)}"
73
+ }
74
+
75
+ @staticmethod
76
+ def update_pet(pet_id: str, updates: Dict) -> Dict:
77
+ """Update pet details"""
78
+ try:
79
+ response = supabase.table("pets").update(updates).eq("id", pet_id).execute()
80
+ if not response.data:
81
+ return {
82
+ "success": False,
83
+ "error": "Pet not found or not updated"
84
+ }
85
+ return {
86
+ "success": True,
87
+ "pet": response.data[0],
88
+ "message": "Pet updated successfully!"
89
+ }
90
+ except Exception as e:
91
+ return {
92
+ "success": False,
93
+ "error": f"Error updating pet: {str(e)}"
94
+ }
95
+
96
+ @staticmethod
97
+ def upload_vaccine_record(
98
+ pet_id: str,
99
+ file_data: bytes,
100
+ file_name: str,
101
+ file_type: str,
102
+ uploaded_by: str,
103
+ upload_date: Optional[str] = None,
104
+ ) -> Dict:
105
+ """Upload vaccine record file"""
106
+ try:
107
+ from datetime import datetime
108
+ if not upload_date:
109
+ upload_date = datetime.now().strftime("%Y-%m-%d")
110
+
111
+ file_path = f"vaccine-records/{pet_id}/{int(time.time())}-{file_name}"
112
+
113
+ # Upload to storage
114
+ SupabaseStorage.ensure_bucket("vaccine-documents", public=False)
115
+ supabase.storage.from_("vaccine-documents").upload(
116
+ file=file_data,
117
+ path=file_path,
118
+ file_options={
119
+ "content-type": "application/pdf" if file_type == "pdf" else "image/jpeg",
120
+ "upsert": "false",
121
+ },
122
+ )
123
+
124
+ # Save record to database
125
+ record_data = {
126
+ "pet_id": pet_id,
127
+ "file_name": file_name,
128
+ "file_url": file_path,
129
+ "file_type": file_type,
130
+ "file_size": len(file_data),
131
+ "upload_date": upload_date,
132
+ "uploaded_by": uploaded_by,
133
+ }
134
+
135
+ response = supabase.table("vaccine_records").insert(record_data).execute()
136
+ if not response.data:
137
+ return {"success": False, "error": "Failed to save vaccine record"}
138
+
139
+ return {
140
+ "success": True,
141
+ "record_id": response.data[0]["id"],
142
+ "file_url": file_path,
143
+ "message": "Vaccine record uploaded successfully!"
144
+ }
145
+ except Exception as e:
146
+ return {
147
+ "success": False,
148
+ "error": f"Error uploading vaccine record: {str(e)}"
149
+ }
150
+
151
+ @staticmethod
152
+ def get_pet_vaccine_records(pet_id: str) -> Dict:
153
+ """Get all vaccine records for a pet"""
154
+ try:
155
+ response = supabase.table("vaccine_records").select("*").eq("pet_id", pet_id).execute()
156
+ return {
157
+ "success": True,
158
+ "records": response.data or [],
159
+ "count": len(response.data or [])
160
+ }
161
+ except Exception as e:
162
+ return {
163
+ "success": False,
164
+ "error": f"Error fetching vaccine records: {str(e)}"
165
+ }
backend/services/predictor.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from backend.core.config import DEVICE
3
+
4
+ def predict(model, class_names, tensor):
5
+ tensor = tensor.to(DEVICE)
6
+
7
+ with torch.no_grad():
8
+ outputs = model(tensor)
9
+ probs = torch.nn.functional.softmax(outputs, dim=1)
10
+ conf, pred = torch.max(probs, 1)
11
+
12
+ return {
13
+ "class": class_names[pred.item()],
14
+ "confidence": float(conf.item())
15
+ }
backend/services/router.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def route_prediction(app, animal, disease_type, tensor):
2
+
3
+ if animal == "dog" and disease_type == "skin":
4
+ model, classes = app.state.dog_skin
5
+ elif animal == "dog" and disease_type == "eye":
6
+ model, classes = app.state.dog_eye
7
+ elif animal == "cat" and disease_type == "skin":
8
+ model, classes = app.state.cat_skin
9
+ else:
10
+ return {"error": "Invalid input"}
11
+
12
+ from backend.services.predictor import predict
13
+ return predict(model, classes, tensor)
backend/utils/__pycache__/image.cpython-313.pyc ADDED
Binary file (894 Bytes). View file
 
backend/utils/image.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from PIL import Image
2
+ import torchvision.transforms as transforms
3
+
4
+ transform = transforms.Compose([
5
+ transforms.Resize((224,224)),
6
+ transforms.ToTensor(),
7
+ transforms.Normalize([0.485,0.456,0.406],
8
+ [0.229,0.224,0.225])
9
+ ])
10
+
11
+ def preprocess(image_bytes):
12
+ image = Image.open(image_bytes).convert("RGB")
13
+ return transform(image).unsqueeze(0)
weights/cat_skin_model.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3f454a8526359edd258af304d0f62c8e6e3931232ef96fcd4e69d4cd2d2176ff
3
+ size 94385321
weights/dog_eye_model.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6e9db83fc5e31b46612e23fac2b3fc104f60e5987b96a1b5002e44ec65145aaa
3
+ size 94393187
weights/dog_eye_model_ResNet_NEW.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ffef2d414b30090a756ab3d77be1b9ab2dc7ce82155a63449c046f34afcdcfcf
3
+ size 94396901
weights/dog_skin_model.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:649617acc0d788d10bebe214af294faf447fce5258bac44f3c1b8e4b6f05b29d
3
+ size 94401769