# Project Management

> Creating and managing test projects

## 📋 Overview

Projects are the core entity in Izri. Each project represents a GitHub repository that you want to generate and run tests for. Projects can be owned by individual users or organizations, and include settings for test generation preferences.

## 🎯 User Stories

- **As a developer**, I want to add my repository so I can generate tests for it
- **As a team lead**, I want to manage multiple projects under an organization
- **As a user**, I want to configure test generation preferences per project
- **As a developer**, I want to track which branch to analyze for each project

## 🏗️ Architecture

### Database Schema

**projects table**:

```typescript
{
  id: string              // Primary key (ULID)
  name: string            // Project display name
  description: string?    // Optional description
  repository: string      // GitHub repository URL
  branch: string          // Branch to analyze (default: 'main')
  languages: string[]     // Array of programming languages used
  frameworks: string[]    // Array of frameworks/libraries used
  userId: string          // Owner user ID (ULID)
  organizationId: string  // Required organization ID (ULID)
  createdAt: Date
  updatedAt: Date
}
```

**projectSettings table**:

```typescript
{
  id: string
  projectId: string
  enableUnitTests: boolean        // Generate unit tests (default: true)
  enableIntegrationTests: boolean // Generate integration tests (default: true)
  enableE2ETests: boolean         // Generate E2E tests (default: true)
  autoRunTests: boolean           // Automatically run after generation (default: false)
  notifyOnFailure: boolean        // Send notifications on test failures (default: true)
  aiProvider: string?             // OpenAI, Anthropic, etc.
  aiModel: string?                // gpt-4, claude-3-opus, etc.
  createdAt: Date
  updatedAt: Date
}
```

### Indexes

```sql
CREATE INDEX projects_user_idx ON projects(user_id);
CREATE INDEX projects_org_idx ON projects(organization_id);
```

## 🔌 API Reference

### Create Project

```typescript
// tRPC endpoint: projects.createProject
const result = await trpc.projects.createProject.mutate({
  name: 'My Project',
  description: 'Description of my project',
  repository: 'https://github.com/username/repo',
  branch: 'main',
  languages: ['typescript'],
  frameworks: ['react'],
  organizationId: 'org_123',
  userEmail: 'user@example.com'
})

// Response
{
  project: {
    id: 'proj_123',
    name: 'My Project',
    description: 'Description of my project',
    repository: 'https://github.com/username/repo',
    branch: 'main',
    languages: ['typescript'],
    frameworks: ['react'],
    userId: 'user_123',
    organizationId: 'org_123',
    createdAt: '2024-01-01T00:00:00Z',
    updatedAt: '2024-01-01T00:00:00Z',
    testRuns: []
  }
}
```

**Validation**:

- Project name required (min 1 character)
- Repository URL required
- At least one language required
- Organization ID required (ULID format)
- Valid email required
- Duplicate repository per user not allowed
- Automatically creates default project settings

### Get Projects

```typescript
// Get all projects for current user (optionally filtered by organization)
const projects = await trpc.projects.getProjects.query({
  userEmail: 'user@example.com',
  organizationId: 'org_123' // optional
})

// Response
{
  projects: [
    {
      id: 'proj_123',
      name: 'My Project',
      repository: 'https://github.com/username/repo',
      languages: ['typescript'],
      frameworks: ['react'],
      createdAt: '2024-01-01T00:00:00Z',
      testRuns: [
        {
          id: 'run_456',
          status: 'COMPLETED',
          passedTests: 50,
          failedTests: 2,
          totalTests: 52,
          createdAt: '2024-01-02T00:00:00Z'
        }
      ]
    }
  ]
}
```

**Features**:

- Includes latest test run per project
- Ordered by updated date (newest first)
- Filtered by user ownership and optionally by organization

### Get Project

```typescript
// Get single project with full details
const project = await trpc.projects.getProject.query({
  projectId: 'proj_123',
  userEmail: 'user@example.com'
})

// Response
{
  project: {
    id: 'proj_123',
    name: 'My Project',
    description: 'Description',
    repository: 'https://github.com/username/repo',
    branch: 'main',
    languages: ['typescript'],
    frameworks: ['react'],
    userId: 'user_123',
    organizationId: 'org_123',
    createdAt: '2024-01-01T00:00:00Z',
    updatedAt: '2024-01-01T00:00:00Z',
    testRuns: [...],
    settings: {
      enableUnitTests: true,
      enableIntegrationTests: true,
      enableE2ETests: true,
      autoRunTests: false,
      notifyOnFailure: true,
      aiProvider: 'openai',
      aiModel: 'gpt-4'
    }
  }
}
```

### Update Project

```typescript
// Update project details
const result = await trpc.projects.updateProject.mutate({
  projectId: 'proj_123',
  userEmail: 'user@example.com',
  name: 'Updated Project Name',
  description: 'Updated description',
  branch: 'develop',
  languages: ['typescript', 'javascript'],
  frameworks: ['next.js', 'react']
})

// Response: Updated project object
```

**Updatable fields**:

- name
- description
- branch
- languages (array)
- frameworks (array)

**Non-updatable**:

- id
- repository (would be a different project)
- userId
- organizationId
- createdAt

### Delete Project

```typescript
// Delete project (soft or hard delete)
await trpc.projects.deleteProject.mutate({
  projectId: 'proj_123'
})

// Response
{
  success: true
}
```

**Cascade behavior**:

- Deletes associated project settings
- Deletes associated test runs
- Deletes associated analyses
- Cannot be undone

### Update Project Settings

```typescript
// Update test generation preferences
await trpc.projects.updateProjectSettings.mutate({
  projectId: 'proj_123',
  userEmail: 'user@example.com',
  autoRunTests: true,
  enableE2ETests: true,
  enableIntegrationTests: false,
  enableUnitTests: true,
  notifyOnFailure: true,
  aiProvider: 'anthropic',
  aiModel: 'claude-3-opus-20240229'
})
```

### Get Project Analyses

```typescript
// Get all analyses for a project
const analyses = await trpc.projects.getProjectAnalyses.query({
  projectId: 'proj_123',
  userEmail: 'user@example.com'
})

// Response
{
  analyses: [
    {
      id: 'analysis_456',
      projectId: 'proj_123',
      status: 'COMPLETED',
      summary: 'Analysis completed successfully',
      createdAt: '2024-01-01T00:00:00Z',
      updatedAt: '2024-01-01T00:00:00Z'
    }
  ]
}
```

**Features**:

- Returns all analyses for the project
- Ordered by creation date (newest first)
- Requires project ownership verification

### Get Latest Project Analysis

```typescript
// Get the most recent analysis for a project
const analysis = await trpc.projects.getLatestProjectAnalysis.query({
  projectId: 'proj_123',
  userEmail: 'user@example.com'
})

// Response
{
  analysis: {
    id: 'analysis_456',
    projectId: 'proj_123',
    status: 'COMPLETED',
    summary: 'Analysis completed successfully',
    createdAt: '2024-01-01T00:00:00Z',
    updatedAt: '2024-01-01T00:00:00Z'
  } // or null if no analyses exist
}
```

**Features**:

- Returns the most recent analysis only
- Returns null if no analyses exist
- Requires project ownership verification

## 🎨 Frontend Implementation

### Project List Page

**Route**: `/organizations/$slug/projects`

```tsx
import { trpc } from '~/lib/trpc'

export default function ProjectsPage() {
  const { slug } = useParams()
  const { data, isLoading } = trpc.projects.getProjects.useQuery({
    userEmail: user.email,
    organizationId: currentOrganization?.id
  })

  if (isLoading) return <ProjectsSkeleton />

  return (
    <div className="space-y-4">
      <div className="flex justify-between items-center">
        <h1 className="text-2xl font-bold">Projects</h1>
        <Button asChild>
          <Link to={`/organizations/${slug}/projects/new`}>
            <PlusIcon /> New Project
          </Link>
        </Button>
      </div>

      <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
        {data?.projects.map((project) => (
          <ProjectCard key={project.id} project={project} />
        ))}
      </div>
    </div>
  )
}
```

### Project Card Component

```tsx
interface ProjectCardProps {
  project: {
    id: string
    name: string
    repository: string
    languages: string[]
    frameworks: string[]
    testRuns: Array<{ status: string; passedTests: number; totalTests: number }>
  }
}

function ProjectCard({ project }: ProjectCardProps) {
  const { slug } = useParams()
  const latestRun = project.testRuns[0]

  return (
    <Card>
      <CardHeader>
        <div className="flex items-center justify-between">
          <CardTitle>{project.name}</CardTitle>
          <div className="flex gap-1">
            {project.languages.slice(0, 2).map((lang) => (
              <Badge key={lang} variant="outline" className="text-xs">
                {lang}
              </Badge>
            ))}
            {project.languages.length > 2 && (
              <Badge variant="outline" className="text-xs">
                +{project.languages.length - 2}
              </Badge>
            )}
          </div>
        </div>
        <CardDescription className="truncate">
          {project.repository}
        </CardDescription>
      </CardHeader>

      <CardContent>
        {project.frameworks.length > 0 && (
          <div className="text-sm text-muted-foreground mb-2">
            Frameworks: {project.frameworks.join(', ')}
          </div>
        )}

        {latestRun && (
          <div className="flex items-center gap-2">
            <StatusBadge status={latestRun.status} />
            <span className="text-sm">
              {latestRun.passedTests}/{latestRun.totalTests} tests passed
            </span>
          </div>
        )}
      </CardContent>

      <CardFooter>
        <Button asChild variant="outline" className="w-full">
          <Link to={`/organizations/${slug}/projects/${project.id}`}>
            View Details
          </Link>
        </Button>
      </CardFooter>
    </Card>
  )
}
```

### Create Project Form

**Route**: `/organizations/$slug/projects/new`

```tsx
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import { z } from 'zod'

const projectSchema = z.object({
  name: z.string().min(1, 'Project name is required'),
  description: z.string().optional(),
  repository: z.string().url('Must be a valid URL'),
  branch: z.string().default('main'),
  languages: z.array(z.string()).min(1, 'At least one language is required'),
  frameworks: z.array(z.string()).optional(),
})

export default function NewProjectPage() {
  const { slug } = useParams()
  const navigate = useNavigate()
  const user = useAuth()
  const currentOrganization = useCurrentOrganization()

  const form = useForm({
    resolver: zodResolver(projectSchema),
    defaultValues: {
      branch: 'main',
      languages: [],
      frameworks: []
    }
  })

  const createProject = trpc.projects.createProject.useMutation({
    onSuccess: (data) => {
      navigate(`/organizations/${slug}/projects/${data.project.id}`)
    }
  })

  const onSubmit = (values: z.infer<typeof projectSchema>) => {
    createProject.mutate({
      ...values,
      organizationId: currentOrganization?.id || '',
      userEmail: user.email
    })
  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
        <FormField
          name="name"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Project Name *</FormLabel>
              <FormControl>
                <Input {...field} placeholder="My Awesome Project" />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />

        <FormField
          name="repository"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Repository URL *</FormLabel>
              <FormControl>
                <Input {...field} placeholder="https://github.com/username/repo" />
              </FormControl>
              <FormDescription>
                GitHub repository URL for analysis and testing
              </FormDescription>
              <FormMessage />
            </FormItem>
          )}
        />

        <FormField
          name="branch"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Default Branch</FormLabel>
              <FormControl>
                <Input {...field} placeholder="main" />
              </FormControl>
              <FormDescription>
                Branch to analyze for test generation
              </FormDescription>
              <FormMessage />
            </FormItem>
          )}
        />

        <FormField
          name="languages"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Languages *</FormLabel>
              <FormControl>
                <MultiSelect
                  {...field}
                  options={LANGUAGES}
                  placeholder="Select programming languages..."
                />
              </FormControl>
              <FormDescription>
                Languages used in your project
              </FormDescription>
              <FormMessage />
            </FormItem>
          )}
        />

        <FormField
          name="frameworks"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Frameworks & Libraries</FormLabel>
              <FormControl>
                <MultiSelect
                  {...field}
                  options={FRAMEWORKS}
                  placeholder="Select frameworks..."
                />
              </FormControl>
              <FormDescription>
                Frameworks and libraries used in your project (optional)
              </FormDescription>
              <FormMessage />
            </FormItem>
          )}
        />

        <div className="flex justify-end space-x-4 pt-6">
          <Button asChild variant="outline">
            <Link to={`/organizations/${slug}/projects`}>Cancel</Link>
          </Button>
          <Button type="submit" disabled={createProject.isLoading}>
            {createProject.isLoading && <LoaderIcon className="animate-spin" />}
            Create Project
          </Button>
        </div>
      </form>
    </Form>
  )
}
```

### Project Details Page

**Route**: `/organizations/$slug/projects/$id`

```tsx
export default function ProjectDetailsPage() {
  const { slug, id } = useParams()
  const { data, isLoading } = trpc.projects.getProject.useQuery({
    projectId: id!,
    userEmail: user.email
  })

  if (isLoading) return <Skeleton />

  const project = data?.project

  return (
    <div className="space-y-6">
      <div className="flex justify-between items-start">
        <div>
          <h1 className="text-3xl font-bold">{project.name}</h1>
          <p className="text-muted-foreground">{project.description}</p>
        </div>

        <Button asChild variant="outline">
          <Link to={`/organizations/${slug}/projects/${id}/settings`}>
            <SettingsIcon /> Settings
          </Link>
        </Button>
      </div>

      <Tabs defaultValue="overview">
        <TabsList>
          <TabsTrigger value="overview">Overview</TabsTrigger>
          <TabsTrigger value="tests">Test Runs</TabsTrigger>
          <TabsTrigger value="analysis">Analysis</TabsTrigger>
        </TabsList>

        <TabsContent value="overview">
          <ProjectOverview project={project} />
        </TabsContent>

        <TabsContent value="tests">
          <TestRunsList projectId={id} />
        </TabsContent>

        <TabsContent value="analysis">
          <AnalysisHistory projectId={id} />
        </TabsContent>
      </Tabs>
    </div>
  )
}
```

## ✅ Testing

### Unit Tests

```typescript
import { describe, expect, it } from 'vitest'
import { createCaller } from '../context'

describe('projects.createProject', () => {
  it('creates a project with valid input', async () => {
    const caller = await createCaller({ user: testUser })

    const result = await caller.projects.createProject({
      name: 'Test Project',
      repository: 'https://github.com/test/repo',
      branch: 'main',
      languages: ['typescript'],
      frameworks: ['react'],
      organizationId: 'org_123',
      userEmail: testUser.email
    })

    expect(result.project).toBeDefined()
    expect(result.project.name).toBe('Test Project')
    expect(result.project.languages).toEqual(['typescript'])
    expect(result.project.frameworks).toEqual(['react'])
    expect(result.project.organizationId).toBe('org_123')
    expect(result.project.userId).toBe(testUser.id)
  })

  it('throws error for duplicate repository', async () => {
    const caller = await createCaller({ user: testUser })

    // Create first project
    await caller.projects.createProject({
      name: 'Project 1',
      repository: 'https://github.com/test/repo',
      languages: ['typescript'],
      organizationId: 'org_123',
      userEmail: testUser.email
    })

    // Try to create duplicate
    await expect(
      caller.projects.createProject({
        name: 'Project 2',
        repository: 'https://github.com/test/repo',
        languages: ['typescript'],
        organizationId: 'org_123',
        userEmail: testUser.email
      })
    ).rejects.toThrow('Project with this repository already exists')
  })

  it('requires at least one language', async () => {
    const caller = await createCaller({ user: testUser })

    await expect(
      caller.projects.createProject({
        name: 'Test Project',
        repository: 'https://github.com/test/repo',
        languages: [],
        organizationId: 'org_123',
        userEmail: testUser.email
      })
    ).rejects.toThrow('At least one language is required')
  })
})
```

### Integration Tests

```typescript
describe('Project lifecycle', () => {
  it('creates, updates, and deletes a project', async () => {
    // Create
    const created = await trpc.projects.createProject.mutate({
      name: 'Test Project',
      repository: 'https://github.com/test/repo',
      languages: ['typescript'],
      frameworks: ['react'],
      organizationId: 'org_123',
      userEmail: user.email
    })

    expect(created.project.id).toBeDefined()
    const projectId = created.project.id

    // Update
    await trpc.projects.updateProject.mutate({
      projectId,
      userEmail: user.email,
      name: 'Updated Name',
      languages: ['typescript', 'javascript']
    })

    const updated = await trpc.projects.getProject.query({
      projectId,
      userEmail: user.email
    })
    expect(updated.project.name).toBe('Updated Name')
    expect(updated.project.languages).toEqual(['typescript', 'javascript'])

    // Delete
    await trpc.projects.deleteProject.mutate({
      projectId,
      userEmail: user.email
    })

    await expect(
      trpc.projects.getProject.query({
        projectId,
        userEmail: user.email
      })
    ).rejects.toThrow()
  })
})
```

## 🔗 Related Documentation

- **Repository Analysis**: [How projects are analyzed](./repository-analysis.md)
- **Test Generation**: [Generating tests for projects](./test-generation.md)
- **Organizations**: [Multi-tenancy support](./organizations.md)
- **API Reference**: [Complete API docs](../api-reference/trpc-routers.md)

---

*Next: [Repository Analysis](./repository-analysis.md) →*
