Service layer (production code): - branch_manager: set account_id on SessionBranch (root + fork) and ForkPoint from session.account_id; load session in create_fork for this purpose - handoff_manager: set account_id on SessionHandoff from session.account_id - ai_suggestions endpoint: set account_id on AISuggestion from current_user - steps endpoint (/feedback): set account_id on StepRating from current_user - ratings endpoint: set account_id on StepRating from current_user Test infrastructure: - conftest.py: seed PLATFORM_ACCOUNT_ID (00000000-...-0001) account after Base.metadata.create_all so global categories and gallery items have a valid FK - test_rls_isolation: add _ensure_rls_schema fixture that runs 'alembic upgrade head' before module tests — previous function-scoped test_db fixtures drop the schema, leaving the RLS tests with no tables - test_branding: create Account before User in helper functions - test_admin_gallery: set account_id=PLATFORM_ACCOUNT_ID on Tree/ScriptTemplate - test_public_templates: set account_id=PLATFORM_ACCOUNT_ID on Tree, ScriptTemplate, TreeCategory - test_resolution_outputs: set account_id=session.account_id on SessionResolutionOutput - test_analytics_phase5: set account_id on PsaPostLog - test_draft_trees: replace account_id=None with PLATFORM_ACCOUNT_ID in migration default test (NOT NULL now enforced) - test_maintenance_schedules: set account_id on other_tree - test_save_session_as_tree: set account_id on all 5 Session() constructors Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
126 lines
4.3 KiB
Python
126 lines
4.3 KiB
Python
from uuid import UUID
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from sqlalchemy import select, func
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.core.database import get_db
|
|
from app.api.deps import get_current_active_user
|
|
from app.models import User, Session, SessionRating
|
|
from app.models.step_library import StepLibrary, StepRating
|
|
from app.schemas.analytics import SessionRatingCreate, SessionRatingResponse, StepFeedbackCreate
|
|
|
|
router = APIRouter(tags=["ratings"])
|
|
|
|
|
|
@router.post("/sessions/{session_id}/rate", response_model=SessionRatingResponse, status_code=status.HTTP_201_CREATED)
|
|
async def rate_session(
|
|
session_id: UUID,
|
|
data: SessionRatingCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_active_user),
|
|
):
|
|
"""Submit a CSAT rating (1-5) for a completed session."""
|
|
# Verify session exists and belongs to user
|
|
result = await db.execute(select(Session).where(Session.id == session_id))
|
|
session = result.scalar_one_or_none()
|
|
if not session:
|
|
raise HTTPException(status_code=404, detail="Session not found")
|
|
if session.user_id != current_user.id:
|
|
raise HTTPException(status_code=403, detail="Not your session")
|
|
if not session.completed_at:
|
|
raise HTTPException(status_code=400, detail="Session not completed yet")
|
|
|
|
# Check for duplicate
|
|
existing = await db.execute(
|
|
select(SessionRating).where(SessionRating.session_id == session_id)
|
|
)
|
|
if existing.scalar_one_or_none():
|
|
raise HTTPException(status_code=409, detail="Session already rated")
|
|
|
|
rating = SessionRating(
|
|
session_id=session_id,
|
|
user_id=current_user.id,
|
|
tree_id=session.tree_id,
|
|
account_id=current_user.account_id,
|
|
rating=data.rating,
|
|
comment=data.comment,
|
|
)
|
|
db.add(rating)
|
|
await db.commit()
|
|
await db.refresh(rating)
|
|
|
|
return SessionRatingResponse(
|
|
id=str(rating.id),
|
|
session_id=str(rating.session_id),
|
|
rating=rating.rating,
|
|
comment=rating.comment,
|
|
created_at=rating.created_at,
|
|
)
|
|
|
|
|
|
@router.post("/steps/{step_id}/feedback", status_code=status.HTTP_201_CREATED)
|
|
async def submit_step_feedback(
|
|
step_id: UUID,
|
|
data: StepFeedbackCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_active_user),
|
|
):
|
|
"""Submit thumbs up/down feedback for a step used in a session."""
|
|
# Verify step exists
|
|
result = await db.execute(select(StepLibrary).where(StepLibrary.id == step_id))
|
|
step = result.scalar_one_or_none()
|
|
if not step:
|
|
raise HTTPException(status_code=404, detail="Step not found")
|
|
|
|
session_uuid = UUID(data.session_id)
|
|
|
|
# Check for existing feedback for this step+user+session
|
|
existing_result = await db.execute(
|
|
select(StepRating).where(
|
|
StepRating.step_id == step_id,
|
|
StepRating.user_id == current_user.id,
|
|
StepRating.session_id == session_uuid,
|
|
)
|
|
)
|
|
existing = existing_result.scalar_one_or_none()
|
|
|
|
if existing:
|
|
existing.was_helpful = data.was_helpful
|
|
resp_status = "updated"
|
|
else:
|
|
new_rating = StepRating(
|
|
step_id=step_id,
|
|
user_id=current_user.id,
|
|
account_id=current_user.account_id,
|
|
session_id=session_uuid,
|
|
was_helpful=data.was_helpful,
|
|
# rating is nullable now — thumbs-only mode
|
|
)
|
|
db.add(new_rating)
|
|
resp_status = "created"
|
|
|
|
# Update aggregates on step_library
|
|
await _update_step_helpful_counts(db, step_id)
|
|
await db.commit()
|
|
|
|
return {"step_id": str(step_id), "was_helpful": data.was_helpful, "status": resp_status}
|
|
|
|
|
|
async def _update_step_helpful_counts(db: AsyncSession, step_id: UUID):
|
|
"""Recalculate helpful_yes and helpful_no on step_library."""
|
|
yes_q = await db.execute(
|
|
select(func.count()).select_from(StepRating).where(
|
|
StepRating.step_id == step_id, StepRating.was_helpful == True
|
|
)
|
|
)
|
|
no_q = await db.execute(
|
|
select(func.count()).select_from(StepRating).where(
|
|
StepRating.step_id == step_id, StepRating.was_helpful == False
|
|
)
|
|
)
|
|
await db.execute(
|
|
StepLibrary.__table__.update()
|
|
.where(StepLibrary.id == step_id)
|
|
.values(helpful_yes=yes_q.scalar() or 0, helpful_no=no_q.scalar() or 0)
|
|
)
|