""" Representation Alignment API. Returns how well each followed member's voting record aligns with the current user's bill stances (pocket_veto / pocket_boost). """ from collections import defaultdict from fastapi import APIRouter, Depends from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.core.dependencies import get_current_user from app.database import get_db from app.models import Follow, Member from app.models.user import User from app.models.vote import BillVote, MemberVotePosition router = APIRouter() @router.get("") async def get_alignment( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """ Cross-reference the user's stanced bill follows with how their followed members voted on those same bills. pocket_boost + Yea → aligned pocket_veto + Nay → aligned All other combinations with an actual Yea/Nay vote → opposed Not Voting / Present → excluded from tally """ # 1. Bill follows with a stance bill_follows_result = await db.execute( select(Follow).where( Follow.user_id == current_user.id, Follow.follow_type == "bill", Follow.follow_mode.in_(["pocket_veto", "pocket_boost"]), ) ) bill_follows = bill_follows_result.scalars().all() if not bill_follows: return { "members": [], "total_bills_with_stance": 0, "total_bills_with_votes": 0, } stance_map = {f.follow_value: f.follow_mode for f in bill_follows} # 2. Followed members member_follows_result = await db.execute( select(Follow).where( Follow.user_id == current_user.id, Follow.follow_type == "member", ) ) member_follows = member_follows_result.scalars().all() followed_member_ids = {f.follow_value for f in member_follows} if not followed_member_ids: return { "members": [], "total_bills_with_stance": len(stance_map), "total_bills_with_votes": 0, } # 3. Bulk fetch votes for all stanced bills bill_ids = list(stance_map.keys()) votes_result = await db.execute( select(BillVote).where(BillVote.bill_id.in_(bill_ids)) ) votes = votes_result.scalars().all() if not votes: return { "members": [], "total_bills_with_stance": len(stance_map), "total_bills_with_votes": 0, } vote_ids = [v.id for v in votes] bill_id_by_vote = {v.id: v.bill_id for v in votes} bills_with_votes = len({v.bill_id for v in votes}) # 4. Bulk fetch positions for followed members on those votes positions_result = await db.execute( select(MemberVotePosition).where( MemberVotePosition.vote_id.in_(vote_ids), MemberVotePosition.bioguide_id.in_(followed_member_ids), ) ) positions = positions_result.scalars().all() # 5. Aggregate per member tally: dict[str, dict] = defaultdict(lambda: {"aligned": 0, "opposed": 0}) for pos in positions: if pos.position not in ("Yea", "Nay"): # Skip Not Voting / Present — not a real position signal continue bill_id = bill_id_by_vote.get(pos.vote_id) if not bill_id: continue stance = stance_map.get(bill_id) is_aligned = ( (stance == "pocket_boost" and pos.position == "Yea") or (stance == "pocket_veto" and pos.position == "Nay") ) if is_aligned: tally[pos.bioguide_id]["aligned"] += 1 else: tally[pos.bioguide_id]["opposed"] += 1 if not tally: return { "members": [], "total_bills_with_stance": len(stance_map), "total_bills_with_votes": bills_with_votes, } # 6. Load member details member_ids = list(tally.keys()) members_result = await db.execute( select(Member).where(Member.bioguide_id.in_(member_ids)) ) members = members_result.scalars().all() member_map = {m.bioguide_id: m for m in members} # 7. Build response result = [] for bioguide_id, counts in tally.items(): m = member_map.get(bioguide_id) aligned = counts["aligned"] opposed = counts["opposed"] total = aligned + opposed result.append({ "bioguide_id": bioguide_id, "name": m.name if m else bioguide_id, "party": m.party if m else None, "state": m.state if m else None, "chamber": m.chamber if m else None, "photo_url": m.photo_url if m else None, "effectiveness_percentile": m.effectiveness_percentile if m else None, "aligned": aligned, "opposed": opposed, "total": total, "alignment_pct": round(aligned / total * 100, 1) if total > 0 else None, }) result.sort(key=lambda x: (x["alignment_pct"] is None, -(x["alignment_pct"] or 0))) return { "members": result, "total_bills_with_stance": len(stance_map), "total_bills_with_votes": bills_with_votes, }