Commit Graph

404 Commits

Author SHA1 Message Date
Víctor Falcón 30cc4da6c6
Fix cashflow null category rows (#382)
## Sentry
- Issue: PHP-LARAVEL-1T
- URL: https://whisper-money.sentry.io/issues/PHP-LARAVEL-1T

## Root cause
Cashflow breakdown UI assumed every API row has a category object.
Production returned a breakdown row with `category: null`, so rendering
read `item.category.icon` and crashed.

## Fix
- Allow breakdown rows to carry nullable category/category_id.
- Render null categories as Uncategorized with HelpCircle/gray fallback.
- Add focused Vitest regression.
- Enable Wayfinder Vite plugin in Vitest so components importing
generated actions resolve in tests.

## Verification
- `npm test -- resources/js/components/cashflow/breakdown-card.test.tsx`
- `npx prettier --write
resources/js/components/cashflow/breakdown-card.tsx
resources/js/components/cashflow/breakdown-card.test.tsx
resources/js/hooks/use-cashflow-data.ts vitest.config.ts`
- `npx eslint resources/js/components/cashflow/breakdown-card.tsx
resources/js/components/cashflow/breakdown-card.test.tsx
resources/js/hooks/use-cashflow-data.ts vitest.config.ts --fix`

## Notes
- `npm run types` currently fails on existing unrelated TypeScript
errors across account, transaction, chart, auth, crypto, and dexie
files.
2026-05-11 18:54:26 +02:00
Víctor Falcón 5b8e7e86d7
Add labels to automation rules (#379)
## Summary
- add label selection to automation rule create/edit dialogs
- allow rules with category, labels, or both
- display category, labels, and note actions in rule tables

## Verification
- php artisan test --compact tests/Feature/AutomationRuleTest.php
tests/Feature/AutomationRuleEvaluationTest.php
- npx eslint
resources/js/components/automation-rules/automation-rule-action-badges.tsx
resources/js/components/automation-rules/create-automation-rule-dialog.tsx
resources/js/components/automation-rules/edit-automation-rule-dialog.tsx
resources/js/components/automation-rules/automation-rules-dialog.tsx
resources/js/pages/settings/automation-rules.tsx
- npm run build (exit 0; Sentry sourcemap upload logged SSL error)
2026-05-11 14:56:25 +02:00
Víctor Falcón 69610c57b3
Recover from stale Vite chunks (#374)
## Summary
- reload once when a stale Vite dynamic import chunk fails
- suppress recoverable chunk load failures from Sentry
- add Vitest coverage for recovery and Sentry filtering

Fixes PHP-LARAVEL-1P

## Tests
- npm run test -- resources/js/lib/chunk-load-recovery.test.ts
resources/js/lib/sentry.test.ts
- npm run types *(fails: existing TypeScript errors outside this
change)*
2026-05-10 17:20:24 +02:00
Víctor Falcón 6335287765
fix(sentry): ignore postMessage clone noise (#373)
## Summary
- filter browser postMessage DataCloneError noise before Sentry send
- add focused classifier tests

## Tests
- npm test -- resources/js/lib/sentry.test.ts
- npm run types *(fails: existing repo-wide TypeScript errors, including
missing Wayfinder generated modules)*

Fixes PHP-LARAVEL-1M
2026-05-10 11:16:38 +01:00
Víctor Falcón 718cfa9e8f
Fix Sentry transaction and dashboard crashes (#372)
## Summary
- guard transaction list Inertia callbacks against non-paginated
transaction props
- exclude soft-deleted categories from dashboard category aggregates
- add dashboard category and cursor pagination tests

Fixes PHP-LARAVEL-1K
Fixes PHP-LARAVEL-1J

## Tests
- npm run test -- cursor-pagination.test.ts
- php artisan test --compact tests/Feature/DashboardAnalyticsTest.php
--filter='top categories'
- vendor/bin/pint --dirty --format agent

Note: npm run types still fails on existing unrelated TypeScript errors.
2026-05-10 11:10:31 +01:00
Víctor Falcón 6132a0fa92
Init browser Sentry from env DSN (#370)
## Summary
- read browser Sentry DSN from `SENTRY_LARAVEL_DSN`
- expose `SENTRY_LARAVEL_DSN` through Vite env
- enable default PII and disable client Sentry when DSN missing
- removed temporary welcome test button

## Tests
- `npx eslint resources/js/pages/welcome.tsx resources/js/app.tsx
resources/js/types/vite-env.d.ts vite.config.ts`
- `php artisan test --compact tests/Feature/SentryConfigTest.php`
- `npm run build` (build completed; existing Sentry sourcemap upload
reports SSL error against Bugsink)
2026-05-09 12:07:11 +02:00
Víctor Falcón 164235f6d3
Fix label creation dropdown refresh (#363)
## Summary
- keep newly created labels available in the combobox without reload
- propagate created labels through transaction forms and bulk actions
- add browser coverage for creating a label from transaction label
dropdown

## Tests
- php artisan test --compact tests/Browser/TransactionsTest.php
--filter='newly created labels'
- vendor/bin/pint --dirty --format agent
- npm run build

Note: `npm run types` still fails on pre-existing TypeScript errors
outside this change.
2026-05-07 10:26:43 +01:00
Víctor Falcón 17d952435b
Hide stale onboarding plan warning (#360)
## Summary
- hide connected-account plan warning and price after the user chooses
connected setup once
- seed onboarding state from existing connected accounts
- add hook unit coverage and browser coverage for warning/price
visibility

## Tests
- bun run test -- resources/js/hooks/use-onboarding-state.test.tsx
- php artisan test --compact tests/Browser/OnboardingFlowTest.php
--filter="hides the connected plan warning"
- npm run types -- --pretty false 2>&1 | rg
"resources/js/(components/onboarding/step-create-account|hooks/use-onboarding-state|pages/onboarding/index)"
(no onboarding errors; repo has unrelated existing TS errors)
2026-05-06 15:10:32 +01:00
Víctor Falcón 6cd465bb78
Fix bank connection FAQ (#359)
## Summary
- update landing FAQ to confirm secure Open Banking connections
- clarify bank connections are Pro-only and free users import via
CSV/Excel
- add regression coverage for FAQ copy and Spanish translation

## Tests
- vendor/bin/pint --dirty --format agent
- php artisan test --compact tests/Unit/WelcomeFaqTest.php
- git diff --check
2026-05-06 11:28:26 +01:00
Víctor Falcón 22043ced29
fix(budgets): remove Custom period type to fix duplicate-key crash (#355)
## Why merge this

Production bug
**[PHP-LARAVEL-1B](https://whisper-money.sentry.io/issues/PHP-LARAVEL-1B)**
— `/budgets/{budget}` throws a 500 (`UniqueConstraintViolationException`
on `budget_periods_budget_id_start_date_unique`) for the affected user.
Page is unusable for them until fixed.

## Root cause

The Custom period type let users create budgets with arbitrary
`period_duration`. Combined with `calculatePeriodDates()`'s day-of-month
snap (`startDate->day(period_start_day)`), short durations produced
periods where `end_date == start_date == period_start_day`. On the next
visit after that day, regeneration computed a new period whose start
snapped backward to the same day → unique key collision.

Concrete trace from the Sentry event:

- Budget: `Seguro de Salud`, `period_type=custom, period_duration=1,
period_start_day=1`, created `2026-05-05`.
- Initial period created at budget creation: `start=2026-05-01,
end=2026-05-01` (already 4 days stale because Custom snapped
`now()=05-05` back to day 1, then `addDays(1).subDay()` produced same
date).
- Today `05-05` → `getCurrentPeriod()` returns null → `generatePeriod()`
snaps back to `05-01` again → `INSERT (budget_id, 05-01)` → 1062.

Verified in prod DB: this is the **only** budget in the entire database
with `period_type='custom'` and the only period row with `end_date <=
start_date`.

## Fix

Custom is the only period type that exposes this snap-back collision
(Monthly/Weekly/Biweekly all use a self-consistent advance). It also has
no defensible UX — every legitimate use case is covered by
Monthly/Weekly/Biweekly. So we remove it end-to-end:

- `BudgetPeriodType::Custom` enum case dropped.
- `BudgetPeriodService`: Custom branches removed from
`calculatePeriodDates()` and `calculateNextPeriodStartDate()`.
- `Budget` model: `period_duration` removed from `$fillable` and
`casts()`.
- `StoreBudgetRequest` / `UpdateBudgetRequest`: `period_duration` rule
removed.
- `BudgetController`: `period_duration` no longer passed in
store/update.
- Create + edit budget dialogs: Custom option and `period_duration`
input removed.
- `ResetDemoAccountCommand`: Custom switch case removed.
- `BudgetFactory`: `custom()` state and `period_duration` default
removed.
- `types/budget.ts`: `'custom'` removed from `BUDGET_PERIOD_TYPES` and
`Budget.period_duration` field dropped.
- Migration `2026_05_05_132023_convert_custom_budgets_to_monthly`:
rewrites any existing `custom` rows to `monthly` with
`period_duration=null, period_start_day=1` so the dropped enum case
can't crash on hydration.

Period generation logic for Monthly/Weekly/Biweekly is **unchanged**.

## Manual prod cleanup (separate, after merge)

The single buggy budget in prod will be deleted manually:

```sql
DELETE FROM budget_periods WHERE id = '019df7fd-6073-731b-9c08-f3723515292b';
DELETE FROM budgets WHERE id = '019df7fd-6071-7219-b190-ece22ccdb63f';
```

## Tests

`tests/Feature/BudgetPeriodServiceTest.php` covers Monthly + Weekly
advance and Monthly first-period generation. Existing
`BudgetPeriodDateTest` unaffected.

## Risk

- `period_duration` column kept in DB (nullable) to avoid data loss; no
migration needed.
- Migration ensures any environment with `period_type='custom'` rows is
converted before the enum case is removed, preventing hydration errors.
- No customers actively rely on Custom: prod query confirmed only the
single broken budget uses it, and that one is being deleted.

Fixes PHP-LARAVEL-1B
2026-05-05 16:07:16 +02:00
Víctor Falcón b1709b714e
fix(onboarding): guard window access in SSR (#351)
## Problem

Production SSR crashes repeatedly when rendering the Onboarding page:

```
ReferenceError: window is not defined
  at Onboarding (/app/bootstrap/ssr/assets/index-BEtdcHXd.js:2295)
```

The `useMemo` reading `?step=` from the URL accessed
`window.location.search` directly, which fails during Inertia SSR.

Observed ~7+ times in production logs between 18:35–21:37 UTC.

## Fix

Add `typeof window === 'undefined'` guard before reading the URL.
Returns `undefined` on the server so the step initializer falls back to
default behavior; client hydration re-runs and reads the URL.

## Related logs

- `Inertia\Ssr\SsrException` — Onboarding page
- Other SSR-touching files (`welcome.tsx`, `auth/login.tsx`) verified
safe (window inside `useEffect` or already guarded).
2026-05-05 08:45:44 +01:00
Víctor Falcón ab3d6e9fca
feat(leads): cohort-based launch invitations with per-user Stripe coupons (#333)
## Summary

Wires up the launch flow: each waitlist lead gets a per-cohort
invitation email, a personal single-use Stripe promo code matching their
reward, and a signed landing link that unlocks register/install.

## Cohorts (resolved at send-time, by queue rank ASC, ignoring
`position` null/0)

| Cohort | Rule | Reward | Stripe coupon |
|---|---|---|---|
| `founder` | ranks 1–10 | Free forever | `wm_founder_forever` (100%
off, forever) |
| `founder_referrer` | referred any current founder (overrides rank) |
Free forever | `wm_founder_forever` |
| `early_bird` | ranks 11–100 | 2 mo free monthly / 3 mo free yearly
first year | `wm_earlybird_monthly` (100% off, 2 mo, monthly only) +
`wm_earlybird_yearly` (25% off once, yearly only) |
| `waitlist` | ranks 101+ | same as early bird | same coupons |

## What's added

- **DB**: `cohort`, `promo_code_monthly`, `promo_code_yearly`,
`invitation_sent_at` on `user_leads`.
- **Services**: `LeadCohortResolver`, `LeadPromoCodeAllocator`.
- **Commands**:
- `php artisan stripe:ensure-launch-coupons` — idempotent Stripe coupon
setup (run once per env).
- `php artisan leads:send-invitations --limit=N [--cohort=…] [--dry-run]
[--force]` — wave-by-wave delivery, idempotent across runs
(`invitation_sent_at` gate). Lazily generates Stripe promo codes per
lead it touches.
- **Checkout**: `SubscriptionController::checkout` resolves the auth
user's `UserLead` by email and applies the matching promo code
(`monthly`/`yearly`) via Cashier's `withPromotionCode()`.
- **Invite link**:
`LandingAuthOverrideService::generateInvitationUrl($leadId, days: 30)` —
signed lead-bound URL that unlocks auth buttons (existing override
cookie) and stores `invited_lead_id` in session. The register view
prefills the email from the lead.
- **Emails**: 4 cohort markdown templates (`founder`,
`founder-referrer`, `early-bird`, `waitlist`) with per-cohort subject +
body. Locale set from `$lead->preferredLocale()`. 28 Spanish keys added.

## Tests (Pest)

- `LeadCohortResolverTest` — boundaries + founder-referrer override +
null/0 position skip.
- `SendUserLeadInvitationsTest` — batch ordering, idempotent across
runs, ignores null/0, persists cohort.
- `UserLeadInvitationTest` — per-cohort body content, signed lead-bound
signup URL, Spanish locale.
- `LandingAuthOverrideTest` — invitation URL unlocks + stores session
lead.

Full suite green (1262 passed).

## Launch checklist

1. Merge + deploy.
2. `php artisan stripe:ensure-launch-coupons` against staging Stripe →
verify in dashboard → run on prod.
3. Optional preflight: `php artisan leads:send-invitations --limit=10
--dry-run`.
4. Wave-by-wave: `php artisan leads:send-invitations --limit=50` per
day.
5. Confirm `HIDE_AUTH_BUTTONS=true` on prod so only signed invite links
unlock register/install.

## Notes / non-goals

- No `User → UserLead` foreign key — checkout matches by email per
request.
- Existing `FOUNDER` promo flow is untouched and still kicks in if a
user has no lead-specific code.
- `founder_referrer` cohort is empty in current prod data; logic is in
place for when founders start referring.
2026-04-30 15:10:28 +01:00
Víctor Falcón 0f2300bf3e
feat(accounts): show projection on real estate chart (#338)
Real estate account chart now appends 12 months of projected data when
the property has a revaluation percentage and/or a linked loan:

- Market value projected forward via the configured revaluation rate
  (compounded monthly).
- Mortgage balance projected via LoanAmortizationService on the linked
  loan, so the mortgage decline is visible alongside the property
  revaluation in the same chart.

Backend: DashboardAnalyticsController appends projected points with
display-currency variants. Frontend: chart adds dashed projection lines
for value and mortgage_balance plus a Today reference line. Tooltip and
currency-mode swap propagate the new projected_mortgage_balance field.
2026-04-27 08:08:12 +01:00
Víctor Falcón d697857041
typo: Add missing space (#339) 2026-04-27 07:59:45 +01:00
Víctor Falcón 79075dbcdf
fix(i18n): translate Unknown Income/Expense and other missing ES strings (#331)
## Summary

Fixes untranslated strings surfaced in the Spanish UI (e.g. `Unknown
Income`, `Unknown Expense` on the cashflow breakdown).

## Root cause

`CashflowAnalyticsController` pushed literal English names into the API
response for uncategorized totals, bypassing `__()`. Additional `__()`
calls across the sync flow and validation had no matching keys in
`lang/es.json`. Several frontend toasts/errors were also hardcoded.

## Changes

**Controller (P0)**
- `app/Http/Controllers/Api/CashflowAnalyticsController.php` — wrap
`Unknown Income`/`Unknown Expense` with `__()` (lines 250, 323).

**Translations (P0 + P1 + P2)**
- `lang/es.json` — added 23 keys (1730 → 1753):
  - `Unknown Income`, `Unknown Expense`, `Unknown Bank`, `Unknown error`
- Sync/banking messages: `Invalid credentials…`, `Rate limit exceeded…`,
`Failed to sync with the provider…`, `The provider is experiencing
issues…`, `An unexpected error occurred during sync…`, `Credentials
updated. Sync started.`, `Action required: :provider connection needs
attention`, `:count new transactions synced on Whisper Money`
- Validation: `The selected property cannot be linked.`, `The selected
property is already linked to a loan.`, `The verification link is
invalid.`, `This field is required.`
- Misc: `Confirm your waitlist spot - Whisper Money`, `Log in to your
bank`
- Frontend toasts: `Failed to re-evaluate rules. Please try again.`,
`Failed to load import data`, `All transactions failed to import`,
`Failed to update transactions`, `Failed to update transactions with
labels`

**Frontend hardcoded strings wrapped in `__()` (P2)**
- `resources/js/components/transactions/import-transactions-drawer.tsx`
- `resources/js/components/accounts/import-balances-drawer.tsx`
- `resources/js/lib/sync-manager.ts` (added `@/utils/i18n` import)
- `resources/js/components/transactions/transaction-list.tsx`
- `resources/js/components/transactions/import-transactions-button.tsx`
- `resources/js/pages/transactions/index.tsx`

## Verification

- `vendor/bin/pint --dirty` → pass
- `php artisan test --filter=CashflowAnalytics` → 24 passed
- `php artisan test --filter=Sync` → 166 passed
- `npm run build` → OK
- Post-fix audit: 0 real missing `__()` keys remaining
2026-04-24 17:58:26 +01:00
Víctor Falcón 4b145e230b
feat(accounts): add today marker on projected balance chart (#321)
## Summary

Adds a dashed 'Today' vertical reference line on the account balance
chart when a loan amortization projection is rendered. Makes it obvious
which months on the chart are historical (carry-forward of the last
recorded balance) and which are future projections.

## Context

Loan accounts extend their balance-evolution series with 12 months of
projected amortization values (see
`DashboardAnalyticsController::accountBalanceEvolution`). Without a
marker, past carry-forward months and future projected months looked
like a single continuous history.

## Changes

- `resources/js/components/accounts/account-balance-chart.tsx`
  - Import `ReferenceLine` from recharts.
  - Compute `todayMarker` (`yyyy-MM` monthly, `yyyy-MM-dd` daily).
- Render `<ReferenceLine>` inside the projected `ComposedChart` branch.
- Styling matches the budget spending chart (1px dashed, foreground
stroke, 'Today' label on top).

## Screenshots

_Visual change only — renders a dashed vertical line at the current date
on loan account charts with projection._
2026-04-24 12:45:51 +01:00
Víctor Falcón 38e1976270
fix(chart): hide tooltip on scroll with opacity fade (#320)
## Summary

Follow-up to #317. Portaled tooltip uses `position: fixed`; on scroll it
stayed anchored to stale viewport coords.

- Listen to `scroll` (capture) and `resize`, flip a `hidden` flag to
hide the tooltip.
- Reset flag whenever Recharts reports a new `coordinate` (pointer back
on a bar).
- Replace `visibility` binary toggle with `opacity` + 120ms transition
so the whole tooltip (bg, border, text) fades as one layer instead of
revealing staggered unmount order.

## Test plan
- Hover a bar → tooltip fades in at cursor.
- Scroll the page while hovering → tooltip fades out smoothly, whole
element at once.
- Hover another bar after scrolling → tooltip reappears.
2026-04-24 08:28:14 +01:00
Víctor Falcón 753002f930
fix(dashboard): dismiss account card tooltip when tapping outside (#318)
## Problem

On mobile, the sparkline tooltip on the dashboard account cards covered
almost the entire card, making it very hard to dismiss — you had to tap
very specific spots inside the card to clear it.

## Fix

Listen for `pointerdown` events outside the chart wrapper and unmount
the Recharts `<Tooltip>` to dismiss it. Because only the Tooltip is
toggled (not `<Line>`), there's no redraw animation.

Gated behind `matchMedia('(hover: hover)')` so desktop hover behavior is
left alone — tooltip still appears on hover without any click needed.

Works across multiple cards: tapping one dismisses tooltips on all
others.
2026-04-23 08:35:21 +01:00
Víctor Falcón e4d2ade92f
fix(chart): tooltip escapes overflow, truncates long labels (#317)
## Summary

- Portal chart tooltip to `document.body` to escape ancestor
`overflow-hidden` (CardContent, scroll container). Positioned via
Recharts' `coordinate` prop + `.recharts-wrapper` viewport rect, with
viewport-edge flipping.
- Cap tooltip width and truncate long account/liability names with
ellipsis; values stay nowrap.
- Force `grid-cols-[minmax(0,1fr)]` on tooltip grid tracks so `truncate`
actually engages (grid items default to min-width auto).

## Test plan
- Hover net-worth chart: tooltip no longer clipped by container.
- Position follows cursor; flips near viewport edges.
- Long liability/account names show ellipsis within tooltip bounds.
2026-04-23 08:17:38 +01:00
Víctor Falcón 2604f1158c
Support soft-deleted users with reusable emails (#316)
## Summary
- soft-delete users by adding `deleted_at` to `users`
- rename deleted user emails with a timestamp prefix so original email
can be reused
- block email sends and banking follow-up work for deleted users while
preserving data

## Testing
- php artisan test --compact tests/Feature/DeleteUserCommandTest.php
tests/Feature/Settings/ProfileUpdateTest.php
tests/Feature/Auth/RegistrationTest.php
tests/Feature/Auth/AuthenticationTest.php
tests/Feature/Jobs/Drip/SendWelcomeEmailJobTest.php
tests/Feature/Console/SendUpdateEmailCommandTest.php
- vendor/bin/pint --dirty --format agent
2026-04-22 11:41:41 +01:00
Víctor Falcón 905edeb4a2
fix: route new PWA guests to signup (#313)
## Summary
- redirect guest access to protected routes based on returning-user
cookie
- send new PWA guests to registration and returning guests to login
- persist returning-user cookie after register, login, and 2FA login
- keep explicit login links working via `force=1`

## Testing
- vendor/bin/pint --dirty --format agent
- php artisan test --compact tests/Feature/Auth/AuthenticationTest.php
tests/Feature/DashboardTest.php tests/Feature/Auth/RegistrationTest.php
2026-04-21 10:53:05 +01:00
Víctor Falcón 240fcf1703
feat(landing): add signed auth links (#312)
## Summary
- add signed landing links that unlock auth buttons while
HIDE_AUTH_BUTTONS is enabled
- persist the unlock in a secure cookie so desktop and installed PWA
users can still sign up
- add artisan command to generate signed landing auth links and test
coverage for the flow

## Testing
- php artisan test --compact tests/Feature/LandingAuthOverrideTest.php
tests/Feature/Console/GenerateLandingAuthLinkCommandTest.php
tests/Feature/Auth/RegistrationTest.php
- php artisan test --compact tests/Feature/WelcomeBanksOrderingTest.php
tests/Feature/SubscriptionTest.php
- vendor/bin/pint --dirty --format agent
2026-04-21 08:28:59 +01: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 75736f3e59
fix(auth): allow forced registration (#307)
## Summary
- allow `/register?force=1` to bypass the hidden-auth redirect and
render the registration page
- preserve the `force=1` query on form submit so hidden signup still
works end-to-end
- enforce hidden-signup blocking in the Fortify user creation path and
cover the forced/unforced flows with tests
2026-04-20 10:00:52 +01:00
Víctor Falcón 22952c4e75
fix(cashflow): read period from server props instead of window (#302)
## Summary
- Fixes
[PHP-LARAVEL-9](https://whisper-money.sentry.io/issues/PHP-LARAVEL-9):
`ReferenceError: window is not defined` during Inertia SSR of
`CashflowPage`.
- `CashflowController` now reads + validates the `period` query param
(`YYYY-MM`) and passes it as an Inertia prop.
- Client `useState` initializer reads from `usePage().props.period`
instead of `window.location.search`, making SSR safe and avoiding
hydration mismatch.
- `useEffect` URL sync now compares against the prop, not
`window.location.search`.

## Why (c) not a `typeof window` guard
Server-provided params avoid hydration mismatch and keep URL as single
source of truth.

## Tests
New `tests/Feature/CashflowPageTest.php`:
- guest redirect
- no query param → `period` prop `null`
- valid `?period=2025-03` → prop `'2025-03'`
- invalid value → `null`
- malformed format (`2025-3`) → `null`

All 5 pass (50 assertions).

## Scope
Cashflow only. Other pages with similar `window.location.search` in
`useState` initializers (`onboarding/index.tsx`, `welcome.tsx`,
`auth/login.tsx`) left for follow-up.

Fixes PHP-LARAVEL-9
2026-04-17 10:27:49 +01:00
Víctor Falcón 244344e953
feat(open-banking): remove feature flag gating (#297)
## Summary
- remove the Pennant-based `open-banking` flag and middleware gating so
open banking is always available for authenticated users
- simplify landing, onboarding, settings, and subscription flows to use
always-on open-banking behavior and remove stale frontend/shared flag
plumbing
- update open-banking tests and purge stored `open-banking` Pennant rows

## Testing
- `php artisan test --compact
tests/Feature/OpenBanking/InstitutionControllerTest.php`
- `php artisan test --compact
tests/Feature/OpenBanking/BinanceControllerTest.php`
- `php artisan test --compact
tests/Feature/OpenBanking/IndexaCapitalControllerTest.php`
- `php artisan test --compact
tests/Feature/OpenBanking/BitpandaControllerTest.php`
- `php artisan test --compact
tests/Feature/OpenBanking/AuthorizationControllerTest.php`
- `php artisan test --compact
tests/Feature/OpenBanking/ConnectionControllerTest.php`
- `php artisan test --compact
tests/Feature/OpenBanking/AccountMappingTest.php`
- `php artisan test --compact
tests/Feature/OpenBanking/OpenBankingFeatureFlagTest.php`
- `php artisan test --compact
tests/Feature/OpenBanking/SyncRetryAndLoggingTest.php`
- `php artisan test --compact tests/Feature/SubscriptionTest.php`
- `php artisan test --compact
tests/Feature/WelcomeBanksOrderingTest.php`
- `vendor/bin/pint --dirty --format agent`

## Notes
- `php artisan pennant:purge open-banking` was run to remove stale
stored values
- `php artisan test --compact tests/Browser/OnboardingFlowTest.php`
still has one unrelated real-estate onboarding browser failure (`it
creates a real estate account during onboarding when feature is
enabled`)
2026-04-17 10:20:05 +02:00
Víctor Falcón fde5405777
fix(user): persist detected timezones (#296)
## Summary
- store browser-detected IANA timezones on users during registration and
share the value to the frontend
- silently backfill missing timezones for authenticated users without
overwriting existing saved values
- add focused feature coverage for registration, shared props, and the
timezone backfill endpoint

## Testing
- php artisan test --compact tests/Feature/Auth/RegistrationTest.php
tests/Feature/InertiaSharedDataTest.php
tests/Feature/Settings/TimezoneTest.php
- npm test -- resources/js/utils/currency.test.ts
2026-04-16 11:36:57 +01:00
Víctor Falcón 319ca758e1
fix(pricing): update final release prices (#288)
## Summary
- update subscription pricing to the final release amounts for monthly
and annual billing
- keep billing UI formatting aligned with the configured currency so
1.99 €/month displays correctly
- refresh subscription and Stripe sync tests to lock the new values in
place
2026-04-15 14:49:02 +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 d0aab3d11b
feat: verify waitlist leads (#285)
## Summary
- gate waitlist signup behind email confirmation so only verified leads
receive a queue position and referral code
- add signed lead verification routes, a check-email page, and a
dedicated verification email for the waitlist flow
- update lead syncing and feature tests so only verified leads are
exported to Resend and referral movement happens after verification

## Testing
- php artisan test --compact tests/Feature/UserLeadTest.php
- php artisan test --compact
tests/Feature/ResendSyncLeadsCommandTest.php
- php artisan test --compact tests/Feature/MailSenderTest.php
- vendor/bin/pint --dirty --format agent
2026-04-14 11:26:01 +01:00
Víctor Falcón ea9956f21d
fix: keep iOS popovers below the notch (#282)
## Summary
- add safe-area CSS variables for the viewport top and bottom insets
- apply safe-area-aware collision padding to the shared popover
primitive so iOS overlays stay below the notch
- add a focused regression test for the popover safe-area guard

## Testing
- vendor/bin/pint --dirty --format agent
- php artisan test --compact tests/Feature/PopoverSafeAreaTest.php
2026-04-13 15:19:56 +01:00
Víctor Falcón b505d68ef0
fix: keep iOS content below the notch (#280)
## Summary
- move the top safe-area inset from the mobile header to the shared
authenticated content shell
- keep scrolling content on iOS sidebar pages from sliding under the
notch
- add a focused regression test for the shared layout safe-area
placement

## Testing
- vendor/bin/pint --dirty --format agent
- php artisan test --compact
tests/Feature/AuthenticatedLayoutSafeAreaTest.php
2026-04-13 13:59:05 +01:00
Víctor Falcón a7c1bd35ef
feat: link loans to existing properties (#275)
## Summary
- add an optional linked property selector when creating loan accounts
in both the main create dialog and onboarding
- validate that only the current user's unlinked real-estate accounts
can be selected and persist the reciprocal link after loan creation
- expose linked property state in shared account props and cover the new
flow with focused loan feature tests

## Testing
- php artisan test --compact tests/Feature/LoanTest.php
- vendor/bin/pint --dirty --format agent
2026-04-13 09:51:13 +01:00
Víctor Falcón d91d9d3b3e
fix: default to standard onboarding option (#276)
## Summary
- default the onboarding account setup choice to the connected Standard
option when open banking is available
- change the Standard onboarding card and connected-bank step accents
from blue to emerald for consistent plan styling
- update the onboarding browser test comment to reflect the new default
selection

## Verification
- `php artisan test --compact tests/Feature/SubscriptionTest.php`
- `php artisan test --compact tests/Browser/OnboardingFlowTest.php` *(3
unrelated existing failures in browser assertions)*
2026-04-13 08:54:12 +01:00
Víctor Falcón dafc58f49f
fix: clarify account creation modal copy (#274)
## Summary
- update the create account modal copy to explicitly mention loans and
property accounts
- clarify the manual option so users understand it also covers manually
created real-estate accounts
- update the browser test assertion to match the revised dialog copy
2026-04-12 16:38:00 +01:00
Víctor Falcón 80274e03a8
fix: align onboarding account types with current asset support (#273)
## Summary
- add real estate to the onboarding account type explainer and update
investment copy to mention crypto and cold wallets
- align onboarding manual account creation with the real-estate feature
flag and real-estate payload requirements
- cover the onboarding copy and real-estate creation flow with browser
tests

## Testing
- php artisan test --compact tests/Browser/OnboardingFlowTest.php
2026-04-12 16:22:38 +01:00
Víctor Falcón 62ab1b38db
fix: clarify mobile settings navigation (#272)
## Summary
- add a hamburger icon to the mobile settings dropdown trigger so it
reads more clearly as navigation
- reduce the mobile spacing between the settings subtitle and the
dropdown trigger
- keep the desktop settings sidebar unchanged

## Testing
- php artisan test --compact tests/Feature/Settings/CategoryTest.php
2026-04-10 17:59:52 +01:00
Víctor Falcón 3acb277fb5
feat: add appearance shortcut to user menu (#269)
## Summary
- add an Appearance shortcut to the user dropdown between Privacy mode
and Settings
- use the existing appearance route so users can jump straight to
`/settings/appearance`
- add a focused frontend test covering the new menu item and its order
2026-04-10 14:59:16 +01:00
Víctor Falcón 38cf672c8e
fix: default account charts to user currency (#271)
## Summary
- default account charts to the user's currency when a currency toggle
is available
- reverse the account chart currency toggle order so the user currency
appears first
- add a focused frontend test covering the toggle order and selected
state

## Testing
- npm test -- chart-currency-toggle
2026-04-10 14:41:36 +01:00
Víctor Falcón 0735ee6d69
fix: preserve cents in chart amounts (#270)
## Summary
- keep two decimal places in net worth and account chart amounts instead
of rounding to whole units
- update shared chart tooltip, trend tooltip, and MoM tooltip currency
formatting so related chart values stay consistent
- add targeted formatter coverage for chart-style balances with decimals

## Testing
- npm test -- resources/js/utils/currency.test.ts
2026-04-10 15:41:24 +02:00
Víctor Falcón 1b76729154
chore: Change mobile menu translation on spanish (#268) 2026-04-10 13:46:26 +01:00
Víctor Falcón c3ff4c684a
feat: store invested_amount in user currency instead of account currency (#262)
## Summary

- **Store `invested_amount` in user's currency** (e.g. EUR) instead of
the account's currency (e.g. BTC), since it represents "how much of my
money did I put in" — a concept tied to the user's home currency.
- **Flip `display_invested_amount`** in API responses: now converts from
user currency → account currency (previously account → user), for the
chart's account-currency toggle mode.
- **Remove redundant conversions** in `AccountMetricsService` since
invested amounts are already in user currency after sync.

## Changes

### Backend (5 files)
- **BinanceBalanceSyncService** — convert to
`$account->user->currency_code` instead of `$account->currency_code`
- **BitpandaBalanceSyncService** — same currency target change
- **IndexaCapitalBalanceSyncService** — inject
`CurrencyConversionService`, convert invested amount float to user
currency before storing as cents
- **DashboardAnalyticsController** — flip `display_invested_amount`
conversion direction (user→account) in both monthly and daily endpoints
- **AccountMetricsService** — remove 4 now-redundant invested_amount
conversion spots (values already in user currency)

### Frontend (6 files)
- **update-balance-dialog.tsx** — invested amount input uses user
currency
- **balances-modal.tsx** — invested amount display and edit uses user
currency
- **account-balance-chart.tsx** — currency toggle: user-currency mode
keeps invested_amount as-is; account-currency mode swaps to
`display_invested_amount`
- **import-balances-drawer.tsx** — passes `investedAmountCurrencyCode`
(user currency) to sub-components
- **import-balance-step-mapping.tsx** /
**import-balance-step-preview.tsx** — format invested amounts in user
currency

### Tests (2 files)
- **IndexaCapitalBalanceSyncTest** — use `app()` instead of `new`
(constructor now has DI), pin account currency to USD for deterministic
assertions
- **DashboardAnalyticsTest** — add EUR-based exchange rate, flip
assertion from `/ 0.90` to `* 0.90`
2026-04-06 12:48:13 +02: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 6ce5b123ce
fix: add missing port to frontend Bugsink DSN (#260)
## Summary
- Adds the missing `:8000` port to the frontend Sentry/Bugsink DSN in
`app.tsx`, so errors are sent to the correct endpoint at
`bugsink.whisper.money:8000`.
2026-04-04 15:10:14 +00:00
Víctor Falcón 3d5823728a
feat(settings): centralize currency options and split profile/account support (#256)
## Summary

- **Bug fix:** Dashboard and accounts index cards displayed the
account's original currency code (e.g. `BTC`) even though balances were
already converted to the user's currency (e.g. `EUR`). Now passes
`displayCurrencyCode` to card components so the label matches the
converted amount.
- **Feature:** Added a currency toggle on the account detail chart to
switch between the account's native currency and the user's main
currency. Extends the balance evolution APIs with `display_*` fields
when conversion applies.
- **First-account restriction:** Restricts first-account creation to
primary (fiat) currencies only, ensuring the user's base currency is
always widely supported.

## Changes

### Bug fix — currency label on cards
- `AccountBalanceCard` / `AccountListCard`: Added `displayCurrencyCode`
prop; all amount renders now use it instead of `account.currency_code`
- `dashboard.tsx`: Passes `netWorthEvolution.currency_code` as
`displayCurrencyCode`
- `Accounts/Index.tsx`: Passes `auth.user.currency_code` as
`displayCurrencyCode`

### Backend — API extension
- `DashboardAnalyticsController`: Both `accountBalanceEvolution()` and
`accountDailyBalanceEvolution()` now return `display_value`,
`display_invested_amount`, `display_mortgage_balance` per data point and
a top-level `display_currency_code` when the account currency differs
from the user's

### Frontend — currency toggle
- New `ChartCurrencyToggle` component with `ToggleGroup` showing
currency code labels (e.g. `BTC` / `EUR`)
- `ChartSettingsPopover`: Extended with optional `currencyToggle` prop
for mobile
- `AccountBalanceChart`: Full integration — all amounts, trends,
tooltips, MoM chart, and equity swap to `display_*` values when toggle
is set to user currency

### First-account currency restriction
- `StoreAccountRequest`: First account limited to primary currency codes
- `AccountForm` / `CreateAccountDialog` / `StepCreateAccount`: Pass
`usePrimaryCurrenciesOnly` when applicable

### Tests
- 5 new Pest tests for `display_*` fields in balance evolution endpoints
- 3 new Vitest tests for `deriveAccountMetrics` (mixed-currency, empty
data, invested amounts)
- 1 new Pest test for first-account BTC rejection
2026-04-02 19:23:10 +02:00
Víctor Falcón c42a48a952
chore: Remove account-mapping feature flag (#252)
## Summary

- Removes the `account-mapping` Pennant feature flag entirely, making
the account mapping flow (pending accounts data + map-accounts page) the
default and only code path
- Removes the old direct-creation branches from all banking controllers
(Authorization, Bitpanda, Binance, IndexaCapital)
- Extracts a shared `CreatesAccountsFromPending` trait for the
onboarding auto-create logic
- Updates all controller tests to reflect the always-on mapping behavior

## Changes

### Backend
- **AppServiceProvider** — removed `account-mapping` flag definition
- **AuthorizationController** — removed flag check +
`createAccountsFromSession()` dead code; always stores
`pending_accounts_data` and redirects to mapping
- **BitpandaController / BinanceController / IndexaCapitalController** —
removed flag checks, old inline account creation, and unused imports
- **HandleInertiaRequests / ActivateDevelopmentFeatures /
ResolvesFeatures** — removed `account-mapping` from flag arrays
- **New `CreatesAccountsFromPending` trait** — shared auto-create logic
for onboarding path (to be consumed next)

### Frontend
- Removed `'account-mapping'` from the TypeScript `Features` interface

### Tests
- Merged flag-specific test variants into single always-on tests
- Removed redundant tests that tested the old disabled-flag code path
- Updated assertions to check `pending_accounts_data` instead of direct
account creation
2026-04-01 12:09:22 +02:00
Víctor Falcón 755452d6ce
Fix net worth chart config UX and color scheme issues (#250)
## Summary

- **Clickable toggle rows**: The full row in the chart settings popover
now toggles the checkbox, not just the checkbox itself.
- **Chart stacking order**: When toggling "Include loans" or "Include
real estate", the chart now re-mounts so accounts appear in the correct
sorted position (biggest at bottom) instead of being appended on top.
- **Scheme-aware liability dot**: The red liability indicator dot in the
chart tooltip now respects the user's chart color scheme (neutral, blue,
pink) instead of always using the destructive/red color.
2026-03-31 11:19:35 +01:00
Víctor Falcón 50a80bf8ee refactor(dashboard): move mortgage subtitle under property name and always show account type icon 2026-03-27 09:09:22 +01:00
Víctor Falcón 752176e80d feat(dashboard): merge real estate accounts with linked mortgages on dashboard
Extend the dashboard to hide linked loan accounts and display merged
real estate cards showing equity as the primary balance, a dual-line
sparkline (solid market value, dashed mortgage owed), 'Mortgage at'
subtitle with bank logo, and a rich tooltip with Market Value,
Mortgage Owed, and color-coded Equity.
2026-03-26 21:04:13 +01:00
Víctor Falcón 6e976354ba feat(accounts): merge real estate accounts with linked mortgages in UI (#248)
Combine real estate and linked loan accounts into a unified card on the
accounts list, showing equity as the primary balance with a dual-line
sparkline (solid for market value, dashed for mortgage owed). Hide linked
loans from the loan group and display mortgage bank info in the subtitle.

On the detail page, render LoanDetailsCard for real estate accounts with
linked loans and add header actions for editing loan details and updating
owed amounts via dialogs.
2026-03-26 20:54:12 +01:00