Commit Graph

8 Commits

Author SHA1 Message Date
Víctor Falcón 08d367a5c8
fix(balances): stop historical-balance generator OOM on ancient purchase/loan dates (PHP-LARAVEL-49) (#661)
## What & why

A queued historical-balance generator job exhausted the worker's 128MB
PHP memory limit and **died on every retry** — Sentry `PHP-LARAVEL-49` +
`PHP-LARAVEL-4A` (two fingerprints of the *same* poison message:
identical `trace_id`/`message.id`, `retry.count` 1 then 2, OOM at
slightly different `Arr.php` lines).

### Root cause (trace-confirmed)
The Sentry trace localized the OOMing message to
`App\Jobs\GenerateHistoricalRealEstateBalancesJob`, dispatched from
`Settings\AccountController::store` for the "older than 12 months" slice
of history. The fatal frame is `Arr::map` inside Eloquent's
`AccountBalance::upsert()` value-prep (`addUniqueIds`/`addTimestamps`
per-row `array_merge`, `Query\Builder::upsert` `Arr::flatten` bindings,
and a compiled multi-row SQL string — all **O(n)** over the rows).

`purchase_date` (`before_or_equal:today`) and `loan_start_date` (no
constraint) had **no lower bound**, so a mistyped/ancient year (e.g.
`0201`, `1080`) made the generator build a **multi-century monthly
series** and hand it all to a single `upsert`, blowing past 128MB.

## The fix (two commits)

1. **`fix(balances): upsert historical balances in batches`** — build +
upsert in batches of 500 (`UPSERT_CHUNK_SIZE`) in both
`RealEstateBalanceGeneratorService` and `LoanBalanceGeneratorService`,
instead of one giant operation. Same rows, same `(account_id,
balance_date)` conflict target — peak memory is now bounded regardless
of series length. Defense-in-depth.
2. **`fix(accounts): floor purchase/loan start dates at 1900`** — the
actual root cause: add `after_or_equal:1900-01-01` to `purchase_date`
and `loan_start_date`. Rejects the data-entry typos that drive the
runaway series (and the resulting DB bloat / misleading multi-century
net-worth ramp) while accepting any legitimately old asset in a
personal-finance app.

## Review (two agents, before this PR)
Both reviewed against the live trace and the code.

- **Root cause & fix confirmed** — the batching caps exactly the
`Arr::map`-over-N-rows frame from the trace; no independent OOM source
exists (no per-row model events; `upsert` bypasses events).
- **Chunking is correct** — the unique index `(account_id,
balance_date)` exists, so the conflict target is real; `buildDateList`
yields a strictly-increasing de-duplicated list, so batches are disjoint
(no dropped/duplicated dates, no off-by-one). The fresh per-row `id`
UUID is discarded on conflict → idempotent retries.
- **No material user-facing regression.** Balance consumption
(dashboard/net-worth) is date-range-clamped. Batching introduces a
theoretical partial-write-on-mid-job-failure window, but it's idempotent
and self-heals on retry — agents advised **against** wrapping it in a
transaction (holds locks, doesn't help memory).

Applied recommendation #2 (the date floor) as its own commit. Deferred
(both agents agree): extracting the two near-identical generators into a
shared base/trait — a larger refactor that shouldn't ride a hotfix.

## Tests
- Regression test in each service: generate a ~46-year (>500-point)
series and assert the batched writes produce no dropped/duplicated dates
and keep endpoints anchored (crosses multiple batches).
- Validation test: a pre-1900 `purchase_date` is rejected at
`accounts.store`.

Note: the PHP feature suite boots a MySQL testcontainer (Docker) that
isn't available in my environment, so these were validated by
static/`pint`/`php -l` locally and rely on CI for execution — auto-merge
is gated on green CI.

Fixes PHP-LARAVEL-49
Fixes PHP-LARAVEL-4A
2026-07-08 21:34:43 +00:00
Víctor Falcón cd3080ec52
feat(accounts): reorder accounts with drag-and-drop (#575)
## What

Let users reorder their accounts by drag-and-drop. The order is shared
between the **dashboard** and the **accounts page**, and persisted
server-side.

## Why

The account order was fixed (by type, then name). Users want to put the
accounts they care about first, consistently across both views.

## How

**Backend**
- New `position` column on `accounts`, backfilled per user from the
previous type/name ordering so existing layouts are preserved.
- `PATCH /accounts/reorder` (`AccountController@reorder` +
`ReorderAccountsRequest`) persists the order and validates ownership of
every id.
- Dashboard and accounts queries now `orderBy('position')`. `position`
is cast to int and hidden from the serialized payload (order is conveyed
by array order).

**Frontend**
- Shared `SortableGrid` component built on `@dnd-kit` (new dependency).
Pointer drag starts after a small move (clicks still work); touch drag
starts on a long press, so quick swipes still scroll.
- The drag handle swaps with the account type icon on hover — top-right
on the dashboard card, bottom-left on the accounts card.
- The accounts page is now a flat list (type grouping dropped) so its
order matches the dashboard exactly.
- Reorder is optimistic and avoids refetching the deferred dashboard
metrics.
- Haptic feedback (`'selection'`, same as the mobile menu) fires when a
drag starts on touch.
- On mobile the accounts card stacks vertically (name / amount / trend)
and hides the redundant bank-name subtitle.

## Tests

- `reorder` persists positions and rejects accounts the user doesn't
own.
- Index ordering updated to assert `position` order.
- Existing account/dashboard/real-estate suites updated and green.

## Notes / follow-ups

- New accounts get `position = 0` (appear first) — can add `position =
max+1` on create later.
- On mobile the whole subtitle is hidden, including "Mortgage at X" for
real estate.
- Mobile drag-and-drop discoverability (the handle only shows on hover)
is still open — discussed but not yet decided.
2026-06-21 11:17:45 +02:00
Víctor Falcón 3500eaa469
refactor(real-estate): remove Pennant gating (#308)
## Summary
- remove the `real-estate` Pennant flag definition, request gating,
local auto-activation middleware, and frontend shared flag plumbing
- make real-estate account creation and onboarding availability the
default path for all users
- update feature and browser tests to cover default availability instead
of flag enable/disable behavior

## Testing
- vendor/bin/pint --dirty --format agent
- php artisan test --compact tests/Feature/RealEstateTest.php
- php artisan test --compact
tests/Feature/RealEstateAvailabilityTest.php
- php artisan test --compact tests/Browser/RealEstateAccountTest.php
- php artisan test --compact tests/Browser/OnboardingFlowTest.php
- php artisan test --compact --filter=\"can create a real estate account
linked to an existing loan\" tests/Browser/BankAccountsTest.php

## Notes
- `npm run build` completed the client build, but the Sentry sourcemap
upload step failed with an SSL/TLS error from the configured plugin
- deploy follow-up: run `php artisan pennant:purge real-estate` to
remove stale stored flag values
2026-04-20 13:31:49 +01:00
Víctor Falcón 094fb1b744
feat(real-estate): auto-calculate revaluation % and generate historical balances (#253)
## Summary

- **Auto-calculates Annual Revaluation %** (CAGR) on the frontend when
purchase price, purchase date, and current market value are all provided
— pre-fills the revaluation field while still allowing manual override
- **Generates historical monthly balance records** via linear
interpolation from purchase date to today when creating a real estate
account with complete purchase data
- New `RealEstateBalanceGeneratorService` handles balance generation
with dates on the 1st of each month (matching the existing
`ApplyRealEstateRevaluationCommand` convention)

## Changes

### Backend
- **`app/Services/RealEstateBalanceGeneratorService.php`** (new) —
Linear interpolation service that builds balance dates (purchase date,
1st of each intermediate month, today) and creates `AccountBalance`
records using `updateOrCreate`
- **`app/Http/Controllers/Settings/AccountController.php`** — Calls the
service after real estate detail creation when all three inputs are
present

### Frontend
- **`resources/js/components/accounts/account-form.tsx`** — Added CAGR
auto-calculation via `useEffect` with a `useRef` flag
(`isRevaluationManuallySet`) to avoid overwriting manual user input

### Tests
- **`tests/Feature/Services/RealEstateBalanceGeneratorServiceTest.php`**
(new) — 7 unit tests covering interpolation, edge cases (same-day
purchase, flat values, single month spans)
- **`tests/Feature/RealEstateTest.php`** — 6 new feature tests covering
historical balance generation through the controller (with/without
purchase data, same-day, flat values)

All 38 tests in `RealEstateTest.php` and all 7 service tests pass.
2026-04-15 12:18:33 +00:00
Víctor Falcón 3990472249
Make bank selection optional when creating or updating accounts (#261)
## Summary

- Makes `bank_id` nullable for all account types in both
`StoreAccountRequest` and `UpdateAccountRequest` (previously it was only
nullable for real estate)
- Shows the bank combobox field for all account types in the
`AccountForm` component, including real estate
- Removes the `required` attribute from the hidden bank input and the
logic that cleared bank selection when switching to real estate type

## Test changes

- Updated "validates required fields" test to no longer expect `bank_id`
as a required field
- Added "can create a new account without a bank" test in `AccountTest`
- Updated `RealEstateTest` to verify non-real-estate accounts can also
be created without a bank (was previously asserting the opposite)
2026-04-04 16:33:26 +01:00
Víctor Falcón bb65bdc16e
feat(accounts): add loan amortization projections for loan accounts (#246)
## Summary

- Adds loan detail tracking (interest rate, term, start date, original
amount) to loan-type accounts, following the existing
`real_estate_details` pattern
- Implements `LoanAmortizationService` with standard amortization math
to automatically project month-to-month balance evolution
- Shows projected future balances as a dashed line on the account
balance chart, visually distinct from historical data
- Adds a scheduled command (`loans:generate-balances`) to auto-generate
monthly balance entries for loan accounts
- Includes 36 tests (10 unit for pure math, 26 feature for CRUD, API
projections, command, and authorization)

## Changes

### Backend
- **Migration**: `loan_details` table (UUID PK, unique `account_id` FK,
interest rate, term, start date, original amount)
- **LoanDetail model** + factory with `Account::loanDetail()` HasOne
relationship
- **LoanAmortizationService**: `calculateMonthlyPayment`,
`calculateRemainingBalance`, `generateProjection`, `projectFromBalance`,
`calculateRemainingMonths`, `getBalanceAtDate`
- **GenerateMonthlyLoanBalances** command: runs monthly on 1st, skips
existing entries
- **LoanDetailController**: PATCH endpoint for editing loan details from
show page
- **Settings\AccountController**: creates/updates `LoanDetail` on
store/update
- **DashboardAnalyticsController**: appends projected data points with
`projected: true` flag
- **Validation**: loan-specific rules in StoreAccountRequest and
UpdateAccountRequest

### Frontend
- **AccountForm**: loan fields (rate, term, start date, original amount)
shown when `type === 'loan'`
- **Create/Edit dialogs**: pass loan data fields in POST/PATCH payloads
- **Show page**: `LoanDetailsCard` component with edit/read modes,
computed monthly payment and remaining months
- **Balance chart**: new `ComposedChart` branch with dashed `Line`
overlay for projected data points
- **Types**: `LoanDetail` interface in `account.ts`
2026-03-26 15:06:09 +01:00
Víctor Falcón fa11dc78e0
feat(accounts): add market value and annual revaluation to real estate accounts (#245)
## Summary

- Allow setting the **current market value** (balance) when creating a
real estate account — the balance field existed for other account types
but was hidden for real estate
- Add an **annual revaluation percentage** field to real estate
accounts, editable from both the creation form and the property details
card on the account show page
- Create a scheduled command (`real-estate:apply-revaluation`) that runs
monthly on the 1st to automatically adjust property values by
`revaluation_percentage / 12 / 100`

## Details

### Backend
- Migration adds `decimal('revaluation_percentage', 5, 2)` nullable
column to `real_estate_details`
- Validation: nullable numeric, range -100 to +100 (negative for
depreciation)
- `ApplyRealEstateRevaluationCommand` finds all accounts with a
non-null/non-zero percentage, gets the latest balance, and upserts the
new balance for today
- Scheduled `->monthlyOn(1, '00:00')` in `routes/console.php`

### Frontend
- Added `real_estate` to `BALANCE_ACCOUNT_TYPES` so the Market Value
field appears during creation
- Added Annual Revaluation (%) input to both the creation form and the
property details card
- Read-mode displays formatted percentage with sign (e.g.,
"+3.50%/year")

### Tests
- 6 new tests in `RealEstateTest.php` covering creation with balance,
revaluation %, negative %, validation, PATCH update, and clearing to
null
- 8 tests in `ApplyRealEstateRevaluationTest.php` covering
positive/negative revaluation, skip conditions, latest balance usage,
multiple accounts, and upsert behavior
2026-03-26 11:02:20 +01:00
Víctor Falcón 395c4ad2c3
feat(accounts): add real estate asset tracking (#241)
## Summary

- Adds **real estate** as a new account type (`real_estate`) for
tracking property assets within the existing Accounts page
- Properties store metadata (type, address, purchase price/date, area,
notes) in a dedicated `real_estate_details` table with a one-to-one
relationship to accounts
- Properties can be **linked to a loan account** (mortgage) to
implicitly calculate equity — property value counts as asset (+), linked
loan counts as liability (-)
- Market value is tracked as the account balance and uses "market value"
terminology throughout the UI

## What's included

### Backend
- `PropertyType` enum (Residential, Commercial, Land, Vacation, Other)
- `RealEstateDetail` model, factory, migration, policy
- `StoreRealEstateDetailRequest` and `UpdateRealEstateDetailRequest`
form requests
- `RealEstateDetailController` with `update()` for editing property
details
- Extended `Settings\AccountController@store` to create
`RealEstateDetail` when type is `real_estate`
- Extended `AccountController@show` to load real estate detail, linked
loan with bank info, and available loan accounts
- Extended `AccountController@index` SQL ordering to include
`real_estate`
- Conditional validation rules in `StoreAccountRequest` for real estate
fields

### Frontend
- New `real_estate` type in `account.ts` with `PropertyType`,
`AreaUnit`, `RealEstateDetail` interface, and helper functions
- Conditional real estate fields in `account-form.tsx` (property type,
address, purchase price, purchase date, area, linked loan, notes)
- `PropertyDetailsCard` component in `Accounts/Show.tsx` with view and
inline edit modes
- "Update market value" terminology in balance update buttons for real
estate accounts
- `real_estate` added to account type ordering and groups on the Index
page
- Wayfinder routes regenerated

### Tests
- 22 feature tests covering creation, validation, show page, index
ordering, updating details, IDOR protection, model relationships, and
soft delete behavior
- Unit test updates for `AccountType` enum (`reducesNetWorth`,
`isNonTransactional`)

### Translations
- 32 new Spanish translation strings for all real estate UI

## Design decisions
- Real estate accounts are **non-transactional** (like
investment/retirement) — balance-only tracking for now. Future iteration
will link transactions directly for rental income/expense tracking
- **Single loan per property** via direct FK from
`real_estate_details.linked_loan_account_id`
- No encryption on address/notes fields (opted out per discussion)
- No separate sidebar page — real estate is a grouped section within the
existing Accounts page
2026-03-24 10:21:32 +00:00