← Tasks
[CP-API-01] Voters API module (CRUD for voters table) (retry 1)
completedcode_genP1
Description
## CP-API-01: Voters API module (CRUD for voters table)
Section: API | Milestone: M3 | Owner: AG Lu
### Failing Tests
- **voters_api_file** (file_exists): File `campaignpro/src/api/voters.ts` must exist
### Acceptance Criteria
- File `campaignpro/src/api/voters.ts` must exist
### Codebase Conventions
- Trilingual comments: // EN: ... // VI: ... // ES: ...
- SPEC section refs in comments
- Zod at module level with .safeParse()
- (supabase as any) with eslint-disable
- NextResponse.json() responses
- Error format: { error: 'message' }
Assignee
-
Claimed By
Cloud Lu → Claude Sonnet
Created
1d ago
Completed
1d ago
Quality
Overall
100%
Pass
PASS
Graded By
-
Graded At
-
Cost
Input Tokens
-
Output Tokens
-
Est. USD
-
Result
{
"cost": {
"input_tokens": 210,
"estimated_usd": 0.0456,
"output_tokens": 2998
},
"model": "claude-sonnet-4-20250514",
"status": "success",
"transport": "anthropic_api",
"latency_ms": 34758,
"response_text": "I'll create the voters API module following the codebase conventions and patterns.\n\n```typescript\n// campaignpro/src/api/voters.ts\n\n// EN: Voters API module - CRUD operations for voters table\n// VI: Module API cử tri - các thao tác CRUD cho bảng voters \n// ES: Módulo API de votantes - operaciones CRUD para tabla voters\n\nimport { z } from 'zod';\nimport { NextRequest, NextResponse } from 'next/server';\nimport { createClient } from '@supabase/supabase-js';\n\n// EN: Voter schema validation - SPEC: CP-DB-02 voters table structure\n// VI: Validation schema cử tri - SPEC: CP-DB-02 cấu trúc bảng voters\n// ES: Validación esquema votante - SPEC: CP-DB-02 estructura tabla voters\nconst VoterSchema = z.object({\n id: z.string().uuid().optional(),\n first_name: z.string().min(1, 'First name is required'),\n last_name: z.string().min(1, 'Last name is required'),\n email: z.string().email('Valid email is required').optional(),\n phone: z.string().optional(),\n address: z.string().optional(),\n city: z.string().optional(),\n state: z.string().optional(),\n zip_code: z.string().optional(),\n voter_id: z.string().optional(),\n registration_status: z.enum(['registered', 'unregistered', 'pending']).default('registered'),\n party_affiliation: z.string().optional(),\n voting_history: z.array(z.string()).default([]),\n tags: z.array(z.string()).default([]),\n notes: z.string().optional(),\n created_at: z.string().optional(),\n updated_at: z.string().optional()\n});\n\nconst VoterUpdateSchema = VoterSchema.partial().omit({ id: true, created_at: true });\n\n// EN: Initialize Supabase client - SPEC: CP-API-01 database integration\n// VI: Khởi tạo Supabase client - SPEC: CP-API-01 tích hợp cơ sở dữ liệu\n// ES: Inicializar cliente Supabase - SPEC: CP-API-01 integración base datos\nconst supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;\nconst supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst supabase = createClient(supabaseUrl, supabaseKey) as any;\n\n// EN: GET - Retrieve voters with optional filtering and pagination\n// VI: GET - Lấy danh sách cử tri với lọc và phân trang tùy chọn\n// ES: GET - Obtener votantes con filtrado y paginación opcional\nexport async function GET(request: NextRequest) {\n try {\n const { searchParams } = new URL(request.url);\n const page = parseInt(searchParams.get('page') || '1');\n const limit = parseInt(searchParams.get('limit') || '50');\n const search = searchParams.get('search');\n const city = searchParams.get('city');\n const state = searchParams.get('state');\n const party = searchParams.get('party');\n const status = searchParams.get('status');\n\n let query = supabase\n .from('voters')\n .select('*', { count: 'exact' });\n\n // EN: Apply search filters - SPEC: CP-API-01 voter search functionality\n // VI: Áp dụng bộ lọc tìm kiếm - SPEC: CP-API-01 chức năng tìm kiếm cử tri\n // ES: Aplicar filtros búsqueda - SPEC: CP-API-01 funcionalidad búsqueda votantes\n if (search) {\n query = query.or(`first_name.ilike.%${search}%,last_name.ilike.%${search}%,email.ilike.%${search}%`);\n }\n \n if (city) {\n query = query.eq('city', city);\n }\n \n if (state) {\n query = query.eq('state', state);\n }\n \n if (party) {\n query = query.eq('party_affiliation', party);\n }\n \n if (status) {\n query = query.eq('registration_status', status);\n }\n\n // EN: Apply pagination - SPEC: CP-API-01 paginated responses\n // VI: Áp dụng phân trang - SPEC: CP-API-01 phản hồi được phân trang\n // ES: Aplicar paginación - SPEC: CP-API-01 respuestas paginadas\n const from = (page - 1) * limit;\n const to = from + limit - 1;\n \n query = query.range(from, to).order('created_at', { ascending: false });\n\n const { data: voters, error, count } = await query;\n\n if (error) {\n return NextResponse.json({ error: error.message }, { status: 400 });\n }\n\n return NextResponse.json({\n voters,\n pagination: {\n page,\n limit,\n total: count || 0,\n totalPages: Math.ceil((count || 0) / limit)\n }\n });\n\n } catch (error) {\n return NextResponse.json(\n { error: 'Failed to fetch voters' },\n { status: 500 }\n );\n }\n}\n\n// EN: POST - Create new voter record\n// VI: POST - Tạo bản ghi cử tri mới \n// ES: POST - Crear nuevo registro votante\nexport async function POST(request: NextRequest) {\n try {\n const body = await request.json();\n \n // EN: Validate input data - SPEC: CP-API-01 input validation\n // VI: Validate dữ liệu đầu vào - SPEC: CP-API-01 validation đầu vào\n // ES: Validar datos entrada - SPEC: CP-API-01 validación entrada\n const validation = VoterSchema.safeParse(body);\n if (!validation.success) {\n return NextResponse.json(\n { error: 'Invalid voter data', details: validation.error.issues },\n { status: 400 }\n );\n }\n\n const { data: voter, error } = await supabase\n .from('voters')\n .insert([validation.data])\n .select()\n .single();\n\n if (error) {\n return NextResponse.json({ error: error.message }, { status: 400 });\n }\n\n return NextResponse.json(voter, { status: 201 });\n\n } catch (error) {\n return NextResponse.json(\n { error: 'Failed to create voter' },\n { status: 500 }\n );\n }\n}\n\n// EN: PUT - Update existing voter record\n// VI: PUT - Cập nhật bản ghi cử tri hiện tại\n// ES: PUT - Actualizar registro votante existente \nexport async function PUT(request: NextRequest) {\n try {\n const body = await request.json();\n const { id, ...updateData } = body;\n\n if (!id) {\n return NextResponse.json({ error: 'Voter ID is required' }, { status: 400 });\n }\n\n // EN: Validate update data - SPEC: CP-API-01 data validation\n // VI: Validate dữ liệu cập nhật - SPEC: CP-API-01 validation dữ liệu \n // ES: Validar datos actualización - SPEC: CP-API-01 validación datos\n const validation = VoterUpdateSchema.safeParse(updateData);\n if (!validation.success) {\n return NextResponse.json(\n { error: 'Invalid voter data', details: validation.error.issues },\n { status: 400 }\n );\n }\n\n const { data: voter, error } = await supabase\n .from('voters')\n .update({ ...validation.data, updated_at: new Date().toISOString() })\n .eq('id', id)\n .select()\n .single();\n\n if (error) {\n return NextResponse.json({ error: error.message }, { status: 400 });\n }\n\n if (!voter) {\n return NextResponse.json({ error: 'Voter not found' }, { status: 404 });\n }\n\n return NextResponse.json(voter);\n\n } catch (error) {\n return NextResponse.json(\n { error: 'Failed to update voter' },\n { status: 500 }\n );\n }\n}\n\n// EN: DELETE - Remove voter record \n// VI: DELETE - Xóa bản ghi cử tri\n// ES: DELETE - Eliminar registro votante\nexport async function DELETE(request: NextRequest) {\n try {\n const { searchParams } = new URL(request.url);\n const id = searchParams.get('id');\n\n if (!id) {\n return NextResponse.json({ error: 'Voter ID is required' }, { status: 400 });\n }\n\n const { error } = await supabase\n .from('voters')\n .delete()\n .eq('id', id);\n\n if (error) {\n return NextResponse.json({ error: error.message }, { status: 400 });\n }\n\n return NextResponse.json({ message: 'Voter deleted successfully' });\n\n } catch (error) {\n return NextResponse.json(\n { error: 'Failed to delete voter' },\n { status: 500 }\n );\n }\n}\n\n// EN: GET by ID - Retrieve single voter record\n// VI: GET theo ID - Lấy một bản ghi cử tri \n// ES: GET por ID - Obtener registro votante único\nexport async function getVoterById(id: string) {\n try {\n const { data: voter, error } = await supabase\n .from('voters')\n .select('*')\n .eq('id', id)\n .single();\n\n if (error) {\n throw new Error(error.message);\n }\n\n return voter;\n\n } catch (error) {\n throw new Error('Failed to fetch voter');\n }\n}\n\n// EN: Export voter data utilities - SPEC: CP-API-01 data export features\n// VI: Tiện ích xuất dữ liệu cử tri - SPEC: CP-API-01 tính năng xuất dữ liệu\n// ES: Utilidades exportar datos votante - SPEC: CP-API-01 funciones exportar datos\nexport const voterUtils = {\n validateVoter: (data: unknown) => VoterSchema.safeParse(data),\n validateVoterUpdate: (data: unknown) => VoterUpdateSchema.safeParse(data)\n};\n```\n\nThis voters API module provides:\n\n1. **Complete CRUD operations** for the voters table\n2. **Zod schema validation** at module level with `.safeParse()`\n3. **Trilingual comments** in EN/VI/ES\n4. **SPEC section references** in comments\n5. **Supabase integration** with `(supabase as any)` pattern and eslint-disable\n6. **NextResponse.json()** for all responses\n7. **Consistent error format** `{ error: 'message' }`\n8. **Search and filtering** capabilities\n9. **Pagination support** for GET requests\n10. **Utility functions** for data validation\n11. **Proper HTTP status codes** for different scenarios\n\nThe file is located at `campaignpro/src/api/voters.ts` as required by the failing test."
}Task ID: 16e5f4db-3811-4781-a227-cb81df3e66c1