# Changelog

All notable changes to the **Personal Finance** Laravel backend (`personal-finance-app-laravel`).

This app is the backend for the Phase 1 Flutter mobile app (`../personal-finance-app-flutter`).
It follows `../Personal_Finance_App_UI_Architecture_Guide.md`: a versioned REST API for
Flutter, shared service classes holding the business rules, and MySQL as the single
source of truth. The future Blade web app will reuse the same services and models.

Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

---

## [Unreleased]

### Pending

- **Laravel Boost:** the package is installed, but `php artisan boost:install` still has
  to be run by hand. The automated attempt was blocked.
- **Admin password:** set `ADMIN_EMAIL` / `ADMIN_PASSWORD` in `.env`. The local default
  login is `admin@example.com` / `password`.
- **Database name (optional):** consider a dedicated database (e.g. `personal_finance`)
  instead of the generic `laravel`. The same MySQL server also holds other projects' databases.
- **Token expiry:** decide whether to set `expiration` in `config/sanctum.php`.
  Tokens currently never expire.
- **Historical Excel import:** not built yet. The schema already supports it
  (`source = import`, nullable `account_id`).
- **Laravel web (Blade) app:** future phase (guide section 2).

---

## [1.0.0] - 2026-11-07

First complete version of the API covering everything the Phase 1 Flutter app needs.

### Stack

| Item | Version |
|---|---|
| PHP | 8.5 (WAMP) |
| Laravel | 13.x (`laravel/framework ^13.17`) |
| Database | MySQL 8.4 (InnoDB, utf8mb4); tests use in-memory SQLite |
| Auth | Laravel Sanctum ^4.0 (personal access tokens) |
| Dev tooling | Laravel Boost ^2.10, Pint, PHPUnit 12 |

### Added: database schema (`database/migrations/`)

| Migration | Table | Key points |
|---|---|---|
| `2026_11_06_000001_create_categories_table` | `categories` | See [categories](#categories) |
| `2026_11_06_000002_create_accounts_table` | `accounts` | See [accounts](#accounts) |
| `2026_11_06_000003_create_expenses_table` | `expenses` | See [expenses](#expenses) |
| `2026_11_06_171547_create_personal_access_tokens_table` | `personal_access_tokens` | Sanctum tokens for the mobile app |

#### `categories`

- **Unlimited nesting:** a single table where `parent_id` points to the parent category.
  A parent can't be deleted while it has children.
- **Columns:** `name`, `icon` (Material icon key), `color` (`#RRGGBB`), `sort_order`, `is_active`, timestamps.
- **Inheritance:** `icon` and `color` may be null, meaning a subcategory inherits them from its parent.
- **Indexes:** unique (`parent_id`, `name`), and (`parent_id`, `is_active`, `sort_order`).

#### `accounts`

- **Columns:** `name` (unique), `type`, `currency_code` (default `LKR`), `opening_balance`, `sort_order`, `is_active`.
- **`type`:** stored as a string (`cash`, `bank`, `credit_card`, `e_wallet`, `bnpl`, `other`),
  so adding a type later doesn't need a schema change.
- **`opening_balance`:** `DECIMAL(15,2)`, nullable. Added for the "Initial Balance" field in
  the UI collage; the guide flagged this as an open schema decision.
- Never stores full account numbers, card numbers, CVVs or PINs.

#### `expenses`

- **`uuid`:** unique, generated by the app; this is what makes retries safe.
- **Links:** `category_id` (FK, delete blocked) and `account_id` (FK, nullable, delete blocked).
  `account_id` may be null for imported history.
- **Values:** `amount` `DECIMAL(15,2)`, `description`, `notes`.
- **`expensed_at` DATETIME:** resolves the guide's date-vs-datetime question in favour of storing date and time.
- **`source`:** `manual` or `import`.
- **Deletes are soft** (`deleted_at`).
- **Indexes:** `expensed_at`, (`category_id`, `expensed_at`), (`account_id`, `expensed_at`).

The existing default Laravel migrations (users, cache, jobs) are unchanged.

### Added: enums (`app/Enums/`)

- `AccountType`: Cash, Bank, Credit Card, E-wallet, BNPL, Other, each with a `label()`.
- `ExpenseSource`: `manual`, `import`.

### Added: models (`app/Models/`)

- **`Category`:**
  - Relations: `parent`, `children` (sorted), `expenses`.
  - Filters: `active`, `roots`, `leaves` (categories with no subcategories).
  - Helpers:
    - `isLeaf()`, `lineage()` (root down to this category), `path()` → `Food > Meals > Lunch`.
    - `isSelectableForNewExpense()`: the category has no subcategories, and it and all its parents are active.
    - `isSelfOrAncestorOf()`: cycle check.
- **`Account`:** `type` read as `AccountType`, `opening_balance` as `decimal:2`. Filters: `active`, `ordered`.
- **`Expense`:**
  - Soft deletes.
  - Generates `uuid` when the client doesn't send one; the primary key stays a normal integer.
  - Casts for amount, date, source.
  - Filters: `inMonth()`, `latestFirst()`.
- **`User`:** added Sanctum's `HasApiTokens` trait.

### Added: factories and seeders (`database/`)

- Factories: `CategoryFactory` (`childOf()`, `inactive()`), `AccountFactory`
  (`inactive()`), `ExpenseFactory` (`imported()`).
- **`CategorySeeder`:** 31 categories based on the collage, with icons and accent colours:
  - Food › Meals › Breakfast / Lunch / Dinner, and Food › Drinks, Snacks.
  - Transportation, Shopping, Bills & Utilities, Health, Education, Sports & Fitness, Others.
  - Safe to re-run (matched by parent + name).
- **`AccountSeeder`:** Cash Wallet, Commercial Bank, Sampath Credit Card, KOKO. Safe to re-run.
- **`DemoExpenseSeeder`:** about 60 sample expenses over the last ~2.5 months.
  Local environment only, and skipped if any expenses already exist.
- **`DatabaseSeeder`:**
  - Creates the single application user from `config/finance.php` (`ADMIN_NAME`, `ADMIN_EMAIL`, `ADMIN_PASSWORD`). This replaces the template `test@example.com` user.
  - Refuses to run outside local/testing without `ADMIN_PASSWORD`.
  - Never overwrites an existing user's password.

### Added: shared business rules (`app/Services/`)

All rules live in services and throw `ValidationException`, so the API and the future
web controllers behave the same way (guide section 10.4).

- **`CategoryService`:**
  - Creating: sibling names must be unique (case-insensitive), and `sort_order` is assigned automatically.
  - Updating, including moving to another parent: **cycle prevention** stops a category moving under itself or a descendant.
  - Deleting is blocked when there are subcategories or any expenses, including soft-deleted
    ones. The error says "Deactivate it instead."
  - Adding a child to a category that already has expenses is allowed. Those expenses
    still roll up in reports; only the new subcategory accepts new expenses.
- **`AccountService`:**
  - Creating uses defaults for currency, order and active status.
  - Deleting is blocked when the account has expenses (deactivate instead).
- **`ExpenseService`:**
  - **Safe retries:** re-sending the same `uuid` returns the existing expense with
    `created = false` instead of a duplicate. This also holds when two requests race,
    because the unique-index error is caught. Re-sending the UUID of a deleted expense is rejected.
  - **New expenses:** the category must have no subcategories and be active, with all parents
    active. The account must be active.
  - **Edits:** an unchanged category or account is kept even if it has since been deactivated.
  - **List filters:** `month`, `from`/`to`, `category_id` (includes all subcategories),
    `account_id`, and `q` (searches description and notes; LIKE wildcards escaped).
  - **Daily totals:** `dailyTotals()` returns each day's full total, even when a day spans two pages.
- **`ReportService`:**
  - **By category:** rolls spending up to top-level categories.
  - **By account:** imported expenses with no account appear as "Unspecified".
  - Returns totals, counts and percentages, sorted by amount.

### Added: support classes (`app/Support/`)

- **`CategoryTree`:**
  - Loads every category in one query, and is shared within a request.
  - Provides paths, child counts, leaf checks, effective active status (all parents active),
    inherited icon/colour, all descendants, the top-level category, and cycle checks.
  - Avoids running one query per row when listing expenses and building reports.
- **`Money`:** formats amounts as strings with exactly 2 decimals (`"2500.00"`) so clients
  never receive floating-point numbers. Also computes percentages to one decimal.

### Added: REST API v1 (`routes/api.php`)

Base URL `/api/v1`. JSON only. Every endpoint except login needs a Sanctum bearer token.

| Method | Endpoint | Purpose |
|---|---|---|
| POST | `/auth/login` | Email + password → token + user (limited to 5 attempts/minute). No registration endpoint. |
| GET | `/auth/me` | Current user; the app uses it to check a saved token |
| POST | `/auth/logout` | Revoke the current token |
| GET | `/categories` | Nested tree (default). `?view=flat`, `?view=selectable` (categories new expenses can use), `?q=` search |
| POST/GET/PUT/DELETE | `/categories/{id}` | Create, show (with direct children), update or move, delete |
| GET | `/account-types` | Options for the Account Type dropdown |
| GET | `/accounts` | List; `?active_only=1` for pickers |
| POST/GET/PUT/DELETE | `/accounts/{id}` | Create, show, update or deactivate, delete |
| GET | `/expenses` | Paginated list, newest first. Filters: `month`, `from`, `to`, `category_id`, `account_id`, `q`, `per_page` (≤100). `meta.total_amount` and `meta.daily_totals` |
| POST | `/expenses` | Create. **201** when new, **200** when the same `uuid` was already recorded (safe retry) |
| GET/PUT/DELETE | `/expenses/{id}` | Show, partial update, soft delete (204) |
| GET | `/reports/monthly` | `?month=YYYY-MM&group_by=category\|account`. Used by both Home and Monthly Report |

- **Controllers** (`app/Http/Controllers/Api/V1/`): only handle input and output; all rules
  live in the services. `Auth`, `Category`, `Account`, `Expense`, `Report`.
- **Form Requests** (`app/Http/Requests/Api/V1/`): input validation.
  - Amounts must be greater than 0, with at most 2 decimals.
  - `uuid` is required for new expenses.
  - Colours must be hex; account types must be a valid enum value.
  - Updates are partial.
  - Imported expenses may keep an empty account; manual ones may not.
- **API Resources** (`app/Http/Resources/Api/V1/`):
  - Money is sent as strings and dates as ISO 8601 with the offset.
  - Categories include `path`, `effective_icon`/`effective_color`, `depth`, `is_leaf`,
    `is_selectable` and `children_count`.
  - Expenses embed their category (path, icon, colour) and account (type label).
- **Errors:** standard Laravel JSON. Validation failures are 422 `{message, errors}`;
  missing records are 404; authentication failures are 401.

### Changed: configuration

- **`config/app.php`:** the timezone is now `env('APP_TIMEZONE', 'Asia/Colombo')`, so
  month boundaries in reports and filters follow Sri Lankan time. It was `UTC`.
- **`config/database.php`:** the `mysql` and `mariadb` connections now set
  `'engine' => env('DB_ENGINE', 'InnoDB')`. See [Fixed](#fixed).
- **`config/finance.php`** (new): default currency (`FINANCE_CURRENCY`, LKR) and the admin user settings.
- **`.env.example`:** added `ADMIN_NAME`, `ADMIN_EMAIL`, `ADMIN_PASSWORD`.
- **`.env`:** switched to MySQL (`DB_CONNECTION=mysql`, database `laravel`).
- **API routes enabled** with `php artisan install:api`. This installed Sanctum,
  registered `routes/api.php` and published the tokens migration.
- **Placeholder removed:** the template's `/user` route in `routes/api.php` was replaced by the v1 routes.

### Added: tests (`tests/Feature/Api/V1/`)

38 feature tests run against in-memory SQLite (`phpunit.xml`):

- **`AuthTest`:**
  - Login returns a token; a wrong password gives 422.
  - Protected routes return 401 without a token.
  - Logout revokes the token.
- **`CategoryTest`:**
  - Tree paths and inherited accent colours; the selectable view; deactivating a parent hides its subcategories.
  - Search; details with children.
  - Creating top-level and child categories; case-insensitive duplicate names rejected.
  - Cycle prevention; moving a category.
  - Delete blocked by subcategories or expenses; deleting an unused category.
- **`AccountTest`:**
  - Create with opening balance; type and unique-name validation.
  - Active filter; update and deactivate.
  - Delete blocked by expenses; deleting an unused account; account types list.
- **`ExpenseTest`:**
  - Create; same-`uuid` retry returns the original with no duplicate; retry of a deleted expense rejected.
  - Required-field and amount rules.
  - Category must have no subcategories and be active; account must be active.
  - Show, update and soft delete; editing keeps an unchanged deactivated category;
    a manual expense can't clear its account.
  - List: month and category (with subcategories) filters, search, and daily totals that
    span pages.
- **`ReportTest`:**
  - Rolls categories up to the top level, with month boundaries respected.
  - By account, including "Unspecified".
  - Empty month; month is required.

### Fixed

- **MySQL error 1071 "Specified key was too long; max key length is 1000 bytes"**
  when migrating.
  - **Cause:** WAMP's MySQL creates new tables as MyISAM by default. MyISAM's 1000-byte
    index limit is too small for the utf8mb4 unique index on `users.email`, and MyISAM
    also ignores foreign keys.
  - **Fix:** `config/database.php` now forces InnoDB. The database was then rebuilt with
    `php artisan migrate:fresh --seed`.
- **The InnoDB fix had no effect at first.** A stale config cache
  (`bootstrap/cache/config.php`) was still serving `engine => null`. Fixed with
  `php artisan config:clear`.
- **Every API route returned 404.** A stale route cache (`bootstrap/cache/routes-v7.php`)
  was serving the routes from before the API existed. Fixed with `php artisan optimize:clear`.
  - Don't run `php artisan optimize` or `config:cache` during development: they freeze
    config and routes until you clear them again.
- **Demo expenses duplicated on every seeder run.** `DemoExpenseSeeder` now skips when
  any expenses exist.
- **Type mismatches** for `parent_id` and `account_id` compared as strings vs integers
  in the services; now cast explicitly.

### Verified

- `php artisan test`: **38/38 passing**.
- `vendor/bin/pint`: passing (code style).
- Migrations and seeders run cleanly on SQLite and on MySQL 8.4 with InnoDB. Re-seeding
  creates no duplicates.
- **Against real MySQL:**
  - Expense search, daily-total and monthly report queries return correct results.
  - Timezone is `Asia/Colombo`.
- **Live HTTP smoke test** (`php artisan serve`):
  - CORS preflight returns 204 with `Access-Control-Allow-Origin: *`.
  - Login, monthly report, categories and logout all work.
- **End to end with the Flutter app (web):**
  - An expense created from the app was saved, rolled up into Home and the report, and shown in detail.
  - It was then deleted (soft delete).

### Notes

- **Running locally:**
  - Start the API with `php artisan serve --host=0.0.0.0 --port=8000`.
  - From web or desktop, call `http://127.0.0.1:8000`, not `localhost`. On this PC
    `localhost` resolves to IPv6 `::1`, which the dev server doesn't listen on.
  - From a USB-connected phone, run `adb reverse tcp:8000 tcp:8000` first.
- **Single-user design:** there's no `user_id` on categories, accounts or expenses, and no
  registration endpoint. Adding multi-user support later would need a schema change.
- **Test data left in the local database:** one soft-deleted test expense
  ("Lunch with team (UI test)") and a few tokens from test logins in
  `personal_access_tokens`. Both are harmless.
- **Not built (out of Phase 1 scope):** the Blade web app, Excel import/export, income,
  budgets, balance tracking, transfers and receipts (guide section 13).
