Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions .claude/commands/pre-push.md

This file was deleted.

7 changes: 7 additions & 0 deletions .claude/commands/update-doc-logs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Do the following TODO. If the instructions are not relevant, do nothing.

1. Check any files that were created in the recent commits that part within the `docs/logs` folder.

2. If the changes are related to a different domain or topics altogether, create a new log file with similar front matter to record the topic.

3. Update any existing logs that have relevant content that is out of date with the code changes. If there are major changes to an existing log file. Instead of editing what is there already, add a new section in the same file with a record of the datetime of the change.
193 changes: 193 additions & 0 deletions docs/logs/add-expenses-feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
---
title: Adding Expenses Feature to SplitApp
date: 2025-08-24
time: 20:35:00
author: Claude
tags: [expenses, feature, money, liveview, associations]
---

# Adding Expenses Feature to SplitApp

## Overview
This document outlines the implementation of the Expenses feature, which allows users to create, track, and manage expenses. The feature integrates with the existing Groups system and includes proper money handling with multi-currency support.

## Database Changes

### 1. Expenses Table
Created a new `expenses` table with the following fields:
- `id` - Primary key
- `title` - String field for expense title (required)
- `description` - Text field for expense description (optional)
- `amount` - Map field storing Money type with amount and currency (required)
- `created_by_id` - Foreign key to users table (required)
- `inserted_at` - Timestamp
- `updated_at` - Timestamp

**Migration file:** `priv/repo/migrations/20250824203547_create_expenses.exs`

### 2. Expense Assignments Join Table
Created an `expense_assignments` join table for many-to-many relationship between expenses and users:
- `expense_id` - Foreign key to expenses table
- `user_id` - Foreign key to users table
- `inserted_at` - Timestamp
- `updated_at` - Timestamp
- Composite unique index on `[expense_id, user_id]`

### 3. Expense Groups Join Table
Created an `expense_groups` join table for many-to-many relationship between expenses and groups:
- `expense_id` - Foreign key to expenses table
- `group_id` - Foreign key to groups table
- `inserted_at` - Timestamp
- `updated_at` - Timestamp
- Composite unique index on `[expense_id, group_id]`

## Code Changes

### 1. Money Library Integration (`mix.exs`)
Added `money` library dependency for proper currency handling:
```elixir
{:money, "~> 1.12"}
```

### 2. Context and Schemas

#### Expenses Context (`lib/split_app/expenses.ex`)
Generated using Phoenix context generator with functions:
- `list_expenses/0` - List all expenses
- `list_expenses_by_user/1` - List expenses created by specific user
- `get_expense!/1` - Get a single expense
- `create_expense/1` - Create a new expense
- `update_expense/2` - Update an expense
- `delete_expense/1` - Delete an expense
- `change_expense/2` - Generate changeset for forms

#### Expense Schema (`lib/split_app/expenses/expense.ex`)
- Fields: `title`, `description`, `amount` (Money type)
- Associations:
- `belongs_to :created_by` (User who created the expense)
- `many_to_many :assigned_users` through `expense_assignments`
- `many_to_many :groups` through `expense_groups`
- Custom validation for Money type ensuring:
- Amount is greater than 0
- Currency is present
- Valid Money struct format

### 3. Web Layer

#### Expense LiveViews (Generated)
- `lib/split_app_web/live/expense_live/index.ex` - List user's expenses
- `lib/split_app_web/live/expense_live/show.ex` - Show single expense
- `lib/split_app_web/live/expense_live/form_component.ex` - Create/edit expenses
- Custom money input handling
- Currency selection support
- Integration with current user context

#### Dashboard Updates (`lib/split_app_web/live/dashboard_live.ex`)
Enhanced dashboard to show user's recent expenses alongside groups.

#### Live Helpers (`lib/split_app_web/live/live_helpers.ex`)
Added utility functions for LiveView components and common patterns.

### 4. Router Updates (`lib/split_app_web/router.ex`)

Added authenticated routes for expenses:
```elixir
live "/expenses", ExpenseLive.Index, :index
live "/expenses/new", ExpenseLive.Index, :new
live "/expenses/:id/edit", ExpenseLive.Index, :edit
live "/expenses/:id", ExpenseLive.Show, :show
live "/expenses/:id/show/edit", ExpenseLive.Show, :edit
```

### 5. Group Integration

#### Group LiveViews Updates
Updated group show and index pages to display related expenses and provide links to create expenses within group context.

## Features Implemented

### Money Handling
- **Multi-Currency Support**: Uses Money library for proper currency handling
- **Validation**: Ensures positive amounts and valid currency codes
- **Display**: Proper formatting of money amounts in UI
- **Input**: Custom form inputs for amount and currency selection

### Expense Management
- **Create Expenses**: Users can create expenses with title, description, and amount
- **Edit/Delete**: Full CRUD operations for user's own expenses
- **User Context**: Expenses are automatically associated with creator
- **Filtering**: Users see only their own expenses by default

### Associations
- **User Assignment**: Expenses can be assigned to multiple users (foundation for splitting)
- **Group Integration**: Expenses can be associated with groups
- **Creator Tracking**: System tracks who created each expense

### UI/UX Features
- **Expense List**: Clean table view of user's expenses with amounts, titles, and dates
- **Money Display**: Proper currency formatting (e.g., "$25.50 USD")
- **Form Validation**: Real-time validation of money amounts and required fields
- **Integration**: Seamless navigation between expenses, groups, and dashboard

## Testing

### Test Files Created
- `test/split_app/expenses/expense_test.exs` - Unit tests for Expense schema
- `test/split_app_web/live/expense_live_test.exs` - Integration tests for LiveViews
- `test/support/fixtures/expenses_fixtures.ex` - Test data fixtures

### Test Coverage
- Schema validations for Money type
- CRUD operations in context
- LiveView interactions and form submissions
- Money formatting and display

## Migration Commands

```bash
# Add money library
# (Added to mix.exs manually)

# Generate the Expenses context and schema
mix phx.gen.context Expenses Expense expenses title:string description:text amount:map created_by_id:references:users

# Generate LiveView pages
mix phx.gen.live Expenses Expense expenses title:string description:text amount:map created_by_id:references:users --no-context --no-schema

# Run migrations
mix ecto.migrate

# Install new dependencies
mix deps.get
```

## Future Enhancements

This foundation enables future expense splitting functionality:
- **Expense Splitting**: Calculate splits between assigned users
- **Settlement Tracking**: Track who owes what to whom
- **Group Expense Views**: Show all expenses within a group
- **Balance Calculations**: Calculate user balances across groups
- **Payment Tracking**: Mark when debts are settled
- **Expense Categories**: Categorize expenses (food, transport, etc.)
- **Receipt Uploads**: Attach receipt images to expenses
- **Recurring Expenses**: Support for recurring/scheduled expenses

## Technical Notes

### Money Type Implementation
The Money library stores currency amounts as a map in the database:
```elixir
%{amount: 2550, currency: :USD} # Represents $25.50 USD
```

This ensures:
- Precise decimal arithmetic (no floating point errors)
- Multi-currency support
- Consistent currency handling across the application
- Proper serialization/deserialization from database

### Performance Considerations
- Indexes on `created_by_id` for efficient user expense queries
- Composite unique indexes on join tables prevent duplicates
- Ordered queries for chronological expense display
136 changes: 136 additions & 0 deletions lib/split_app/expenses.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
defmodule SplitApp.Expenses do
@moduledoc """
The Expenses context.
"""

import Ecto.Query, warn: false
alias SplitApp.Repo

alias SplitApp.Expenses.Expense

@doc """
Returns the list of expenses.

## Examples

iex> list_expenses()
[%Expense{}, ...]

"""
def list_expenses do
Repo.all(Expense)
end

@doc """
Returns the list of expenses for a specific user.

## Examples

iex> list_expenses_by_user(123)
[%Expense{}, ...]

"""
def list_expenses_by_user(user_id) do
from(e in Expense,
where: e.created_by_id == ^user_id,
order_by: [desc: e.inserted_at]
)
|> Repo.all()
end

@doc """
Gets a single expense.

Raises `Ecto.NoResultsError` if the Expense does not exist.

## Examples

iex> get_expense!(123)
%Expense{}

iex> get_expense!(456)
** (Ecto.NoResultsError)

"""
def get_expense!(id), do: Repo.get!(Expense, id)

@doc """
Gets a single expense with preloaded associations.

## Examples

iex> get_expense_with_associations!(123)
%Expense{assigned_users: [...], groups: [...]}

"""
def get_expense_with_associations!(id) do
Expense
|> Repo.get!(id)
|> Repo.preload([:created_by, :assigned_users, :groups])
end

@doc """
Creates a expense.

## Examples

iex> create_expense(%{field: value})
{:ok, %Expense{}}

iex> create_expense(%{field: bad_value})
{:error, %Ecto.Changeset{}}

"""
def create_expense(attrs \\ %{}) do
%Expense{}
|> Expense.changeset(attrs)
|> Repo.insert()
end

@doc """
Updates a expense.

## Examples

iex> update_expense(expense, %{field: new_value})
{:ok, %Expense{}}

iex> update_expense(expense, %{field: bad_value})
{:error, %Ecto.Changeset{}}

"""
def update_expense(%Expense{} = expense, attrs) do
expense
|> Expense.changeset(attrs)
|> Repo.update()
end

@doc """
Deletes a expense.

## Examples

iex> delete_expense(expense)
{:ok, %Expense{}}

iex> delete_expense(expense)
{:error, %Ecto.Changeset{}}

"""
def delete_expense(%Expense{} = expense) do
Repo.delete(expense)
end

@doc """
Returns an `%Ecto.Changeset{}` for tracking expense changes.

## Examples

iex> change_expense(expense)
%Ecto.Changeset{data: %Expense{}}

"""
def change_expense(%Expense{} = expense, attrs \\ %{}) do
Expense.changeset(expense, attrs)
end
end
33 changes: 33 additions & 0 deletions lib/split_app/expenses/expense.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
defmodule SplitApp.Expenses.Expense do
use Ecto.Schema
import Ecto.Changeset

schema "expenses" do
field :title, :string
field :description, :string
field :amount, Money.Ecto.Map.Type

belongs_to :created_by, SplitApp.Accounts.User, foreign_key: :created_by_id
many_to_many :assigned_users, SplitApp.Accounts.User, join_through: "expense_assignments"
many_to_many :groups, SplitApp.Groups.Group, join_through: "expense_groups"

timestamps(type: :utc_datetime)
end

@doc false
def changeset(expense, attrs) do
expense
|> cast(attrs, [:title, :description, :amount, :created_by_id])
|> validate_required([:title, :amount, :created_by_id])
|> validate_money(:amount)
end

defp validate_money(changeset, field) do
validate_change(changeset, field, fn
_, %Money{amount: amount, currency: currency} when amount > 0 and not is_nil(currency) -> []
_, %Money{amount: amount} when amount <= 0 -> [{field, "amount must be greater than 0"}]
_, %Money{currency: nil} -> [{field, "currency is required"}]
_, _ -> [{field, "must be a valid money amount"}]
end)
end
end
Loading
Loading