feat: Add Findagram and FindADispo consumer frontends
- Add findagram.co React frontend with product search, brands, categories - Add findadispo.com React frontend with dispensary locator - Wire findagram to backend /api/az/* endpoints - Update category/brand links to route to /products with filters - Add k8s manifests for both frontends - Add multi-domain user support migrations 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
234
findadispo/backend/routes/alerts_routes.py
Normal file
234
findadispo/backend/routes/alerts_routes.py
Normal file
@@ -0,0 +1,234 @@
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from pydantic import BaseModel
|
||||
|
||||
from database import get_db
|
||||
from auth import get_current_active_user
|
||||
from models import User, PriceAlert
|
||||
|
||||
router = APIRouter(prefix="/alerts", tags=["Price Alerts"])
|
||||
|
||||
|
||||
class AlertCreate(BaseModel):
|
||||
product_name: str
|
||||
dispensary_id: Optional[int] = None
|
||||
dispensary_name: Optional[str] = None
|
||||
target_price: float
|
||||
current_price: Optional[float] = None
|
||||
|
||||
|
||||
class AlertUpdate(BaseModel):
|
||||
target_price: Optional[float] = None
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
|
||||
class AlertResponse(BaseModel):
|
||||
id: int
|
||||
product_name: str
|
||||
dispensary_id: Optional[int]
|
||||
dispensary_name: Optional[str]
|
||||
target_price: float
|
||||
current_price: Optional[float]
|
||||
is_active: bool
|
||||
is_triggered: bool
|
||||
created_at: str
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
@router.get("/", response_model=List[AlertResponse])
|
||||
def get_alerts(
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get all price alerts for the current user"""
|
||||
alerts = db.query(PriceAlert).filter(
|
||||
PriceAlert.user_id == current_user.id
|
||||
).order_by(PriceAlert.created_at.desc()).all()
|
||||
|
||||
return [
|
||||
AlertResponse(
|
||||
id=a.id,
|
||||
product_name=a.product_name,
|
||||
dispensary_id=a.dispensary_id,
|
||||
dispensary_name=a.dispensary_name,
|
||||
target_price=a.target_price,
|
||||
current_price=a.current_price,
|
||||
is_active=a.is_active,
|
||||
is_triggered=a.is_triggered,
|
||||
created_at=a.created_at.isoformat() if a.created_at else ""
|
||||
)
|
||||
for a in alerts
|
||||
]
|
||||
|
||||
|
||||
@router.post("/", response_model=AlertResponse, status_code=status.HTTP_201_CREATED)
|
||||
def create_alert(
|
||||
alert_data: AlertCreate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Create a new price alert"""
|
||||
alert = PriceAlert(
|
||||
user_id=current_user.id,
|
||||
product_name=alert_data.product_name,
|
||||
dispensary_id=alert_data.dispensary_id,
|
||||
dispensary_name=alert_data.dispensary_name,
|
||||
target_price=alert_data.target_price,
|
||||
current_price=alert_data.current_price,
|
||||
is_triggered=alert_data.current_price and alert_data.current_price <= alert_data.target_price
|
||||
)
|
||||
db.add(alert)
|
||||
db.commit()
|
||||
db.refresh(alert)
|
||||
|
||||
return AlertResponse(
|
||||
id=alert.id,
|
||||
product_name=alert.product_name,
|
||||
dispensary_id=alert.dispensary_id,
|
||||
dispensary_name=alert.dispensary_name,
|
||||
target_price=alert.target_price,
|
||||
current_price=alert.current_price,
|
||||
is_active=alert.is_active,
|
||||
is_triggered=alert.is_triggered,
|
||||
created_at=alert.created_at.isoformat() if alert.created_at else ""
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{alert_id}", response_model=AlertResponse)
|
||||
def get_alert(
|
||||
alert_id: int,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get a specific price alert"""
|
||||
alert = db.query(PriceAlert).filter(
|
||||
PriceAlert.id == alert_id,
|
||||
PriceAlert.user_id == current_user.id
|
||||
).first()
|
||||
|
||||
if not alert:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Alert not found"
|
||||
)
|
||||
|
||||
return AlertResponse(
|
||||
id=alert.id,
|
||||
product_name=alert.product_name,
|
||||
dispensary_id=alert.dispensary_id,
|
||||
dispensary_name=alert.dispensary_name,
|
||||
target_price=alert.target_price,
|
||||
current_price=alert.current_price,
|
||||
is_active=alert.is_active,
|
||||
is_triggered=alert.is_triggered,
|
||||
created_at=alert.created_at.isoformat() if alert.created_at else ""
|
||||
)
|
||||
|
||||
|
||||
@router.put("/{alert_id}", response_model=AlertResponse)
|
||||
def update_alert(
|
||||
alert_id: int,
|
||||
alert_update: AlertUpdate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Update a price alert"""
|
||||
alert = db.query(PriceAlert).filter(
|
||||
PriceAlert.id == alert_id,
|
||||
PriceAlert.user_id == current_user.id
|
||||
).first()
|
||||
|
||||
if not alert:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Alert not found"
|
||||
)
|
||||
|
||||
update_data = alert_update.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(alert, field, value)
|
||||
|
||||
# Check if alert should be triggered
|
||||
if alert.current_price and alert.current_price <= alert.target_price:
|
||||
alert.is_triggered = True
|
||||
|
||||
db.commit()
|
||||
db.refresh(alert)
|
||||
|
||||
return AlertResponse(
|
||||
id=alert.id,
|
||||
product_name=alert.product_name,
|
||||
dispensary_id=alert.dispensary_id,
|
||||
dispensary_name=alert.dispensary_name,
|
||||
target_price=alert.target_price,
|
||||
current_price=alert.current_price,
|
||||
is_active=alert.is_active,
|
||||
is_triggered=alert.is_triggered,
|
||||
created_at=alert.created_at.isoformat() if alert.created_at else ""
|
||||
)
|
||||
|
||||
|
||||
@router.patch("/{alert_id}/toggle")
|
||||
def toggle_alert(
|
||||
alert_id: int,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Toggle alert active status"""
|
||||
alert = db.query(PriceAlert).filter(
|
||||
PriceAlert.id == alert_id,
|
||||
PriceAlert.user_id == current_user.id
|
||||
).first()
|
||||
|
||||
if not alert:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Alert not found"
|
||||
)
|
||||
|
||||
alert.is_active = not alert.is_active
|
||||
db.commit()
|
||||
return {"message": f"Alert {'activated' if alert.is_active else 'deactivated'}"}
|
||||
|
||||
|
||||
@router.delete("/{alert_id}")
|
||||
def delete_alert(
|
||||
alert_id: int,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Delete a price alert"""
|
||||
alert = db.query(PriceAlert).filter(
|
||||
PriceAlert.id == alert_id,
|
||||
PriceAlert.user_id == current_user.id
|
||||
).first()
|
||||
|
||||
if not alert:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Alert not found"
|
||||
)
|
||||
|
||||
db.delete(alert)
|
||||
db.commit()
|
||||
return {"message": "Alert deleted"}
|
||||
|
||||
|
||||
@router.get("/stats/summary")
|
||||
def get_alert_stats(
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get alert statistics for dashboard"""
|
||||
alerts = db.query(PriceAlert).filter(
|
||||
PriceAlert.user_id == current_user.id
|
||||
).all()
|
||||
|
||||
return {
|
||||
"total": len(alerts),
|
||||
"active": len([a for a in alerts if a.is_active]),
|
||||
"triggered": len([a for a in alerts if a.is_triggered]),
|
||||
}
|
||||
Reference in New Issue
Block a user