- 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>
235 lines
6.6 KiB
Python
235 lines
6.6 KiB
Python
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]),
|
|
}
|