# How TREK's Budget Splitting and Expense Settlement Feature Works

> Discover how TREK's budget splitting and expense settlement feature works. It simplifies multi-currency expense tracking and minimizes transfers using optimal repayment flows.

- Repository: [Maurice/TREK](https://github.com/mauriceboe/TREK)
- Tags: feature-explanation
- Published: 2026-07-04

---

**TREK automatically calculates optimal repayment flows by converting multi-currency expenses to a base currency, computing net balances per participant, and applying a greedy algorithm to minimize the number of required transfers.**

TREK's budgeting addon provides a complete expense management system for group trips. The budget splitting and expense settlement functionality persists expense data in relational tables and executes settlement calculations through dedicated service methods. This implementation supports uneven payer distributions, manual settlement recording, and automatic currency conversion while ensuring debtors remit payments to creditors through the fewest possible transactions.

## Database Schema for Expense Tracking

The system relies on four core tables to track financial obligations. In [`server/src/services/budgetService.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/services/budgetService.ts), the data model separates expense metadata from participant assignments and payment records.

### budget_items

The `budget_items` table stores the total expense amount, currency code, and optional foreign exchange rate. Each row represents a single trip expense with a frozen exchange rate that prevents historical calculations from shifting when live rates update.

### budget_item_members

The `budget_item_members` table links users to specific expenses and tracks assignment through a `paid` boolean flag. This flag indicates UI state only—**the settlement algorithm ignores this flag** and calculates obligations based on actual payer records stored separately.

### budget_item_payers

When one or more members cover an expense, `budget_item_payers` stores the explicit payment amounts per user. If no payer rows exist for an item, the system treats the full `budget_items` amount as the paid total.

### budget_settlements

Completed transfers between users persist in `budget_settlements` with `from_user_id`, `to_user_id`, and amount columns. These historic settlements offset future balance calculations, ensuring settled debts don't appear in subsequent computations.

## Calculating Per-Person Balances

The `getPerPersonSummary()` function in [`server/src/services/budgetService.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/services/budgetService.ts) aggregates financial standing for each trip participant. The method iterates through all `budget_items` joined to their members and payers, calculating each user's total assigned share versus their total paid amount.

For every expense item, the service converts the paid amount to the trip's base currency using either the stored `exchange_rate` or live FX data fetched via `getRates()`. It then divides the converted total by the number of assigned members to determine individual shares. The function returns an array containing `user_id`, `username`, `total_assigned`, `total_paid`, and `items_count` for each participant.

## The Minimal-Transfer Settlement Algorithm

The `calculateSettlement()` method implements a greedy optimization algorithm to resolve debts. Located in [`server/src/services/budgetService.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/services/budgetService.ts), this function first builds a net balance map by processing all expenses and applying historic settlements, then matches debtors with creditors to minimize transaction count.

The algorithm executes six discrete steps:

```ts
// 1️⃣ Load all items, members and payers for the trip
const items   = db.prepare('SELECT * FROM budget_items WHERE trip_id = ?').all(tripId);
const members = db.prepare('SELECT … FROM budget_item_members …').all(tripId);
const payers  = db.prepare('SELECT … FROM budget_item_payers …').all(tripId);

// 2️⃣ For each item compute the total paid in the *base* currency
const paidBase = payers.reduce(
  (a, p) => a + toBase(p.amount, item.currency, item.exchange_rate), 0);

// 3️⃣ Split the paid amount evenly among the members that are assigned to the item
const sharePerMember = paidBase / members.length;

// 4️⃣ Update the balance map:
//   – each payer gets a credit for what they actually paid
//   – each member gets a debit for the equal share
ensure(payerId, …).balance += toBase(p.amount, …);
ensure(memberId, …).balance -= sharePerMember;

// 5️⃣ Apply persisted settlements (recorded transfers)
ensureSettled(s.from_user_id, …).balance += s.amount;
ensureSettled(s.to_user_id, …).balance   -= s.amount;

// 6️⃣ Greedy matching of remaining debtors ↔ creditors
while (debtors && creditors) {
  const transfer = Math.min(debtor.amount, creditor.amount);
  flows.push({ from: debtor, to: creditor, amount: round(transfer) });
  debtor.amount   -= transfer;
  creditor.amount -= transfer;
}

```

**Debtor-creditor matching** works by sorting participants by net balance and iteratively matching the largest remaining debtor with the largest remaining creditor. This greedy approach produces the minimal set of transfers required to zero all balances, though it does not attempt combinatorial optimization.

## Multi-Currency Support and Exchange Rates

TREK handles expenses in any supported currency through the `exchange_rate` field in `budget_items`. When present, this frozen rate applies to calculations; otherwise, the system fetches live rates via `getRates()` and converts amounts to the base currency specified by the UI (defaulting to the trip's primary currency). All settlement calculations occur in this normalized base currency to ensure accurate balancing across mixed-currency trips.

## Recording Payments vs. Settlement Status

The system distinguishes between **UI state** and **financial reality** through two separate mechanisms. The `toggleMemberPaid()` method in [`server/src/nest/budget/budget.service.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/nest/budget/budget.service.ts) updates the `paid` boolean in `budget_item_members`, which drives avatar highlighting in the frontend but does not affect balance mathematics.

Actual monetary movements require `createSettlement()`, which persists records to `budget_settlements` and immediately affects future `calculateSettlement()` calls. This architectural separation allows users to track visual "paid" status independently from verified fund transfers.

## Service API Implementation Examples

The following TypeScript examples demonstrate how to interact with the budget splitting and expense settlement services in [`server/src/nest/budget/budget.service.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/nest/budget/budget.service.ts).

### Listing Budget Items

```ts
import { BudgetService } from './server/src/nest/budget/budget.service';

async function showBudget(tripId: string) {
  const svc = new BudgetService();
  const items = await svc.list(tripId);
  console.log('Budget items →', items);
}

```

### Retrieving Per-Person Summaries

```ts
async function summary(tripId: string) {
  const svc = new BudgetService();
  const summary = await svc.perPersonSummary(tripId);
  // → [{ user_id, username, total_assigned, total_paid, items_count }]
  console.log(summary);
}

```

### Computing Settlement Flows

```ts
async function settlement(tripId: string, baseCurrency?: string) {
  const svc = new BudgetService();
  // `baseCurrency` is the currency the UI wants the result in (e.g. "USD")
  const result = await svc.settlement(tripId, baseCurrency, 'EUR');
  console.log('Balances:', result.balances);
  console.log('Transfer flows:', result.flows);
}

```

### Persisting Manual Settlements

```ts
async function settle(tripId: string, fromUserId: number, toUserId: number, amount: number) {
  const svc = new BudgetService();
  const settlement = await svc.createSettlement(tripId, {
    from_user_id: fromUserId,
    to_user_id: toUserId,
    amount,
  }, /* createdByUserId */ fromUserId);
  console.log('Created settlement →', settlement);
}

```

### Toggling Member Paid Status

```ts
async function markPaid(itemId: string, tripId: string, userId: number) {
  const svc = new BudgetService();
  const member = await svc.toggleMemberPaid(itemId, tripId, `${userId}`, true);
  console.log('Member marked paid →', member);
}

```

## Summary

- **Four database tables** (`budget_items`, `budget_item_members`, `budget_item_payers`, `budget_settlements`) store expense data, assignments, and transfer history according to the schema in [`server/src/services/budgetService.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/services/budgetService.ts).
- **Even splitting** is enforced by dividing the total paid amount by the number of assigned members in `calculateSettlement()`.
- **Greedy algorithm** matches the largest debtor with the largest creditor to minimize the number of settlement transactions.
- **Currency normalization** converts all amounts to a base currency using stored exchange rates or live FX data before balance calculations.
- **UI state separation** means the `paid` flag in `budget_item_members` controls visual indicators only, while `createSettlement()` records actual financial transfers.

## Frequently Asked Questions

### How does TREK handle multi-currency expenses during settlement?

TREK converts all expense amounts to a common base currency using either the frozen `exchange_rate` stored in `budget_items` or live rates fetched via `getRates()`. The `calculateSettlement()` function performs all balance calculations in this normalized currency, ensuring accurate debt computation across mixed-currency trips before converting final settlement amounts back to the display currency.

### What is the difference between marking a member as paid and creating a settlement?

Marking a member as paid via `toggleMemberPaid()` updates the `paid` boolean in `budget_item_members` and triggers a green avatar outline in the UI, but this state does not affect the mathematical settlement calculation. Creating a settlement via `createSettlement()` persists a monetary transfer to `budget_settlements`, which immediately adjusts net balances in subsequent `calculateSettlement()` calls.

### Why does the settlement algorithm use greedy matching instead of optimal solving?

The greedy algorithm in `calculateSettlement()` sorts participants by net balance and matches the largest remaining debtor with the largest remaining creditor until all balances zero. While not mathematically guaranteed to find the absolute minimum in all theoretical cases, this approach produces the minimal number of transfers in practice for typical group expense scenarios while maintaining O(n log n) performance suitable for real-time API responses.

### Can expenses be split unevenly among members?

Currently, the budget splitting and expense settlement feature in TREK divides the total paid amount evenly among all assigned members in `budget_item_members`. The `sharePerMember` calculation in `calculateSettlement()` uses a simple division: `paidBase / members.length`. Uneven splitting would require extending the schema to store proportional weights per member assignment.