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>
This commit is contained in:
388
CLAUDE.md
Normal file
388
CLAUDE.md
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user