← 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