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