File size: 11,239 Bytes
6ce841a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
"""
Verification endpoints for OpenCouncil.

All endpoints require JWT authentication via Supabase Auth.
Volunteers can view pending summaries, start verification sessions,
approve or reject summaries, and track their hours.
"""

import asyncio
import logging
from datetime import datetime, timezone
from typing import Optional

from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, Field

from api.auth import get_current_volunteer
from models.schemas import Summary, Volunteer
from social.pipeline import run_social_pipeline
from storage import (
    complete_verification_session,
    create_verification_session,
    get_minutes,
    get_summary,
    get_volunteer_sessions,
    list_pending_summaries,
    update_summary_status,
    update_summary_social,
    update_volunteer_hours,
)

logger = logging.getLogger(__name__)

router = APIRouter(prefix="/api/verify", tags=["verify"])


# --- Request/Response Models ---


class StartSessionResponse(BaseModel):
    session_id: str


class ApproveRequest(BaseModel):
    session_id: str
    edited_summary: Optional[str] = None
    notes: Optional[str] = None


class RejectRequest(BaseModel):
    session_id: str
    reason: str


class SuccessResponse(BaseModel):
    success: bool
    summary_id: str


class HoursResponse(BaseModel):
    total_hours: float
    sessions: list


# --- Endpoints ---


@router.get("/pending")
async def get_pending_summaries(volunteer: Volunteer = Depends(get_current_volunteer)):
    """List all summaries with status='pending'.
    
    Includes the related minute's title, meeting_date, and raw OCR text.
    Requires a valid Bearer token.
    """
    try:
        pending = list_pending_summaries()
        return {"summaries": pending, "count": len(pending)}
    except Exception as e:
        logger.error(f"Failed to list pending summaries: {e}")
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Failed to list pending summaries: {str(e)}",
        )


@router.get("/{summary_id}")
async def get_summary_detail(
    summary_id: str,
    volunteer: Volunteer = Depends(get_current_volunteer),
):
    """Get full detail of a single summary, including raw OCR text.
    
    Requires a valid Bearer token.
    Returns 404 if the summary is not found.
    """
    summary = get_summary(summary_id)
    if not summary:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Summary {summary_id} not found",
        )

    return {
        "id": summary.id,
        "minutes_id": summary.minutes_id,
        "summary": summary.summary,
        "key_decisions": summary.key_decisions,
        "budget_items": summary.budget_items,
        "public_comment_opportunities": summary.public_comment_opportunities,
        "items": summary.items,
        "big_picture": summary.big_picture,
        "what_you_can_do": summary.what_you_can_do,
        "category": summary.category,
        "neighborhood_impact": summary.neighborhood_impact,
        "status": summary.status,
        "created_at": summary.created_at.isoformat() if summary.created_at else None,
    }


@router.post("/{summary_id}/start", response_model=StartSessionResponse)
async def start_verification_session(
    summary_id: str,
    volunteer: Volunteer = Depends(get_current_volunteer),
):
    """Start a new verification session for a summary.
    
    Creates a new verification_sessions row with started_at = NOW().
    Requires a valid Bearer token.
    Returns 404 if the summary is not found.
    Returns 409 if the summary is not in 'pending' status.
    """
    summary = get_summary(summary_id)
    if not summary:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Summary {summary_id} not found",
        )

    if summary.status != "pending":
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=f"Summary is already {summary.status}. Only pending summaries can be verified.",
        )

    try:
        session_id = create_verification_session(
            volunteer_id=volunteer.user_id,
            summary_id=summary_id,
        )
        logger.info(
            f"Verification session started: volunteer={volunteer.user_id}, "
            f"summary={summary_id}, session={session_id}"
        )
        return StartSessionResponse(session_id=str(session_id))
    except Exception as e:
        logger.error(f"Failed to create verification session: {e}")
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Failed to start verification session: {str(e)}",
        )


@router.post("/{summary_id}/approve", response_model=SuccessResponse)
async def approve_summary(
    summary_id: str,
    request: ApproveRequest,
    volunteer: Volunteer = Depends(get_current_volunteer),
):
    """Approve a summary after verification.
    
    Updates the verification session with ended_at, duration_seconds,
    and action='approved'. Updates the summary with status='verified',
    verified_by, and verified_at. If edited_summary is provided, updates
    the summary text. Triggers the social media pipeline as a background
    task (card generation, storage upload, Facebook/Instagram posting).
    
    Requires a valid Bearer token.
    Returns 404 if the summary or session is not found.
    """
    summary = get_summary(summary_id)
    if not summary:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Summary {summary_id} not found",
        )

    if summary.status != "pending":
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=f"Summary is already {summary.status}. Only pending summaries can be approved.",
        )

    try:
        # Complete the verification session
        complete_verification_session(
            session_id=request.session_id,
            action="approved",
            notes=request.notes,
        )

        # Update the summary status
        now = datetime.now(timezone.utc)
        update_summary_status(
            summary_id=summary_id,
            status="verified",
            verified_by=volunteer.user_id,
            verified_at=now,
            edited_summary=request.edited_summary,
        )

        # Add volunteer hours (e.g., 0.5 hours per verification)
        update_volunteer_hours(volunteer.user_id, 0.5)

        # Fire social pipeline as a background task (non-blocking)
        async def _post_to_social():
            try:
                # Fetch minutes to get city and meeting_date
                minutes = get_minutes(summary.minutes_id)
                city = f"{minutes.city}, {minutes.state}" if minutes else "Paris, Texas"
                meeting_date = minutes.meeting_date.isoformat() if minutes and minutes.meeting_date else ""

                result = await run_social_pipeline(
                    summary_id=str(summary_id),
                    headline=summary.neighborhood_impact or "",
                    theme=summary.category or "City Council",
                    summary_text=(summary.summary or "")[:200],
                    city=city,
                    meeting_date=meeting_date,
                )
                if result["success"]:
                    update_summary_social(summary_id, result["image_url"])
                    logger.info(
                        f"Social pipeline completed for summary {summary_id}: "
                        f"fb={result['facebook_post_id']}, ig={result['instagram_media_id']}"
                    )
                else:
                    logger.warning(f"Social pipeline completed with partial/failure for summary {summary_id}")
            except Exception as e:
                logger.error(f"Social pipeline failed for summary {summary_id}: {e}")

        asyncio.create_task(_post_to_social())

        logger.info(
            f"Summary approved: summary={summary_id}, "
            f"volunteer={volunteer.user_id}, session={request.session_id}"
        )

        return SuccessResponse(success=True, summary_id=summary_id)

    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Failed to approve summary: {e}")
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Failed to approve summary: {str(e)}",
        )


@router.post("/{summary_id}/reject", response_model=SuccessResponse)
async def reject_summary(
    summary_id: str,
    request: RejectRequest,
    volunteer: Volunteer = Depends(get_current_volunteer),
):
    """Reject a summary after verification.
    
    Updates the verification session with ended_at, duration_seconds,
    and action='rejected'. Updates the summary with status='rejected'
    and rejection_reason.
    
    Requires a valid Bearer token.
    Returns 404 if the summary or session is not found.
    """
    summary = get_summary(summary_id)
    if not summary:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Summary {summary_id} not found",
        )

    if summary.status != "pending":
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=f"Summary is already {summary.status}. Only pending summaries can be rejected.",
        )

    try:
        # Complete the verification session
        complete_verification_session(
            session_id=request.session_id,
            action="rejected",
            notes=request.reason,
        )

        # Update the summary status
        update_summary_status(
            summary_id=summary_id,
            status="rejected",
            verified_by=volunteer.user_id,
            rejection_reason=request.reason,
        )

        logger.info(
            f"Summary rejected: summary={summary_id}, "
            f"volunteer={volunteer.user_id}, session={request.session_id}, "
            f"reason={request.reason}"
        )

        return SuccessResponse(success=True, summary_id=summary_id)

    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Failed to reject summary: {e}")
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Failed to reject summary: {str(e)}",
        )


@router.get("/hours", response_model=HoursResponse)
async def get_volunteer_hours(volunteer: Volunteer = Depends(get_current_volunteer)):
    """Get the current volunteer's total hours and session history.
    
    Requires a valid Bearer token.
    Returns total_hours and a list of past verification sessions.
    """
    try:
        sessions = get_volunteer_sessions(volunteer.user_id)
        return HoursResponse(
            total_hours=volunteer.hours_earned,
            sessions=sessions,
        )
    except Exception as e:
        logger.error(f"Failed to get volunteer hours: {e}")
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Failed to get volunteer hours: {str(e)}",
        )