diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..03e4382 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,388 @@ +# 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". + +## 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/casera-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 + +Casera API is a Go REST API for the MyCrib/Casera 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 + +``` +myCribAPI-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/casera-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/casera-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**: `../MyCribKMM` - Kotlin Multiplatform iOS/Android app +- **Root Docs**: `../CLAUDE.md` - Full-stack documentation