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