Commit Graph

549 Commits

Author SHA1 Message Date
Víctor Falcón d56760c6e8 Add falcode config file 2026-03-11 15:28:52 +01:00
Víctor Falcón 1f5e6ac450
feat(connections): add EnableBanking reconnect flow (#218)
## Summary

- Add `reauthorize` endpoint and reconnect detection in OAuth
`callback()` so users can re-authorize a revoked EnableBanking session
without losing their accounts or transaction history
- Replace "Retry" with "Reconnect" button (dropdown + error panel) for
EnableBanking authentication errors; catch-up sync of missed
transactions is handled automatically by the existing
`SyncBankingConnectionJob` via `last_synced_at`
- Add 5 missing Spanish translations (`Reconectar`, `Autenticación
fallida...`, `Error al iniciar la reautorización.`, `Error al
reconectar...`, `Cuenta bancaria reconectada exitosamente.`) and wrap
the reconnect flash message in `__()`
- 7 new Pest tests covering all reauthorize scenarios and the reconnect
callback path (15 total passing)
2026-03-11 12:58:29 +00:00
Víctor Falcón 1058904b14
feat(connections): filter already-connected institutions from connect bank dialog (#217)
## Summary

- When a user already has a connection to a bank/provider, that
institution is now hidden from the list when they open the \"Connect
Bank\" dialog
- Applies to all providers: EnableBanking institutions (matched by
`aspsp_name`), Binance, Bitpanda, and Indexa Capital (matched by
`provider`)
- `ConnectAccountDialog` accepts a new `connections` prop (passed from
the connections settings page where it's already available);
`ConnectAccountInline` (used in onboarding) accepts an optional
`connections` prop defaulting to `[]`

## Tests

- Added 2 backend tests to `ConnectionControllerTest` verifying the data
contract: the connections page sends `provider` and `aspsp_name` fields
for all provider types, which the frontend filtering logic depends on
2026-03-11 12:06:49 +00:00
Víctor Falcón 28c8df34d5
fix(transactions): cap description column width to prevent horizontal overflow (#216)
## Summary

- Adds `whitespace-normal` to the description column's cell class to
override the base `TableCell`'s `whitespace-nowrap`, which was
preventing `max-width` from being respected in HTML auto table layout
- Reduces the description column max-width from unlimited (`max-w-none`)
to `300px` at `md` breakpoint and `250px` at `sm`, eliminating
horizontal scroll at ~1220px viewport widths
2026-03-09 15:14:55 +00:00
Víctor Falcón e9572e4031
feat(landing): billing period toggle with yearly discount on pricing section (#215)
## Summary

- Adds a **Monthly / Yearly** billing period toggle to the pricing
section on the welcome page, shown only when the `open-banking` feature
flag is active and both monthly and yearly plan variants exist in the
pricing config.
- Defaults to **Yearly**; the Yearly button displays a dynamically
computed **Save X%** badge (currently 50%, derived from actual plan
prices).
- Plan cards filter to the selected billing period; the Free plan card
is always visible regardless of the selected period.
- Enables `open-banking` for unauthenticated landing page visitors so
the richer variant of the page (bank connections section + pricing
toggle) is always shown.
2026-03-08 18:33:40 +00:00
Víctor Falcón cbe28ff708
fix(banks:set-logo): add JPEG support test coverage and prompt for missing arguments (#214)
## Summary

- Fixes `banks:set-logo` for JPEG images by adding a `makeJpeg()` test
helper and a full JPEG test case — the pipeline was working but
completely untested, meaning any regression would go undetected
- Implements `PromptsForMissingInput` so the command interactively
prompts for `bank` and `url` when they are not passed as arguments
2026-03-07 16:33:54 +00:00
Víctor Falcón 2f1b9065f0
Add banks:set-logo artisan command (#213)
## Summary

- Adds `banks:set-logo {bank} {url}` artisan command to download,
process, and assign a logo to a bank by UUID
- Installs `intervention/image:^3.0` (GD driver) for image processing
- Image is validated to be square (returns an error if not), resized
down to max 250×250px if needed, stored as PNG on the `public` disk at
`banks/logos/{uuid}.png`, and the bank's `logo` field is updated with
the public URL

## Usage

```
php artisan banks:set-logo <bank-uuid> <image-url>
```

## Tests

7 feature tests covering success, no-resize, bank not found, HTTP
failure, non-image content type, non-square image, and invalid image
content.
2026-03-07 15:19:28 +00:00
Víctor Falcón 3cbca1a17d
ci: add retry with exponential backoff to deploy step (#212)
## Summary

- Replaces curl's built-in `--retry` with a shell loop that retries up
to 5 times on any non-2xx response (not just connection failures)
- Delays increase with each attempt: 10s → 20s → 30s → 40s between
retries
- Exits immediately on success so successful deploys are unaffected
2026-03-07 14:18:14 +00:00
Víctor Falcón 93369d8b6f
feat(landing): open-banking feature section with conditional grid layout (#209)
## Summary

- Adds a **Connect Your Banks** feature card (2-col wide) shown only
when the `open-banking` Pennant flag is active
- When **open-banking is ON**: row 1 = Connect Your Banks (col-span-2) +
Import in Seconds (col-span-1), row 2 = three feature cards
- When **open-banking is OFF**: row 1 = three feature cards, row 2 =
Import in Seconds (full width, unchanged behaviour)
- **Cashflow at a Glance** moved from its own standalone section into
the feature grid as a full-width row 3 (always visible)



https://github.com/user-attachments/assets/5151c141-60e2-4117-b1f2-629f2ef4c9ed

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-07 13:41:25 +00:00
Víctor Falcón 2763846329
Add weekly bank logo audit command (#211)
## Summary
- add a new `banks:check-logos` console command that validates all
non-null bank logo URLs weekly
- set broken/invalid bank logos to `null` and send an admin report email
to `ADMIN_EMAIL` when updates occur
- add weekly scheduling, admin mail config wiring, and feature tests for
valid/broken/head-fallback flows

## Testing
- vendor/bin/pint --dirty
- php artisan test tests/Feature/Console/CheckBankLogosCommandTest.php

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-06 11:05:13 +00:00
Víctor Falcón dbc356ab7b chore: Script to setup a worktree 2026-03-06 11:58:21 +01:00
Víctor Falcón 8ca4c8d6c6
feat(emails): co-founder language, welcome rewrite, and Spanish translations (#208)
## Summary

- **Welcome email rewritten** with benefit-focused content (understand
your finances, smart spending & investing, connect your banks,
private/own your data — no AI/data-mining narrative)
- **All email signatures updated** to reflect both co-founders: Víctor &
Álvaro, Founders of Whisper Money (drip, transactional, and waitlist
templates)
- **`user-lead-invitation.blade.php`** fully wrapped with `{{ __() }}` —
was entirely hardcoded English
- **Promo code email disabled** — removed `SendPromoCodeEmailJob`
dispatch from `ScheduleDripEmailsListener`; test updated to assert it is
not dispatched
- **60 missing Spanish translation keys** added to `lang/es.json`,
alphabetically sorted

## Files changed

- `app/Listeners/ScheduleDripEmailsListener.php`
- `lang/es.json`
- `resources/views/mail/**` (11 blade templates)
- `tests/Feature/Listeners/ScheduleDripEmailsListenerTest.php`
2026-03-05 15:22:07 +01:00
Víctor Falcón 21b03c7c36
feat(pricing): update landing page pricing table (#207)
## Summary

- **Monthly equivalent pricing**: yearly plan now shows the per-month
price (e.g. €3.90/month) with a \"Billed annually at €46.80\" note
beneath, matching the paywall behaviour
- **Plan rename**: \"Pro Monthly\" and \"Pro Yearly\" renamed to
\"Standard Monthly\" and \"Standard Yearly\" in config
- **Free plan card**: when the `open-banking` feature flag is active, a
Free card is shown first in the pricing grid — same features as paid
plans but without \"Connect bank accounts\" and \"Priority support\"
- **Connect bank accounts feature**: conditionally prepended to paid
plan feature lists when `open-banking` is enabled
- **Grid layout**: column count dynamically accounts for the optional
free plan card

## Screenshots
<img width="1172" height="862" alt="image"
src="https://github.com/user-attachments/assets/1f2895ed-12fe-4b32-a117-19a584014ae7"
/>
<img width="1173" height="821" alt="image"
src="https://github.com/user-attachments/assets/6f56a697-9908-43c6-9b15-6dd8cec35826"
/>
2026-03-05 13:39:52 +00:00
Víctor Falcón dc6f12989e chore: release v0.1.17 2026-03-05 13:34:23 +01:00
Víctor Falcón 8518d46c96
Gate bank connections for free-tier accounts & upgrade billing page (#205)
## Summary

- **Gate bank connection flow** — free-tier users see an upgrade dialog
instead of the bank connection flow (connections page, account creation
dialog). Backend 402 guard added as safety net in
`AuthorizationController`.
- **Billing page upgrade UI** — non-subscribed users see a plan selector
with monthly/yearly options and an upgrade CTA; subscribed users see the
existing Stripe portal button. Connected Bank Accounts added as the
headline benefit.
- **Upgrade dialog** now redirects to `/settings/billing` (plan
selector) instead of the checkout page directly.
- **Spanish translations** added for all new UI strings.

## Screenshots
<img width="804" height="509" alt="image"
src="https://github.com/user-attachments/assets/6238f72a-a5e2-45f8-974a-7a664f976b83"
/>
<img width="1122" height="892" alt="image"
src="https://github.com/user-attachments/assets/8438bbe2-d3ec-48fb-b3c0-33a940dd94a7"
/>
2026-03-05 12:13:01 +00:00
Víctor Falcón e8bc5fd786
fix(billing): create Stripe customer before redirecting to billing portal (#206)
## Summary

- Users without a Stripe customer ID (`stripe_id = null`) would hit an
`InvalidCustomer` exception when visiting the billing portal
- Added a `hasStripeId()` check before calling
`redirectToBillingPortal()`, creating the Stripe customer on-the-fly if
needed
- Added two tests covering both branches (with and without an existing
Stripe customer ID)
2026-03-05 11:58:04 +00:00
Víctor Falcón ac1476eeff
feat(pricing): dynamic Stripe pricing with locale-aware formatting (#204)
## Summary

- **Dynamic Stripe price resolution**: Replaces hardcoded
`stripe_price_id` env vars with lookup-key-based resolution
(`stripe_lookup_key`). A new `php artisan stripe:sync-prices` command
creates/updates Stripe prices from `config/subscriptions.php`
automatically.
- **Locale-aware currency formatting**: Replaces all
`getCurrencySymbol() + toFixed(2)` patterns with `formatCurrency()`
(backed by `Intl.NumberFormat`) across `welcome.tsx`, `paywall.tsx`,
`billing.tsx`, and `step-create-account.tsx`, so symbol position and
separators are correct for the user's locale (e.g. `3,90 €` in Spanish).
- **EUR defaults and updated plan prices**: Cashier currency defaulted
to EUR, plan prices updated to €7.80/month and €46.80/year, and
`pricing.currency` is now shared as an Inertia prop.
- **Promo/discount cleanup**: Removed all FOUNDER discount mentions and
Discord community links from the paywall, landing pricing section, and
invitation email.
2026-03-05 11:41:59 +00:00
Víctor Falcón 3f6c67631b
fix(browser-test): reload transactions in syncing step and fix Skip button selector (#203)
## Summary

- Reverts the CSV special-case added in `55f35c6` — `router.reload({
only: ['transactions'] })` now always fires in the syncing step so
CSV-imported transactions correctly appear in the categorize step.
- Fixes the `click('Skip')` selector in `OnboardingFlowTest` to use
`button:has-text("Skip")` instead of `'Skip'`, bypassing the Pest
browser plugin's exact-text fallback which timed out because the
button's full text content is `"Skip Ctrl+N"` (due to the `<Kbd>` child
element).
- Updates the onboarding flow browser test to exercise the full
categorize flow (skipping all 5 CSV transactions) instead of asserting
the empty state.

This commit was pushed after PR #201 was merged and did not make it into
main.
2026-03-05 11:29:06 +00:00
Víctor Falcón 011ba13114
fix(testcontainers): stop and remove MySQL container on signal and shutdown (#202)
## Summary

- `register_shutdown_function` is not invoked on SIGINT (Ctrl+C) or
SIGTERM, leaving the ephemeral MySQL container running after an
interrupted test run
- Added `pcntl_async_signals` + `pcntl_signal` handlers for SIGTERM and
SIGINT so the container is cleaned up on interruption
- Wrapped `$container->stop()` in try/catch to prevent an uncaught
exception inside the shutdown function from becoming a PHP fatal error
(which would also leave the container running)
2026-03-05 10:57:28 +00:00
Víctor Falcón a8dfac1422
feat: (Onboarding) add categorization intro screen with benefit cards (#201)
## Summary

- Adds an intro screen to the categorize transactions onboarding step
that shows before the categorizer UI, explaining that at least 5
transactions must be categorized to continue
- Displays four benefit cards (see where you spend, build better
budgets, spot savings, automate over time) to motivate the user
- Adds missing Spanish translation for \"Back to accounts\" and all new
strings introduced by the intro screen
2026-03-05 11:47:12 +01:00
Víctor Falcón cf9071c11b
fix(migration): make add_waitlist_columns migration idempotent (#200)
## Summary

- The `2026_03_04_085843_add_waitlist_columns_to_user_leads_table`
migration was failing on deploy because the `position`, `referral_code`,
and `referred_by_id` columns already existed in the production database.
- Wrapped each column addition with a `Schema::hasColumn()` guard so the
migration skips columns that already exist, making it safe to run
regardless of the current schema state.
2026-03-04 12:06:38 +00:00
Víctor Falcón 4d0d203fd3
feat(waitlist): waiting list with referral system (#199)
## Summary

- Adds an inline email capture form on the landing page when
`HIDE_AUTH_BUTTONS=true`, replacing the previous CTA buttons
- Each signup gets a queue position (starting at #500), a unique
referral link, and a welcome email from `victor@whisper.money`
- Referring 10 people via the personal link moves the referrer 10
positions forward in the queue (floor: 1), triggering a notification
email
- Full Spanish localisation for all UI strings and email copy; `locale`
is stored on each lead so emails are sent in the lead's own language
- 14 feature tests covering all waitlist behaviour (positions,
referrals, emails, thank-you page)

## How to activate

Set `HIDE_AUTH_BUTTONS=true` in `.env`. The waitlist form and thank-you
page are completely dormant otherwise.
2026-03-04 12:36:47 +01:00
Víctor Falcón d8f6a680ce
feat(subscription): allow free plan for open banking users without connected banks (#188)
## Summary

- Open Banking users who complete onboarding **without** connecting a
bank are no longer blocked at `/subscribe` — they can continue for free
via a new \"Continue for free\" button on the paywall
- Users who **did** connect a bank during onboarding see the standard
paywall with no free option (bank sync is a paid/Standard feature)
- Renamed the paid plan badge from **Pro** → **Standard** in the
onboarding UI

## Changes

### Backend
- `EnsureUserIsSubscribed` middleware: grants free access when
`open-banking` feature is active and the user has no
`banking_connections`
- `SubscriptionController::index()`: passes a `canUseFreePlan` boolean
prop to the paywall page

### Frontend
- `paywall.tsx`: accepts `canUseFreePlan` prop and renders a "Continue
for free" button that navigates to the dashboard
- `step-create-account.tsx`: badge and info text updated from "Pro" →
"Standard"

### Tests
- Two new browser tests in `OnboardingFlowTest.php`:
  - Manual account flow → `/subscribe` shows "Continue for free"
- BBVA connected bank flow (EnableBanking sandbox) → `/subscribe` does
NOT show "Continue for free"
2026-03-03 22:28:50 +00:00
Víctor Falcón fdc9d14c47
fix(haptics): restore haptic feedback to MobileBackButton (#198)
## Summary

- Restores `useWebHaptics` import and `trigger('light')` call to
`MobileBackButton` that was lost during rebase
- Both the floating and inline back buttons now fire haptic feedback on
tap via the shared `handleBack` function
2026-03-03 21:40:50 +00:00
Víctor Falcón 3d742677b5
feat(haptics): add haptic feedback to nav items and back buttons (#196)
## Summary

- Installs `web-haptics` via bun
- Triggers `selection` haptic feedback on all navigation items (mobile
bottom bar and desktop sidebar)
- Triggers `light` haptic feedback on all back buttons
(`connect-account-inline.tsx`, `categorize.tsx`)
2026-03-03 22:03:58 +01:00
Víctor Falcón 09d81ac7e7
fix(onboarding): gate connect bank option behind open-banking feature flag (#197)
## Summary

- The \"Connected\" account card in `StepCreateAccount` was rendered
unconditionally during onboarding, allowing users without the
`open-banking` Pennant flag to see and interact with the bank connection
option
- Now follows the same pattern as `CreateAccountDialog`, which already
correctly checks `features['open-banking']` before showing the Connected
option
- The grid also collapses to a single column when only the Manual option
is available

## Changes

- **`resources/js/components/onboarding/step-create-account.tsx`** —
reads `features['open-banking']` from shared Inertia props and
conditionally renders the Connected card, the connected inline flow, and
the "Pro feature" hint message
- **`tests/Feature/OpenBanking/OpenBankingFeatureFlagTest.php`** — two
new tests asserting the flag value is correctly shared to the frontend
on the onboarding page (both enabled and disabled states)
2026-03-03 20:49:27 +00:00
Víctor Falcón 7fec8514e4
feat(mobile): add scroll-aware back button on detail pages (#194)
## Summary

- Adds a `MobileBackButton` component (mobile only) on Budget and
Account detail pages
- At rest: replaces the logo icon in the header with a styled back
button
- On scroll (>40px): a floating pill button appears top-right with the
same glass/backdrop-blur style as the bottom nav bar, with a smooth
300ms slide+fade animation
- Extends `AppSidebarLayout` and `AppSidebarHeader` with an optional
`mobileLeading` slot that falls back to `AppLogo` when not provided
2026-03-03 19:21:49 +00:00
Víctor Falcón be2e205965
fix(i18n): force thousands separator for 4-digit amounts in es-ES locale (#193)
## Summary

- Modern ICU/CLDR sets `useGrouping: 'auto'` for `es-ES`, which
suppresses the thousands separator for numbers between 1,000 and 9,999
(e.g. `1560,07 €` instead of `1.560,07 €`)
- Added `useGrouping: 'always'` to `Intl.NumberFormat` options in
`formatCurrency` to force the separator at 1,000+ for all locales
- Added test cases covering `es-ES` + `EUR` for 4-digit amounts
(1,000–9,999)
2026-03-03 14:50:41 +00:00
Víctor Falcón b455ad71dd
fix(transactions): move clear button inline with filters row on all screen sizes (#192)
## Summary

- Moves the **Clear** button into the same flex row as the search input
and Filters button, so it always appears to the right of the Filters
button instead of wrapping to a new row
- Makes the filter row (`w-full`) stretch to 100% width on small
displays
- Fixes the search input sizing to use `min-w-0 flex-1` so it shrinks
properly on narrow screens
2026-03-03 14:06:51 +00:00
Víctor Falcón 956b661448
fix(amount-display): eliminate float round-trip causing missing thousands separator (#191)
## Summary

- Fixes intermittent missing thousands separator on the dashboard
account card balance display, reproducible with Binance accounts whose
crypto-derived balances land on specific cent values.
- Root cause: `AmountDisplay` was doing `amountInCents / 100` → state →
`amount * 100` back into `formatCurrency`. This IEEE 754 round-trip
introduces floating-point noise (e.g. `1234567 / 100 * 100 =
1234566.9999...`), causing `Intl.NumberFormat` to produce unexpected
output.
- Fix: pass `amountInCents` directly to `formatCurrency` (which already
handles the `/100` division internally). The encryption fake-amount
display is also simplified from `useState + useEffect` to a single
`useMemo`.

## Changes

- `resources/js/components/ui/amount-display.tsx` — remove intermediate
`amount` state, pass `amountInCents` directly to `formatCurrency`
- `resources/js/utils/currency.test.ts` — new test file covering correct
formatting, thousands separators, and the specific float round-trip edge
cases
2026-03-03 12:40:30 +00:00
Víctor Falcón 832fc6177e
feat(dashboard): make top spending categories clickable with transaction filter link (#189)
## Summary

- Makes each top spending category in the dashboard card a clickable
link to the transactions page, pre-filtered by that category and the
last 30 days
- Uses the same muted background hover effect (`hover:bg-muted`) as the
account title links on the dashboard for visual consistency
2026-03-03 11:24:50 +00:00
Víctor Falcón 970e85814e
feat(budgets): make budget title clickable with muted hover effect (#186)
## Summary

- Wraps the budget name in `BudgetListCard` with an Inertia `<Link>`
that navigates to the budget detail page
- Applies the same `-my-1 -ml-1.5 ... rounded-md px-1.5 py-1
transition-colors hover:bg-muted` hover effect used on dashboard account
card titles
- The existing "View Details" button in the card footer is unchanged
2026-03-03 10:47:37 +00:00
Víctor Falcón 40a7942b85
fix(i18n): fix unlocalised string in create budget form (#187)
## Summary

- The `__()` call in `create-budget-dialog.tsx` contained an embedded
newline and indentation whitespace in the string literal, causing the
translation key to not match the clean key in `lang/es.json`
- Collapsed the multi-line call into a single line with the clean string
- Removed the stale duplicate key with the malformed `\n` + whitespace
from `lang/es.json`
2026-03-03 10:31:24 +00:00
Víctor Falcón 993c91a6b6
feat(onboarding): inline connected account flow with auto-account creation and step deep-linking (#184)
## 🚪 Why?

### Problem
The onboarding flow had no integration with connected (bank-linked)
accounts. Users who connected a bank via OAuth were redirected to a
separate account-mapping page, breaking the onboarding flow. After
returning, there was no way to resume at the correct step. Additionally,
connected accounts were incorrectly shown in the manual import steps
(import-transactions / import-balances), which don't apply to them.

## 🔑 What?

### Changes
- **Inline bank connect wizard**: Added `ConnectAccountInline` component
that embeds the full 3-step bank OAuth flow within the onboarding
`create-account` step, replacing the redirect to a separate page.
- **Manual / Connected selection UI**: `StepCreateAccount` now presents
a choice between manual and connected account setup before proceeding.
- **Auto-account creation on OAuth callback**:
`AccountMappingController` and `AuthorizationController` auto-create
accounts for non-onboarded users and redirect back to
`/onboarding?step=create-account` instead of showing the mapping UI.
- **`?step=` deep-linking**: The onboarding page reads a `?step=` query
param on mount and starts at that step, so returning from bank OAuth
lands at the right place.
- **Skip import steps for connected accounts**: `handleAccountCreated`
detects `connected: true` and skips `import-transactions` /
`import-balances`, going directly to `category-types` (first account) or
`more-accounts` (subsequent accounts).
- **Fix phantom account in more-accounts**: Connected accounts are no
longer added to `createdAccounts` state (they already appear via
`existingAccounts`), preventing a duplicate nameless entry in the final
account list.
- **Feature flags enabled in non-production**: `open-banking` and
`account-mapping` Pennant flags now resolve to `! app()->isProduction()`
instead of always `false`.
- **Open-banking routes accessible during onboarding**: Removed
`onboarded`/`subscribed` middleware from open-banking routes;
`EnsureOnboardingComplete` middleware explicitly allows `open-banking.*`
routes through.

##  Verification

### Tests
- Updated `tests/Browser/OnboardingFlowTest.php` to reflect new UI
(manual/connected selection).
- Existing feature tests for open-banking controllers pass (pre-existing
failures in Binance/Bitpanda/IndexaCapital/AuthorizationController tests
are from a prior commit unrelated to this work).

### Manual Verification
- Connected account flow tested end-to-end: bank OAuth → auto-account
creation → redirect to `create-account` step → Continue → skip import
steps → `category-types`.
- 7-account BBVA user correctly shows all accounts in `more-accounts`
via `filteredExistingAccounts` with no phantom entry.
- Feature flags confirmed active in local environment via tinker.
2026-03-03 10:49:42 +01:00
Víctor Falcón c391732d0d
chore(mail): update verify email copy to remove personal intro (#185)
## 🚪 Why?

The verify email template included a personal introduction ("Hi! I'm
Victor, the founder of Whisper Money.") which is redundant and overly
informal for a transactional email. Removing it makes the email more
concise and professional.

## 🔑 What?

- Removed the personal introduction line from the verify email Blade
template
- Updated the corresponding Spanish translation key to match the new
string
2026-03-02 14:10:33 +00:00
Víctor Falcón 3e087bdcd7
fix(static-analysis): clear phpstan-baseline by fixing all suppressed errors (#183)
## 🚪 Why?

### Problem

PHPStan was running with a baseline of 56 suppressed errors, meaning
static analysis was not enforcing type safety across a significant
portion of the codebase. These errors were real type mismatches,
redundant null-safety operators, and incorrect PHPDoc annotations that
could mask bugs and make the code harder to reason about.

## 🔑 What?

### Changes

- Add `@property` PHPDoc annotations to `Account`, `BankingConnection`,
`ExchangeRate`, and `Transaction` models so Enum casts and typed columns
are visible to PHPStan
- Add `instanceof User` guards in `ScheduleDripEmailsListener`,
`SyncUserToResendListener`, and `FortifyServiceProvider` to properly
narrow `Authenticatable` to `App\Models\User`
- Remove redundant `?? false` and unnecessary nullsafe `?->value` in
`HandleInertiaRequests`
- Fix `SyncBankingConnectionJob`: use `->name` instead of `?->name` on
an always-loaded `bank` relation
- Remove `is_countable()` guard in `BalanceLookup` (parameter is always
`Collection|array`, both countable)
- Remove `?? []` / `?? default` fallbacks on fully-typed array keys
across `BalanceSyncService`, `BinanceBalanceSyncService`,
`BinanceClient`, `BitpandaBalanceSyncService`, `BitpandaClient`,
`IndexaCapitalClient`, `IndexaCapitalBalanceSyncService`, and
`AuthorizationController`
- Fix `BinanceClient::publicClient()` `retry()` call: use `when:` named
argument and `\Throwable` type hint to match `PendingRequest::retry()`
signature
- Update `IndexaCapitalClient::getPerformance()` `@return` to include
`portfolios` and `net_amounts` keys; simplify sync service to remove
dead null checks
- Replace nullsafe chain with ternary in `BudgetPeriodService`
- Replace `match` statement in `SetupMainUser` with `if/else` to
eliminate always-true comparison
- Clear `phpstan-baseline.neon` entirely (was 56 suppressed errors, now
0)

##  Verification

### Tests

- Existing tests pass: PHPStan level 5 reports 0 errors with empty
baseline
2026-03-02 12:22:30 +00:00
Víctor Falcón 2a1286e98a
chore(frontend): add orphan component detection and remove dead components (#181)
## 🚪 Why?

### Problem

Dead frontend components were accumulating in the codebase with no
automated way to detect them. Unused code increases maintenance burden,
confuses contributors, and inflates bundle analysis noise. Additionally,
several React hooks had incorrect dependency arrays causing stale
closures or unnecessary re-renders, and an unused variable was left in
the new test file.

## 🔑 What?

### Changes

- Add `resources/js/lib/orphan-components.test.ts` — a Vitest test that
scans every file under `resources/js/components/` and fails if any
component is never imported anywhere in the codebase (supports `@/`
alias imports, relative imports, and barrel `index.*` re-exports)
- Delete 14 orphan components identified by the new test:
-
`accounts/import-balances/import-balance-step-{account,mapping,preview,upload}`
  - `app-mobile-nav`
  - `appearance-dropdown`
  - `dashboard/stat-card`
  - `landing/encryption-video-player`
  - `sync-status-button`
  - `transactions/date-header`
  - `ui/{icon,input-group,navigation-menu,placeholder-pattern}`
- Restore `ui/avatar.tsx` which was incorrectly included in the deletion
(it is imported via relative path in `user-info.tsx`)
- Fix ESLint warnings across 5 files:
- `orphan-components.test.ts` — remove unused `componentDir` variable in
`importStrings()`
- `transaction-list.tsx` — remove `categories`, `accounts`, `banks`,
`automationRules` from `useCallback` deps (none referenced in callback
body)
- `amount-input.tsx` — add missing `locale` dep to `useEffect` (used in
`formatCurrency`)
- `use-dashboard-data.ts` — wrap `fetchData` in `useCallback([locale])`
and add to `useEffect` deps to prevent stale closure on locale
- `dashboard.tsx` — wrap `netWorthEvolution` fallback in
`useMemo([props.netWorthEvolution])` to stabilise the reference passed
to the downstream `useMemo`

##  Verification

### Tests

The new test runs as part of `bun run test` (Vitest), already executed
in the `linter` CI job. All 48 tests pass:

```
✓ resources/js/lib/chart-calculations.test.ts (32 tests)
✓ resources/js/lib/file-parser.test.ts (15 tests)
✓ resources/js/lib/orphan-components.test.ts (1 test)
```

`bun run lint` and `bun run build` now exit clean.

Going forward, any new component added to `resources/js/components/`
that is not imported anywhere will fail CI automatically.
2026-03-02 12:43:27 +01:00
Víctor Falcón 152b186c10
feat(privacy): enable privacy mode for all users and extend amount masking (#182)
## 🚪 Why?

### Problem
Privacy mode — which masks financial amounts so users can share their
screen without leaking account balances — was previously gated behind an
admin-only check. Regular users had no way to toggle it from the UI.
Additionally, several places in the app (chart tooltips, budget pages)
were rendering raw amounts via \`formatCurrency\` directly, bypassing
the privacy mode check entirely.

## 🔑 What?

### Changes
- Remove the \`isAdmin()\` guard from the privacy mode toggle in the
user menu, making it available to all users
- Mask digits with \`*\` in \`AmountDisplay\` when privacy mode is
enabled (e.g. \`$1,234.56\` → \`$*,***.**\`)
- Apply the same digit masking to the net worth chart tooltip totals in
\`chart.tsx\` (both single-currency and multi-currency paths)
- Replace raw \`formatCurrency\` calls with \`AmountDisplay\` in
\`budget-list-card.tsx\` (spent, allocated, remaining)
- Add a \`maskIfPrivate\` helper in \`budget-spending-chart.tsx\`
CustomTooltip to mask allocated, spent, last period, and available
amounts when privacy mode is active

##  Verification
<img width="1858" height="1193" alt="image"
src="https://github.com/user-attachments/assets/ec734841-c5cb-44e9-a9f5-ddee7a53a78c"
/>
<img width="1255" height="762" alt="image"
src="https://github.com/user-attachments/assets/b1b93615-41a0-4e8d-8dea-7b27e227181c"
/>
2026-03-02 10:48:45 +00:00
Víctor Falcón 186ae57c2d
refactor(accounts): unify account type icons via accountIconByType helper (#180)
## Why

### Problem
Account type icons were defined in two separate places —
`step-account-types.tsx` (onboarding) and `account-type-icon.tsx`
(dashboard) — using different icon choices for the same account types.
For example, `checking` showed `Building2` on the dashboard but `Wallet`
in onboarding, and `retirement` showed `Umbrella` vs `TrendingUp`. This
created a visually inconsistent experience.

## What

### Changes
- Added `accountIconByType(type: AccountType): LucideIcon` helper to
`resources/js/types/account.ts` as the single source of truth for
account type icons
- Updated `AccountTypeIcon` component to delegate to the new helper
instead of maintaining its own map
- Updated `StepAccountTypes` to derive icons from the helper instead of
storing them inline per entry
- Dashboard account cards now use the same icon set as the onboarding
screen (Wallet, PiggyBank, CreditCard, LineChart, TrendingUp, Building2)

## Verification

### Tests
No behaviour change — purely a refactor consolidating icon mapping into
one place. Existing tests remain unaffected.
2026-03-02 08:47:44 +00:00
Víctor Falcón edcadace1a chore: Welcome header text UI 2026-03-02 08:33:06 +00:00
Víctor Falcón 370e71b254 chore: release v0.1.16 2026-03-01 20:16:11 +00:00
Víctor Falcón 866f90838e
fix(tooling): fix stringWidth error in release-it interactive prompt (#179)
## Why

### Problem
Running `bun release -i patch` (or any interactive `release-it`
invocation) always failed with:

```
ERROR stringWidth is not a function
```

### Root Cause
`@inquirer/core` (used by `inquirer@12`, which `release-it@19` depends
on) bundles its own `wrap-ansi@6.2.0` (CJS). That package calls
`require('string-width')`, but the top-level `string-width` in this
project is v8 — pure ESM. When required via CJS, Node returns a module
namespace object instead of a function, causing the crash when
`release-it` tried to render its interactive confirmation prompt.

## What

- Added `scripts/patch-inquirer-string-width.js`: installs
CJS-compatible versions of `string-width@4`, `strip-ansi@6`,
`ansi-regex@5`, and `is-fullwidth-code-point@3` into
`node_modules/@inquirer/core/node_modules/` so `wrap-ansi@6` resolves
them correctly.
- Added a `postinstall` script to `package.json` so the patch is
re-applied automatically after every `bun install`.

## Verification

### Tests
No automated tests — this is a dev tooling fix. Verified manually by
running `node node_modules/release-it/bin/release-it.js --dry-run` and
confirming the interactive prompt renders without the `stringWidth`
error.

### Manual
1. `bun install` — postinstall script runs and outputs `Patch applied.`
2. `bun release -i patch` — interactive prompt now renders correctly.
2026-03-01 19:53:01 +00:00
Víctor Falcón 6c5961da05 fix: Missing space between page sections and create button 2026-03-01 19:31:28 +00:00
Víctor Falcón efd86bc8d7
feat(nav): add icon+label mobile nav with active pill and full-width buttons (#178)
## Why

### Problem
The mobile bottom navigation bar showed only icons with no labels,
making it hard to identify each section at a glance — especially for new
users.

## What

### Changes
- Each nav item now renders its icon above a short text label
- Active item gets a subtle pill background (`bg-primary/15`) matching
the reference design
- Each button uses `flex-1` so items stretch to fill all available width
evenly
- Added `mobileTitle` to all nav items with short, space-conscious
labels: `Panel`, `Flujo`, `Cuentas`, `Movim.`, `Presup.`
- Removed the `__()` translation dependency from the mobile nav to avoid
a runtime crash that was causing icons to disappear entirely
2026-03-01 19:43:18 +01:00
Víctor Falcón 0388705c12
fix(transactions): fix toolbar overflow on mobile and shorten button label (#175)
## Why

### Problem
On iOS (and narrow mobile screens), the transactions toolbar overflows
the viewport — the Columns button is pushed off-screen because the
actions row does not wrap.

Additionally, the \"Add Transaction\" button label was too long for
small screens, contributing to the overflow.

## What

### Changes
<img width="388" height="374" alt="image"
src="https://github.com/user-attachments/assets/7962f21d-70b7-427b-b01b-a855b687452d"
/>
2026-03-01 17:57:59 +00:00
Víctor Falcón 7260525890
fix(i18n): localise missing strings in budget dialogs to Spanish (#177)
## Summary

- Wrap all hardcoded strings in `create-budget-dialog.tsx` and
`edit-budget-dialog.tsx` with `__()` so they pass through the i18n
system (submit button label, start day labels, helper text for
carry-over/reset, and the selection-required validation message)
- Update `getBudgetPeriodTypeLabel` and `getRolloverTypeLabel` in
`budget.ts` to use `__()` so period type options (Monthly, Weekly,
Bi-weekly, Custom) and rollover types (Carry Over, Reset/Pool) are
translated
- Add all missing Spanish translations to `lang/es.json`: period type
labels, rollover type labels, start-day helpers, carry-over/reset
descriptions, and validation messages
2026-03-01 17:56:02 +00:00
Víctor Falcón 7a8eda9d90
fix(i18n): localize billing settings page into Spanish (#176)
## Summary

- Wrap benefit card titles and descriptions in `__()` — they were
hardcoded strings never passed through the translation function
- Move the `benefits` array inside the component so `__()` is called at
render time (after translations are loaded)
- Add 7 missing Spanish translation keys in `lang/es.json`
- Fix the untranslated `"— $9/month"` entry that had the English string
as its own translation

## Missing keys added

| English | Spanish |
|---|---|
| Unlimited Everything | Sin Límites en Todo |
| No limits on bank accounts, transactions, or categories. | Sin límites
en cuentas bancarias, transacciones o categorías. |
| Your data is never shared with third parties. You are always the
owner. | Tus datos nunca se comparten con terceros. Tú eres siempre el
propietario. |
| Smart Automation | Automatización Inteligente |
| Automation rules to categorize transactions automatically. | Reglas de
automatización para categorizar transacciones automáticamente. |
| Priority Support | Soporte Prioritario |
| Get help when you need it with priority email support. | Obtén ayuda
cuando la necesites con soporte prioritario por correo electrónico. |
| — $9/month | — $9/mes |
2026-03-01 18:43:09 +01:00
Víctor Falcón b433f1f07e chore: Improve create account dialog 2026-03-01 12:50:42 +00:00
Víctor Falcón 9317238c49
feat(i18n): add localization test and fix missing Spanish translations (#174)
## 🚪 Why?

### Problem
Untranslated strings were silently reaching production. Several UI
components rendered raw English literals without going through \`__()\`,
and 15+ strings used via \`__()\` had no entry in \`lang/es.json\`,
causing Spanish-locale users to see untranslated text. There was also no
automated safeguard to catch new regressions.

## 🔑 What?

### Changes
- Add \`tests/Feature/LocalizationTest.php\` with two test groups that
run in CI:
1. **PHP translation files** — verifies every key in \`lang/en/*.php\`
exists in the matching \`lang/es/*.php\` file
2. **Spanish JSON translations** — scans all \`.ts\`/\`.tsx\` files for
static \`__()\` calls and asserts each key exists in \`lang/es.json\`
- Wrap previously unwrapped literals in \`__()\` in `header.tsx`
(Github, Discord, Dashboard, Log in, Register), \`appearance.tsx\`
(Chart color scheme heading and color scheme labels), and
\`rule-builder.tsx\` (field config labels and operator labels at render
site)
- Add 15 missing Spanish translations to \`lang/es.json\`: Discord,
Chart color scheme, Choose the color palette for your charts, Blue,
Colorful, Neutral, Pink, Account Name, Bank Name, contains, equals,
greater than, is empty, is not empty, less than

##  Verification

### Tests
- New test: `tests/Feature/LocalizationTest.php` — 2 tests, 4
assertions, all passing
- Runs automatically in CI via the existing `./vendor/bin/pest` job (no
CI config changes needed)

### Manual Verification
- `php artisan test --compact tests/Feature/LocalizationTest.php` →
`Tests: 2 passed (4 assertions)`
2026-03-01 12:32:39 +00:00
Víctor Falcón 0493b87562
feat(Budgets): add period navigation and unify period selector UI (#171)
## Why

### Problem

Users viewing a budget had no way to look back at historical periods —
the show page always displayed the current period with no navigation.
The cashflow page's period selector also used a different visual style
(loose buttons with gaps) compared to the new grouped button pattern
established for budgets.

## What

### Changes

- **Budget period navigation**: new `BudgetPeriodNavigation` component
renders a `ButtonGroup` with `[<] [date range] [>]` buttons. Clicking
the label returns to the current period; arrows navigate between
existing periods.
- **Backend**: `BudgetController::show` accepts an optional
`?period=<uuid>` query param to serve a specific period. Future periods
are blocked at the query level on both direct access and `nextPeriod`
resolution.
- **Dropdown**: removed the split `ButtonGroup` from the budget header;
"Edit budget" and "Delete budget" now live together in a single `˅`
dropdown.
- **Cashflow period selector**: updated `PeriodNavigation` to use
`ButtonGroup` + `size="icon"` buttons to match the same visual style.
- **Tests**: 4 new feature tests covering period navigation,
future-period blocking, cross-budget access (404), and `nextPeriod`
boundary behaviour.

## Verification
<img width="921" height="683" alt="image"
src="https://github.com/user-attachments/assets/ceb5f70b-a15a-4a36-ae49-5d84054a62f9"
/>
2026-03-01 12:25:01 +00:00