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:
Kelly
2025-12-05 16:10:15 -07:00
parent d120a07ed7
commit a0f8d3911c
179 changed files with 140234 additions and 600 deletions

View 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]),
}