Commit Graph

101 Commits

Author SHA1 Message Date
Víctor Falcón 370d388d99
feat: Enable email verification on sign up (#97)
## Summary
- Enables `MustVerifyEmail` on the `User` model so new users must verify
their email before accessing protected routes
- Unverified users are redirected to `/email/verify` when attempting to
access routes behind the `verified` middleware (subscribe, onboarding,
dashboard)
- A verification email is automatically sent on registration via Fortify

## Screenshot

![Email verification
page](https://raw.githubusercontent.com/whisper-money/whisper-money/mail-verification/storage/videos/email-verification-page.png)

## Video


https://github.com/whisper-money/whisper-money/raw/mail-verification/storage/videos/ac0945c527f014b9cd657f18c911f496.webm

## Test plan
- [x] New test: registration sends a `VerifyEmail` notification
- [x] New test: newly registered users have `email_verified_at` as null
- [x] New test: unverified users are redirected to `verification.notice`
from protected routes (subscribe, onboarding, dashboard)
- [x] New test: verified users are not redirected to verification notice
- [x] Existing email verification and notification tests still pass (31
auth tests, 52 settings tests)
- [x] Browser walkthrough: registration → redirected to `/email/verify`
page with "Resend verification email" button
2026-02-03 10:15:07 +01:00
Víctor Falcón 9bbd91ac12
feat: Add previous period comparison to budget chart (#93)
## Summary
- Load the previous budget period (with transactions) in
`BudgetController::show()` and pass it to the frontend
- Overlay previous period's cumulative spending as a dashed line on the
budget spending chart
- Use day-of-period alignment (Day 1, Day 2...) when comparing so
periods of different lengths align
- Tooltip shows both "Spent" (current) and "Last period" values when
hovering
- Chart now shows the full period timeline (not just up to today)
- When no previous period exists, chart behavior is unchanged

## Screenshots

### Light mode
![Budget chart - Light
mode](https://raw.githubusercontent.com/whisper-money/whisper-money/1cad899e771752e46183f8bb882f6eec4e3dd5de/.github/budget-final-light.png)

### Dark mode
![Budget chart - Dark
mode](https://raw.githubusercontent.com/whisper-money/whisper-money/1cad899e771752e46183f8bb882f6eec4e3dd5de/.github/budget-final-dark.png)

## Test plan
- [x] Tests pass: `php artisan test --compact
tests/Feature/BudgetTest.php` (9 tests)
- [x] Pint formatting passes
- [x] ESLint + Prettier pass
- [ ] Manual: view a budget with a prior period — dashed line appears
- [ ] Manual: view a budget with no prior period — chart unchanged
2026-02-01 14:21:13 +01:00
Víctor Falcón 00b2ca7c55 fix: Budget period not found on last day of period (#91)
## Summary

- Budget periods use `date` columns (`start_date`, `end_date`) but
queries compared them against `now()` which includes a time component
(e.g. `2026-01-31 15:30:00`). MySQL converts the date to midnight for
comparison, so `end_date >= now()` evaluates to `'2026-01-31 00:00:00'
>= '2026-01-31 15:30:00'` → **false** — causing "No active period" on
the last day of every period.
- Replaced `now()` with `today()` (date-only) in all budget period date
comparisons across `Budget::getCurrentPeriod()`,
`BudgetController::index()`, and `GenerateBudgetPeriods` command.

## Test plan

- [x] Added 4 Pest tests covering the edge case (last day at various
times, first day at end of day, index/show endpoints)
- [x] All existing budget tests pass
2026-02-01 11:33:10 +01:00
Víctor Falcón cc666a3111 test: Fix Carbon month overflow in date calculations (#87)
## Summary
- Replace `subMonths()` with `subMonthsNoOverflow()` across controllers,
commands, and tests to prevent date overflow on months with 31 days
- On Jan 31, `subMonths(2)` overflows: Nov 31 doesn't exist → Carbon
rolls forward to Dec 1, producing wrong date ranges and off-by-one month
counts

## Root cause
Carbon's `subMonths()` allows day overflow. When the target month has
fewer days than the source date, it rolls into the next month. For
example:
- `2026-01-31 subMonths(2)` → Nov 31 → **2025-12-01** (expected Nov 30)
- `2026-01-31 subMonths(11)` → Feb 31 → **2025-03-03** (expected Feb 28)

`subMonthsNoOverflow()` clamps to the last valid day of the target month
instead.

## Files changed
- `app/Http/Controllers/Api/CashflowAnalyticsController.php` — cashflow
trend start date calculation
- `app/Console/Commands/ResetDemoAccountCommand.php` — balance history
date generation (was causing duplicate `balance_date` entries)
- `tests/Feature/AccountControllerTest.php` — balance evolution `from`
param
- `tests/Feature/CashflowAnalyticsTest.php` — transaction date
generation in loop
- `tests/Feature/DashboardAnalyticsTest.php` — net worth evolution
`from` param

## Test plan
- [x] `account balance evolution returns data` — expects 3 months, was
getting 2
- [x] `cashflow trend returns monthly data` — expects 3 months, was
getting 2
- [x] `cashflow trend defaults to 12 months` — expects 12 months, was
getting 11
- [x] `net worth evolution returns monthly data points` — expects 3
months, was getting 2
- [x] `demo:reset creates demo user` — was throwing
UniqueConstraintViolation on duplicate balance dates
2026-02-01 11:33:09 +01:00
Víctor Falcón 952a5d4be7
feat: Sync new users to Resend contacts (#85)
## Summary
- Add `SyncUserToResendListener` to automatically sync users to Resend
contacts on registration
- Add `ResendService` for Resend API interactions
- Add `resend:sync` command to bulk sync existing users

## Changes
- **New**: `app/Listeners/SyncUserToResendListener.php` -
Auto-discovered queued listener
- **New**: `app/Services/ResendService.php` - Service for Resend
contacts API
- **New**: `app/Console/Commands/ResendSyncCommand.php` - Bulk sync
command
- **New**: Tests for listener and command

## Usage
New users are automatically synced on registration. For existing users:
```bash
php artisan resend:sync
```

## Test plan
- [x] New user registration triggers contact sync
- [x] `resend:sync` command syncs all existing users
- [x] Graceful handling when API key is not configured
- [x] Duplicate contacts are handled by Resend (no errors)
2026-01-28 21:25:58 +01:00
Víctor Falcón a9b889b145
feat: Release budgets feature to all users (#84)
## Summary
- Enable the budgets Pennant feature flag for all users by changing the
default from `false` to `true`

## Test plan
- [x] Existing `BudgetFeatureFlagTest` passes (9 tests, 39 assertions)
2026-01-28 09:56:52 +01:00
Víctor Falcón a6a2a0d58c
fix: Apply automation rule labels on transaction creation and import (#79)
## Summary
- Fixes automation rules not applying labels when creating transactions
manually or via CSV import
- Eager-loads the `labels` relationship on automation rules in both
`TransactionController` and `HandleInertiaRequests`
- Syncs `label_ids` on the transaction `store` endpoint (was accepted
but never persisted)
- Passes `automationRules` prop through the full component chain:
`index.tsx` → `TransactionActionsMenu` → `ImportTransactionsDrawer`, and
`index.tsx` → `EditTransactionDialog`
- Passes `automationRules` as the missing 5th argument to
`reEvaluateAll()`

Closes #61

## Test plan
- [x] Existing feature tests pass (`php artisan test
--filter=Transaction`, `--filter=AutomationRule`)
- [x] Pint, ESLint, and Prettier all pass
- [x] Manually verify: create a transaction matching an automation rule
with labels → labels are applied
- [x] Manually verify: import CSV with transactions matching rules with
labels → labels are applied
- [x] Manually verify: "Re-evaluate All" applies labels from matched
rules
2026-01-27 11:11:29 +01:00
Víctor Falcón 8a8d5962b5
refactor: Simplify transaction endpoints architecture (#76)
## Summary

Simplifies transaction endpoints by separating sync API (read-only) from
web routes (mutations). This creates clearer architectural boundaries
and fixes inconsistent label behavior.

## Architecture Changes

### Before
- Sync API handled both sync (GET) and mutations (POST/PATCH/DELETE)
- Frontend used sync API for all operations
- Bulk updates merged labels, single updates replaced them

### After  
- **Sync API**: Read-only GET endpoint for IndexedDB sync
- **Web Routes**: All mutations (create, update, delete, bulk
operations)
- **Consistent behavior**: All label updates replace instead of merge

## Endpoint Mapping

| Operation | Old Endpoint | New Endpoint |
|-----------|-------------|--------------|
| Fetch/Sync | `GET /api/sync/transactions` | `GET
/api/sync/transactions`  |
| Create | `POST /api/sync/transactions` | `POST /transactions` |
| Update | `PATCH /api/sync/transactions/{id}` | `PATCH
/transactions/{id}` |
| Delete | `DELETE /api/sync/transactions/{id}` | `DELETE
/transactions/{id}` |
| Bulk Update | `PATCH /transactions/bulk` | `PATCH /transactions/bulk`
 |

## Backend Changes

### TransactionSyncController
-  Simplified to read-only `index()` method
-  Removed `store()`, `update()`, `destroy()` methods
-  Added docblock clarifying purpose

### TransactionController
-  Fixed `store()` to return labels with `id, name, color`
-  Fixed `update()` to return labels with `id, name, color`  
-  Changed `bulkUpdate()` to replace labels instead of merging

### Cleanup
-  Removed `UpdateTransactionSyncRequest` (no longer needed)
-  Updated `routes/api.php` to only have GET for sync

## Frontend Changes

### transaction-sync.ts
-  Updated `create()` → `POST /transactions`
-  Updated `update()` → `PATCH /transactions/{id}`
-  Updated `delete()` → `DELETE /transactions/{id}`
-  Replaced all `fetch()` calls with `axios`
-  Removed manual CSRF token handling

## Test Changes

### TransactionSyncTest
-  Removed create/update/delete tests
-  Kept only read-only sync tests
-  Added test for labels format

### BulkUpdateTransactionsTest  
-  Added test verifying label replacement behavior

## Test Results

All tests passing! 

```
Tests:    42 passed (235 assertions)
Duration: 6.17s

✓ TransactionTest: 29 tests
✓ TransactionSyncTest: 4 tests
✓ BulkUpdateTransactionsTest: 9 tests
```

## Benefits

1. **Clear separation of concerns**: Sync API is read-only, web routes
handle mutations
2. **Consistent label behavior**: All updates replace labels (not merge)
3. **Standardized HTTP client**: Axios everywhere, automatic CSRF
handling
4. **Reduced complexity**: Removed duplicate form request class
5. **Better architecture**: Aligns with intended design

## Breaking Changes

None - All changes are internal to how the frontend calls the backend.
The functionality remains the same from the user's perspective.

## Files Changed

- `app/Http/Controllers/Sync/TransactionSyncController.php`
- `app/Http/Controllers/TransactionController.php`
- `app/Http/Requests/UpdateTransactionSyncRequest.php` (deleted)
- `resources/js/services/transaction-sync.ts`
- `routes/api.php`
- `tests/Feature/Sync/TransactionSyncTest.php`
- `tests/Feature/BulkUpdateTransactionsTest.php`
2026-01-25 16:15:17 +01:00
Víctor Falcón 91dd23edc0 fix: Update transactions 2026-01-25 11:31:17 +01:00
Víctor Falcón e5eca1eacb
feat: Add label support to single transaction update endpoint (#75)
## Summary
- Added label update support to the single transaction `update()`
endpoint
- Previously, labels could only be updated via the bulk update endpoint
- Now both endpoints support label updates with consistent behavior

## Changes
- **UpdateTransactionRequest**: Added `label_ids` validation (array of
UUIDs that must belong to user)
- **TransactionController@update**: Now handles label updates alongside
other attributes
  - Syncs labels when `label_ids` is provided
  - Reloads labels relationship for fresh data in events
  - Fires single `updated` event after all changes
- **AssignTransactionToBudget listener**: Ensures labels are loaded
fresh after queue serialization
- **Tests**: Added 6 comprehensive tests covering all label update
scenarios

## Behavior
- **Don't send `label_ids`** → labels unchanged (partial update)
- **Send `label_ids: []`** → removes all labels
- **Send `label_ids: [uuid1, uuid2]`** → replaces labels with these

## Testing
When labels are updated, the `TransactionUpdated` event fires and the
transaction is automatically assigned to matching budgets (via queued
listener).

All tests pass (29 tests, 134 assertions).

Fixes the issue where transactions wouldn't appear in budget after
adding a matching label via the single transaction update endpoint.
2026-01-24 17:54:54 +01:00
Víctor Falcón 134a292ddb
fix: Fire transaction updated event after a label change (#73) 2026-01-22 15:25:52 +01:00
Víctor Falcón fee7ad36ab
feat: Load transactions history on budget created (#72) 2026-01-22 11:10:15 +01:00
Víctor Falcón 9b6c30775f
Add Budgeting Feature to Track and Manage Spending (#36)
## Overview

We're excited to introduce budgeting capabilities to Whisper Money! This
feature helps you take control of your finances by setting spending
limits and tracking your progress over time.

## Screenshots
<img width="1316" height="793" alt="image"
src="https://github.com/user-attachments/assets/ac394d36-cded-4ea4-9883-120785e260f1"
/>
<img width="1315" height="907" alt="image"
src="https://github.com/user-attachments/assets/7c682474-5aa7-4388-b626-29b56f5ebbef"
/>
<img width="1315" height="992" alt="image"
src="https://github.com/user-attachments/assets/21eace45-23c6-472d-9aa0-0feb6db3fba4"
/>

## What's New

### Create Flexible Budgets
- Set budgets for specific categories or labels
- Choose from monthly, weekly, bi-weekly, or custom periods
- Set your own budget start date for better alignment with your pay
schedule

### Track Your Spending
- Visual spending charts show how much you've spent vs. your budget
- See at a glance which budgets are on track and which need attention
- View all transactions that count toward each budget

### Smart Budget Management
- **Carry Over**: Unused budget amounts automatically roll into the next
period
- **Reset**: Unused amounts return to your available money pool
- Edit or delete budgets anytime as your needs change

### Easy Access
- New Budgets section in the main navigation
- Quick overview cards showing budget status
- Detailed budget pages with spending history and transaction lists

## How It Works

1. Create a budget by selecting a category or label and setting your
spending limit
2. Your transactions are automatically matched to relevant budgets
3. Track your progress with visual charts and spending summaries
4. Adjust your budgets as needed to stay on track with your financial
goals

This feature is now available behind a feature flag and can be enabled
for users who want to start budgeting their expenses.
2026-01-21 15:25:50 +01:00
Víctor Falcón 439ec86722
chore: Simplify IndexedDB sync by moving to Inertia shared props (#63)
This PR simplifies the IndexedDB synchronization mechanism by removing
individual sync controllers and services, and instead using Inertia.js
shared props to provide data globally across the application.

## Benefits

1. **Simplified Architecture**: Removed complex sync logic for
static/semi-static data (accounts, categories, banks, labels, automation
rules)
2. **Better Performance**: Data is now shared via Inertia props,
eliminating unnecessary API calls and IndexedDB operations
3. **Reduced Complexity**: Significantly reduced codebase size (~2000
lines removed)
4. **Better UX**: Data is immediately available on page load without
waiting for sync operations
5. **Maintainability**: Fewer moving parts means easier to maintain and
debug

## Migration Notes

- Transaction syncing still uses IndexedDB for offline support
- All other data (accounts, categories, banks, labels, automation rules)
is now fetched via Inertia shared props
- Components automatically receive updated data on navigation without
manual sync operations
2026-01-19 19:15:26 +01:00
Víctor Falcón 80117c3ede
fix: Check IDOR vulnerabilities (#60) 2026-01-17 09:58:52 +01:00
Víctor Falcón d16282dbad
feat: Auto-open encryption key modal after login (#54)
## Summary

This PR implements automatic opening of the encryption key unlock modal
immediately after user login when they visit the dashboard. The modal
will only appear once per login session and will not show up on
subsequent dashboard visits or if the user manually clears their
encryption key.

## Problem

Previously, users had to manually click the encryption key button in the
sidebar after logging in to unlock their encrypted data. This extra step
could be confusing for new users and added friction to the login flow.

## Solution

The encryption key modal now automatically opens when users visit the
dashboard immediately after logging in, but **only if**:
1. The user just completed a login (session flag is set)
2. Their encryption key is not already in browser storage
3. Encrypted challenge data is available

## Implementation Details

### Backend Changes

#### 1. Custom Login Response Classes
Created two new response classes that override Fortify's default login
responses:

**`app/Http/Responses/LoginResponse.php`**
- Implements `LoginResponse` contract
- Sets `show_encryption_prompt` session flash variable after successful
login
- Handles both regular login and JSON API responses

**`app/Http/Responses/TwoFactorLoginResponse.php`**
- Implements `TwoFactorLoginResponse` contract  
- Sets the same session flag after successful 2FA authentication
- Ensures the modal appears for 2FA users as well

#### 2. Service Provider Registration
**`app/Providers/FortifyServiceProvider.php`**
- Registered custom response classes as singletons in the container
- Fortify now uses our custom responses instead of defaults

#### 3. Dashboard Controller
**`app/Http/Controllers/DashboardController.php`**
- Passes `showEncryptionPrompt` flag from session to frontend
- Uses `session('show_encryption_prompt', false)` which automatically
consumes the flash data

### Frontend Changes

**`resources/js/pages/dashboard.tsx`**
- Added `DashboardProps` interface extending `SharedData` with
`showEncryptionPrompt` boolean
- Updated `useEffect` hook to check three conditions before auto-opening
modal:
1. `props.showEncryptionPrompt` - User just logged in (from session
flash)
  2. `!isKeySet` - Encryption key not in browser storage
  3. `encryptedMessageData` - Encrypted challenge data loaded
- Includes `UnlockMessageDialog` component that conditionally renders

## How It Works

### User Flow

1. **User logs in** → Custom `LoginResponse` or `TwoFactorLoginResponse`
sets `show_encryption_prompt` flash session variable
2. **Redirect to dashboard** → `DashboardController` retrieves and
passes the flag to frontend (consuming it)
3. **Dashboard loads** → React component checks all three conditions
4. **Modal appears** → If conditions are met, `UnlockMessageDialog`
opens automatically
5. **Session flag consumed** → Subsequent page loads won't trigger the
modal

### Why Session Flash?

Laravel's session flash data is perfect for this use case because:
- It's automatically removed after being accessed once
- It survives redirects (crucial for post-login flow)
- It's server-side, so it can't be manipulated by client-side code
- No database changes or additional state management needed

### Edge Cases Handled

 **User clears encryption key manually** → Modal won't auto-open on
dashboard visit (no session flag)
 **User refreshes dashboard** → Modal won't re-appear (flash data
consumed)
 **User logs out and back in** → Modal appears again (new session flag)
 **2FA enabled users** → Modal appears after successful 2FA challenge
 **Key already in browser** → Modal doesn't appear (key check passes)

## Testing

-  All existing dashboard tests pass (2 tests)
-  All authentication tests pass (58 tests)
-  Code formatted with Laravel Pint
-  Code formatted with Prettier
-  ESLint validation passes

## Files Changed

- `app/Http/Responses/LoginResponse.php` (new)
- `app/Http/Responses/TwoFactorLoginResponse.php` (new)
- `app/Providers/FortifyServiceProvider.php` (modified)
- `app/Http/Controllers/DashboardController.php` (modified)
- `resources/js/pages/dashboard.tsx` (modified)

## Breaking Changes

None. This is an additive feature that enhances the existing
authentication flow without breaking any existing functionality.
2026-01-11 20:29:49 +01:00
Víctor Falcón 4a5c60c630 feat: New discord offer 2026-01-10 10:27:32 +01:00
Víctor Falcón 8ac25200dc fix: delay emails to avoid reaching daily resend limit 2026-01-09 11:11:38 +01:00
Víctor Falcón 683b3f32a7
feat: Send custom emails to users (#52)
## Overview

This PR adds a flexible system for sending update emails to all Whisper
Money users. As the solo developer, you can now easily communicate
product updates, announcements, and news to your growing user base.

## Problem Solved

- **No easy way to send announcements**: Previously, there was no simple
way to broadcast important updates to all users
- **Manual process**: Would require writing custom scripts each time
- **No duplicate prevention**: Risk of accidentally sending the same
email multiple times
- **Version control**: Email content wasn't tracked in git

## Solution

A complete email system that lets you:
1. Write update emails as markdown templates (version controlled)
2. Send them with a single command
3. Automatic duplicate prevention (users only receive each update once)
4. Track who received what in the database

## How It Works

### 1. Create Email Template

Create a markdown file in `resources/views/mail/updates/`:

```blade
<x-mail::message>
# What's New in January 2026

Hi {{ $user->name }},

Your update content here...

<x-mail::button :url="'https://discord.gg/9UQWZECDDv'">
Join the Discord Community
</x-mail::button>

Víctor Falcón Ruíz
Founder & Solo Developer, Whisper Money
</x-mail::message>
```

### 2. Send to All Users

```bash
# Simple - one command
php artisan email:update first-update-jan-2026

# With options
php artisan email:update first-update-jan-2026 --subject="Personal Thank You" --exclude-demo
```

### 3. Automatic Tracking

- Users only receive each update once (tracked by identifier)
- Can send different updates to same users
- View history in `user_mail_logs` table

## First Email Included

The PR includes the first update email (`first-update-jan-2026`)
announcing:
- 240+ GitHub stars milestone
- First paying users
- Discord community invitation
- Canny roadmap links
- Personal message from Víctor as the solo developer

## Technical Details

### What's New

**Database:**
- Migration: Add `email_identifier` column to `user_mail_logs`
- Migration: Update unique constraint to `(user_id, email_type,
email_identifier)`

**Backend:**
- `DripEmailType` enum: Added `Update` case
- `UpdateEmail` mailable: Generic mailable accepting any view
- `SendUpdateEmailJob`: Handles sending + duplicate prevention
- `SendUpdateEmailCommand`: Artisan command with progress bar
- Updated all 6 drip email jobs to support new constraint

**Frontend:**
- Email template directory: `resources/views/mail/updates/`
- Comprehensive README with examples

**Tests:**
- Complete test suite: 11 tests covering all scenarios
- Tests duplicate prevention, filtering, queue behavior

### Command Options

```bash
php artisan email:update <view> [identifier] [options]

Arguments:
  view          Template name (e.g., "jan-2026-updates")
  identifier    Tracking ID (defaults to view name)

Options:
  --subject=    Custom email subject
  --exclude-demo   Skip demo account
  --force       Skip confirmation prompt
```

## Benefits

1. **Simple**: One command to send any update email
2. **Flexible**: Just create a new markdown view, no code changes needed
3. **Safe**: Confirmation prompt and duplicate prevention
4. **Fast**: Queued delivery, non-blocking
5. **Tracked**: Full audit trail in UserMailLog
6. **Consistent**: Follows existing drip email patterns
7. **Reusable**: Same command for all future update emails
8. **Version Controlled**: Email content is in git, reviewable

## Migration Path

To use in production:

```bash
# 1. Run migrations
php artisan migrate

# 2. Create your email template
vim resources/views/mail/updates/your-update.blade.php

# 3. Commit and deploy
git add resources/views/mail/updates/your-update.blade.php
git commit -m "Add update email"
git push

# 4. Send on production
php artisan email:update your-update
```

## Example Use Cases

- Product announcements
- New feature launches  
- Community updates
- Milestone celebrations
- Important notifications
- Partnership announcements

## Files Changed

### Created
- `database/migrations/*_add_email_identifier_to_user_mail_logs.php`
- `database/migrations/*_update_user_mail_logs_unique_constraint.php`
- `app/Mail/UpdateEmail.php`
- `app/Jobs/SendUpdateEmailJob.php`
- `app/Console/Commands/SendUpdateEmailCommand.php`
- `resources/views/mail/updates/first-update-jan-2026.blade.php`
- `resources/views/mail/updates/README.md`
- `tests/Feature/Console/SendUpdateEmailCommandTest.php`

### Modified
- `app/Enums/DripEmailType.php` (added Update case)
- `app/Models/UserMailLog.php` (added email_identifier field)
- All 6 drip email jobs (added email_identifier support)

## Testing

All tests passing (11 tests, 27 assertions):
-  Command dispatches jobs for all users
-  Command excludes demo account when flag is set
-  Command fails when view does not exist
-  Command uses custom subject when provided
-  Job is dispatched to emails queue
-  Command handles empty user database gracefully
-  Job skips users who already received the update
-  Job creates mail log entry after sending
-  Job sends email with correct view and user data
-  Command requires confirmation by default
-  Command skips confirmation with force flag
2026-01-09 09:33:19 +01:00
Víctor Falcón fc5cb67fe3 feature: enable cashflow for everyone 2026-01-07 11:06:54 +01:00
Víctor Falcón 9bd1fcea37
Demo Account Experience (#51)
## Summary

<img width="1220" height="1001" alt="whispermoney test_"
src="https://github.com/user-attachments/assets/c35751d5-385b-449c-81d6-14b5b6577ff2"
/>

Introduces a fully-functional demo account that lets prospective users
explore Whisper Money without creating an account. Users can click
"Check Demo" on the welcome page to instantly access a pre-populated
account with realistic financial data spanning 12 months.

## What's New

**Try Before You Sign Up**
- New "Check Demo" button on the welcome page for instant access
- Pre-configured demo account with real-world financial scenarios
- 12 months of sample transactions across multiple account types
(checking, savings, credit cards, investments)
- Pre-built automation rules, labels, and categories to showcase the
full app experience

**Demo Account Limitations**
- Demo accounts are read-only for sensitive operations (can't change
password, email, or payment settings)
- Clear messaging throughout the UI when demo restrictions apply
- Settings pages show helpful notices about demo limitations
- Automatic daily reset to maintain fresh demo experience

**Developer Experience**
- `php artisan demo:reset` command for manual resets
- Configurable via environment variables (DEMO_EMAIL, DEMO_PASSWORD,
DEMO_ENCRYPTION_KEY)
- Comprehensive test coverage for demo restrictions and data generation

## User Impact

This feature removes the friction of signing up before understanding the
product's value. Users can:
- Explore all features with realistic data
- Test automation rules and see them in action
- View charts and insights based on a year of financial activity
- Experience the full privacy-first encryption workflow
- Understand the product before committing to create an account

Perfect for demos, screenshots, documentation, and helping users make
informed decisions about whether Whisper Money fits their needs.
2026-01-07 10:58:14 +01:00
Víctor Falcón 1cec4a6421
Add Cashflow Analytics Feature (#49)
Summary
- Add new Cashflow page with income/expense analytics, Sankey chart
visualization, and trend analysis
- Add Cashflow summary widget to the dashboard
- Implement feature flag system using Laravel Pennant to control feature
visibility per user
- Add artisan commands to enable/disable features: `php artisan
feature:enable/disable [feature] [email|all]`

## New Features
- **Cashflow Page**: Period-based income/expense breakdown with category
analysis
- **Sankey Chart**: Visual flow diagram showing money movement between
categories
- **Trend Chart**: 12-month historical cashflow visualization
- **Dashboard Widget**: Quick cashflow summary card
- **Feature Flags**: Cashflow feature hidden behind feature flag,
controllable per-user

## How to enable it
This feature it's under a feature flag until we are sure it's working
fine. If you want to test it, [send us a message on the general channel
in our discord](https://discord.gg/9UQWZECDDv). If you're self hosting
just run `php artisan feature:enable CashflowFeature {your_email}`.

## New Cashflow Page
<img width="1278" height="1185" alt="image"
src="https://github.com/user-attachments/assets/83469f24-9f96-4e5f-bef9-86c076d31243"
/>
<img width="1274" height="1035" alt="image"
src="https://github.com/user-attachments/assets/8949ff9d-c370-4fc4-81c6-268307c587a5"
/>

## New Dashboard Widget
<img width="1180" height="289" alt="image"
src="https://github.com/user-attachments/assets/ad984a66-9ffd-4079-85ce-0b2d28278d76"
/>
<img width="1179" height="286" alt="image"
src="https://github.com/user-attachments/assets/bff60286-8ea2-4e9c-8fa6-22755566648f"
/>
2026-01-05 13:06:50 +01:00
Víctor Falcón beecff404a
Add subscription cancellation email with manual trigger command (#47)
## Summary

- Add a subscription cancellation email that can be manually triggered
via artisan command
- Include CONTINUE50 coupon code (50% off current and future payments
for monthly/yearly subscriptions)
- Add option to disable all drip emails via `DRIP_EMAILS_ENABLED` env
var

## Usage

```bash
# Send to single user
php artisan email:subscription-cancelled user@example.com

# Send to multiple users
php artisan email:subscription-cancelled user1@example.com,user2@example.com

# Disable all drip emails
DRIP_EMAILS_ENABLED=false
```
2025-12-30 20:32:44 +01:00
Víctor Falcón 3bcb4875f0 chose: increase mail limit per second 2025-12-30 08:38:28 +01:00
Víctor Falcón a961b24386 recover idempotent transaction sync 2025-12-30 07:50:04 +01:00
Víctor Falcón 700f9325a3 Add currency selection to user profile settings (#41) 2025-12-30 07:22:19 +01:00
Víctor Falcón db81c9b888 feat: add version tracking with git tags and changelog
- Add version field to package.json (0.1.0)
- Install release-it for automated releases with conventional changelog
- Create .release-it.json configuration
- Create initial CHANGELOG.md
- Share version via Inertia (reads from package.json)
- Display version in user dropdown menu
2025-12-30 07:22:19 +01:00
Víctor Falcón c5df59c285 feat: add multiple chart view modes for net worth evolution (#37)
## Summary

- Add four view modes for net worth and account balance charts:
- **Stacked Accounts**: existing stacked bar chart showing balances by
account
  - **Line**: net worth line chart with linear/log scale toggle
  - **Change**: bar chart showing MoM%, YoY%, Rolling 12M changes
- **Waterfall**: bridge chart showing Start -> Change -> End transition

- Add Vitest for frontend unit testing with 52 tests covering all
calculation functions
- Proper sign handling for liabilities (credit cards and loans subtract
from net worth)
- Log scale disabled with warning when net worth includes zero or
negative values
- Shared components and logic between dashboard and account detail
charts

## New Files

- `vitest.config.ts` - Vitest configuration
- `resources/js/lib/chart-calculations.ts` - Pure computation functions
- `resources/js/lib/chart-calculations.test.ts` - 52 unit tests
- `resources/js/hooks/use-chart-views.ts` - Shared chart view state hook
- `resources/js/components/charts/` - Reusable chart components

## Test plan

- [x] Dashboard net worth chart shows view toggle and all four views
work
- [ ] Account detail page chart shows view toggle and three views work
(no stacked)
- [x] Line chart scale toggle works (log disabled with warning when
applicable)
- [x] Change chart series toggle (MoM, YoY, 12M EUR, 12M %) works
- [x] Waterfall chart month selector works
- [x] All 52 unit tests pass (`bun run test`)
- [x] Build succeeds (`bun run build`)
- [x] Dark mode styling correct
2025-12-30 07:22:19 +01:00
Víctor Falcón 3cbe0a7879 fix(sync): make transaction creation idempotent (#38)
## Summary
- When a transaction ID is provided during sync, check if it already
exists before creating
- If it exists, return the existing transaction with 200 status instead
of failing with duplicate key error
- Prevents duplicate transactions when sync retries occur due to network
issues

## Test plan
- [x] Attempt to create a transaction with a specific ID
- [x] Attempt to create another transaction with the same ID
- [x] Verify the second request returns the existing transaction instead
of an error
2025-12-30 07:22:19 +01:00
Víctor Falcón 2662c1f873 Add retry mechanism to email mailables and update supervisor config 2025-12-30 07:22:18 +01:00
Víctor Falcón 3d0d6c8bef feat(queue): Implement queueable email jobs with rate limiting 2025-12-30 07:22:18 +01:00
Víctor Falcón 1e9566a289 fix: use markdown to send user lead invitation mail 2025-12-30 07:22:18 +01:00
Víctor Falcón b88df76ce9 command: send email to leads 2025-12-30 07:22:18 +01:00
Víctor Falcón 6b0ce387d7 Reapply "swap horizon -> queue:work on mysql"
This reverts commit 43d615869c.
2025-12-30 07:22:18 +01:00
Víctor Falcón f8c4fb0def add best value to yearly price 2025-12-30 07:22:18 +01:00
Víctor Falcón 520578c48a horizon: notify admin is env var is set 2025-12-30 07:22:18 +01:00
Víctor Falcón 03880ca492 Revert "swap horizon -> queue:work on mysql"
This reverts commit 214ad684a8.
2025-12-30 07:22:18 +01:00
Víctor Falcón c79222870d swap horizon -> queue:work on mysql 2025-12-30 07:22:18 +01:00
Víctor Falcón 53c124a01f chore(commands): unify command signatures 2025-12-30 07:22:18 +01:00
Víctor Falcón 735345f5b9 command: delete user 2025-12-30 07:22:18 +01:00
Víctor Falcón 89624db5e7 Test mail drip command 2025-12-30 07:22:18 +01:00
Víctor Falcón da7d32771e Event listeners are autowired 2025-12-30 07:22:18 +01:00
Víctor Falcón 095f9ea94e Horizon: remove basic auth, add allowed emails list to env 2025-12-30 07:22:18 +01:00
Víctor Falcón 46c5b13739 feat: Implement drip email campaign system (#35)
## Summary

- Implement event-driven drip email campaign system for new user signups
- Add 5 targeted emails based on user journey (welcome, onboarding
reminder, promo code, import help, feedback)
- Configure Laravel Horizon with Redis queue for reliable job processing
- Track sent emails in `user_mail_logs` table to prevent duplicates

## Email Schedule

| Email | Timing | Condition |
|-------|--------|-----------|
| Welcome | 30 min after signup | All users |
| Onboarding Reminder | 1 day after signup | Not onboarded |
| Promo Code (FOUNDER) | 1 day after signup | Onboarded + has
transactions + not subscribed |
| Import Help | 1 day after signup | Onboarded + no transactions + not
subscribed |
| Feedback | 5 days after signup | Not subscribed |

## Architecture

```
User Registers → ScheduleDripEmailsListener
    ├─> SendWelcomeEmailJob (30 min delay)
    ├─> SendOnboardingReminderEmailJob (1 day delay)
    ├─> SendPromoCodeEmailJob (1 day delay)
    ├─> SendImportHelpEmailJob (1 day delay)
    └─> SendFeedbackEmailJob (5 days delay)
```

Each job checks conditions at execution time and either sends the email
+ logs it, or silently completes.

## Test plan

- [x] All 23 drip email tests pass
- [x] Migration creates `user_mail_logs` table
- [x] Jobs dispatch with correct delays
- [x] Emails only sent when conditions are met
- [x] Duplicate emails prevented via UserMailLog check
- [ ] Verify Horizon processes jobs in production
2025-12-30 07:22:18 +01:00
Víctor Falcón f8bea791e8
Bulk actions/select all (#33) 2025-12-15 17:59:58 +01:00
Víctor Falcón c9e08766c1 Update automated rule timestamp when adding/removing a category or label 2025-12-15 14:30:35 +01:00
Víctor Falcón bf0c9ae989
fix: Automated rules with labels (#32) 2025-12-15 13:42:12 +01:00
Víctor Falcón 4b5d65ba03
feat: add transaction labels feature (#24)
## Summary

Add a labeling system for transactions that differs from categories in
that labels are ephemeral and created on-the-fly when assigned (no
pre-creation required). A transaction can have multiple labels
(many-to-many relationship), and labels can be used for filtering in the
transaction table and as actions in automation rules.

<img width="1372" height="484" alt="image"
src="https://github.com/user-attachments/assets/fd342b11-dafb-44ed-b818-578e1ea856e6"
/>
<img width="1373" height="613" alt="image"
src="https://github.com/user-attachments/assets/aa5cfb8b-50b7-4101-a872-54904924f234"
/>
<img width="1035" height="594" alt="image"
src="https://github.com/user-attachments/assets/ffa946b2-b01f-496b-8cb8-ab55ab5b79ed"
/>


## Changes

### Backend (Laravel)

- Add `labels` table with user_id, name, color, and soft deletes
- Add `label_transaction` pivot table for transaction-label many-to-many
- Add `automation_rule_labels` pivot table for automation rule-label
many-to-many
- Create Label model with relationships to User, Transaction, and
AutomationRule
- Add LabelController for CRUD operations (returns JSON for on-the-fly
creation)
- Add LabelSyncController for frontend sync
- Add LabelPolicy for authorization
- Update TransactionController to handle bulk label updates
- Update AutomationRuleController to handle label actions
- Add validation for label_ids in transactions and automation rules

### Frontend (React/TypeScript)

- Add Label type and color utilities
- Add labels table to IndexedDB (Dexie version 6)
- Create label-sync service with findOrCreate functionality
- Create LabelCombobox component with multi-select and create-on-type
- Add labels column to transactions table
- Add labels filter to transaction filters
- Add bulk label assignment in bulk actions bar
- Add labels action in create automation rule dialog
- Update rule engine to include labels in evaluation results

## Testing

- 11 feature tests for label CRUD
- 3 tests for label sync
- All 241 feature tests pass

## Screenshots

N/A - UI changes can be reviewed in browser
2025-12-13 13:02:19 +01:00
Víctor Falcón c433088806
User Onboarding Flow (#23)
## Summary

Implements a complete user onboarding flow that guides new users through
setting up their account after registration.

## Features

- **Step-by-step wizard UI** with progress indicator and smooth
animations
- **E2E Encryption setup** with password strength indicator and
explanation of how encryption works
- **Account creation** supporting all account types (checking, savings,
credit card, investment, pension)
- **Category customization** with explanation of category types
(expenses, income, transfer)
- **Smart rules explanation** covering why there is no AI
auto-categorization (privacy & E2EE)
- **Transaction/balance import** based on account type
- **Sync integration** to ensure data consistency with backend

## Flow Diagram

```mermaid
flowchart TD
    A[User Registration] --> B[Welcome]

    B --> E[Encryption Explained]
    
    E --> C{Has encryption key?}

    C -->|No|F[Encryption Setup]
    C -->|Yes|G{Has Existing Accounts?}
    G -->|Yes| H[Show Existing Accounts]
    G -->|No| I[Create First Account]
    
    H --> J[Continue]
    I --> K{Account Type?}
    
    K -->|Checking/Savings/Credit Card| L[Import Transactions]
    K -->|Investment/Pension| M[Import Balances]
    
    J --> N[Category Types Explanation]
    L --> N
    M --> N
    
    N --> O[Customize Categories]
    O --> P[Smart Rules Explanation]
    P --> Q[More Accounts?]
    
    Q -->|Add More| I
    Q -->|Finish| R[Complete]
    
    R --> S[Redirect to Dashboard]
    S --> T{Subscribed?}
    T -->|No| U[Subscribe Page]
    T -->|Yes| V[Dashboard]
```

## Technical Changes

### Backend
- Added `onboarded_at` field to users table with migration
- Created `EnsureOnboardingComplete` middleware for redirect logic
- Created `OnboardingController` with index and complete actions
- Custom `RegisterResponse` to redirect new users to onboarding
- Updated `AccountController::store` to return JSON for fetch requests

### Frontend
- `OnboardingLayout` - fullscreen layout with step progress
- `useOnboardingState` hook - manages step navigation and state
- 12 step components for each onboarding screen
- Backend sync after account creation and imports

### Tests
- Feature tests for onboarding middleware
- Updated existing tests to use `onboarded()` factory state
2025-12-12 13:06:08 +01:00
Víctor Falcón e51bad0cae
Quick Categorization Wizard (#21)
## Summary

Add a new quick categorization page that allows users to categorize
transactions without categories one by one, in a game-like interface.


https://github.com/user-attachments/assets/14fd7386-e87e-4a75-ae94-e487e3a48112

## Features

- **Quick Categorization Page**: A clean, focused interface for
categorizing transactions
  - White screen with centered transaction card
  - Categories sorted by usage frequency (stored in localStorage)
- Smooth animations when categorizing (card exits, success feedback,
next card enters)
  - Progress indicator showing remaining transactions

- **Keyboard Shortcuts**:
  - `Ctrl+R`: Open automation rules dialog
  - `Ctrl+N`: Skip current transaction
  - `Escape`: Go back to transactions page
  - Arrow keys + Enter: Navigate and select categories

- **Automation Rules Integration**:
  - Rules button in header to manage automation rules
- When closing the rules dialog, all remaining transactions are checked
against rules
  - Matching transactions are automatically categorized

- **UI/UX**:
  - Command component for category search and selection
  - Encrypted text component for account names
  - Responsive amount display with positive/negative styling
  - Skip button moved to header for cleaner card design

## Access

New "Categorize" button added to the transactions page toolbar (left of
"Add Transaction" button) showing the count of uncategorized
transactions.
2025-12-09 14:38:39 +01:00