# Transactions — New Fields (API guide)

This document describes **four new fields** added to the Transaction resource and how they
behave in the request body, the response JSON, and the list filters. It is written so it can be
forwarded as-is to an LLM (e.g. Claude) that needs to construct or read these payloads.

> All four fields are **optional, free-text strings (max 255 chars)**. None are validated against
> a fixed list of values — the client may send any string. They are independent of each other and
> of the existing income/expense `type`.

---

## 1. The new fields at a glance

| Field | Type | Required | Example | Meaning |
|---|---|---|---|---|
| `classification` | string \| null | no | `"investment"`, `"non_investment"`, `"utilities"` | A free-text business classification of the transaction. **This is NOT the income/expense `type`** (see the note below). Use it to tag what kind of money movement this is. |
| `account_type` | string \| null | no | `"bank"`, `"cash"`, `"wallet"`, `"card"` | The kind of account the money moved through, stored directly on the transaction as free text. Independent of the linked `account_id` (which points to a named payment account). |
| `merchant` | string \| null | no | `"Amazon"`, `"Starbucks"` | The merchant / counterparty / payee name. |
| `transaction_reference` | string \| null | no | `"INV-2026-0042"`, `"TXN9981234"` | An external reference number (bank statement ref, invoice no., import id). Not unique — duplicates are allowed. Indexed per client for fast lookup. |

### ⚠️ `classification` vs. the existing `type`

The Transaction resource **already has a `type`** field, which is a fixed enum:

- `type` ∈ `"income" | "expense"` — drives all the income/expense logic, scopes, and insights. **Unchanged.**
- `classification` is the **new** free-text field for things like *investment / non-investment / utilities*.

Do not put `"investment"` etc. into `type`. `type` only accepts `income` or `expense`.

---

## 2. Endpoints

| Method | URL | Purpose |
|---|---|---|
| `GET` | `/api/v1/transactions` | List (paginated, filterable) |
| `GET` | `/api/v1/transactions/{id}` | Show one |
| `POST` | `/api/v1/transactions` | Create |
| `PUT` / `PATCH` | `/api/v1/transactions/{id}` | Update |
| `DELETE` | `/api/v1/transactions/{id}` | Delete |

All require `Authorization: Bearer <token>`.

---

## 3. Request body (create / update) — full field set

Below is **every** accepted field. The four new fields are marked **NEW**. Send `multipart/form-data`
only if you include `receipt` (a file); otherwise JSON is fine.

### 3.1 Complete field reference

| Field | Required? | Type | Notes |
|---|---|---|---|
| `type` | **required** | string enum | `income` or `expense` only. Drives all income/expense logic. |
| `classification` **NEW** | optional | string \| null | Free-text business tag (e.g. `investment`, `utilities`). Not the same as `type`. |
| `account_type` **NEW** | optional | string \| null | Free-text account kind (`bank`, `cash`, …). Separate from `account_id`. |
| `merchant` **NEW** | optional | string \| null | Merchant / payee name. |
| `transaction_reference` **NEW** | optional | string \| null | External reference (bank ref, invoice no.). Non-unique. |
| `label` | optional* | string \| null | Short title. *Required when `is_recurring_payment` is true. |
| `client_id` | conditional | integer | The owning user. **Required unless `asset_id` is sent** (then derived from the asset's owner). |
| `currency_id` | **required** | integer | Must exist in `currencies`. |
| `currency_rate_to_client_real_currency` | optional | number ≥ 0 | Conversion rate to the client's base currency. Defaults to `1`. |
| `category_id` | optional* | integer \| null | Must belong to the client. *Required for a recurring payment. |
| `tag_id` | optional | integer \| null | Must belong to the client. |
| `account_id` | optional | integer \| null | FK to a named `payment_accounts` row owned by the client. |
| `asset_id` | optional | integer \| null | Links the transaction to an asset (also resolves `client_id`). |
| `saving_goal_id` | optional | integer \| null | **Income only** — prohibited when `type=expense`. Must belong to the client. |
| `goal_currency_amount` | conditional | number ≥ 0 | Required when the income currency differs from the goal's currency. Required with `goal_currency_rate`. |
| `goal_currency_rate` | conditional | number ≥ 0 | Conversion rate into the goal's currency. Required when currencies differ. |
| `amount` | **required** | number ≥ 0 | Transaction amount in `currency_id`. |
| `amount_in_client_currency` | optional | number ≥ 0 | Defaults to `amount` if omitted. |
| `date` | **required** | date `YYYY-MM-DD` | Transaction date. For a recurring payment this is the schedule **start**. |
| `payment_date` | optional* | date \| null | When actually paid. *Required if `receipt` is sent. Null = unpaid/outstanding. |
| `notes` | optional | string \| null | Free text. |
| `receipt` | optional | file ≤ 2 MB | Upload; stored and returned as `receipt_url`. |
| `is_recurring_payment` | optional | boolean | `true` turns an **expense** into a recurring schedule (see 3.3). |
| `frequency` | optional* | string enum | `daily` \| `weekly` \| `monthly` \| `quarterly` \| `yearly`. *Required for a recurring payment. |
| `end_date` | optional* | date | Must be ≥ `date`. *Required for a recurring payment. |
| `paid_occurrences` | optional | array | Occurrences to log as paid immediately at setup (recurring only). |
| `paid_occurrences[].due_date` | required-in-array | date | Must align to the schedule cadence between `date` and `end_date`. |
| `paid_occurrences[].paid_at` | optional | date \| null | When that occurrence was paid. |
| `paid_occurrences[].notes` | optional | string \| null | Per-occurrence note. |

> On **update** (`PUT`/`PATCH`) every field above becomes `sometimes` — i.e. validated only when
> present. Send only what you want to change; nullable fields accept `null` to clear them.

### 3.2 `POST /api/v1/transactions` — full one-off example

```json
{
  "type": "expense",
  "classification": "utilities",
  "account_type": "bank",
  "merchant": "DEWA",
  "transaction_reference": "INV-2026-0042",
  "label": "Electricity",
  "client_id": 12,
  "currency_id": 1,
  "currency_rate_to_client_real_currency": 1,
  "category_id": 5,
  "tag_id": 8,
  "account_id": 3,
  "asset_id": null,
  "saving_goal_id": null,
  "amount": 450.00,
  "amount_in_client_currency": 450.00,
  "date": "2026-06-11",
  "payment_date": "2026-06-11",
  "notes": "June electricity bill"
}
```

An **income linked to a saving goal** (different currency) adds the goal fields:

```json
{
  "type": "income",
  "classification": "investment",
  "client_id": 12,
  "currency_id": 2,
  "category_id": 9,
  "saving_goal_id": 4,
  "goal_currency_amount": 1000.00,
  "goal_currency_rate": 3.67,
  "amount": 272.00,
  "date": "2026-06-11"
}
```

### 3.3 Recurring expense (schedule) body

When `is_recurring_payment` is `true`, `type` **must** be `expense` and `label`, `category_id`,
`frequency`, and `end_date` become required. This creates a **schedule**, not a single transaction.

```json
{
  "type": "expense",
  "is_recurring_payment": true,
  "label": "Office rent",
  "classification": "non_investment",
  "account_type": "bank",
  "merchant": "Landlord LLC",
  "client_id": 12,
  "currency_id": 1,
  "category_id": 5,
  "account_id": 3,
  "amount": 5000.00,
  "date": "2026-01-01",
  "end_date": "2026-12-31",
  "frequency": "monthly",
  "paid_occurrences": [
    { "due_date": "2026-01-01", "paid_at": "2026-01-01", "notes": "Jan" },
    { "due_date": "2026-02-01", "paid_at": "2026-02-03", "notes": "Feb" }
  ]
}
```

### 3.4 `PUT /api/v1/transactions/{id}` — partial update

Send only changed fields. The four new fields accept `null` to clear them:

```json
{
  "classification": "investment",
  "merchant": "Interactive Brokers",
  "account_type": "bank",
  "transaction_reference": null
}
```

---

## 4. Response JSON — everything that can be included

Every response uses the standard API envelope. The transaction object is returned in `data`
(single) or `data.items[]` (paginated list). Below is a transaction with **all** attributes and
**all** eager-loaded relations populated. In practice, nullable attributes can be `null` and a
relation is `null` when its FK is null.

### 4.1 Attribute reference (top-level transaction object)

| Attribute | Type | Notes |
|---|---|---|
| `id` | integer | |
| `type` | string enum | `income` / `expense`. |
| `classification` **NEW** | string \| null | Free-text business tag. |
| `account_type` **NEW** | string \| null | Free-text account kind. |
| `merchant` **NEW** | string \| null | Merchant / payee. |
| `transaction_reference` **NEW** | string \| null | External reference. |
| `label` | string \| null | |
| `is_recurring_payment` | boolean | |
| `recurring_payment_id` | integer \| null | Set on occurrences generated from a schedule. |
| `recurring_payment_log_id` | integer \| null | The schedule-log row that produced this occurrence. |
| `currency_id` | integer | |
| `currency_rate_to_client_real_currency` | number | |
| `category_id` | integer \| null | |
| `tag_id` | integer \| null | |
| `account_id` | integer \| null | FK to `payment_accounts`. |
| `asset_id` | integer \| null | |
| `saving_goal_id` | integer \| null | |
| `amount` | number | |
| `amount_in_client_currency` | number | |
| `date` | date `YYYY-MM-DD` | |
| `payment_date` | date \| null | Null = unpaid/outstanding. |
| `notes` | string \| null | |
| `receipt_url` | string \| null | Public URL of the uploaded receipt (the raw `receipt_path` is hidden). |
| `created_at` / `updated_at` | datetime ISO-8601 | |
| `deleted_at` | datetime \| null | Soft-delete timestamp (usually null). |

**Relations** (eager-loaded on show/list/create/update): `currency`, `category`, `tag`,
`payment_account`, `asset` (+ nested `asset_class`), `client`, `saving_goal`, `recurring_payment`.

### 4.2 `POST` / `PUT` / `GET {id}` → single transaction (everything populated)

```json
{
  "status": 200,
  "message": "Transaction created successfully",
  "data": {
    "id": 101,
    "type": "income",

    "classification": "investment",
    "account_type": "bank",
    "merchant": "Interactive Brokers",
    "transaction_reference": "INV-2026-0042",

    "label": "Dividend payout",//description
    "is_recurring_payment": false,
    "recurring_payment_id": null,
    "recurring_payment_log_id": null,
    "client_id": 12,
    "currency_id": 1,
    "currency_rate_to_client_real_currency": 1.0,
    "category_id": 5,
    "tag_id": 8,
    "account_id": 3,
    "asset_id": 7,
    "saving_goal_id": 4,
    "amount": 450.0,
    "amount_in_client_currency": 450.0,
    "date": "2026-06-11",
    "payment_date": "2026-06-11",
    "notes": "June dividend",
    "receipt_url": "https://api.example.com/storage/transaction-receipts/abc.pdf",
    "created_at": "2026-06-11T10:00:00.000000Z",
    "updated_at": "2026-06-11T10:00:00.000000Z",
    "deleted_at": null,

    "currency": { "id": 1, "code": "AED", "symbol": "د.إ" },
    "category": { "id": 5, "name": "Bills", "category_group_id": 2, "order": 1, "icon": "bolt", "type": "income" },
    "tag": { "id": 8, "name": "Recurring" },
    "payment_account": { "id": 3, "client_id": 12, "name": "Emirates NBD Current", "balance": 12000.0, "currency_id": 1 },
    "asset": {
      "id": 7,
      "name": "Apartment 12B",
      "asset_class": { "id": 1, "name": "Property", "slug": "property" } //Asset Category
    },
    "client": { "id": 12, "name": "Jane Client" },
    "saving_goal": { "id": 4, "name": "House fund", "currency_id": 1 },
    "recurring_payment": {
      "id": 22,
      "label": "Office rent",
      "frequency": "monthly",
      "start_date": "2026-01-01",
      "end_date": "2026-12-31"
    }
  },
  "errors": []
}
```

> The new fields are **plain strings on the transaction itself** — not nested objects, no
> relationships. `account_type` (`"bank"`) is separate from the `payment_account` relation
> (the actual named account row).

### 4.3 `POST` of a recurring payment → schedule (not a transaction)

When `is_recurring_payment` is true, the response `data` is the created **schedule**, not a
transaction (note the different message and `transactions_count`):

```json
{
  "status": 201,
  "message": "Recurring payment schedule created successfully (2 occurrence(s) logged as paid)",
  "data": {
    "id": 22,
    "label": "Office rent",
    "frequency": "monthly",
    "start_date": "2026-01-01",
    "end_date": "2026-12-31",
    "amount": 5000.0,
    "transactions_count": 2
  },
  "errors": []
}
```

### 4.4 `GET /api/v1/transactions` → paginated list

Same transaction shape as 4.2, wrapped in `items` + `meta`:

```json
{
  "status": 200,
  "message": "Transactions retrieved successfully",
  "data": {
    "items": [
      { "id": 101, "type": "income", "classification": "investment", "merchant": "Interactive Brokers", "...": "(all attributes + relations as in 4.2)" }
    ],
    "meta": { "current_page": 1, "per_page": 20, "total": 57, "last_page": 3 }
  },
  "errors": []
}
```

---

## 5. Filtering the list

The new fields are queryable on `GET /api/v1/transactions` via query-string params:

| Query param | Behaviour |
|---|---|
| `?classification=investment` | Exact match on `classification`. |
| `?account_type=bank` | Exact match on `account_type`. |
| `?merchant=Amaz` | Partial (`LIKE`) match — `?merchant=Amaz` matches `"Amazon"`. |
| `?transaction_reference=INV-2026-0042` | Exact match on the reference. |
| `?search=DEWA` | The general `search` param now **also** scans `merchant` and `transaction_reference` (in addition to `label` and `notes`). |

Example:

```
GET /api/v1/transactions?classification=utilities&account_type=bank&merchant=DEWA
```

---

## 6. Export

CSV / XLSX exports (`GET /api/v1/transactions?export=csv`) now include the columns
**Classification**, **Account Type**, **Merchant**, and **Reference**.

---

## 7. How to explain this to the model (prompt hint)

When forwarding to Claude, the key points to convey are:

1. Four new **optional free-text** string fields exist on a transaction:
   `classification`, `account_type`, `merchant`, `transaction_reference`.
2. `classification` is a business tag (investment / utilities / …) and **must not be confused with
   `type`**, which is strictly `income`/`expense`.
3. `account_type` is a free-text kind-of-account on the transaction, distinct from the
   `account_id` → `payment_account` relationship.
4. Any of the four can be sent as `null` on update to clear it; omit them entirely to leave unchanged.
5. They are filterable (`classification`, `account_type` exact; `merchant` partial;
   `transaction_reference` exact) and the global `search` covers `merchant` + `transaction_reference`.
