README.md

# Ledgex

A double-entry accounting system for Elixir applications, ported from Ruby's [double_entry](https://github.com/envato/double_entry) gem.

Ledgex provides a robust framework for tracking financial transactions using double-entry bookkeeping principles. Each transfer creates two ledger entries ensuring balanced records and data integrity.

## Features

- **Dynamic Account Management**: Define accounts at runtime, stored in ETS for fast access
- **Scoped Accounts**: Per-user or per-entity account isolation
- **Balance Tracking**: Current and historical balance calculations with efficient caching
- **Currency Support**: Integration with the [Money](https://hex.pm/packages/money) library
- **Transfer Validation**: Configurable allowed transfers with business codes
- **Metadata Support**: Attach arbitrary data to transfers via JSON
- **Concurrency Safe**: Database-level locking prevents race conditions
- **Flexible Queries**: Historical snapshots, date ranges, and code filtering
- **Idiomatic Elixir**: ETS tables, tagged tuples, functional patterns

## Installation

Add to `mix.exs`:

```elixir
def deps do
  [
    {:ledgex, "~> 1.0"},
    {:money, "~> 1.12"},
    {:decimal, "~> 2.0"},
    {:postgrex, ">= 0.0.0"}  # or {:myxql, ">= 0.0.0"} for MySQL
  ]
end
```

Then generate and run the migration:

```bash
mix deps.get
mix ledgex.gen.migration
mix ecto.migrate
```

## Quick Start

```elixir
# 1. Configure the repo (in application.ex)
Ledgex.Config.put(:repo, MyApp.Repo)

# 2. Define accounts (can be done anytime, even at runtime!)
Ledgex.Account.define!(identifier: :cash, currency: "USD")
Ledgex.Account.define!(
  identifier: :checking,
  scope_identity_fun: &(&1.id),  # Scoped per-user
  currency: "USD"
)
Ledgex.Account.define!(
  identifier: :savings,
  scope_identity_fun: &(&1.id),
  positive_only: true,  # Can't go negative
  currency: "USD"
)

# 3. Define allowed transfers
Ledgex.Transfer.define!(from: :cash, to: :checking, code: :deposit)
Ledgex.Transfer.define!(from: :checking, to: :savings, code: :save)
Ledgex.Transfer.define!(from: :savings, to: :checking, code: :withdraw)

# 4. Get account references (simple tuples!)
cash = Ledgex.account_ref(:cash)
user_checking = Ledgex.account_ref(:checking, user)
user_savings = Ledgex.account_ref(:savings, user)

# 5. Transfer money
{:ok, {from_line, to_line}} = Ledgex.transfer(
  Money.new(10000, :USD),  # $100.00
  from: cash,
  to: user_checking,
  code: :deposit
)

# Or use bang version
Ledgex.transfer!(
  Money.new(5000, :USD),
  from: user_checking,
  to: user_savings,
  code: :save
)

# 6. Check balances
balance = Ledgex.balance(user_checking)  # Returns Money struct
```

## Core Concepts

### Account References

Account references are simple tuples: `{%Account{}, scope_identity}`. This functional approach makes pattern matching easy:

```elixir
# Unscoped account
cash_ref = Ledgex.account_ref(:cash)
# => {%Ledgex.Account{identifier: :cash, ...}, nil}

# Scoped account
checking_ref = Ledgex.account_ref(:checking, user)
# => {%Ledgex.Account{identifier: :checking, ...}, 123}

# Pattern match on refs
{account, scope_id} = checking_ref
```

### Tagged Tuples

Ledgex uses idiomatic Elixir error handling with tagged tuples:

```elixir
case Ledgex.transfer(amount, from: acc1, to: acc2, code: :transfer) do
  {:ok, {from_line, to_line}} ->
    # Success! Process lines
    
  {:error, :negative_amount} ->
    # Handle negative amount error
    
  {:error, :not_allowed} ->
    # Handle unauthorized transfer
    
  {:error, :same_account} ->
    # Handle same-account transfer
end

# Or use bang version to raise on error
Ledgex.transfer!(amount, from: acc1, to: acc2, code: :transfer)
```

### Dynamic Configuration

Unlike traditional configuration, Ledgex allows runtime changes:

```elixir
# Add accounts dynamically
{:ok, account} = Ledgex.Account.define(
  identifier: :new_account,
  currency: "EUR"
)

# Remove accounts (careful!)
Ledgex.Account.delete(:old_account)

# List all accounts
accounts = Ledgex.Account.list()

# Same for transfers
Ledgex.Transfer.define!(from: :new_account, to: :cash, code: :exchange)
Ledgex.Transfer.delete(:old_account, :cash, :old_code)
```

## Usage Examples

### Basic Transfer

```elixir
# Define what you need
Ledgex.Account.define!(identifier: :revenue, currency: "USD")
Ledgex.Account.define!(identifier: :bank, currency: "USD")
Ledgex.Transfer.define!(from: :revenue, to: :bank, code: :collect)

# Transfer
revenue = Ledgex.account_ref(:revenue)
bank = Ledgex.account_ref(:bank)

Ledgex.transfer!(
  Money.new(50000, :USD),
  from: revenue,
  to: bank,
  code: :collect
)
```

### With Metadata

```elixir
Ledgex.transfer!(
  Money.new(25000, :USD),
  from: checking,
  to: savings,
  code: :save,
  metadata: %{
    note: "Monthly savings",
    category: "savings",
    tags: ["automated", "recurring"]
  }
)
```

### Multi-Account Operations

```elixir
# Lock accounts to ensure atomicity
Ledgex.lock([acc1, acc2, acc3], fn ->
  # All transfers happen atomically
  Ledgex.transfer!(Money.new(1000, :USD), from: acc1, to: acc2, code: :transfer)
  Ledgex.transfer!(Money.new(500, :USD), from: acc2, to: acc3, code: :transfer)
  
  # Other database operations...
  update_user_stats(user)
end)
```

### Balance Queries

```elixir
# Current balance
current = Ledgex.balance(account)

# Historical balance
historical = Ledgex.balance(account, 
  at: ~U[2024-01-01 00:00:00Z]
)

# Balance for time range
range_balance = Ledgex.balance(account,
  from: ~U[2024-01-01 00:00:00Z],
  to: ~U[2024-12-31 23:59:59Z]
)

# Balance filtered by code
deposits = Ledgex.balance(account, code: :deposit)
withdrawals = Ledgex.balance(account, code: :withdraw)

# Multiple codes
inflows = Ledgex.balance(account, codes: [:deposit, :income, :refund])
```

### Per-User Accounts

```elixir
# Define scoped account
Ledgex.Account.define!(
  identifier: :wallet,
  scope_identity_fun: &(&1.id),
  currency: "USD"
)

# Each user has their own wallet
user1_wallet = Ledgex.account_ref(:wallet, user1)  # {Account, 1}
user2_wallet = Ledgex.account_ref(:wallet, user2)  # {Account, 2}

# Balances are separate
Ledgex.balance(user1_wallet)  # User 1's balance
Ledgex.balance(user2_wallet)  # User 2's balance (independent)
```

### Account Constraints

```elixir
# Positive-only (e.g., savings, can't overdraft)
Ledgex.Account.define!(
  identifier: :savings,
  positive_only: true,
  currency: "USD"
)

# Negative-only (e.g., debt, should stay negative/zero)
Ledgex.Account.define!(
  identifier: :debt,
  negative_only: true,
  currency: "USD"
)
```

## Validation & Integrity

```elixir
# Validate all ledger lines
Ledgex.Validation.LineCheck.perform!()

# Validate and automatically fix issues
Ledgex.Validation.LineCheck.perform!(auto_fix: true)

# Validate from specific line ID
Ledgex.Validation.LineCheck.perform!(from_line_id: 12345)
```

## Testing

```elixir
# In test_helper.exs or data_case.ex
setup do
  # Clear state between tests
  Ledgex.Account.clear()
  Ledgex.Transfer.clear()
  
  # Configure for testing
  Ledgex.Config.put(:repo, MyApp.Test.Repo)
  Ledgex.Config.put(:transactional_fixtures, true)
  
  :ok
end
```

## Database Schema

The `mix ledgex.gen.migration` task generates a migration that creates four tables:

- **ledgex_lines**: Core ledger entries with running balances
- **ledgex_account_balances**: Cached balances for locking
- **ledgex_line_checks**: Audit log for validation
- **ledgex_line_metadata**: Optional metadata table

You can review and customize the generated migration before running `mix ecto.migrate`.

## Architecture Highlights

- **ETS Tables**: Fast concurrent reads for account/transfer lookups
- **Database Locking**: Pessimistic locks prevent race conditions
- **Running Balances**: Stored in each line for efficient queries
- **Sorted Locking**: Accounts locked in order to prevent deadlocks
- **Tagged Tuples**: Standard Elixir error handling pattern
- **Pattern Matching**: Account refs are simple tuples for easy matching

## Comparison to Ruby double_entry

Ledgex faithfully ports double_entry while embracing Elixir idioms:

| Feature | Ruby double_entry | Ledgex |
|---------|-------------------|---------|
| Storage | Class variables | ETS tables |
| Config | DSL callbacks | Direct function calls |
| Errors | Raise exceptions | Tagged tuples + bangs |
| Account refs | OOP objects | Functional tuples |
| State | Agent/GenServer | ETS (faster) |
| API | Object methods | Module functions |

## Performance

- **ETS**: Constant-time account/transfer lookups
- **Running Balances**: No need to sum on every query
- **Efficient Indexes**: Optimized for common query patterns
- **Connection Pooling**: Uses your app's Ecto repo

## License

MIT License - see LICENSE file

## Credits

Ported from [double_entry](https://github.com/envato/double_entry) by Envato.