diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..4a9b7dc --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,102 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Build and Test Commands + +### Building +```bash +# Build the executable +go build -o dave.exe + +# Download dependencies +go mod download +``` + +### Testing +```bash +# Run all tests +go test ./tests/... + +# Run a specific test file +go test ./tests/calculator_test.go + +# Run a specific test function +go test -run TestCalculateMonthlyInterest ./tests/ +``` + +### Running +```bash +# Run the built executable +./dave.exe + +# Or run directly with go +go run main.go +``` + +## Project Architecture + +Dave is a CLI debt tracking tool built with Go using the Cobra command framework and SQLite for persistence. + +### Core Package Structure + +- **`main.go`**: Entry point that delegates to `cmd.Execute()` +- **`cmd/`**: Cobra command definitions (add, pay, remove, adjust-*, mode, snowball, reset, show) + - `root.go`: Base command setup, database initialization lifecycle, and command registration + - Each command file defines a single Cobra command +- **`internal/models/`**: Data models and database operations + - `debt.go`: Debt CRUD operations with support for position-based (#) or name-based lookup + - `payment.go`: Payment history tracking + - `settings.go`: User settings (sort mode, snowball amount) +- **`internal/database/`**: SQLite database wrapper and schema management + - `db.go`: Database connection wrapper + - `schema.go`: Table creation and schema initialization +- **`internal/calculator/`**: Financial calculation engine + - `interest.go`: Monthly interest calculations + - `projections.go`: Simulates monthly payments with compound interest to project payoff timelines +- **`internal/display/`**: Terminal UI rendering with Lipgloss + - `table.go`: Debt table formatting with ASCII art header + - `formatter.go`: Currency, date, and percentage formatting utilities + - `styles.go`: Lipgloss style definitions +- **`internal/config/`**: Configuration and path management + - `paths.go`: Database path resolution (`~/.dave/debts.db`) + +### Key Architecture Patterns + +**Database Lifecycle**: The database connection is initialized in `cmd/root.go`'s `init()` via Cobra's `OnInitialize` hook and closed in `PersistentPostRun`. All commands access the shared DB via `GetDB()`. + +**Dual Identifier System**: Most commands accept either a position number (1, 2, 3...) or creditor name. The `GetDebtByIndexOrName()` function handles this by attempting integer parsing first, then falling back to name lookup. + +**Sort Modes**: Three modes control debt ordering: +- `snowball`: Smallest balance first (psychological wins) +- `avalanche`: Highest APR first (mathematically optimal) +- `manual`: Custom ordering via `custom_order` column + +Mode changes are handled in `models/settings.go` and affect how `GetAllDebts()` sorts results. + +**Projection Algorithm** (`calculator/projections.go`): +The `ProjectPayoffTimeline()` function simulates month-by-month payments: +1. Apply monthly interest to all active debts +2. Apply minimum payments to all active debts +3. Apply snowball amount to highest-priority unpaid debt +4. When a debt is paid off, add its minimum payment to the snowball (auto-snowball) +5. Repeat for up to 600 months (50 years) or until all debts are paid + +This simulation accounts for compound interest and the cascading snowball effect. + +**Hidden Paid Debts**: Debts with `current_balance = 0` are automatically filtered out by `GetAllDebts()` (via `WHERE current_balance > 0`), but remain in the database for historical tracking. + +**Payment History**: The `payments` table tracks all payments with interest/principal breakdown. When recording a payment, the debt balance is updated and a payment record is inserted. + +## Data Storage + +SQLite database at `~/.dave/debts.db` with three tables: +- **debts**: id, creditor, original_balance, current_balance, apr, minimum_payment, custom_order (nullable), created_at, updated_at +- **payments**: id, debt_id (FK), amount, interest_paid, principal_paid, payment_date, created_at +- **settings**: key-value pairs (sort_mode, snowball_amount) + +## Dependencies + +- **Cobra**: CLI framework for command structure +- **Lipgloss**: Terminal UI styling and table rendering +- **modernc.org/sqlite**: Pure-Go SQLite driver (no CGo) diff --git a/internal/display/formatter.go b/internal/display/formatter.go index 7208083..316721d 100644 --- a/internal/display/formatter.go +++ b/internal/display/formatter.go @@ -3,14 +3,16 @@ package display import ( "fmt" "time" + + "github.com/dustin/go-humanize" ) // FormatCurrency formats a float as currency with comma separators func FormatCurrency(amount float64) string { if amount < 0 { - return fmt.Sprintf("-$%.2f", -amount) + return "-$" + humanize.CommafWithDigits(amount*-1, 2) } - return fmt.Sprintf("$%.2f", amount) + return "$" + humanize.CommafWithDigits(amount, 2) } // FormatPercent formats a float as a percentage diff --git a/internal/display/table.go b/internal/display/table.go index 9646c26..95bb0c9 100644 --- a/internal/display/table.go +++ b/internal/display/table.go @@ -70,7 +70,15 @@ func RenderDebtsTable(debts []models.Debt, projections []calculator.DebtProjecti t := table.New(). Border(lipgloss.NormalBorder()). BorderStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("8"))). - Headers("#", "Creditor", "Original", "Current", "Rate", "Payment", "Interest", "Payoff", "Months", "Years") + Headers("#", "Creditor", "Original", "Current", "Rate", "Payment", "Interest", "Payoff", "Months", "Years"). + StyleFunc(func(row, col int) lipgloss.Style { + style := lipgloss.NewStyle() + // Right-align numeric columns (all except #=0 and Creditor=1) + if col >= 2 { + style = style.Align(lipgloss.Right) + } + return style + }) // Add data rows for i, debt := range debts {