From 72e801eca6f7ed36f42e248562813c95741c484e Mon Sep 17 00:00:00 2001 From: chihlasm Date: Wed, 18 Feb 2026 17:43:21 -0500 Subject: [PATCH] feat: add POST /feedback endpoint with DB persistence and dual emails Co-Authored-By: Claude Opus 4.6 --- backend/app/api/endpoints/feedback.py | 88 +++++++++++++++++++++++++++ backend/app/api/router.py | 2 + 2 files changed, 90 insertions(+) create mode 100644 backend/app/api/endpoints/feedback.py diff --git a/backend/app/api/endpoints/feedback.py b/backend/app/api/endpoints/feedback.py new file mode 100644 index 00000000..a642071c --- /dev/null +++ b/backend/app/api/endpoints/feedback.py @@ -0,0 +1,88 @@ +import logging +from typing import Annotated + +from fastapi import APIRouter, Depends, HTTPException, Request, status +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select + +from app.api.deps import get_current_active_user +from app.core.config import settings +from app.core.database import get_db +from app.core.email import EmailService +from app.core.rate_limit import limiter +from app.models.user import User +from app.models.account import Account +from app.models.feedback import Feedback +from app.schemas.feedback import FeedbackSubmission, FeedbackResponse + +logger = logging.getLogger(__name__) + +router = APIRouter(tags=["feedback"]) + +# TODO: Post-session contextual feedback prompt — when building the post-session +# feedback flow, reuse this endpoint by adding optional session_id/tree_id fields +# to FeedbackSubmission. The Feedback model and email infrastructure are already +# in place. See design doc for details. + + +@router.post("/feedback", response_model=FeedbackResponse) +@limiter.limit("1/minute") +async def submit_feedback( + request: Request, + data: FeedbackSubmission, + current_user: Annotated[User, Depends(get_current_active_user)], + db: Annotated[AsyncSession, Depends(get_db)], +): + """Submit user feedback. Saves to DB and sends notification email.""" + if not settings.FEEDBACK_EMAIL: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="Feedback submission is not configured", + ) + + # Get account info for the email + account_name = None + account_code = None + if current_user.account_id: + result = await db.execute( + select(Account).where(Account.id == current_user.account_id) + ) + account = result.scalar_one_or_none() + if account: + account_name = account.name + account_code = account.display_code + + # Always persist to DB first — email failure should not lose feedback + feedback_record = Feedback( + account_id=current_user.account_id, + user_id=current_user.id, + email=data.email, + feedback_type=data.feedback_type.value, + message=data.message, + ) + db.add(feedback_record) + await db.commit() + + # Send notification email to admin (best-effort) + sent = await EmailService.send_feedback_email( + to_email=settings.FEEDBACK_EMAIL, + reply_to_email=data.email, + feedback_type=data.feedback_type.value, + message=data.message, + user_email=current_user.email, + account_name=account_name, + account_code=account_code, + ) + + if not sent: + logger.warning("Feedback saved to DB but notification email failed for user %s", current_user.email) + + # Send confirmation email to submitter (fire-and-forget) + message_preview = data.message[:100] + ("..." if len(data.message) > 100 else "") + await EmailService.send_feedback_confirmation_email( + to_email=data.email, + feedback_type=data.feedback_type.value, + message_preview=message_preview, + ) + + return FeedbackResponse(success=True, message="Thank you! Your feedback has been submitted.") diff --git a/backend/app/api/router.py b/backend/app/api/router.py index 08580d80..3aac1d7f 100644 --- a/backend/app/api/router.py +++ b/backend/app/api/router.py @@ -4,6 +4,7 @@ from app.api.endpoints import admin_dashboard, admin_audit, admin_plan_limits, a from app.api.endpoints import ratings, analytics from app.api.endpoints import target_lists from app.api.endpoints import maintenance_schedules +from app.api.endpoints import feedback api_router = APIRouter() @@ -32,3 +33,4 @@ api_router.include_router(ratings.router) api_router.include_router(analytics.router) api_router.include_router(target_lists.router) api_router.include_router(maintenance_schedules.router) +api_router.include_router(feedback.router)