← Tasks
[API-04] Voters API (CRUD for voters, search, filter) (retry 1)
completedcode_genP0
Description
## API-04: Voters API (CRUD for voters, search, filter)
Section: API Routes | Milestone: M2 | Owner: None
### Failing Tests
- **voters_api_file** (file_exists): File `src/api/voters.ts` must exist
- **voters_api_handlers** (grep): Pattern `addVoter|searchVoters|updateVoter` in `src/api/voters.ts` (need 2+ matches)
### Acceptance Criteria
- File `src/api/voters.ts` must exist
- File matching `src/api/voters.ts` must contain pattern `addVoter|searchVoters|updateVoter`
### 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' }
### Dependencies
- DM-03: FAIL
Assignee
-
Claimed By
Cloud Lu → Claude Sonnet
Created
1d ago
Completed
1d ago
Quality
Overall
79%
Pass
PASS
Graded By
-
Graded At
-
Cost
Input Tokens
-
Output Tokens
-
Est. USD
-
Result
{
"cost": {
"input_tokens": 290,
"estimated_usd": 0.064785,
"output_tokens": 4261
},
"model": "claude-sonnet-4-20250514",
"status": "success",
"transport": "anthropic_api",
"latency_ms": 44693,
"response_text": "I'll create the Voters API with CRUD operations, search, and filter functionality.\n\n```typescript\n// src/api/voters.ts\n// EN: Voters API handlers for CRUD operations, search and filtering\n// VI: Các handler API cho cử tri với các thao tác CRUD, tìm kiếm và lọc\n// ES: Manejadores de API para votantes con operaciones CRUD, búsqueda y filtrado\n// SPEC: API-04 - Voters API (CRUD for voters, search, filter)\n\nimport { NextRequest, NextResponse } from 'next/server'\nimport { createClient } from '@supabase/supabase-js'\nimport { z } from 'zod'\n\n// EN: Initialize Supabase client\n// VI: Khởi tạo client Supabase\n// ES: Inicializar cliente Supabase\nconst supabase = createClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.SUPABASE_SERVICE_ROLE_KEY!\n)\n\n// EN: Voter validation schema\n// VI: Schema xác thực thông tin cử tri\n// ES: Schema de validación de votante\nconst VoterSchema = z.object({\n id: z.string().optional(),\n firstName: z.string().min(1, 'First name is required'),\n lastName: z.string().min(1, 'Last name is required'),\n email: z.string().email('Invalid email format'),\n phone: z.string().optional(),\n address: z.string().optional(),\n city: z.string().optional(),\n state: z.string().optional(),\n zipCode: z.string().optional(),\n dateOfBirth: z.string().optional(),\n registrationDate: z.string().optional(),\n status: z.enum(['active', 'inactive', 'pending']).default('active'),\n votingHistory: z.array(z.string()).optional().default([])\n})\n\n// EN: Search parameters schema\n// VI: Schema tham số tìm kiếm\n// ES: Schema de parámetros de búsqueda\nconst SearchSchema = z.object({\n query: z.string().optional(),\n city: z.string().optional(),\n state: z.string().optional(),\n status: z.string().optional(),\n page: z.string().optional().default('1'),\n limit: z.string().optional().default('20')\n})\n\n// EN: Add new voter\n// VI: Thêm cử tri mới\n// ES: Agregar nuevo votante\nexport async function addVoter(req: NextRequest) {\n try {\n const body = await req.json()\n const validation = VoterSchema.safeParse(body)\n \n if (!validation.success) {\n return NextResponse.json(\n { error: 'Invalid voter data', details: validation.error.errors },\n { status: 400 }\n )\n }\n\n const voterData = validation.data\n \n // EN: Check if voter already exists by email\n // VI: Kiểm tra cử tri đã tồn tại theo email\n // ES: Verificar si el votante ya existe por email\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { data: existingVoter } = await (supabase as any)\n .from('voters')\n .select('id')\n .eq('email', voterData.email)\n .single()\n\n if (existingVoter) {\n return NextResponse.json(\n { error: 'Voter with this email already exists' },\n { status: 409 }\n )\n }\n\n // EN: Insert new voter\n // VI: Thêm cử tri mới\n // ES: Insertar nuevo votante\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { data, error } = await (supabase as any)\n .from('voters')\n .insert([{\n ...voterData,\n registrationDate: voterData.registrationDate || new Date().toISOString()\n }])\n .select()\n .single()\n\n if (error) {\n return NextResponse.json(\n { error: 'Failed to create voter' },\n { status: 500 }\n )\n }\n\n return NextResponse.json(data)\n } catch (error) {\n console.error('Add voter error:', error)\n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n )\n }\n}\n\n// EN: Search and filter voters\n// VI: Tìm kiếm và lọc cử tri\n// ES: Buscar y filtrar votantes\nexport async function searchVoters(req: NextRequest) {\n try {\n const { searchParams } = new URL(req.url)\n const queryParams = Object.fromEntries(searchParams.entries())\n \n const validation = SearchSchema.safeParse(queryParams)\n \n if (!validation.success) {\n return NextResponse.json(\n { error: 'Invalid search parameters' },\n { status: 400 }\n )\n }\n\n const { query, city, state, status, page, limit } = validation.data\n const pageNum = parseInt(page)\n const limitNum = parseInt(limit)\n const offset = (pageNum - 1) * limitNum\n\n // EN: Build query with filters\n // VI: Xây dựng truy vấn với các bộ lọc\n // ES: Construir consulta con filtros\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let supabaseQuery = (supabase as any).from('voters').select('*', { count: 'exact' })\n\n // EN: Apply text search\n // VI: Áp dụng tìm kiếm văn bản\n // ES: Aplicar búsqueda de texto\n if (query) {\n supabaseQuery = supabaseQuery.or(`firstName.ilike.%${query}%,lastName.ilike.%${query}%,email.ilike.%${query}%`)\n }\n\n // EN: Apply city filter\n // VI: Áp dụng lọc theo thành phố\n // ES: Aplicar filtro de ciudad\n if (city) {\n supabaseQuery = supabaseQuery.eq('city', city)\n }\n\n // EN: Apply state filter\n // VI: Áp dụng lọc theo bang\n // ES: Aplicar filtro de estado\n if (state) {\n supabaseQuery = supabaseQuery.eq('state', state)\n }\n\n // EN: Apply status filter\n // VI: Áp dụng lọc theo trạng thái\n // ES: Aplicar filtro de estado\n if (status) {\n supabaseQuery = supabaseQuery.eq('status', status)\n }\n\n // EN: Apply pagination\n // VI: Áp dụng phân trang\n // ES: Aplicar paginación\n supabaseQuery = supabaseQuery\n .range(offset, offset + limitNum - 1)\n .order('registrationDate', { ascending: false })\n\n const { data, count, error } = await supabaseQuery\n\n if (error) {\n return NextResponse.json(\n { error: 'Failed to search voters' },\n { status: 500 }\n )\n }\n\n return NextResponse.json({\n voters: data || [],\n pagination: {\n page: pageNum,\n limit: limitNum,\n total: count || 0,\n totalPages: Math.ceil((count || 0) / limitNum)\n }\n })\n } catch (error) {\n console.error('Search voters error:', error)\n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n )\n }\n}\n\n// EN: Update voter information\n// VI: Cập nhật thông tin cử tri\n// ES: Actualizar información del votante\nexport async function updateVoter(req: NextRequest, { params }: { params: { id: string } }) {\n try {\n const body = await req.json()\n const validation = VoterSchema.partial().safeParse(body)\n \n if (!validation.success) {\n return NextResponse.json(\n { error: 'Invalid voter data', details: validation.error.errors },\n { status: 400 }\n )\n }\n\n const voterData = validation.data\n const { id } = params\n\n // EN: Check if voter exists\n // VI: Kiểm tra cử tri có tồn tại\n // ES: Verificar si el votante existe\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { data: existingVoter } = await (supabase as any)\n .from('voters')\n .select('id')\n .eq('id', id)\n .single()\n\n if (!existingVoter) {\n return NextResponse.json(\n { error: 'Voter not found' },\n { status: 404 }\n )\n }\n\n // EN: Check email uniqueness if email is being updated\n // VI: Kiểm tra tính duy nhất của email nếu email được cập nhật\n // ES: Verificar unicidad del email si se actualiza el email\n if (voterData.email) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { data: emailExists } = await (supabase as any)\n .from('voters')\n .select('id')\n .eq('email', voterData.email)\n .neq('id', id)\n .single()\n\n if (emailExists) {\n return NextResponse.json(\n { error: 'Email already exists for another voter' },\n { status: 409 }\n )\n }\n }\n\n // EN: Update voter\n // VI: Cập nhật cử tri\n // ES: Actualizar votante\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { data, error } = await (supabase as any)\n .from('voters')\n .update(voterData)\n .eq('id', id)\n .select()\n .single()\n\n if (error) {\n return NextResponse.json(\n { error: 'Failed to update voter' },\n { status: 500 }\n )\n }\n\n return NextResponse.json(data)\n } catch (error) {\n console.error('Update voter error:', error)\n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n )\n }\n}\n\n// EN: Get voter by ID\n// VI: Lấy thông tin cử tri theo ID\n// ES: Obtener votante por ID\nexport async function getVoter(req: NextRequest, { params }: { params: { id: string } }) {\n try {\n const { id } = params\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { data, error } = await (supabase as any)\n .from('voters')\n .select('*')\n .eq('id', id)\n .single()\n\n if (error || !data) {\n return NextResponse.json(\n { error: 'Voter not found' },\n { status: 404 }\n )\n }\n\n return NextResponse.json(data)\n } catch (error) {\n console.error('Get voter error:', error)\n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n )\n }\n}\n\n// EN: Delete voter\n// VI: Xóa cử tri\n// ES: Eliminar votante\nexport async function deleteVoter(req: NextRequest, { params }: { params: { id: string } }) {\n try {\n const { id } = params\n\n // EN: Check if voter exists\n // VI: Kiểm tra cử tri có tồn tại\n // ES: Verificar si el votante existe\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { data: existingVoter } = await (supabase as any)\n .from('voters')\n .select('id')\n .eq('id', id)\n .single()\n\n if (!existingVoter) {\n return NextResponse.json(\n { error: 'Voter not found' },\n { status: 404 }\n )\n }\n\n // EN: Delete voter\n // VI: Xóa cử tri\n // ES: Eliminar votante\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { error } = await (supabase as any)\n .from('voters')\n .delete()\n .eq('id', id)\n\n if (error) {\n return NextResponse.json(\n { error: 'Failed to delete voter' },\n { status: 500 }\n )\n }\n\n return NextResponse.json({ message: 'Voter deleted successfully' })\n } catch (error) {\n console.error('Delete voter error:', error)\n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n )\n }\n}\n\n// EN: Get voters statistics\n// VI: Lấy thống kê cử tri\n// ES: Obtener estadísticas de votantes\nexport async function getVotersStats(req: NextRequest) {\n try {\n // EN: Get total voters count\n // VI: Lấy tổng số cử tri\n // ES: Obtener total de votantes\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { count: totalVoters } = await (supabase as any)\n .from('voters')\n .select('*', { count: 'exact', head: true })\n\n // EN: Get voters by status\n // VI: Lấy cử tri theo trạng thái\n // ES: Obtener votantes por estado\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { data: statusData } = await (supabase as any)\n .from('voters')\n .select('status')\n\n const statusStats = statusData?.reduce((acc: Record<string, number>, voter: { status: string }) => {\n acc[voter.status] = (acc[voter.status] || 0) + 1\n return acc\n }, {}) || {}\n\n // EN: Get voters by state\n // VI: Lấy cử tri theo bang\n // ES: Obtener votantes por estado\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { data: stateData } = await (supabase as any)\n .from('voters')\n .select('state')\n .not('state', 'is', null)\n\n const stateStats = stateData?.reduce((acc: Record<string, number>, voter: { state: string }) => {\n if (voter.state) {\n acc[voter.state] = (acc[voter.state] || 0) + 1\n }\n return acc\n }, {}) || {}\n\n return NextResponse.json({\n totalVoters: totalVoters || 0,\n byStatus: statusStats,\n byState: stateStats\n })\n } catch (error) {\n console.error('Get voters stats error:', error)\n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n )\n }\n}\n```\n\nThis implementation provides:\n\n1. **CRUD Operations**:\n - `addVoter`: Create new voters with validation\n - `getVoter`: Retrieve individual voter by ID\n - `updateVoter`: Update voter information\n - `deleteVoter`: Remove voters\n\n2. **Search and Filter**:\n - `searchVoters`: Text search across name and email\n - Filter by city, state, and status\n - Pagination support\n\n3. **Additional Features**:\n - `getVotersStats`: Statistics and analytics\n - Email uniqueness validation\n - Comprehensive error handling\n\n4. **Follows Conventions**:\n - Trilingual comments\n - SPEC section references\n - Zod validation with safeParse()\n - Supabase with eslint-disable\n - NextResponse.json() responses\n - Consistent error format\n\nThe API handles all voter management needs with proper validation, search capabilities, and robust error handling."
}Task ID: 937cad9f-8469-4bc4-bad5-2baee7870ed1