Files
honeyDueAPI/CLAUDE.md
Trey t b62da4e303 Add CLAUDE.md with comprehensive developer documentation
Documents the Go API architecture and patterns:
- Project structure and tech stack (Echo, GORM, Asynq, Redis)
- Task logic package usage (predicates, scopes, categorization)
- Layer architecture (Handler → Service → Repository)
- Build commands, testing, and environment configuration
- API endpoints reference
- Links to related docs (TASK_LOGIC_ARCHITECTURE.md)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 21:04:49 -06:00

11 KiB

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".

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:

// 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

# 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:

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:

// 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/:

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:

// 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:
    db.Preload("Completions").Preload("Priority").Find(&task)
    
  3. Timezone: Server uses UTC; client sends timezone header for overdue calculations

Migrations

# 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/:
    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/:
    type MyService struct {
        repo *repositories.MyRepository
    }
    
    func (s *MyService) DoSomething(id uint) error {
        // Business logic here
    }
    
  4. Create handler in internal/handlers/:
    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

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

# 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

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
  • 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
  • Mobile App: ../MyCribKMM - Kotlin Multiplatform iOS/Android app
  • Root Docs: ../CLAUDE.md - Full-stack documentation