← Tasks
[CP-API-02] Territories API module (CRUD + geo boundaries) (retry 1) (retry 2)
completedcode_genP0
Description
## CP-API-02: Territories API module (CRUD + geo boundaries)
Section: API | Milestone: M3 | Owner: AG Lu
### Failing Tests
- **territories_api_file** (file_exists): File `campaignpro/src/api/territories.ts` must exist
- **territories_get** (grep): Pattern `getTerritories|listTerritories` in `campaignpro/src/api/territories.ts` (need 1+ matches)
### Acceptance Criteria
- File `campaignpro/src/api/territories.ts` must exist
- File matching `campaignpro/src/api/territories.ts` must contain pattern `getTerritories|listTerritories`
### 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
92%
Pass
PASS
Graded By
-
Graded At
-
Cost
Input Tokens
-
Output Tokens
-
Est. USD
-
Result
{
"cost": {
"input_tokens": 293,
"estimated_usd": 0.063039,
"output_tokens": 4144
},
"model": "claude-sonnet-4-20250514",
"status": "success",
"transport": "anthropic_api",
"latency_ms": 43937,
"response_text": "I'll create the Territories API module with CRUD operations and geo boundaries support.\n\n<blovk type=\"code\" language=\"typescript\">\n<bloc path=\"campaignpro/src/api/territories.ts\">\n// EN: Territories API module with CRUD operations and geographic boundaries\n// VI: Module API lãnh thổ với các thao tác CRUD và ranh giới địa lý\n// ES: Módulo API de territorios con operaciones CRUD y límites geográficos\n// SPEC: REF-CP-API-02\n\nimport { NextRequest, NextResponse } from 'next/server';\nimport { z } from 'zod';\nimport { createClient } from '@supabase/supabase-js';\n\n// EN: Territory validation schemas\n// VI: Schemas xác thực lãnh thổ\n// ES: Esquemas de validación de territorio\nconst TerritorySchema = z.object({\n id: z.string().uuid().optional(),\n name: z.string().min(1).max(100),\n description: z.string().optional(),\n boundaries: z.object({\n type: z.literal('Polygon'),\n coordinates: z.array(z.array(z.array(z.number()))),\n }).optional(),\n center_lat: z.number().min(-90).max(90).optional(),\n center_lng: z.number().min(-180).max(180).optional(),\n zoom_level: z.number().min(1).max(20).optional(),\n created_at: z.string().optional(),\n updated_at: z.string().optional(),\n});\n\nconst CreateTerritorySchema = TerritorySchema.omit({\n id: true,\n created_at: true,\n updated_at: true,\n});\n\nconst UpdateTerritorySchema = CreateTerritorySchema.partial();\n\ntype Territory = z.infer<typeof TerritorySchema>;\ntype CreateTerritory = z.infer<typeof CreateTerritorySchema>;\ntype UpdateTerritory = z.infer<typeof UpdateTerritorySchema>;\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: Get all territories with optional filtering\n// VI: Lấy tất cả lãnh thổ với lọc tùy chọn\n// ES: Obtener todos los territorios con filtrado opcional\nexport async function getTerritories(req: NextRequest): Promise<NextResponse> {\n try {\n const { searchParams } = new URL(req.url);\n const page = parseInt(searchParams.get('page') || '1');\n const limit = parseInt(searchParams.get('limit') || '10');\n const search = searchParams.get('search');\n \n let query = (supabase as any)\n .from('territories')\n .select('*')\n .order('created_at', { ascending: false });\n\n // EN: Apply search filter if provided\n // VI: Áp dụng bộ lọc tìm kiếm nếu được cung cấp\n // ES: Aplicar filtro de búsqueda si se proporciona\n if (search) {\n query = query.ilike('name', `%${search}%`);\n }\n\n // EN: Apply pagination\n // VI: Áp dụng phân trang\n // ES: Aplicar paginación\n const from = (page - 1) * limit;\n const to = from + limit - 1;\n query = query.range(from, to);\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { data, error, count } = await query;\n\n if (error) {\n console.error('Error fetching territories:', error);\n return NextResponse.json(\n { error: 'Failed to fetch territories' },\n { status: 500 }\n );\n }\n\n return NextResponse.json({\n territories: data || [],\n pagination: {\n page,\n limit,\n total: count || 0,\n totalPages: Math.ceil((count || 0) / limit),\n },\n });\n } catch (error) {\n console.error('Unexpected error in getTerritories:', error);\n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n );\n }\n}\n\n// EN: Alternative function name for listing territories\n// VI: Tên hàm thay thế để liệt kê lãnh thổ\n// ES: Nombre de función alternativo para listar territorios\nexport const listTerritories = getTerritories;\n\n// EN: Get a single territory by ID\n// VI: Lấy một lãnh thổ theo ID\n// ES: Obtener un territorio por ID\nexport async function getTerritory(req: NextRequest): Promise<NextResponse> {\n try {\n const { searchParams } = new URL(req.url);\n const id = searchParams.get('id');\n\n if (!id) {\n return NextResponse.json(\n { error: 'Territory ID is required' },\n { status: 400 }\n );\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { data, error } = await (supabase as any)\n .from('territories')\n .select('*')\n .eq('id', id)\n .single();\n\n if (error) {\n if (error.code === 'PGRST116') {\n return NextResponse.json(\n { error: 'Territory not found' },\n { status: 404 }\n );\n }\n console.error('Error fetching territory:', error);\n return NextResponse.json(\n { error: 'Failed to fetch territory' },\n { status: 500 }\n );\n }\n\n return NextResponse.json({ territory: data });\n } catch (error) {\n console.error('Unexpected error in getTerritory:', error);\n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n );\n }\n}\n\n// EN: Create a new territory\n// VI: Tạo lãnh thổ mới\n// ES: Crear un nuevo territorio\nexport async function createTerritory(req: NextRequest): Promise<NextResponse> {\n try {\n const body = await req.json();\n const validation = CreateTerritorySchema.safeParse(body);\n\n if (!validation.success) {\n return NextResponse.json(\n { error: 'Invalid territory data', details: validation.error.errors },\n { status: 400 }\n );\n }\n\n const territoryData = validation.data;\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { data, error } = await (supabase as any)\n .from('territories')\n .insert([territoryData])\n .select()\n .single();\n\n if (error) {\n console.error('Error creating territory:', error);\n return NextResponse.json(\n { error: 'Failed to create territory' },\n { status: 500 }\n );\n }\n\n return NextResponse.json(\n { territory: data, message: 'Territory created successfully' },\n { status: 201 }\n );\n } catch (error) {\n console.error('Unexpected error in createTerritory:', error);\n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n );\n }\n}\n\n// EN: Update an existing territory\n// VI: Cập nhật lãnh thổ hiện có\n// ES: Actualizar un territorio existente\nexport async function updateTerritory(req: NextRequest): Promise<NextResponse> {\n try {\n const { searchParams } = new URL(req.url);\n const id = searchParams.get('id');\n\n if (!id) {\n return NextResponse.json(\n { error: 'Territory ID is required' },\n { status: 400 }\n );\n }\n\n const body = await req.json();\n const validation = UpdateTerritorySchema.safeParse(body);\n\n if (!validation.success) {\n return NextResponse.json(\n { error: 'Invalid territory data', details: validation.error.errors },\n { status: 400 }\n );\n }\n\n const updateData = {\n ...validation.data,\n updated_at: new Date().toISOString(),\n };\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { data, error } = await (supabase as any)\n .from('territories')\n .update(updateData)\n .eq('id', id)\n .select()\n .single();\n\n if (error) {\n if (error.code === 'PGRST116') {\n return NextResponse.json(\n { error: 'Territory not found' },\n { status: 404 }\n );\n }\n console.error('Error updating territory:', error);\n return NextResponse.json(\n { error: 'Failed to update territory' },\n { status: 500 }\n );\n }\n\n return NextResponse.json({\n territory: data,\n message: 'Territory updated successfully',\n });\n } catch (error) {\n console.error('Unexpected error in updateTerritory:', error);\n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n );\n }\n}\n\n// EN: Delete a territory\n// VI: Xóa lãnh thổ\n// ES: Eliminar un territorio\nexport async function deleteTerritory(req: NextRequest): Promise<NextResponse> {\n try {\n const { searchParams } = new URL(req.url);\n const id = searchParams.get('id');\n\n if (!id) {\n return NextResponse.json(\n { error: 'Territory ID is required' },\n { status: 400 }\n );\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { error } = await (supabase as any)\n .from('territories')\n .delete()\n .eq('id', id);\n\n if (error) {\n console.error('Error deleting territory:', error);\n return NextResponse.json(\n { error: 'Failed to delete territory' },\n { status: 500 }\n );\n }\n\n return NextResponse.json({\n message: 'Territory deleted successfully',\n });\n } catch (error) {\n console.error('Unexpected error in deleteTerritory:', error);\n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n );\n }\n}\n\n// EN: Get territories within a geographic boundary\n// VI: Lấy các lãnh thổ trong ranh giới địa lý\n// ES: Obtener territorios dentro de un límite geográfico\nexport async function getTerritoriesInBounds(req: NextRequest): Promise<NextResponse> {\n try {\n const { searchParams } = new URL(req.url);\n const north = parseFloat(searchParams.get('north') || '');\n const south = parseFloat(searchParams.get('south') || '');\n const east = parseFloat(searchParams.get('east') || '');\n const west = parseFloat(searchParams.get('west') || '');\n\n if (isNaN(north) || isNaN(south) || isNaN(east) || isNaN(west)) {\n return NextResponse.json(\n { error: 'Invalid boundary coordinates' },\n { status: 400 }\n );\n }\n\n // EN: Query territories within bounds using center coordinates\n // VI: Truy vấn các lãnh thổ trong ranh giới sử dụng tọa độ trung tâm\n // ES: Consultar territorios dentro de los límites usando coordenadas del centro\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { data, error } = await (supabase as any)\n .from('territories')\n .select('*')\n .gte('center_lat', south)\n .lte('center_lat', north)\n .gte('center_lng', west)\n .lte('center_lng', east);\n\n if (error) {\n console.error('Error fetching territories in bounds:', error);\n return NextResponse.json(\n { error: 'Failed to fetch territories in bounds' },\n { status: 500 }\n );\n }\n\n return NextResponse.json({\n territories: data || [],\n bounds: { north, south, east, west },\n });\n } catch (error) {\n console.error('Unexpected error in getTerritoriesInBounds:', error);\n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n );\n }\n}\n\n// EN: Export territory data for analysis\n// VI: Xuất dữ liệu lãnh thổ để phân tích\n// ES: Exportar datos de territorio para análisis\nexport async function exportTerritories(req: NextRequest): Promise<NextResponse> {\n try {\n const { searchParams } = new URL(req.url);\n const format = searchParams.get('format') || 'json';\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { data, error } = await (supabase as any)\n .from('territories')\n .select('*')\n .order('name');\n\n if (error) {\n console.error('Error exporting territories:', error);\n return NextResponse.json(\n { error: 'Failed to export territories' },\n { status: 500 }\n );\n }\n\n if (format === 'geojson') {\n // EN: Convert to GeoJSON format for mapping applications\n // VI: Chuyển đổi sang định dạng GeoJSON cho ứng dụng bản đồ\n // ES: Convertir a formato GeoJSON para aplicaciones de mapeo\n const geojson = {\n type: 'FeatureCollection',\n features: (data || []).map((territory: Territory) => ({\n type: 'Feature',\n properties: {\n id: territory.id,\n name: territory.name,\n description: territory.description,\n },\n geometry: territory.boundaries || {\n type: 'Point',\n coordinates: [territory.center_lng || 0, territory.center_lat || 0],\n },\n })),\n };\n\n return NextResponse.json(geojson);\n }\n\n return NextResponse.json({ territories: data || [] });\n } catch (error) {\n console.error('Unexpected error in exportTerritories:', error);\n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n );\n }\n}\n</bloc>\n</blovk>\n\nThe Territories API module has been created with:\n\n## Key Features\n- **CRUD Operations**: Complete Create, Read, Update, Delete functionality\n- **Geographic Boundaries**: Support for polygon boundaries and center coordinates\n- **Search & Pagination**: Territory listing with search and pagination\n- **Boundary Queries**: Get territories within geographic bounds\n- **Data Export**: Export territories in JSON or GeoJSON format\n\n## Required Functions\n- ✅ `getTerritories()` - Main function for listing territories\n- ✅ `listTerritories()` - Alternative alias for listing\n\n## Codebase Conventions\n- ✅ Trilingual comments (EN/VI/ES)\n- ✅ SPEC section reference (REF-CP-API-02)\n- ✅ Zod validation with `.safeParse()`\n- ✅ `(supabase as any)` with eslint-disable\n- ✅ `NextResponse.json()` responses\n- ✅ Consistent error format: `{ error: 'message' }`\n\nThe module includes comprehensive territory management with geographic capabilities for campaign mapping and analysis."
}Task ID: f2e9d52d-c1a7-4e1c-b9a4-05992ce9fd64