Files
honeyDueAPI/CLAUDE.md
Trey t 4976eafc6c Rebrand from Casera/MyCrib to honeyDue
Total rebrand across all Go API source files:
- Go module path: casera-api -> honeydue-api
- All imports updated (130+ files)
- Docker: containers, images, networks renamed
- Email templates: support email, noreply, icon URL
- Domains: casera.app/mycrib.treytartt.com -> honeyDue.treytartt.com
- Bundle IDs: com.tt.casera -> com.tt.honeyDue
- IAP product IDs updated
- Landing page, admin panel, config defaults
- Seeds, CI workflows, Makefile, docs
- Database table names preserved (no migration needed)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 06:33:38 -06:00

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

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)

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

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

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

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