FOUNDRY
C8 Platform
← 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