# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Important Guidelines

**⚠️ DO NOT auto-commit code changes.** Always ask the user before committing. Only create commits when the user explicitly requests it with commands like "commit this work" or "create a commit".

## Temporarily Disabled Features

### Kanban Cancel Column (disabled)

The "Cancelled" column has been temporarily hidden from the kanban board API response to reduce UI clutter. The underlying code is preserved and can be re-enabled by searching for `TEMPORARILY DISABLED - Cancel column hidden from kanban` in `internal/repositories/task_repo.go`.

**What's disabled:**
- Cancel column not returned in kanban API responses (`/api/tasks/` and `/api/tasks/by-residence/:id/`)
- `GetCancelledTasks()` query not executed

**What's preserved:**
- `ColumnCancelled` constant in categorization
- `CancelledHandler` and `ArchivedHandler` in categorization chain
- `IsCancelled()` predicate
- `GetCancelledTasks()` function
- All database models and fields (`is_cancelled`, `is_archived`)

## Required Reading Before Task-Related Work

**STOP! Before writing ANY task-related code, you MUST read:**

📖 **`docs/TASK_LOGIC_ARCHITECTURE.md`**

This document contains:
- The consolidated task logic architecture (predicates, scopes, categorization)
- Developer checklist for adding task features
- Common pitfalls (PostgreSQL DATE vs TIMESTAMP, preload requirements)
- Examples of correct vs incorrect patterns

**Why this matters:** Task logic (completion detection, overdue calculation, kanban categorization) is centralized in `internal/task/`. Duplicating this logic elsewhere causes bugs where different parts of the app show different results.

**Quick reference:**
```go
// Use these - DON'T write inline task logic
import "github.com/treytartt/honeydue-api/internal/task"

task.IsCompleted(t)           // Check if task is completed
task.IsOverdue(t, now)        // Check if task is overdue
task.ScopeOverdue(now)        // GORM scope for overdue tasks
task.CategorizeTask(t, 30)    // Get kanban column for task
```

## Project Overview

honeyDue API is a Go REST API for the HoneyDue/honeyDue property management platform. It provides backend services for iOS and Android mobile apps built with Kotlin Multiplatform.

**Tech Stack:**
- **HTTP Framework**: Echo v4
- **ORM**: GORM with PostgreSQL
- **Push Notifications**: APNs (apns2) + FCM HTTP API
- **Admin Panel**: GoAdmin
- **Background Jobs**: Asynq with Redis
- **Caching**: Redis
- **Logging**: zerolog
- **Configuration**: Viper

## Project Structure

```
honeyDueAPI-go/
├── cmd/
│   ├── api/main.go           # API server entry point
│   └── worker/main.go        # Background worker entry point
├── internal/
│   ├── config/               # Configuration (Viper)
│   ├── database/             # Database connection setup
│   ├── models/               # GORM models
│   ├── repositories/         # Data access layer
│   ├── services/             # Business logic
│   ├── handlers/             # HTTP handlers (Echo)
│   ├── middleware/           # Echo middleware (auth, logging, etc.)
│   ├── router/               # Route setup
│   ├── dto/                  # Request/Response DTOs
│   ├── task/                 # Task logic (predicates, scopes, categorization)
│   ├── push/                 # APNs/FCM push notifications
│   ├── worker/               # Asynq background jobs
│   ├── admin/                # GoAdmin tables
│   ├── apperrors/            # Custom error types
│   ├── i18n/                 # Internationalization
│   ├── monitoring/           # Health checks, metrics
│   ├── validator/            # Custom validation rules
│   └── integration/          # Integration tests
├── migrations/               # SQL migrations
├── seeds/                    # Seed data (lookups, test data)
├── docs/                     # Documentation
├── docker/                   # Docker files
├── go.mod
└── Makefile
```

## Build & Run Commands

```bash
# Install dependencies
make deps

# Run the API server (development)
make run
# or
go run ./cmd/api

# Run the background worker
make run-worker

# Build binaries
make build          # API only
make build-all      # API, Worker, Admin

# Run tests
make test                    # All tests
make test-coverage           # With coverage report
go test ./internal/handlers  # Specific package

# Lint and format
make lint
make fmt

# Docker
make docker-up       # Start all services
make docker-down     # Stop services
make docker-logs     # View logs
make docker-restart  # Restart services

# Database migrations
make migrate-up
make migrate-down
```

## Architecture Patterns

### Layer Architecture

```
Request → Router → Middleware → Handler → Service → Repository → Database
                                    ↓
                              Response DTO
```

1. **Handlers** (`internal/handlers/`): HTTP request/response handling, input validation
2. **Services** (`internal/services/`): Business logic, orchestration
3. **Repositories** (`internal/repositories/`): Database operations (GORM)
4. **Models** (`internal/models/`): GORM entities (map to PostgreSQL tables)

### Task Logic Package

The `internal/task/` package centralizes all task-related logic:

```
internal/task/
├── predicates/predicates.go  # Pure functions: IsCompleted, IsOverdue, etc.
├── scopes/scopes.go          # GORM scopes: ScopeOverdue, ScopeActive, etc.
├── categorization/chain.go   # Kanban column determination
└── task.go                   # Re-exports for single import
```

**Always use this package for task logic:**
```go
import "github.com/treytartt/honeydue-api/internal/task"

// Predicates (in-memory checks)
if task.IsCompleted(t) { ... }
if task.IsOverdue(t, time.Now()) { ... }

// Scopes (database queries)
db.Scopes(task.ScopeActive).Scopes(task.ScopeOverdue(now)).Find(&tasks)

// Categorization
column := task.CategorizeTask(t, 30)  // 30 = "due soon" threshold days
```

### Authentication

Token-based authentication using Django's authtoken format for compatibility:

```go
// Middleware extracts user from token
user := middleware.GetUserFromContext(c)

// Protected routes require auth middleware
api := e.Group("/api")
api.Use(middleware.AuthRequired(db))
```

### Error Handling

Use structured errors from `internal/apperrors/`:

```go
import "github.com/treytartt/honeydue-api/internal/apperrors"

// Return typed errors
return apperrors.NewNotFoundError("task", taskID)
return apperrors.NewValidationError("title is required")
return apperrors.NewForbiddenError("not authorized to access this residence")

// Handler converts to HTTP response
// 404, 400, 403 respectively
```

## Database

### GORM Models

Models map to Django's existing PostgreSQL tables:

```go
// internal/models/task.go
type Task struct {
    ID           uint      `gorm:"primaryKey"`
    ResidenceID  uint      `gorm:"column:residence_id"`
    Title        string    `gorm:"column:title"`
    DueDate      *time.Time `gorm:"column:due_date;type:date"`
    NextDueDate  *time.Time `gorm:"column:next_due_date;type:date"`
    IsCancelled  bool      `gorm:"column:is_cancelled"`
    IsArchived   bool      `gorm:"column:is_archived"`
    InProgress   bool      `gorm:"column:in_progress"`
    // ...
}

func (Task) TableName() string {
    return "task_task"  // Django table name
}
```

### Common Pitfalls

1. **DATE vs TIMESTAMP**: Use `type:date` for date-only fields, not `type:timestamp`
2. **Preloading**: Always preload associations when needed:
   ```go
   db.Preload("Completions").Preload("Priority").Find(&task)
   ```
3. **Timezone**: Server uses UTC; client sends timezone header for overdue calculations

### Migrations

```bash
# Create new migration
make migrate-create name=add_new_column

# Run migrations
make migrate-up

# Rollback
make migrate-down
```

## API Endpoints

### Public Endpoints

| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/health/` | Health check |
| POST | `/api/auth/login/` | Login |
| POST | `/api/auth/register/` | Register |
| GET | `/api/static_data/` | Cached lookups (ETag support) |
| GET | `/api/upgrade-triggers/` | Subscription upgrade triggers |

### Protected Endpoints (Token Auth)

| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/residences/my/` | User's residences with summaries |
| GET | `/api/residences/:id/` | Residence detail |
| GET | `/api/tasks/` | All user's tasks (kanban columns) |
| GET | `/api/tasks/by-residence/:id/` | Tasks for residence (kanban) |
| POST | `/api/tasks/` | Create task |
| POST | `/api/tasks/:id/complete/` | Complete task |
| GET | `/api/contractors/` | User's contractors |
| GET | `/api/documents/` | User's documents |
| GET | `/api/subscription/status/` | Subscription status |

## Adding New Features

### Adding a New API Endpoint

1. **Create/update model** in `internal/models/`
2. **Create repository** in `internal/repositories/`:
   ```go
   type MyRepository struct {
       db *gorm.DB
   }

   func (r *MyRepository) FindByID(id uint) (*models.MyModel, error) {
       var m models.MyModel
       if err := r.db.First(&m, id).Error; err != nil {
           return nil, err
       }
       return &m, nil
   }
   ```
3. **Create service** in `internal/services/`:
   ```go
   type MyService struct {
       repo *repositories.MyRepository
   }

   func (s *MyService) DoSomething(id uint) error {
       // Business logic here
   }
   ```
4. **Create handler** in `internal/handlers/`:
   ```go
   type MyHandler struct {
       service *services.MyService
   }

   func (h *MyHandler) Get(c echo.Context) error {
       id := c.Param("id")
       result, err := h.service.DoSomething(id)
       if err != nil {
           return err
       }
       return c.JSON(http.StatusOK, result)
   }
   ```
5. **Register routes** in `internal/router/router.go`

### Adding Task-Related Logic

**ALWAYS use the task package. Never write inline task logic.**

1. Add predicate to `internal/task/predicates/predicates.go`
2. Add corresponding scope to `internal/task/scopes/scopes.go`
3. Re-export in `internal/task/task.go`
4. Add tests for both predicate and scope
5. Update `docs/TASK_LOGIC_ARCHITECTURE.md`

## Testing

```bash
# Run all tests
go test ./...

# Run specific package tests
go test ./internal/handlers -v

# Run with race detection
go test -race ./...

# Integration tests (requires database)
TEST_DATABASE_URL="..." go test ./internal/integration -v
```

### Test Patterns

```go
func TestTaskHandler_Create(t *testing.T) {
    // Setup test database
    db := testutil.SetupTestDB(t)

    // Create handler with dependencies
    handler := NewTaskHandler(db)

    // Create test request
    req := httptest.NewRequest(http.MethodPost, "/api/tasks/", body)
    rec := httptest.NewRecorder()
    c := echo.New().NewContext(req, rec)

    // Execute
    err := handler.Create(c)

    // Assert
    assert.NoError(t, err)
    assert.Equal(t, http.StatusCreated, rec.Code)
}
```

## Environment Variables

| Variable | Description | Required |
|----------|-------------|----------|
| `PORT` | Server port (default: 8000) | No |
| `DEBUG` | Enable debug mode | No |
| `SECRET_KEY` | Token signing secret | Yes |
| `POSTGRES_HOST` | Database host | Yes |
| `POSTGRES_PORT` | Database port | Yes |
| `POSTGRES_USER` | Database user | Yes |
| `POSTGRES_PASSWORD` | Database password | Yes |
| `POSTGRES_DB` | Database name | Yes |
| `REDIS_URL` | Redis connection URL | Yes |
| `APNS_KEY_ID` | Apple Push key ID | For push |
| `APNS_TEAM_ID` | Apple Team ID | For push |
| `APNS_TOPIC` | App bundle ID | For push |
| `FCM_SERVER_KEY` | Firebase server key | For push |

## Related Documentation

- `docs/TASK_LOGIC_ARCHITECTURE.md` - Task logic patterns (MUST READ)
- `docs/PUSH_NOTIFICATIONS.md` - Push notification setup
- `docs/SUBSCRIPTION_WEBHOOKS.md` - App Store/Play Store webhooks
- `docs/DOKKU_SETUP.md` - Production deployment

## Related Repositories

- **Mobile App**: `../HoneyDueKMM` - Kotlin Multiplatform iOS/Android app
- **Root Docs**: `../CLAUDE.md` - Full-stack documentation
