Files
terminalShop/MODELS.md

1215 lines
34 KiB
Markdown

# Technical Specifications & Architecture
This document defines the technical stack, architecture patterns, code standards, and implementation details for the Terminal Coffee Shop project. **Read this before writing any code.**
---
## Table of Contents
1. [Development Workflow](#development-workflow)
2. [Technology Stack](#technology-stack)
3. [Database Schema](#database-schema)
4. [API Specifications](#api-specifications)
5. [Code Organization](#code-organization)
6. [Code Style Guide](#code-style-guide)
7. [Error Handling](#error-handling)
8. [Testing Standards](#testing-standards)
9. [Common Patterns](#common-patterns)
---
## Development Workflow
### Documentation-First Approach
**IMPORTANT**: Before implementing features using any library or technology, **always fetch and read the official documentation first**.
#### Process for Using New Libraries
1. **Identify the library/technology** you need to use (e.g., Chi router, GORM, Stripe SDK)
2. **Fetch the documentation** using WebFetch:
```
WebFetch the official documentation URL for the library
Read through relevant sections (getting started, API reference, examples)
```
3. **Understand the current best practices** from the docs (not from memory/assumptions)
4. **Implement** following the documented patterns
5. **Reference the docs** if you encounter issues
#### Why This Matters
- Libraries evolve - documentation ensures you use current patterns
- Prevents deprecated patterns or outdated approaches
- Helps discover features you might not know about
- Ensures compatibility with the version in go.mod
#### Example Workflow
```
Task: Implement Chi router middleware
Step 1: Check go.mod for version
Step 2: WebFetch https://go-chi.io/#/pages/middleware
Step 3: Read middleware documentation
Step 4: Implement following documented patterns
Step 5: Test implementation
```
#### Documentation Sources
- **Chi Router**: https://go-chi.io/
- **GORM**: https://gorm.io/docs/
- **Stripe Go**: https://stripe.com/docs/api?lang=go
- **Bubbletea**: https://github.com/charmbracelet/bubbletea
- **Lipgloss**: https://github.com/charmbracelet/lipgloss
---
## Technology Stack
### TUI (Terminal User Interface)
| Package | Version | Purpose | Documentation |
|---------|---------|---------|---------------|
| `github.com/charmbracelet/bubbletea` | v1.3.10+ | Main TUI framework (The Elm Architecture) | https://github.com/charmbracelet/bubbletea |
| `github.com/charmbracelet/lipgloss` | v1.1.0+ | Styling and layout | https://github.com/charmbracelet/lipgloss |
| `github.com/charmbracelet/bubbles` | v0.21.0+ | Pre-built UI components (viewport, spinner, etc.) | https://github.com/charmbracelet/bubbles |
| `github.com/charmbracelet/huh` | v0.8.0+ | Forms and input validation | https://github.com/charmbracelet/huh |
| `github.com/charmbracelet/wish` | v1.4.7+ | SSH server framework | https://github.com/charmbracelet/wish |
### Backend API
| Package | Version | Purpose | Documentation |
|---------|---------|---------|---------------|
| `github.com/go-chi/chi/v5` | v5.0.0+ | HTTP router | https://go-chi.io/ |
| `gorm.io/gorm` | v1.25.0+ | ORM for database operations | https://gorm.io/docs/ |
| `gorm.io/driver/postgres` | v1.5.0+ | PostgreSQL driver | https://gorm.io/docs/connecting_to_the_database.html |
| `github.com/golang-jwt/jwt/v5` | v5.0.0+ | JWT token handling | https://github.com/golang-jwt/jwt |
| `github.com/go-playground/validator/v10` | v10.0.0+ | Struct validation | https://github.com/go-playground/validator |
| `github.com/joho/godotenv` | v1.5.0+ | Environment variables | https://github.com/joho/godotenv |
### Payments & Services
| Package | Version | Purpose | Documentation |
|---------|---------|---------|---------------|
| `github.com/stripe/stripe-go/v78` | v78.12.0+ | Payment processing | https://stripe.com/docs/api?lang=go |
| `rsc.io/qr` | v0.2.0 | QR code generation | https://pkg.go.dev/rsc.io/qr |
### Utilities
| Package | Version | Purpose | Documentation |
|---------|---------|---------|---------------|
| `github.com/google/uuid` | v1.6.0+ | UUID generation | https://github.com/google/uuid |
| `golang.org/x/crypto` | latest | Cryptography utilities | https://pkg.go.dev/golang.org/x/crypto |
---
## Database Schema
### Schema Design Principles
- Use UUIDs for primary keys (not auto-incrementing integers)
- Always include `created_at` and `updated_at` timestamps
- Use soft deletes (`deleted_at`) for important records
- Normalize data appropriately (3NF where possible)
- Add indexes on foreign keys and frequently queried columns
### Complete Schema (PostgreSQL)
```sql
-- Users table
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
fingerprint VARCHAR(255) UNIQUE, -- SSH public key fingerprint
email VARCHAR(255) UNIQUE,
name VARCHAR(255),
stripe_customer_id VARCHAR(255), -- Stripe customer ID
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP NULL
);
-- Products table
CREATE TABLE products (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
description TEXT,
roast_type VARCHAR(100), -- "Dark Roast", "Medium Roast", etc.
ounces INTEGER, -- Size in ounces
bean_type VARCHAR(100), -- "Arabica", "Robusta", etc.
price INTEGER NOT NULL, -- Price in cents (e.g., 350 = $3.50)
color VARCHAR(7), -- Hex color for UI (e.g., "#8B4513")
image_url VARCHAR(500),
stock INTEGER DEFAULT 0,
active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP NULL
);
-- Carts table
CREATE TABLE carts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Cart items table
CREATE TABLE cart_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
cart_id UUID REFERENCES carts(id) ON DELETE CASCADE,
product_id UUID REFERENCES products(id),
quantity INTEGER NOT NULL DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(cart_id, product_id) -- One entry per product per cart
);
-- Addresses table
CREATE TABLE addresses (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id),
name VARCHAR(255), -- Recipient name
street VARCHAR(255) NOT NULL,
street2 VARCHAR(255), -- Apt, suite, etc.
city VARCHAR(100) NOT NULL,
state VARCHAR(2) NOT NULL, -- US state code
zip VARCHAR(10) NOT NULL,
country VARCHAR(2) DEFAULT 'US',
is_default BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Payment methods table (stores Stripe tokens, not raw card data)
CREATE TABLE payment_methods (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id),
stripe_payment_method_id VARCHAR(255) NOT NULL, -- Stripe PM ID
brand VARCHAR(50), -- "Visa", "Mastercard", etc.
last4 VARCHAR(4), -- Last 4 digits
exp_month INTEGER,
exp_year INTEGER,
is_default BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Orders table
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id),
address_id UUID REFERENCES addresses(id),
payment_method_id UUID REFERENCES payment_methods(id),
-- Amounts in cents
subtotal INTEGER NOT NULL,
shipping INTEGER NOT NULL DEFAULT 0,
tax INTEGER NOT NULL DEFAULT 0,
total INTEGER NOT NULL,
status VARCHAR(50) DEFAULT 'pending', -- pending, paid, shipped, delivered, cancelled
stripe_payment_intent_id VARCHAR(255), -- Stripe Payment Intent ID
tracking_number VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Order items table (snapshot of products at time of order)
CREATE TABLE order_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
order_id UUID REFERENCES orders(id) ON DELETE CASCADE,
product_id UUID REFERENCES products(id),
-- Snapshot product details (in case product changes later)
product_name VARCHAR(255),
product_description TEXT,
price INTEGER NOT NULL, -- Price at time of order
quantity INTEGER NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Indexes for performance
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_fingerprint ON users(fingerprint);
CREATE INDEX idx_carts_user_id ON carts(user_id);
CREATE INDEX idx_cart_items_cart_id ON cart_items(cart_id);
CREATE INDEX idx_addresses_user_id ON addresses(user_id);
CREATE INDEX idx_payment_methods_user_id ON payment_methods(user_id);
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status);
CREATE INDEX idx_order_items_order_id ON order_items(order_id);
```
### GORM Model Examples
```go
// models/user.go
package models
import (
"time"
"gorm.io/gorm"
"github.com/google/uuid"
)
type User struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()"`
Fingerprint string `gorm:"uniqueIndex;size:255"`
Email string `gorm:"uniqueIndex;size:255"`
Name string `gorm:"size:255"`
StripeCustomerID string `gorm:"size:255"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
// models/product.go
type Product struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()"`
Name string `gorm:"size:255;not null" json:"name"`
Description string `gorm:"type:text" json:"description"`
RoastType string `gorm:"size:100" json:"roast_type"`
Ounces int `json:"ounces"`
BeanType string `gorm:"size:100" json:"bean_type"`
Price int `gorm:"not null" json:"price"` // cents
Color string `gorm:"size:7" json:"color"`
ImageURL string `gorm:"size:500" json:"image_url"`
Stock int `gorm:"default:0" json:"stock"`
Active bool `gorm:"default:true" json:"active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
// models/cart.go
type Cart struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()"`
UserID uuid.UUID `gorm:"type:uuid;not null"`
User User `gorm:"foreignKey:UserID"`
Items []CartItem `gorm:"foreignKey:CartID"`
CreatedAt time.Time
UpdatedAt time.Time
}
type CartItem struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()"`
CartID uuid.UUID `gorm:"type:uuid;not null"`
ProductID uuid.UUID `gorm:"type:uuid;not null"`
Product Product `gorm:"foreignKey:ProductID"`
Quantity int `gorm:"not null;default:1"`
CreatedAt time.Time
UpdatedAt time.Time
}
```
---
## API Specifications
### API Design Principles
1. **RESTful routes** - Use standard HTTP methods (GET, POST, PUT, DELETE)
2. **JSON responses** - All responses in JSON format
3. **Consistent error format** - See Error Handling section
4. **JWT authentication** - Bearer token in Authorization header
5. **Versioning** - All routes prefixed with `/api/v1/`
6. **Pagination** - Use `page` and `limit` query params
### API Routes
#### Authentication
```
POST /api/v1/auth/register - Create new user
POST /api/v1/auth/login - Get JWT token
POST /api/v1/auth/refresh - Refresh token
GET /api/v1/auth/me - Get current user (requires auth)
PUT /api/v1/auth/me - Update current user (requires auth)
```
#### Products
```
GET /api/v1/products - List all products (paginated)
GET /api/v1/products/:id - Get single product
POST /api/v1/products - Create product (admin only)
PUT /api/v1/products/:id - Update product (admin only)
DELETE /api/v1/products/:id - Delete product (admin only)
```
#### Cart
```
GET /api/v1/cart - Get current user's cart
POST /api/v1/cart/items - Add item to cart
PUT /api/v1/cart/items/:id - Update item quantity
DELETE /api/v1/cart/items/:id - Remove item from cart
DELETE /api/v1/cart - Clear entire cart
```
#### Payment Methods
```
GET /api/v1/payment-methods - List saved payment methods
POST /api/v1/payment-methods - Save new payment method
DELETE /api/v1/payment-methods/:id - Remove payment method
```
#### Addresses
```
GET /api/v1/addresses - List user's addresses
POST /api/v1/addresses - Create new address
PUT /api/v1/addresses/:id - Update address
DELETE /api/v1/addresses/:id - Delete address
```
#### Orders
```
GET /api/v1/orders - List user's orders
GET /api/v1/orders/:id - Get order details
POST /api/v1/orders - Create new order (checkout)
```
### Request/Response Examples
#### GET /api/v1/products
**Response (200 OK)**:
```json
{
"success": true,
"data": {
"products": [
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"name": "Espresso",
"description": "Bold coffee...",
"roast_type": "Dark Roast",
"ounces": 2,
"bean_type": "Arabica",
"price": 350,
"color": "#8B4513",
"stock": 100,
"created_at": "2024-01-01T00:00:00Z"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 6,
"total_pages": 1
}
}
}
```
#### POST /api/v1/cart/items
**Request**:
```json
{
"product_id": "123e4567-e89b-12d3-a456-426614174000",
"quantity": 2
}
```
**Response (201 Created)**:
```json
{
"success": true,
"data": {
"cart": {
"id": "cart-uuid",
"items": [
{
"id": "item-uuid",
"product": { /* full product object */ },
"quantity": 2
}
],
"subtotal": 700
}
}
}
```
#### POST /api/v1/orders (Checkout)
**Request**:
```json
{
"address_id": "address-uuid",
"payment_method_id": "pm-uuid"
}
```
**Response (201 Created)**:
```json
{
"success": true,
"data": {
"order": {
"id": "order-uuid",
"subtotal": 700,
"shipping": 500,
"tax": 0,
"total": 1200,
"status": "pending",
"items": [ /* order items */ ]
}
}
}
```
---
## Code Organization
### Directory Structure
```
terminalShop/
├── main.go # Entry point (SSH server)
├── server.go # Current TUI entry point
├── model.go # Bubbletea TUI models and views
│
├── api/ # Backend API server
│ ├── main.go # API server entry point
│ ├── routes/
│ │ ├── auth.go # Auth routes
│ │ ├── products.go # Product routes
│ │ ├── cart.go # Cart routes
│ │ └── orders.go # Order routes
│ ├── middleware/
│ │ ├── auth.go # JWT auth middleware
│ │ ├── logger.go # Request logging
│ │ └── cors.go # CORS middleware
│ └── handlers/
│ ├── auth.go # Auth handler functions
│ ├── products.go # Product handlers
│ └── cart.go # Cart handlers
│
├── pkg/ # Shared packages
│ ├── models/ # Database models (GORM)
│ │ ├── user.go
│ │ ├── product.go
│ │ ├── cart.go
│ │ └── order.go
│ ├── database/ # Database connection
│ │ └── db.go
│ ├── apiclient/ # API client for TUI
│ │ └── client.go
│ ├── auth/ # Auth utilities
│ │ ├── jwt.go # JWT generation/validation
│ │ └── password.go # Password hashing
│ └── utils/ # Helper functions
│ ├── response.go # Standard response helpers
│ └── validation.go # Validation helpers
│
├── migrations/ # Database migrations
│ ├── 001_initial.sql
│ └── migrate.go
│
├── config/ # Configuration
│ └── config.go # Load env vars
│
├── tests/ # Tests
│ ├── api/
│ └── integration/
│
├── .env # Environment variables (not in git)
├── .env.example # Example env file
├── go.mod
├── go.sum
├── README.md
└── MODELS.md # This file
```
### Package Organization Rules
1. **One responsibility per package** - Each package should have a clear, single purpose
2. **No circular dependencies** - Organize imports in a clear hierarchy
3. **Shared code in pkg/** - Anything used by multiple packages goes in `pkg/`
4. **Keep handlers thin** - Business logic goes in separate service functions
5. **Models package is database-only** - No business logic in model structs
---
## Code Style Guide
### General Go Style
Follow the official [Go Code Review Comments](https://go.dev/wiki/CodeReviewComments) and [Effective Go](https://go.dev/doc/effective_go).
### Specific Rules for This Project
#### 1. Imports Organization
Always organize imports in three groups, separated by blank lines:
```go
import (
// Standard library
"context"
"fmt"
"net/http"
// External dependencies
"github.com/go-chi/chi/v5"
"gorm.io/gorm"
// Internal packages
"github.com/yourusername/terminalshop/pkg/models"
"github.com/yourusername/terminalshop/pkg/utils"
)
```
#### 2. Naming Conventions
- **Variables**: `camelCase` for local variables, `PascalCase` for exported
- **Constants**: `PascalCase` or `SCREAMING_SNAKE_CASE` for groups
- **Functions**: `PascalCase` for exported, `camelCase` for private
- **Interfaces**: Suffix with `-er` if single method (e.g., `Reader`, `Writer`)
- **Structs**: `PascalCase`, descriptive nouns
- **Files**: `snake_case.go`
```go
// Good
type UserService struct {
db *gorm.DB
}
func (s *UserService) CreateUser(ctx context.Context, name string) (*models.User, error) {
// ...
}
// Bad
type userservice struct { } // should be PascalCase if exported
func (s *UserService) create_user() { } // should be camelCase if private
```
#### 3. Error Messages
- Start with lowercase
- No punctuation at the end
- Include context about what failed
```go
// Good
return nil, fmt.Errorf("failed to create user: %w", err)
return nil, errors.New("user not found")
// Bad
return nil, fmt.Errorf("Error creating user: %v", err) // no caps, use %w
return nil, errors.New("User not found.") // no caps, no period
```
#### 4. Function Organization
Order functions in files logically:
1. Exported types/constants
2. Constructor functions (`New...`)
3. Public methods (alphabetically)
4. Private methods (alphabetically)
5. Helper functions
```go
// 1. Types
type UserHandler struct {
db *gorm.DB
}
// 2. Constructor
func NewUserHandler(db *gorm.DB) *UserHandler {
return &UserHandler{db: db}
}
// 3. Public methods
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
// ...
}
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
// ...
}
// 4. Private methods
func (h *UserHandler) validateUser(user *models.User) error {
// ...
}
// 5. Helper functions
func hashPassword(password string) (string, error) {
// ...
}
```
#### 5. Comment Style
- Use complete sentences for package and function comments
- Start comments with the name of the thing being described
- Use `//` for single-line, `/* */` for multi-line only when needed
```go
// UserHandler handles HTTP requests related to users.
type UserHandler struct {
db *gorm.DB
}
// CreateUser creates a new user in the database and returns it.
// Returns an error if validation fails or database operation fails.
func (h *UserHandler) CreateUser(ctx context.Context, user *models.User) error {
// Validate user input
if err := h.validateUser(user); err != nil {
return fmt.Errorf("validation failed: %w", err)
}
// Create user in database
if err := h.db.Create(user).Error; err != nil {
return fmt.Errorf("failed to create user: %w", err)
}
return nil
}
```
#### 6. Struct Tags
Always include JSON tags and use snake_case for JSON field names:
```go
type Product struct {
ID uuid.UUID `gorm:"type:uuid;primary_key" json:"id"`
Name string `gorm:"size:255" json:"name"`
Price int `gorm:"not null" json:"price"`
RoastType string `gorm:"size:100" json:"roast_type"` // snake_case!
CreatedAt time.Time `json:"created_at"`
}
```
#### 7. Context Usage
Always pass `context.Context` as the first parameter:
```go
// Good
func (s *UserService) GetUser(ctx context.Context, id uuid.UUID) (*models.User, error) {
return s.db.WithContext(ctx).First(&user, id).Error
}
// Bad
func (s *UserService) GetUser(id uuid.UUID) (*models.User, error) {
// No context!
}
```
---
## Error Handling
### Principles
1. **Always handle errors** - Never ignore errors with `_`
2. **Wrap errors** - Use `fmt.Errorf("context: %w", err)` to add context
3. **Don't panic** - Only panic in `main()` or `init()` for unrecoverable errors
4. **Return early** - Use guard clauses and early returns
### Standard Error Response Format (API)
All API errors should follow this format:
```json
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input data",
"details": {
"email": "must be a valid email address",
"password": "must be at least 8 characters"
}
}
}
```
### Error Response Helper
```go
// pkg/utils/response.go
package utils
import (
"encoding/json"
"net/http"
)
type ErrorResponse struct {
Success bool `json:"success"`
Error Error `json:"error"`
}
type Error struct {
Code string `json:"code"`
Message string `json:"message"`
Details map[string]interface{} `json:"details,omitempty"`
}
func RespondError(w http.ResponseWriter, code int, errCode, message string, details map[string]interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
json.NewEncoder(w).Encode(ErrorResponse{
Success: false,
Error: Error{
Code: errCode,
Message: message,
Details: details,
},
})
}
func RespondSuccess(w http.ResponseWriter, code int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"data": data,
})
}
```
### Common Error Codes
```go
const (
ErrCodeValidation = "VALIDATION_ERROR"
ErrCodeUnauthorized = "UNAUTHORIZED"
ErrCodeForbidden = "FORBIDDEN"
ErrCodeNotFound = "NOT_FOUND"
ErrCodeConflict = "CONFLICT"
ErrCodeInternal = "INTERNAL_ERROR"
ErrCodeBadRequest = "BAD_REQUEST"
ErrCodePaymentFailed = "PAYMENT_FAILED"
)
```
### Error Handling Pattern (API Handlers)
```go
func (h *ProductHandler) GetProduct(w http.ResponseWriter, r *http.Request) {
// Extract ID from URL
productID := chi.URLParam(r, "id")
if productID == "" {
utils.RespondError(w, http.StatusBadRequest, ErrCodeBadRequest, "product ID required", nil)
return
}
// Parse UUID
id, err := uuid.Parse(productID)
if err != nil {
utils.RespondError(w, http.StatusBadRequest, ErrCodeValidation, "invalid product ID", nil)
return
}
// Get product from database
product, err := h.productService.GetProduct(r.Context(), id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
utils.RespondError(w, http.StatusNotFound, ErrCodeNotFound, "product not found", nil)
return
}
// Log the actual error internally
log.Printf("failed to get product: %v", err)
utils.RespondError(w, http.StatusInternalServerError, ErrCodeInternal, "internal server error", nil)
return
}
// Success response
utils.RespondSuccess(w, http.StatusOK, product)
}
```
### Error Handling in TUI
For the TUI, errors should be displayed to the user in a friendly way:
```go
// In model.go Update() function
switch msg := msg.(type) {
case error:
m.errorMessage = api.GetErrorMessage(msg)
m.showError = true
return m, nil
}
// In View() function
func (m model) View() string {
if m.showError {
errorBox := lipgloss.NewStyle().
Foreground(lipgloss.Color("#FF0000")).
Bold(true).
Render("Error: " + m.errorMessage)
return errorBox + "\n" + m.normalView()
}
return m.normalView()
}
```
---
## Testing Standards
### Testing Strategy
1. **Unit tests** for business logic and utilities
2. **Integration tests** for API endpoints with database
3. **No tests for TUI initially** (focus on API)
### Test File Organization
- Test files live next to the code they test
- Name: `<file>_test.go`
- Use table-driven tests for multiple cases
### Example Test Structure
```go
// api/handlers/product_test.go
package handlers
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestProductHandler_GetProduct(t *testing.T) {
tests := []struct {
name string
productID string
setupMock func(*mockDB)
expectedStatus int
expectedError string
}{
{
name: "success",
productID: "valid-uuid",
setupMock: func(m *mockDB) { /* setup */ },
expectedStatus: 200,
},
{
name: "not found",
productID: "nonexistent-uuid",
setupMock: func(m *mockDB) { /* setup */ },
expectedStatus: 404,
expectedError: "NOT_FOUND",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Test implementation
// Use require for setup assertions
// Use assert for test assertions
})
}
}
```
### Testing Libraries
- `testing` - Standard library
- `github.com/stretchr/testify` - Assertions and mocking
- Test database: Use in-memory SQLite or Docker PostgreSQL
---
## Common Patterns
### Pattern 1: Service Layer
Separate business logic from HTTP handlers:
```go
// pkg/services/user_service.go
type UserService struct {
db *gorm.DB
}
func NewUserService(db *gorm.DB) *UserService {
return &UserService{db: db}
}
func (s *UserService) CreateUser(ctx context.Context, name, email string) (*models.User, error) {
user := &models.User{
Name: name,
Email: email,
}
if err := s.db.WithContext(ctx).Create(user).Error; err != nil {
return nil, fmt.Errorf("failed to create user: %w", err)
}
return user, nil
}
// api/handlers/user_handler.go
type UserHandler struct {
userService *services.UserService
}
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
// Parse request
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
utils.RespondError(w, 400, ErrCodeBadRequest, "invalid request body", nil)
return
}
// Call service
user, err := h.userService.CreateUser(r.Context(), req.Name, req.Email)
if err != nil {
// Handle error
return
}
utils.RespondSuccess(w, 201, user)
}
```
### Pattern 2: Middleware for Auth
```go
// api/middleware/auth.go
func AuthMiddleware(jwtSecret string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Extract token from Authorization header
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
utils.RespondError(w, 401, ErrCodeUnauthorized, "missing auth token", nil)
return
}
// Validate token
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return []byte(jwtSecret), nil
})
if err != nil || !token.Valid {
utils.RespondError(w, 401, ErrCodeUnauthorized, "invalid token", nil)
return
}
// Extract user ID from claims
claims := token.Claims.(jwt.MapClaims)
userID := claims["user_id"].(string)
// Add to context
ctx := context.WithValue(r.Context(), "user_id", userID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// Usage in routes
r.Group(func(r chi.Router) {
r.Use(middleware.AuthMiddleware(jwtSecret))
r.Get("/api/v1/cart", handlers.GetCart)
r.Post("/api/v1/cart/items", handlers.AddCartItem)
})
```
### Pattern 3: Repository Pattern (Optional)
For more complex queries, separate database access:
```go
// pkg/repositories/product_repository.go
type ProductRepository struct {
db *gorm.DB
}
func (r *ProductRepository) GetByID(ctx context.Context, id uuid.UUID) (*models.Product, error) {
var product models.Product
err := r.db.WithContext(ctx).Where("id = ? AND active = ?", id, true).First(&product).Error
return &product, err
}
func (r *ProductRepository) List(ctx context.Context, page, limit int) ([]models.Product, int64, error) {
var products []models.Product
var total int64
offset := (page - 1) * limit
r.db.Model(&models.Product{}).Where("active = ?", true).Count(&total)
err := r.db.WithContext(ctx).
Where("active = ?", true).
Limit(limit).
Offset(offset).
Find(&products).Error
return products, total, err
}
```
### Pattern 4: Bubbletea View Switching
```go
// In model.go
type page int
const (
shopPage page = iota
cartPage
accountPage
paymentPage
)
type model struct {
currentPage page
// ... other fields
}
func (m model) SwitchToPage(p page) (model, tea.Cmd) {
m.currentPage = p
// Reset page-specific state
switch p {
case shopPage:
m.cursor = 0
case cartPage:
m.cartCursor = 0
// ...
}
return m, nil
}
func (m model) View() string {
header := m.buildHeader()
footer := m.buildFooter()
var content string
switch m.currentPage {
case shopPage:
content = m.buildShopView()
case cartPage:
content = m.buildCartView()
case accountPage:
content = m.buildAccountView()
default:
content = "Unknown page"
}
return lipgloss.JoinVertical(lipgloss.Left, header, content, footer)
}
```
---
## Configuration Management
### Environment Variables
Use a `.env` file for local development:
```bash
# .env (DO NOT COMMIT)
DATABASE_URL=postgresql://user:password@localhost:5432/terminalshop
JWT_SECRET=your-super-secret-jwt-key-change-this
STRIPE_SECRET_KEY=sk_test_...
STRIPE_PUBLIC_KEY=pk_test_...
API_PORT=8080
SSH_PORT=2222
ENVIRONMENT=development
```
### Config Package
```go
// config/config.go
package config
import (
"os"
"github.com/joho/godotenv"
)
type Config struct {
DatabaseURL string
JWTSecret string
StripeSecretKey string
StripePublicKey string
APIPort string
SSHPort string
Environment string
}
func Load() (*Config, error) {
// Load .env file in development
if os.Getenv("ENVIRONMENT") != "production" {
godotenv.Load()
}
return &Config{
DatabaseURL: os.Getenv("DATABASE_URL"),
JWTSecret: os.Getenv("JWT_SECRET"),
StripeSecretKey: os.Getenv("STRIPE_SECRET_KEY"),
StripePublicKey: os.Getenv("STRIPE_PUBLIC_KEY"),
APIPort: getEnvOrDefault("API_PORT", "8080"),
SSHPort: getEnvOrDefault("SSH_PORT", "2222"),
Environment: getEnvOrDefault("ENVIRONMENT", "development"),
}, nil
}
func getEnvOrDefault(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
```
---
## Security Best Practices
1. **Never commit secrets** - Use `.env` and add to `.gitignore`
2. **Hash passwords** - Use `bcrypt` with cost >= 12
3. **Validate all inputs** - Never trust user input
4. **Use parameterized queries** - GORM does this automatically
5. **HTTPS in production** - Always use TLS
6. **Rate limiting** - Add to API endpoints
7. **CORS configuration** - Only allow trusted origins
---
## Reference Implementation
The `terminal-shop-source/` directory contains the complete terminal.shop codebase. When in doubt:
1. **Check similar functionality** in their code
2. **Copy patterns, not code verbatim** - adapt to our stack
3. **Understand before implementing** - don't blindly copy
Key files to reference:
- `packages/go/pkg/tui/*.go` - TUI views and components
- `packages/go/pkg/api/api.go` - API client integration
- `packages/core/src/` - Backend business logic (TypeScript, but concepts apply)
---
## Quick Reference Checklist
Before writing new code, ask:
- [ ] Does this follow the file organization structure?
- [ ] Are imports organized correctly?
- [ ] Are errors wrapped with context?
- [ ] Are all errors handled?
- [ ] Does this need a test?
- [ ] Are naming conventions followed?
- [ ] Is the API response in the standard format?
- [ ] Are database queries using context?
- [ ] Is sensitive data in environment variables?
- [ ] Does this match patterns from terminal.shop reference code?
---
**Remember**: Consistency is more important than perfection. Follow these patterns throughout the codebase, and reference this document frequently!