Commit Graph
19 Commits
Author SHA1 Message Date
47c46843e1 fix(transfers): prevent duplicate creation on double-submit (#3342)
* fix(transfers): prevent duplicate creation on double-submit

TransfersController#create -> Transfer::Creator had no protection
against a repeated form submission - a double-click, a browser retry,
or two near-simultaneous requests could each create a separate,
identical transfer (and its 2-4 underlying Entry/Transaction rows).

Adds a per-form idempotency key, the same approach already used for
TransactionsController#create: a UUID hidden field generated fresh on
page load, tagging the outflow/inflow (and fee, with a distinguishing
suffix since a fee leg shares its account with its primary leg) entries
via the existing entries(account_id, source, external_id) partial
unique index. A pre-check handles the sequential double-submit case;
rescue ActiveRecord::RecordNotUnique is the authoritative backstop for
genuine concurrent requests - the whole Transfer.transaction block
rolls back cleanly on conflict, so there's no risk of a half-created
transfer.

A same-day duplicate transfer can be legitimate (unlike a duplicate
valuation, see #3339/PR #3340), so this uses the same per-submission
token approach as #3334/PR #3338 rather than a natural-key DB
constraint.

Fixes #3341.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(transfers): store the idempotency key in its own column, isolate the retry with a savepoint

Same two review findings as PR #3338 (transactions) and #3340
(valuations), applied here since this branch shares the same
mechanism:

- Reusing external_id/source for the web-form idempotency token made
  every leg of a manually-created transfer satisfy Entry#linked?,
  incorrectly making it look provider-synced. Uses the same dedicated
  entries.idempotency_key column added in
  db/migrate/20260902180400_add_idempotency_key_to_entries.rb (cherry-picked
  identically from PR #3338 - this branch depends on that migration;
  please merge #3338 first, or merge this after it lands so the
  duplicate migration file is a no-op).

- Transfer::Creator now wraps the actual save in
  Transfer.transaction(requires_new: true) so a RecordNotUnique only
  rolls back to a savepoint rather than aborting any transaction the
  caller might already be in, keeping the rescue's retry lookup usable
  (mirrors the fix already applied to Account::ReconciliationManager
  in PR #3340).

Added a regression test asserting neither leg of a transfer created
via this path is linked? or has external_id/source set.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(transfers): distinct idempotency key per leg, rebuild invalid index on retry

Two more review findings:

- CodeRabbit: the form doesn't prevent selecting the same account as
  both source and destination. The outflow and inflow legs shared the
  bare idempotency key, so on that same-account path they'd collide
  with each other under the same account-scoped unique index (as
  would both fee legs, which shared a single "-fee" suffix). Every
  leg now gets a distinct, role-specific suffix (outflow stays bare -
  that's what find_existing_transfer looks up by - inflow/source_fee/
  destination_fee each get their own).

- Codex (same finding already fixed once for entries.idempotency_key's
  sibling migration, recurring here since this branch carries an
  identical copy): index_exists? alone doesn't distinguish a valid
  index from an INVALID one left behind by an interrupted CREATE INDEX
  CONCURRENTLY, so a retry after a failed build would short-circuit
  and record the migration as applied while the constraint was still
  missing. Now checks pg_index.indisvalid directly before deciding to
  skip.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(transfers): clear idempotency key on destroy so a retry doesn't 500

Codex flagged that Transfer#destroy! (used by reject!) preserves the
outflow/inflow entries but not the Transfer join row - a retried
create request with the same idempotency_key would find no Transfer
via find_existing_transfer, attempt another insert, hit the stale
entry's unique key, and re-raise RecordNotUnique instead of finding
a match. Clear the key on the surviving entries when a transfer is
destroyed.

Also adds a regression test for the CodeRabbit-flagged per-leg key
collision concern (already fixed by role-specific suffixes in the
prior commit) to lock in that fee legs never share a key with their
primary leg.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(transfers): verify idempotency key matches the request, fix stale doc comment

jjmata review on #3342:
- find_existing_transfer matched on idempotency_key + source_account only,
  so a stale key from a cached form could silently return a different,
  older transfer instead of creating the one actually requested. Now
  verifies destination account, date, and amount before treating a key
  match as the same request; a genuine mismatch surfaces as a new
  StaleIdempotencyKeyError (422 + message) instead of a false success or
  a raw 500.
- Removed a comment claiming parity with a
  TransactionsController#new_transaction_idempotency_key method that
  doesn't exist in the codebase.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(transfers): preserve from_account_id on error, match fees/exchange rate in idempotency check

coderabbitai review on #3342:
- All three create rescue blocks (exchange rate unavailable, invalid date,
  stale idempotency key) failed to set @from_account_id, so the re-rendered
  form lost the user's selected source account.
- matches_request? only compared accounts/date/outflow amount, so a retry
  with the same key but a different exchange_rate or fee would be reported
  as success while silently keeping the old inflow amount and fee entries.
  Now recomputes the request's effective inflow amount and compares derived
  fee totals too; a mismatch raises StaleIdempotencyKeyError like other
  stale-key mismatches instead of silently returning the old transfer.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Gerald <248542187+gfr-free@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
2026-09-04 06:25:34 +02:00
DataEnginr e75f2a0c78 Fix fee display consistency, derive fees from entries, clean schema churn
- Show principal-only transfer amounts on both sides with separate fee and total lines (fixes inconsistent gross/net convention)
- Derive displayed fee amounts from fee_transactions entries (single source of truth) instead of stored columns
- Remove stored source_fee_amount/destination_fee_amount columns from transfers table
- Add foreign key for transactions.transfer_id -> transfers.id (replaces invalid CHECK subquery)
- Move destination fee line inside destination side div for consistent layout
- Remove orphaned view_fee_transaction locale keys from 7 locale files
- Rebuild schema.rb from origin/main to eliminate unrelated column reordering churn
2026-06-28 19:47:53 +00:00
DataEnginr 1b21c4dd7b Store fees as separate expense transactions with principal-only entries
Entries now hold principal only (no fee baked into amounts). Fee transactions created as standard kind with Fees category. Transfer#amount_abs returns principal from new amount column. Update handler recomputes entries and fee transactions on edit. Remove dead source_principal/destination_principal helpers. Schema regenerated cleanly with only transfer fee columns.
2026-06-28 18:39:28 +00:00
Shibu M f6b0d42a40 Negative-fee assertions fixes 2026-06-05 11:06:13 +00:00
Shibu M 262d8d4ac2 Negative-fee assertions fixes 2026-06-05 11:06:13 +00:00
Shibu M d917143fd6 resloved issues raised by ai chatbot 2026-06-05 11:06:13 +00:00
Shibu M 7e6d1123dc resloved issues raised by ai chatbot 2026-06-05 11:06:13 +00:00
Shibu M e73006af54 Add transfer fee support for bank charges on account-to-account transfers 2026-06-05 11:06:13 +00:00
Josh Waldrep 52588784d0 Add investment activity detection, labels, and exclusions
- Introduced `InvestmentActivityDetector` to mark internal investment activity as excluded from cashflow and assign appropriate labels.
- Added `exclude_from_cashflow` flag to `entries` and `investment_activity_label` to `transactions` with migrations.
- Implemented rake tasks to backfill and clear investment activity labels.
- Updated `PlaidAccount::Investments::TransactionsProcessor` to map Plaid transaction types to labels.
- Included comprehensive test coverage for new functionality.
2026-01-12 15:35:14 -05:00
Zach Gollwitzer 1aae00f586 perf(transactions): add kind to Transaction model and remove expensive Transfer joins in aggregations (#2388)
* add kind to transaction model

* Basic transfer creator

* Fix method naming conflict

* Creator form pattern

* Remove stale methods

* Tweak migration

* Remove BaseQuery, write entire query in each class for clarity

* Query optimizations

* Remove unused exchange rate query lines

* Remove temporary cache-warming strategy

* Fix test

* Update transaction search

* Decouple transactions endpoint from IncomeStatement

* Clean up transactions controller

* Update cursor rules

* Cleanup comments, logic in search

* Fix totals logic on transactions view

* Fix pagination

* Optimize search totals query

* Default to last 30 days on transactions page if no filters

* Decouple transactions list from transfer details

* Revert transfer route

* Migration reset

* Bundle update

* Fix matching logic, tests

* Remove unused code
2025-06-20 13:31:58 -04:00
Josh Pigford 1aafed5f8b Update error messages in Transfer model tests for clarity and conciseness 2025-04-30 14:45:39 -05:00
Zach Gollwitzer e657c40d19 Account:: namespace simplifications and cleanup (#2110)
* Flatten Holding model

* Flatten balance model

* Entries domain renames

* Fix valuations reference

* Fix trades stream

* Fix brakeman warnings

* Fix tests

* Replace existing entryable type references in DB
2025-04-14 11:40:34 -04:00
Zach Gollwitzer de90b29201 Add RejectedTransfer model, simplify auto matching (#1690)
* Allow transfers to match when inflow is after outflow

* Simplify transfer auto matching with RejectedTransfer model

* Validations

* Reset migrations
2025-01-27 16:56:46 -05:00
Zach Gollwitzer abccba3947 Fix account deletion cascade bug (#1644)
* Fix account deletion cascade bug

* Rubocop fixes
2025-01-20 11:37:01 -05:00
Zach Gollwitzer 1ae4b4d612 Fix transfer matching logic (#1625)
* Fix transfer matching logic

* Fix tests
2025-01-16 17:56:42 -05:00
Zach Gollwitzer 307a3687e8 Transfer and Payment auto-matching, model and UI improvements (#1585)
* Transfer data model migration

* Transfers and payment modeling and UI improvements

* Fix CI

* Transfer matching flow

* Better UI for transfers

* Auto transfer matching, approve, reject flow

* Mark transfers created from form as confirmed

* Account filtering

* Excluded rejected transfers from calculations

* Calculation tweaks with transfer exclusions

* Clean up migration
2025-01-07 09:41:24 -05:00
Zach Gollwitzer 12380dc8ad Account namespace updates: part 5 (valuations) (#901)
* Move Valuation to Account namespace

* Move account history to controller

* Clean up valuation controller and views

* Translations and cleanup

* Remove unused scopes and methods

* Pass brakeman
2024-06-21 16:23:28 -04:00
Zach Gollwitzer bddaab0192 Account namespace updates: part 4 (transfers, singular namespacing) (#896)
* Move Transfer to Account namespace

* Fix partial resolution due to namespacing plurality

* Make category and tag controllers consistent with namespacing convention

* Update stale partial reference
2024-06-20 13:32:44 -04:00
Zach Gollwitzer ca39b26070 Transaction transfers, payments, and matching (#883)
* Add transfer model and clean up family snapshot fixtures

* Ignore transfers in income and expense snapshots

* Add transfer validations

* Implement basic transfer matching UI

* Fix merge conflicts

* Add missing translations

* Tweak selection states for transfer types

* Add missing i18n translation
2024-06-19 06:52:08 -04:00