Commit Graph

6 Commits

Author SHA1 Message Date
Víctor Falcón a31985822c fix(import): match Unicode whitespace in server-side duplicate detection
PHP's default \s is ASCII-only, so the duplicate check missed rows differing
only by a non-breaking space (U+00A0) or other Unicode whitespace — common in
bank statement descriptions — whereas the old client-side check (JS \s) caught
them. Normalize the full Unicode whitespace set before keying, then trim.

Also harden the endpoint's tests: Unicode-whitespace matching, the date-range
boundary (an out-of-range existing row must not match), and request validation.
2026-07-03 16:05:36 +02:00
Víctor Falcón 057e78c3eb refactor(accounts): centralize the "has transaction ledger" rule and stabilize pagination
Follow-up to the backend migration, addressing review feedback.

- Replace the inline non-transactional type list in AccountController@show with
  a purpose-named AccountType::hasTransactionLedger(). This gives the backend a
  single named concept that mirrors the frontend isTransactionalAccount, and its
  docblock spells out why it differs from the existing isNonTransactional()
  (which excludes loan) so a future change won't reach for the wrong method.
- Add an `id` tiebreak to the account_id-filtered index ordering so
  simplePaginate produces a deterministic order for same-day transactions,
  matching AccountController@show.
- Cover the per_page lower clamp and cross-user account scoping with tests.
2026-07-03 16:02:31 +02:00
Víctor Falcón 0976b635e7 feat(transactions): serve import dedup and account ledger from the backend
Migrate the last two flows off the deprecated client-side IndexedDB/Dexie
"offline-first" model so they read from the backend instead of the local DB.

- Import duplicate detection now hits POST /api/transactions/check-duplicates,
  which matches on day + amount (integer cents) + normalized description
  server-side, replacing the sync()-then-read-Dexie approach that silently
  missed duplicates beyond the local cache window.
- The account detail transaction list is now served as an Inertia prop from
  AccountController@show (eager-loading category + labels) instead of fetching
  every transaction from /api/sync/transactions and filtering client-side,
  which could show an incomplete list for accounts with long history.
- The import preview's "existing transactions" panel reads from
  GET /api/transactions?account_id=…&per_page=10 (index gains an account_id
  filter and a clamped per_page) instead of Dexie.
- TransactionList renders only from the provided prop; the /api/sync fetch,
  the Dexie-backed search, and the refreshKey refetch are removed (search is
  now in-memory; create triggers a scoped Inertia reload).

Tests cover the new endpoint (matching, normalization, account scoping, IDOR),
the account_id filter, and the account-show transactions prop.
2026-07-03 16:00:58 +02:00
Víctor Falcón 7fb190683c
refactor(encryption): strip client-side transaction encryption (#514)
## Why

Transactions and account names used to be stored encrypted client-side.
They now live **unencrypted in the backend**, so the browser no longer
needs to encrypt, decrypt, or mask them. A handful of legacy accounts
may still have encrypted transactions, but the auto-run
decrypt-migration handles those — so this PR strips the now-dead
encryption machinery while keeping the migration path fully intact.

Net: **−1213 / +176** lines across 25 files.

## Removed (dead machinery)

- `setup-encryption` page, `POST /api/encryption/setup`,
`SetupEncryptionRequest`, and `encrypt()` / `generateSalt()` — new data
is never encrypted again
- `EncryptedText` and the decrypt-on-render description component →
replaced by a plaintext `TransactionDescription` that keeps the
privacy-mode fake labels
- All `isKeySet`-gated client decryption: transaction list (load /
search / sort), categorize flow, import dedup
- `isKeySet` masking / locking on amount display, app logo, import
button, add-transaction button, account page, and filters
- The unused `?encrypted=false` API filter and `isKeyPersistent()`

## Kept (legacy decrypt-migration)

- The two migration hooks, the unlock dialog, and the dashboard/login
unlock prompt
- `decrypt` / `importKey` + key derivation, key-storage,
`EncryptedMessage`, salt, `GET /api/encryption/message`,
`?encrypted=true`, and the middleware salt-cleanup
- `rule-engine` / edit / import-drawer account-name decryption —
plaintext-first (`if (!account.encrypted) return name`) legacy fallback
on the kept primitives; intentionally untouched

## Testing

- Pint  · ESLint  · Prettier 
- 184 vitest tests 
- 1425 PHP Feature/Unit tests  (Browser suite not run locally — needs
`bun run build`; no Browser test references the encryption UI)

## Follow-up (out of scope)

`DecryptedTransaction.decryptedDescription` / `decryptedNotes` are now
just plaintext aliases. Collapsing that type touches 13+ files (rule
engine, categorizer, edit dialog) and was left as a separate cleanup.
2026-06-20 16:13:26 +00:00
Víctor Falcón e62f1d6aac
refactor(api): standardize serialization via model $hidden (#492)
## What

Standardizes how models are serialized across web, API, and Sync
responses by relying on Eloquent `$hidden` + accessors on the models
themselves, instead of ad-hoc `select` scopes and per-controller field
picking.

Touches 9 models (`Account`, `Bank`, `Budget`, `Category`, `Label`,
`LoanDetail`, `RealEstateDetail`, `Transaction`, plus pivot hiding) and
the controllers/middleware that consumed the old ad-hoc shapes.

## Why

- Single source of truth for response shape lives on the model, aligned
with Wayfinder model typegen.
- Removes duplicated field-selection logic scattered across controllers.
- Continues the duplication-removal PR series (#475–#483).

## How

- Hide internal columns and pivots via `$hidden`; expose computed fields
via accessors.
- Controllers return full models / load full relations rather than
hand-picked columns.
- `HandleInertiaRequests` slimmed down to match.

## Notes for reviewers

- Per-commit breakdown: each model standardized in its own commit for
easy review.
- Tests added/updated for each model to assert the serialized shape
(hidden columns absent, relations present).
- Full suite: 1382 passed / 1 skipped / 0 failed. `pint` clean.
2026-06-05 13:57:34 +02:00
Víctor Falcón 6abec95d0e
feat: Decrypt encrypted transactions on key unlock (#123)
## Summary

- Add `GET /api/transactions?encrypted=true` endpoint for paginated
listing of encrypted transactions
- Add `PATCH /api/transactions/bulk` endpoint for batch-updating
decrypted transaction data (max 50 per request, bypasses model events)
- Add `useDecryptTransactions` hook that mirrors the existing account
name decryption flow: fetches encrypted transactions page-by-page,
decrypts `description`/`notes` client-side via Web Crypto API, sends
plaintext back and clears IVs
- Wire the hook into `AppSidebarLayout` so decryption runs automatically
when the user unlocks their encryption key
- Once all transactions (and accounts) are decrypted, the encryption key
button in the header disappears automatically

## Test plan

- [x] 11 Pest tests covering encrypted/plaintext filtering, pagination,
user scoping, bulk update, authorization, validation, nullable notes,
guest access, and no-model-events behavior
- [x] Manual: create encrypted transactions, unlock encryption key,
verify transactions get decrypted and the key button disappears
2026-02-16 10:37:43 +01:00