FOUNDRY
C8 Platform
← Tasks

[API-05] Territories API (CRUD for territories, assignment) (retry 1)

completedcode_genP0

Description

## API-05: Territories API (CRUD for territories, assignment) Section: API Routes | Milestone: M2 | Owner: None ### Failing Tests - **territories_api_file** (file_exists): File `src/api/territories.ts` must exist - **territories_api_handlers** (grep): Pattern `createTerritory|assignTerritory|getTerritories` in `src/api/territories.ts` (need 2+ matches) ### Acceptance Criteria - File `src/api/territories.ts` must exist - File matching `src/api/territories.ts` must contain pattern `createTerritory|assignTerritory|getTerritories` ### 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-04: FAIL
Assignee
-
Claimed By
Cloud Lu → Claude Sonnet
Created
1d ago
Completed
1d ago

Quality

Overall
99%
Pass
PASS
Graded By
-
Graded At
-

Cost

Input Tokens
-
Output Tokens
-
Est. USD
-

Result

{
  "cost": {
    "input_tokens": 286,
    "estimated_usd": 0.052383,
    "output_tokens": 3435
  },
  "model": "claude-sonnet-4-20250514",
  "status": "success",
  "transport": "anthropic_api",
  "latency_ms": 37015,
  "response_text": "I'll create the territories API file with CRUD operations for territories and assignment functionality.\n\n```typescript\n// src/api/territories.ts\n// EN: Territories API - CRUD operations and assignment management\n// VI: API lãnh thổ - các thao tác CRUD và quản lý phân công\n// ES: API de territorios - operaciones CRUD y gestión de asignaciones\n// SPEC: API-05 - Territories API (CRUD for territories, assignment)\n\nimport { NextRequest, NextResponse } from 'next/server';\nimport { z } from 'zod';\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nimport { createClient } from '@supabase/supabase-js';\n\nconst supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;\nconst supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst supabase = createClient(supabaseUrl, supabaseServiceKey) as any;\n\n// EN: Territory creation schema\n// VI: Schema tạo lãnh thổ\n// ES: Esquema de creación de territorio\nconst CreateTerritorySchema = z.object({\n  name: z.string().min(1, 'Territory name is required'),\n  description: z.string().optional(),\n  boundaries: z.object({\n    coordinates: z.array(z.array(z.number())),\n    type: z.string().default('Polygon')\n  }).optional(),\n  congregation_id: z.string().uuid(),\n  status: z.enum(['available', 'assigned', 'completed']).default('available')\n});\n\n// EN: Territory assignment schema\n// VI: Schema phân công lãnh thổ\n// ES: Esquema de asignación de territorio\nconst AssignTerritorySchema = z.object({\n  territory_id: z.string().uuid(),\n  user_id: z.string().uuid(),\n  assigned_date: z.string().datetime().optional(),\n  expected_return_date: z.string().datetime().optional(),\n  notes: z.string().optional()\n});\n\n// EN: Create new territory\n// VI: Tạo lãnh thổ mới\n// ES: Crear nuevo territorio\nexport async function createTerritory(req: NextRequest) {\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    // EN: Insert territory into database\n    // VI: Chèn lãnh thổ vào cơ sở dữ liệu\n    // ES: Insertar territorio en la base de datos\n    const { data: territory, error } = await supabase\n      .from('territories')\n      .insert([{\n        ...territoryData,\n        created_at: new Date().toISOString(),\n        updated_at: new Date().toISOString()\n      }])\n      .select()\n      .single();\n\n    if (error) {\n      console.error('Territory creation error:', error);\n      return NextResponse.json(\n        { error: 'Failed to create territory' },\n        { status: 500 }\n      );\n    }\n\n    return NextResponse.json({\n      success: true,\n      territory,\n      message: 'Territory created successfully'\n    });\n\n  } catch (error) {\n    console.error('Create territory error:', error);\n    return NextResponse.json(\n      { error: 'Internal server error' },\n      { status: 500 }\n    );\n  }\n}\n\n// EN: Get territories with optional filtering\n// VI: Lấy danh sách lãnh thổ với bộ lọc tùy chọn\n// ES: Obtener territorios con filtrado opcional\nexport async function getTerritories(req: NextRequest) {\n  try {\n    const { searchParams } = new URL(req.url);\n    const congregationId = searchParams.get('congregation_id');\n    const status = searchParams.get('status');\n    const assignedTo = searchParams.get('assigned_to');\n\n    let query = supabase\n      .from('territories')\n      .select(`\n        *,\n        assignments:territory_assignments(\n          id,\n          user_id,\n          assigned_date,\n          returned_date,\n          status,\n          user:users(name, email)\n        ),\n        congregation:congregations(name)\n      `);\n\n    // EN: Apply filters\n    // VI: Áp dụng bộ lọc\n    // ES: Aplicar filtros\n    if (congregationId) {\n      query = query.eq('congregation_id', congregationId);\n    }\n\n    if (status) {\n      query = query.eq('status', status);\n    }\n\n    if (assignedTo) {\n      query = query.eq('assignments.user_id', assignedTo);\n    }\n\n    const { data: territories, error } = await query.order('name');\n\n    if (error) {\n      console.error('Get territories error:', error);\n      return NextResponse.json(\n        { error: 'Failed to fetch territories' },\n        { status: 500 }\n      );\n    }\n\n    return NextResponse.json({\n      success: true,\n      territories: territories || [],\n      count: territories?.length || 0\n    });\n\n  } catch (error) {\n    console.error('Get territories error:', error);\n    return NextResponse.json(\n      { error: 'Internal server error' },\n      { status: 500 }\n    );\n  }\n}\n\n// EN: Assign territory to user\n// VI: Phân công lãnh thổ cho người dùng\n// ES: Asignar territorio a usuario\nexport async function assignTerritory(req: NextRequest) {\n  try {\n    const body = await req.json();\n    const validation = AssignTerritorySchema.safeParse(body);\n    \n    if (!validation.success) {\n      return NextResponse.json(\n        { error: 'Invalid assignment data', details: validation.error.errors },\n        { status: 400 }\n      );\n    }\n\n    const assignmentData = validation.data;\n\n    // EN: Check if territory exists and is available\n    // VI: Kiểm tra lãnh thổ có tồn tại và khả dụng\n    // ES: Verificar si el territorio existe y está disponible\n    const { data: territory, error: territoryError } = await supabase\n      .from('territories')\n      .select('id, status, name')\n      .eq('id', assignmentData.territory_id)\n      .single();\n\n    if (territoryError || !territory) {\n      return NextResponse.json(\n        { error: 'Territory not found' },\n        { status: 404 }\n      );\n    }\n\n    if (territory.status !== 'available') {\n      return NextResponse.json(\n        { error: 'Territory is not available for assignment' },\n        { status: 400 }\n      );\n    }\n\n    // EN: Create assignment record\n    // VI: Tạo bản ghi phân công\n    // ES: Crear registro de asignación\n    const { data: assignment, error: assignmentError } = await supabase\n      .from('territory_assignments')\n      .insert([{\n        ...assignmentData,\n        assigned_date: assignmentData.assigned_date || new Date().toISOString(),\n        status: 'active',\n        created_at: new Date().toISOString()\n      }])\n      .select(`\n        *,\n        territory:territories(name, description),\n        user:users(name, email)\n      `)\n      .single();\n\n    if (assignmentError) {\n      console.error('Assignment creation error:', assignmentError);\n      return NextResponse.json(\n        { error: 'Failed to create assignment' },\n        { status: 500 }\n      );\n    }\n\n    // EN: Update territory status to assigned\n    // VI: Cập nhật trạng thái lãnh thổ thành đã phân công\n    // ES: Actualizar estado del territorio a asignado\n    const { error: updateError } = await supabase\n      .from('territories')\n      .update({ \n        status: 'assigned',\n        updated_at: new Date().toISOString()\n      })\n      .eq('id', assignmentData.territory_id);\n\n    if (updateError) {\n      console.error('Territory update error:', updateError);\n      // EN: Rollback assignment if territory update fails\n      // VI: Hoàn tác phân công nếu cập nhật lãnh thổ thất bại\n      // ES: Revertir asignación si falla la actualización del territorio\n      await supabase\n        .from('territory_assignments')\n        .delete()\n        .eq('id', assignment.id);\n      \n      return NextResponse.json(\n        { error: 'Failed to update territory status' },\n        { status: 500 }\n      );\n    }\n\n    return NextResponse.json({\n      success: true,\n      assignment,\n      message: 'Territory assigned successfully'\n    });\n\n  } catch (error) {\n    console.error('Assign territory error:', error);\n    return NextResponse.json(\n      { error: 'Internal server error' },\n      { status: 500 }\n    );\n  }\n}\n\n// EN: Update territory information\n// VI: Cập nhật thông tin lãnh thổ\n// ES: Actualizar información del territorio\nexport async function updateTerritory(req: NextRequest, territoryId: string) {\n  try {\n    const body = await req.json();\n    const validation = CreateTerritorySchema.partial().safeParse(body);\n    \n    if (!validation.success) {\n      return NextResponse.json(\n        { error: 'Invalid update data', details: validation.error.errors },\n        { status: 400 }\n      );\n    }\n\n    const updateData = validation.data;\n\n    const { data: territory, error } = await supabase\n      .from('territories')\n      .update({\n        ...updateData,\n        updated_at: new Date().toISOString()\n      })\n      .eq('id', territoryId)\n      .select()\n      .single();\n\n    if (error) {\n      console.error('Territory update error:', error);\n      return NextResponse.json(\n        { error: 'Failed to update territory' },\n        { status: 500 }\n      );\n    }\n\n    if (!territory) {\n      return NextResponse.json(\n        { error: 'Territory not found' },\n        { status: 404 }\n      );\n    }\n\n    return NextResponse.json({\n      success: true,\n      territory,\n      message: 'Territory updated successfully'\n    });\n\n  } catch (error) {\n    console.error('Update territory error:', error);\n    return NextResponse.json(\n      { error: 'Internal server error' },\n      { status: 500 }\n    );\n  }\n}\n\n// EN: Delete territory\n// VI: Xóa lãnh thổ\n// ES: Eliminar territorio\nexport async function deleteTerritory(territoryId: string) {\n  try {\n    // EN: Check if territory has active assignments\n    // VI: Kiểm tra lãnh thổ có phân công đang hoạt động\n    // ES: Verificar si el territorio tiene asignaciones activas\n    const { data: activeAssignments, error: assignmentError } = await supabase\n      .from('territory_assignments')\n      .select('id')\n      .eq('territory_id', territoryId)\n      .eq('status', 'active');\n\n    if (assignmentError) {\n      console.error('Assignment check error:', assignmentError);\n      return NextResponse.json(\n        { error: 'Failed to check territory assignments' },\n        { status: 500 }\n      );\n    }\n\n    if (activeAssignments && activeAssignments.length > 0) {\n      return NextResponse.json(\n        { error: 'Cannot delete territory with active assignments' },\n        { status: 400 }\n      );\n    }\n\n    const { error } = await supabase\n      .from('territories')\n      .delete()\n      .eq('id', territoryId);\n\n    if (error) {\n      console.error('Territory deletion error:', error);\n      return NextResponse.json(\n        { error: 'Failed to delete territory' },\n        { status: 500 }\n      );\n    }\n\n    return NextResponse.json({\n      success: true,\n      message: 'Territory deleted successfully'\n    });\n\n  } catch (error) {\n    console.error('Delete territory error:', error);\n    return NextResponse.json(\n      { error: 'Internal server error' },\n      { status: 500 }\n    );\n  }\n}\n```\n\nThis implementation provides:\n\n1. **createTerritory** - Creates new territories with validation\n2. **getTerritories** - Retrieves territories with filtering options\n3. **assignTerritory** - Assigns territories to users with proper validation\n4. **updateTerritory** - Updates territory information\n5. **deleteTerritory** - Safely deletes territories (checks for active assignments)\n\nKey features:\n- Comprehensive Zod validation schemas\n- Proper error handling and status codes\n- Database transaction safety (rollback on failures)\n- Trilingual comments following conventions\n- Support for territory boundaries (geographic coordinates)\n- Assignment tracking with user relationships\n- Status management (available, assigned, completed)\n\nThe API handles the complete territory lifecycle from creation to assignment and management."
}
Task ID: 44c2ea54-a7bd-498b-86a6-95010eebe551