mirror of
https://github.com/we-promise/sure.git
synced 2026-09-09 00:24:15 +00:00
db181eb84fbb9ed9fdcf9c2acaa7efacd6ba3a61
26
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8ca65ffd27 |
Reconcile PDF statement imports against transactions that already exist (#3105)
* Reset to main, keeping only the account matcher improvements Backs out the LLM-driven reconciliation work (PR #1382's approach and the two commits hardening it). That approach compares whole-statement aggregates, which is all-or-nothing: when 18 of 20 transactions have already synced the totals disagree, the import proceeds, and 18 duplicates are created. Reconciliation is a row-level problem and belongs in the import path, where TransactionImport already solves it via Account::ProviderImportAdapter. Kept from that work, because it stands on its own: - AccountMatcher gains a hint-based class-level entry point so callers without an AccountStatement row can score against the same rules. The instance path used by AccountStatement#assign_account_match is unchanged. - It also refuses to guess between equally-confident candidates rather than letting max_by take whichever the scan reached first. Account names are not unique within a family, so that tie was reachable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KvWSJotDiqP6qSR7qEiC8Q * Reconcile PDF statement imports against transactions that already exist Reconciliation is a row-level problem. Comparing whole-statement totals is all-or-nothing: when 18 of 20 transactions have already synced the totals disagree, the import proceeds, and 18 duplicates are created. This matches each extracted transaction against what the account already holds, so only genuinely new transactions are ever offered for import and the rest are marked reconciled. TransactionImport#import! already does this for CSV via Account::ProviderImportAdapter. PdfImport#import! called it zero times and built one Transaction per row unconditionally -- the only import path in the repo with no duplicate protection. Reconciliation state follows Quicken's uncleared / cleared / reconciled, but only the last state is stored: - "Cleared" means the institution acknowledged the transaction, which is exactly what entries.source and entries.external_id already record. It stays accurate on its own, because the adapter stamps both onto a manual entry when a provider transaction claims it, so a hand-entered transaction that later appears in a download becomes cleared with no extra bookkeeping. Deriving it also keeps it non-editable, which is right: it is a fact about provenance, not an opinion. - "Reconciled" means a statement was matched against the transaction. Nothing can derive that, so entries gains reconciled_at and reconciled_by_statement_id. It is a judgement, so it can be set and unset, and it survives the statement being deleted (the FK nullifies, the timestamp stays). Matching: - find_duplicate_transaction grows include_provider_entries, which is what makes this work for Provider-backed accounts -- the existing where(external_id: nil) filter hid synced transactions from every import path, so this gap affected CSV imports equally. Default stays false: provider sync must not claim another provider's entry. - It also grows date_window, because a statement's posting date routinely differs by a day or two from the date a provider recorded. Nearest date wins. - Name is deliberately not matched on: statement descriptions and provider names for the same transaction rarely agree. The adapter makes the same choice for sync. - Candidates are built as real Import::Row objects so matching uses the same signed_amount and date_iso the import itself would write, rather than a second interpretation of signage that could drift. - Matching is per-account, so with no account assigned every row is offered and re-judged on assignment; reassigning also releases the previous account's reconciliations. - A row whose date or amount will not parse is offered for import rather than dropped, so nothing disappears silently. - import! re-checks at publish, since a sync can land between review and publish, and new transactions are born reconciled: the statement is their evidence. Provider-backed accounts are now offered in the import target picker. The manual-only restriction existed because importing into a synced account would duplicate what sync brought in, which is precisely what this removes. Also fixes a bug this uncovered on main: extract_transactions stored the extractor's symbol-keyed hash, while every reader digs with strings. jsonb keeps the hash as assigned until reload and ProcessPdfJob never reloads, so has_extracted_transactions? was false and PDF imports generated zero rows. The existing tests missed it -- one uses a YAML fixture, the other stubs the extractor with string keys. Supersedes #1382. Refs #1379. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KvWSJotDiqP6qSR7qEiC8Q * Fix three review findings in statement reconciliation All three confirmed against the code before fixing. Publish-time recheck consumed the same entry twice. import! started its exclusion list empty, so a statement carrying two same-amount transactions against an account holding only one would re-match the surviving row against the entry row generation had already consumed -- silently dropping a genuinely new transaction instead of creating it. Seed the exclusions with the entries this statement already reconciled; newly synced entries are still caught, since only already-reconciled ones are excluded. A regeneration that emptied the row set left the import stranded. In the normal upload flow the account is assigned after extraction, so assign_account! regenerates -- and if everything then reconciled, rows_count went to zero while status stayed pending. _pdf_import.html.erb renders pending-with-no-rows as the processing screen, and process_with_ai_later cannot restart because ai_processed? is already true, so the import was stuck with no way forward. Status now follows the same rule ProcessPdfJob applies after initial processing, in both directions: no rows completes it, rows returning sends it back to pending. Guarded by data_committed? so a published import is never reopened. Unevaluatable rows went only to the Rails log. AGENTS.md asks for DebugLogEntry.capture on recoverable import failures so they surface in /settings/debug with structured context. Capture family, account, import, statement, row number and the raw date/amount that would not parse. Adds regression coverage for each. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KvWSJotDiqP6qSR7qEiC8Q * Fix row regeneration collision and over-broad reconciliation release test_unit caught one error in 6462 tests, and it was real. Row regeneration collided on the second call. insert_all! bypasses ActiveRecord, so the rows association is never populated with what it wrote. Calling generate_rows_from_extracted_data twice on the same in-memory record -- which assign_account! now does after ProcessPdfJob has already generated once -- made rows.destroy_all clear a stale empty collection, delete nothing, and then collide on (import_id, source_row_number). Reload before destroying, and reset the association after inserting so sync_mappings and the view read what was actually written. Releasing reconciliations was scoped to the statement, not the account. A statement is evidence for exactly one account at a time but can back more than one import, so reassigning an account cleared reconciliations another account still relied on. Scoped to the account being moved away from; a blank scope releases nothing, which is correct because nothing is reconciled while no account is assigned. The second finding was raised by CodeRabbit. Its other flagged risk -- entries being marked reconciled before the import is published -- is deliberate and stays: the statement is the evidence, reconciling is reversible via unmark_reconciled!, and deferring it to publish would leave a fully reconciled statement with nothing to publish and therefore nothing ever marked. Adds regression coverage for both. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KvWSJotDiqP6qSR7qEiC8Q * Address review nitpicks: model validation, scope placement, mock style, lock-safe DDL All four checked against the repo's own conventions before applying. Mirror the check constraint as a model validation. Assigning reconciled_by_statement without reconciled_at raised StatementInvalid rather than a validation error. CLAUDE.md Convention 5 asks for exactly this pairing -- the constraint in the database, an ActiveRecord validation for form-friendly errors. Stop hijacking the pending-scope comment. The reconciliation scopes were inserted directly under "Pending transaction scopes", so that header read as documentation for them and the provider note below read as a continuation of reconciled_by. Given the reconciliation scopes their own header. Use OpenStruct for the provider response double, per "Always prefer OpenStruct when creating mock instances". Verified OpenStruct.new(success?: true) responds to success?, and ostruct is already a dependency used elsewhere in test/. Make the migration lock-safe. entries is the largest table in the app: both indexes now build concurrently, and the check constraint is added unvalidated then validated separately so VALIDATE takes only SHARE UPDATE EXCLUSIVE instead of holding ACCESS EXCLUSIVE for a full scan. This follows existing practice -- 13 migrations already use disable_ddl_transaction! and 11 use algorithm: :concurrently, with add_offline_reason_to_securities combining add_column and a concurrent index in one migration exactly like this. The suggested follow-up migration for validation was not needed: validating in the same non- transactional migration gets the same lock behavior without a second file, and the repo has no validate: false precedent in 400 migrations. schema.rb is unchanged: a validated constraint and a concurrently-built index dump identically. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KvWSJotDiqP6qSR7qEiC8Q * Add the two review-requested regression tests Covers the invalid-amount half of "malformed rows are offered, not dropped" -- the existing test only exercised an unparseable date. Asserts the raw value is stored verbatim rather than coerced to 0, so the review step shows the user what the statement actually said. Also covers the Entry validation added in |
||
|
|
23e7db4f4d |
feat(transactions): surface recently-used categories in the category picker (#2829)
* fix(transactions): don't crash the rule-prompt flash when clearing a category needs_rule_notification? only checked saved_change_to_category_id? and eligible_for_category_rule?, neither of which accounts for category_id being nil. Clearing a category (Clear category / entryable_attributes category_id: nil) satisfies both, so the caller went on to read transaction.category.name against a nil category and crashed. A rule prompt only makes sense when a category was assigned, not cleared, so bail out early when there's no category to build a rule around. * feat(transactions): surface recently-used categories in the category picker Reframes the recency-vs-muscle-memory question as additive, not either/or: a small "Recent" section pinned above the existing alphabetical list, which stays exactly where it always was below it. Precedent for reordering the primary list by frequency (Office's old adaptive menus, browser-history-style resorting) is a well-known anti-pattern — position drifts under the user's hand. Every picker that does recency well (VS Code's command palette, Spotify, Slack's emoji picker) adds a small separate recent cluster instead. - Category#last_used_at, touched only in TransactionCategoriesController#update — the one place a category is actually hand-picked by a person, as opposed to a rule or import auto-assigning one. - Category.recently_used_for(family:, excluding:, limit:) batches the family-scoped query; dropdowns_controller excludes the already- selected category from the Recent section since it's already pinned to the top of the main list. - "Recent" hides itself the moment a search query is typed — it's a pre-search shortcut, not a second copy of search results. Its rows are force-hidden (not just filtered) so keyboard nav can't land on a row that's invisible only because its ancestor section is hidden. * fix(categories): address review feedback on recent-categories picker - Track last_used_at from every manual assignment path (transaction edit form, categorization wizard bulk-update, create-and-assign), not just the category-picker endpoint. Centralized as Transaction#record_category_usage!, called explicitly from each manual controller action rather than wired to a blanket after_save callback, since rule/import auto-assignment must not count as a "recent" pick. - Give recent-section rows a distinct DOM id (recent_category_option_<id>) from their canonical-list counterpart so aria-activedescendant can't resolve to a hidden duplicate during keyboard nav. - Fix migration to ActiveRecord::Migration[7.2] to match the rest of the repo. - Materialize @recent_categories with .to_a to avoid a redundant query. |
||
|
|
b9716a0485 |
fix: Transaction Pagination Skipping Entries (#2179)
* Fix transaction pagination regressions and CI blockers * Rails EOL already fixed --------- Co-authored-by: Juan José Mata <jjmata@jjmata.com> |
||
|
|
ea316b4277 |
fix(merchants): preserve manual merchant edits across provider sync (#1981)
* fix(merchants): preserve manual merchant edits across provider sync Fixes #1977. Merging merchants, converting a synced (provider) merchant to a family merchant, and unlinking a merchant all reassign transactions.merchant_id via update_all without flagging the entries as user_modified. The next provider sync sees the entries as unmodified and reverts the change. Add Entry.mark_user_modified_for_transactions! and call it (before the merchant_id update, so the scope still matches) in Merchant::Merger#merge!, ProviderMerchant#convert_to_family_merchant_for, and #unlink_from_family. The sync skip-guard already honours user_modified, so flagged entries are left untouched on subsequent syncs. * fix(merchants): pass transaction relation to bulk user_modified helper Addresses PR #1981 review (CodeRabbit): mark_user_modified_for_transactions! now accepts an ActiveRecord::Relation and selects ids via subquery, so large merges/unlinks don't materialize ids or hit SQL parameter limits. Array of ids still supported. Callers pass the scope relation directly. |
||
|
|
fc5cc26812 |
fix(entry): Guard entryable_attributes for non-transaction entries in bulk_update! (#1877)
Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com> |
||
|
|
0b7fa732ae |
feat(splits): add exclusion support for splits and improve rendering (#1661)
* feat(splits): add excluded attribute support for split children and improve rendering of split transactions * address coderabbitai suggestions to improve code quality * Fix split excluded coercion, DRY helpers, and clean up view partials Fix boolean coercion bug where string "false" from form params was truthy in Ruby, causing all split children to be marked excluded. Use ActiveModel::Type::Boolean for explicit casting in Entry#split!. Additional changes addressing code review feedback: - Extract duplicated in_split_group logic from TransactionsController and TransactionCategoriesController into TransactionsHelper - Remove redundant local_assigns.fetch calls in partials that already declare defaults via the Rails 7.1 locals: magic comment - Simplify ternary in _transaction.html.erb to pass grouped directly - Guard hidden_field_tag :grouped to only emit when value is "true" - Add model tests for excluded on split children (boolean and string) - Add controller test for excluded param through full HTTP stack - Add test confirming excluded children are dropped from balance queries * fix(splits): simplify excluded attribute boolean check * refactor(splits): extract truthy values constant for excluded check Extract the array of truthy values used for excluded attribute check into a private constant to improve code maintainability and avoid duplication of the magic array. * refactor: simplify split grouping link generation and add test coverage for excluded split parameters |
||
|
|
96b1d28d5d |
feat(enable-banking): safe pending transaction merge with sync re-import prevention (#1709)
* feat(enable-banking): safe pending transaction merge with sync re-import prevention * preserve all merged pending IDs across syncs * fix(enable-banking): harden merge locking, safe logging, and non-blocking index * fix(enable-banking): use safe external ID in invalid currency log * refactor(models): centralize pending transaction SQL logic Move the SQL fragment used to identify pending transactions from the `Entry` model to a constant in the `Transaction` model. This improves maintainability and ensures that the logic for determining if a transaction is pending is defined in a single location. * fix(enable-banking): drop dead manual_merge index, use lateral join for excluded IDs * No net schema changes --------- Co-authored-by: Juan José Mata <jjmata@jjmata.com> |
||
|
|
ee352dada4 |
Added ability to bulk-edit transaction names for multiple selected transactions (#1553)
* Added ability to bulk-edit transaction names for multiple selected transactions. * Added ability to bulk-edit transaction names for multiple selected transactions. * Added ability to bulk-edit transaction names for multiple selected transactions. * Lint, minimize changes --------- Co-authored-by: Juan José Mata <jjmata@jjmata.com> |
||
|
|
5cb474d61c |
Quick Categorize Wizard — follow-up fixes (#1393)
* Extract Entry.uncategorized_transactions scope, remove Family#uncategorized_transaction_count Adds a single Entry.uncategorized_transactions scope containing the shared conditions (transactions join, active accounts, category nil, not transfer kinds, not excluded). All callers now use this scope: - Entry.uncategorized_matching builds on it - Transaction::Grouper::ByMerchantOrName#uncategorized_entries uses it - categorizes_controller#uncategorized_entries_for uses it (also fixes missing status/excluded filters that were silently absent before) - Both controllers replace Current.family.uncategorized_transaction_count with Current.accessible_entries.uncategorized_transactions.count so the button count and wizard count both respect account sharing Family#uncategorized_transaction_count removed as it is now unused and was family-scoped rather than user-scoped. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Scope assign_entry write to Current.accessible_entries Replaces unscoped Entry.where(id:) with Current.accessible_entries.where(id:) so the write path is consistent with the find above it. Not exploitable given the find would 404 first, but removes the pattern inconsistency. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add privacy-sensitive class to amounts in categorize wizard Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Extract uncategorized_count helper in CategorizesController Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix comment on uncategorized_transactions scope to mention draft accounts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Use uncategorized_count helper in assign_entry action Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
0870ebb56b |
Add Quick Categorize Wizard (#1386)
* Add Quick Categorize Wizard (iteration 1) Adds a step-by-step wizard for bulk-categorizing uncategorized transactions and optionally creating auto-categorization rules, reducing friction after connecting a new bank account. New files: - Transaction::Grouper abstraction + ByMerchantOrName strategy (groups by merchant name when present, falls back to entry name; sorted by count desc) - Transactions::CategorizesController (GET show / POST create) - Wizard view at app/views/transactions/categorizes/show.html.erb - Stimulus categorize_controller.js (Enter-key-to-select-first) - Tests for grouper and controller Modified files: - routes.rb: resource :categorize inside namespace :transactions - transactions_controller.rb: expose @uncategorized_count to index - transactions/index.html.erb: Categorize (N) button in header - family.rb: uncategorized_transaction_count query - rules_controller.rb: return_to param support for wizard → rule editor flow - rules/_form.html.erb, rules/new.html.erb: pass return_to through form - i18n: categorizes show/create keys + rules.create.success Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Quick Categorize Wizard — iteration 2 polish Six improvements from live testing: - Breadcrumb: Home > Transactions > Categorize - Layout: category picker + confirmation dialog above transaction list - Inline confirmation dialog: clicking a category pill shows a <dialog> summarising what will happen (N transactions → category, rule if checked) with Confirm and Cancel buttons — no redirect to rule editor - Direct rule creation: rule created with active: true in the controller instead of redirecting to the rule editor; revert return_to plumbing from RulesController, rules/_form, rules/new, rules/en.yml - Individual row assignment: per-row category <select> submits via PATCH /transactions/categorize/assign_entry and removes the row via Turbo Stream (assign_entry action + route) - Enter key guard: selectFirst only fires when exactly 1 pill is visible after filtering Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Quick Categorize Wizard — iteration 3 reliability fixes and UX polish - Fix Stimulus controller not loading: remove invalid `@hotwired/turbo` named import (not in importmap); use global `Turbo.renderStreamMessage` instead - Fix Enter key submitting form with wrong category when search field is unfocused: move keydown listener to document so it fires regardless of focus - Prevent Enter from submitting when multiple categories are visible - Clear search filter after bulk category assignment (pill click or Enter), but not after individual row dropdown assignment - Update group transaction count and total amount live as entries are assigned via row dropdown or partial bulk assignment - Add turbo frames for remaining count and group summary so they update without a full page reload Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Quick categorization polish * refactoring * Remove unused GROUPS_PER_BATCH constant, fix ERB self-closing tags Wizard only ever uses one group at a time so limit: 1 is correct and more honest than fetching 20 and discarding 19. ERB linter fixes are whitespace/void-element corrections with no functional change. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Move Categorize button into ... menu on transactions index Reduces header clutter by putting it in the overflow menu at the bottom, where it only appears when there are uncategorized transactions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Scope categorize wizard to accessible entries only Fixes a security issue where users with restricted account access via account sharing could view and categorize transactions from accounts they cannot access through normal transaction flows. - Pass Current.accessible_entries to Transaction::Grouper so the wizard only displays groups from accounts the user can see - Use Current.accessible_entries on all write paths in create and assign_entry, matching the pattern in TransactionCategoriesController - Refactor Grouper to accept an entries scope instead of a family object, keeping authorization concerns in the controller - Add tests verifying inaccessible entries are hidden from the wizard and cannot be categorized via forged POST/PATCH params Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Clamp position param to >= 0 to guard against negative offset Prevents ArgumentError from Array#drop when a negative position is passed via a tampered query string or form value. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Surface rule creation failure and add accessible names to entry row - Capture Rule.create_from_grouping! return value; set flash[:alert] when nil so users who checked "Create Rule" know it wasn't created (e.g. a duplicate already exists); stream the notification for partial updates - Add aria-label to the per-row checkbox and category select in _entry_row so screen readers can identify which transaction each control belongs to Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Localize breadcrumb labels in categorizes controller Follows the pattern used by FamilyExportsController and ImportsController. Adds 'transactions' and 'categorize' keys to the breadcrumbs locale file. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add error handling to categorize controller fetch calls Check response.ok before parsing the body and add .catch handlers so network failures and non-2xx responses are logged rather than silently swallowed. On assignment failure the per-row select is reset to empty so the user can retry. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Scope preview_rule to accessible entries only Entry.uncategorized_matching now accepts an entries scope instead of a family object, matching the same pattern used for Transaction::Grouper. The preview_rule action passes Current.accessible_entries so rule previews respect account sharing permissions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Scope remaining count to accessible entries Adds Entry.uncategorized_count(entries) following the same pattern as uncategorized_matching. Replaces all three uses of Current.family.uncategorized_transaction_count in the categorize controller so the remaining-count badge reflects only the transactions the current user can actually access and categorize. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Comments got separated from their function * Remove quick-categorize-wizard dev notes This was a planning document used during development, not intended for the final branch. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Recompute remaining entries from server state after writes Adds uncategorized_entries_for helper that reloads remaining entries from the DB with a category_id IS NULL filter after each write, so the partial-update Turbo Stream reflects server-side state rather than trusting the client-provided remaining_ids. This handles the case where a concurrent request has categorized one of the remaining entries between page render and form submit. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Rename create_from_grouping! to create_from_grouping The method rescues RecordInvalid and returns nil, which contradicts the bang convention. Dropping the ! correctly signals that callers should check the return value. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Clamp offset in grouper to guard against negative values The controller already clamps position before passing it as offset, but clamping in the grouper itself prevents ArgumentError from Array#drop if the grouper is ever called directly with a negative offset. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Juan José Mata <jjmata@jjmata.com> |
||
|
|
0cda69ebb0 |
Split UI (#1245)
* Initial split transaction support
* Add support to unsplit and edit split
* Update show.html.erb
* FIX address reviews
* Improve UX
* Update show.html.erb
* Reviews
* Update edit.html.erb
* Add parent category to dialog
* Update en.yml
* Add UI indication to totals
* FIX ui update
* Add category select like rest of app
* Add split ui
* Add settings configuration for split transactions
- Adds a new settings section for appearance changes
- Also adds extra checks for delete and API calls
- Also adds checks for parent/child changes
* fixes
- split transactions dark mode fix
- add split transactions to context menu
* Update entry.rb
1. New validation split_child_date_matches_parent — prevents saving a split child with a date different from its parent. This is the root-cause fix that
protects all flows at once.
2. Bulk update guard — bulk_update! now strips :date from attributes when processing split children, preventing the validation from raising and silently
skipping the date change instead.
* N+1 fix for split_parent?
* Update entry.rb
Problem: In bulk_update!, when a split child has :date removed from attrs (line 432) and the remaining attrs is empty (e.g., the bulk update only
changed the date), entry.update! {} still ran as a no-op. But lock_saved_attributes! and mark_user_modified! at lines 443-444 executed unconditionally,
incorrectly marking untouched split children as user-modified and opting them out of future syncs.
Fix:
1. Added a changed flag to track whether any actual modification happened
2. Wrapped entry.update! in an if attrs.present? check so no-op updates are skipped
3. Gated lock_saved_attributes! and mark_user_modified! behind if changed, so they only run when the entry was actually modified (either via attribute
update or tag update)
* fixes
1. Indentation in show.html.erb Settings section — The split button block and delete block had extra indentation making them appear nested inside guard
blocks they weren't part of. Fixed to match actual nesting.
2. Skip @split_parents query when grouping is off — The controller now only loads split parent entries when show_split_grouped? is true, saving a query
with joins when the feature is disabled.
|
||
|
|
ae5b23fe67 |
Initial split transaction support (#1230)
* Initial split transaction support * Add support to unsplit and edit split * Update show.html.erb * FIX address reviews * Improve UX * Update show.html.erb * Reviews * Update edit.html.erb * Add parent category to dialog * Update en.yml * Add UI indication to totals * FIX ui update * Add category select like rest of app --------- Signed-off-by: Juan José Mata <juanjo.mata@gmail.com> Co-authored-by: Juan José Mata <juanjo.mata@gmail.com> |
||
|
|
c77971ea0d |
fix: Preserve tags on bulk edits (take 3) (#889)
* fix: handle tags separately from entryable_attributes in bulk updates Tags use a join table (taggings) rather than a direct column, which means empty tag_ids clears all tags rather than meaning "no change". This caused bulk category-only edits to accidentally clear existing tags. This fix: - Removes tag_ids from entryable_attributes in Entry.bulk_update! - Adds update_tags parameter to explicitly control tag updates - Uses params.key?(:tag_ids) in controller to detect explicit tag changes - Preserves existing tags when tag_ids is not provided in the request This is a cleaner architectural solution compared to tracking "touched" state in the frontend, as it properly acknowledges the semantic difference between column attributes and join table associations. https://claude.ai/code/session_014CsmTwjteP4qJs6YZqCKnY * fix: handle tags separately in API transaction updates Apply the same pattern to the API endpoint: tags are now handled separately from entryable_attributes to distinguish between "not provided" (preserve existing tags) and "explicitly set to empty" (clear all tags). This allows API consumers to: - Update other fields without affecting tags (omit tag_ids) - Clear all tags (send tag_ids: []) - Set specific tags (send tag_ids: [id1, id2]) https://claude.ai/code/session_014CsmTwjteP4qJs6YZqCKnY * Proposed fix * fix: improve tag handling in bulk updates for transactions * fix: allow bulk edit to clear/preserve tags by omitting hidden multi-select field * PR comments * Dumb copy/paste error * Linter --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Juan José Mata <juanjo.mata@gmail.com> Co-authored-by: Juan José Mata <jjmata@jjmata.com> |
||
|
|
ad386c6e27 |
fix: Lunchflow pending transaction duplicates, missing from search and filter (#859)
* fix: lunchflow parity with simplefin/plaid pending behaviour * fix: don't suggest duplicate if both entries are pending * refactor: reuse the same external_id for re-synced pending transactions * chore: replace illogical duplicate collision test with multiple sync test * fix: prevent duplicates when users edit pending lunchflow transactions * chore: add test for preventing duplicates when users edit pending lunchflow transactions * fix: normalise extra hash keys for pending detection |
||
|
|
946d0cb3ef |
Merge branch 'main' into feature/llm-cache-reset
Signed-off-by: Juan José Mata <juanjo.mata@gmail.com> |
||
|
|
23e9749ed1 |
Refactor clear_ai_cache to use family_scope pattern
- Move family-scoped queries to models via family_scope class method - Remove hardcoded model names from Enrichable concern - Replace inline rescue with proper respond_to? check - Add count tracking for better logging |
||
|
|
6197419f6c |
Add protection indicator to entries and unlock functionality (#765)
* feat: add protection indicator to entries and unlock functionality - Introduced protection indicator component rendering on hover and in detail views. - Added support to unlock entries, clearing protection flags (`user_modified`, `import_locked`, and locked attributes). - Updated routes, controllers, and models to enable unlock functionality for trades and transactions. - Refactored views and localized content to support the new feature. - Added relevant tests for unlocking functionality and attribute handling. * feat: improve sync protection and turbo stream updates for entries - Added tests for turbo stream updates reflecting protection indicators. - Ensured user-modified entries lock specific attributes to prevent overwrites. - Updated controllers to mark entries as user-modified and reload for accurate rendering. - Enhanced protection indicator rendering using turbo frames. - Applied consistent lock state handling across trades and transactions. * Address PR review comments for protection indicator --------- Co-authored-by: luckyPipewrench <luckypipewrench@proton.me> |
||
|
|
c391ba2b23 |
Harden SimpleFIN sync: protect user data, fix stuck syncs, optimize API calls (#671)
* Implement entry protection flags for sync overwrites - Added `user_modified` and `import_locked` flags to `entries` table to prevent provider sync from overwriting user-edited and imported data. - Introduced backfill migration to mark existing entries based on conditions. - Enhanced sync and processing logic to respect protection flags, track skipped entries, and log detailed stats. - Updated UI to display skipped/protected entries and reasons in sync summaries. * Localize error details summary text and adjust `sync_account_later` method placement * Restored schema.rb --------- Co-authored-by: luckyPipewrench <luckypipewrench@proton.me> |
||
|
|
3658e812a8 |
Add pending transaction handling and duplicate reconciliation logic (#602)
* Add pending transaction handling and duplicate reconciliation logic - Implemented logic to exclude pending transactions from budgets and analytics calculations. - Introduced mechanisms for reconciling pending transactions with posted versions. - Added duplicate detection with support for merging or dismissing matches. - Updated transaction search filters to include a `status_filter` for pending/confirmed transactions. - Introduced UI elements for reviewing and resolving duplicates. - Enhanced `ProviderSyncSummary` with stats for reconciled and stale pending transactions. * Refactor translation handling and enhance transaction and sync logic - Moved hardcoded strings to locale files for improved translation support. - Refined styling for duplicate transaction indicators and sync summaries. - Improved logic for excluding stale pending transactions and updating timestamps on batch exclusion. - Added unique IDs to status filters for better element targeting in UI. - Optimized database queries to avoid N+1 issues in stale pending calculations. * Add sync settings and enhance pending transaction handling - Introduced a new "Sync Settings" section in hosting settings with UI to toggle inclusion of pending transactions. - Updated handling of pending transactions with improved inference logic for `posted=0` and `transacted_at` in processors. - Added priority order for pending transaction inclusion: explicit argument > environment variable > runtime configurable setting. - Refactored settings and controllers to store updated sync preferences. * Refactor sync settings and pending transaction reconciliation - Extracted logic for pending transaction reconciliation, stale exclusion, and unmatched tracking into dedicated methods for better maintainability. - Updated sync settings to infer defaults from multiple provider environment variables (`SIMPLEFIN_INCLUDE_PENDING`, `PLAID_INCLUDE_PENDING`). - Refined UI and messaging to handle multi-provider configurations in sync settings. # Conflicts: # app/models/simplefin_item/importer.rb * Debounce transaction reconciliation during imports - Added per-run reconciliation debouncing to prevent repeated scans for the same account during chunked history imports. - Trimmed size of reconciliation stats to retain recent details only. - Introduced error tracking for reconciliation steps to improve UI visibility of issues. * Apply ABS() in pending transaction queries and improve error handling - Updated pending transaction logic to use ABS() for consistent handling of negative amounts. - Adjusted amount bounds calculations to ensure accuracy for both positive and negative values. - Refined exception handling in `merge_duplicate` to log failures and update user alert. - Replaced `Date.today` with `Date.current` in tests to ensure timezone consistency. - Minor optimization to avoid COUNT queries by loading limited records directly. * Improve error handling in duplicate suggestion and dismissal logic - Added exception handling for `store_duplicate_suggestion` to log failures and prevent crashes during fuzzy/low-confidence matches. - Enhanced `dismiss_duplicate` action to handle `ActiveRecord::RecordInvalid` and display appropriate user alerts. --------- Co-authored-by: Josh Waldrep <joshua.waldrep5+github@gmail.com> |
||
|
|
4fb0a3856e |
Providers factory (#250)
* Implement providers factory * Multiple providers sync support - Proper Multi-Provider Syncing: When you click sync on an account with multiple providers (e.g., both Plaid and SimpleFin), all provider items are synced - Better API: The existing account.providers method already returns all providers, and account.provider returns the first one for backward compatibility - Correct Holdings Deletion Logic: Holdings can only be deleted if ALL providers allow it, preventing accidental deletions that would be recreated on next sync TODO: validate this is the way we want to go? We would need to check holdings belong to which account, and then check provider allows deletion. More complex - Database Constraints: The existing validations ensure an account can have at most one provider of each type (one PlaidAccount, one SimplefinAccount, etc.) * Add generic provider_import_adapter * Finish unified import strategy * Update app/models/plaid_account.rb Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: soky srm <sokysrm@gmail.com> * Update app/models/provider/factory.rb Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: soky srm <sokysrm@gmail.com> * Fix account linked by plaid_id instead of external_id * Parse numerics to BigDecimal Parse numerics to BigDecimal before computing amount; guard nils. Avoid String * String and float drift; also normalize date. * Fix incorrect usage of assert_raises. * Fix linter * Fix processor test. * Update current_balance_manager.rb * Test fixes * Fix plaid linked account test * Add support for holding per account_provider * Fix proper account access Also fix account deletion for simpefin too * FIX match tests for consistency * Some more factory updates * Fix account schema for multipe providers Can do: - Account #1 → PlaidAccount + SimplefinAccount (multiple different providers) - Account #2 → PlaidAccount only - Account #3 → SimplefinAccount only Cannot do: - Account #1 → PlaidAccount + PlaidAccount (duplicate provider type) - PlaidAccount #123 → Account #1 + Account #2 (provider linked to multiple accounts) * Fix account setup - An account CAN have multiple providers (the schema shows account_providers with unique index on [account_id, provider_type]) - Each provider should maintain its own separate entries - We should NOT update one provider's entry when another provider syncs * Fix linter and guard migration * FIX linter issues. * Fixes - Remove duplicated index - Pass account_provider_id - Guard holdings call to avoid NoMethodError * Update schema and provider import fix * Plaid doesn't allow holdings deletion * Use ClimateControl for proper env setup * No need for this in .git --------- Signed-off-by: soky srm <sokysrm@gmail.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Juan José Mata <juanjo.mata@gmail.com> |
||
|
|
662f2c04ce |
Multi-step account forms + clearer balance editing (#2427)
* Initial multi-step property form * Improve form structure, add optional tooltip help icons to form fields * Add basic inline alert component * Clean up and improve property form lifecycle * Implement Account status concept * Lint fixes * Remove whitespace * Balance editing, scope updates for account * Passing tests * Fix brakeman warning * Remove stale columns * data constraint tweaks * Redundant property |
||
|
|
03a146222d |
Plaid sync domain improvements (#2267)
Breaks our Plaid sync process out into more manageable classes. Notably, this moves the sync process to a distinct, 2-step flow: 1. Import stage - we first make API calls and import Plaid data to "mirror" tables 2. Processing stage - read the raw data, apply business rules, build internal domain models and sync balances This provides several benefits: - Plaid syncs can now be "replayed" without fetching API data again - Mirror tables provide better audit and debugging capabilities - Eliminates the "all or nothing" sync behavior that is currently in place, which is brittle |
||
|
|
137219c121 | Fix attribute locking namespace conflict, duplicate syncs | ||
|
|
10dd9e061a |
Improve account sync performance, handle concurrent market data syncing (#2236)
* PlaidConnectable concern * Remove bad abstraction * Put sync implementations in own concerns * Sync strategies * Move sync orchestration to Sync class * Clean up sync class, add state machine * Basic market data sync cron * Fix price sync * Improve sync window column names, add timestamps * 30 day syncs by default * Clean up market data methods * Report high duplicate sync counts to Sentry * Add sync states throughout app * account tab session * Persistent account tab selections * Remove manual sleep * Add migration to clear stale syncs on self hosted apps * Tweak sync states * Sync completion event broadcasts * Fix timezones in tests * Cleanup * More cleanup * Plaid item UI broadcasts for sync * Fix account ID namespace conflict * Sync broadcasters * Smoother account sync refreshes * Remove test sync delay |
||
|
|
297a695d0f |
Transaction rules engine V1 (#1900)
* Domain model sketch * Scaffold out rules domain * Migrations * Remove existing data enrichment for clean slate * Sketch out business logic and basic tests * Simplify rule scope building and action executions * Get generator working again * Basic implementation + tests * Remove manual merchant management (rules will replace) * Revert "Remove manual merchant management (rules will replace)" This reverts commit 83dcbd9ff0aa7bbee211796b71aa48b71df5e57e. * Family and Provider merchants model * Fix brakeman warnings * Fix notification loader * Update notification position * Add Rule action and condition registries * Rule form with compound conditions and tests * Split out notification types, add CTA type * Rules form builder and Stimulus controller * Clean up rule registry domain * Clean up rules stimulus controller * CTA message for rule when user changes transaction category * Fix tests * Lint updates * Centralize notifications in Notifiable concern * Implement category rule prompts with auto backoff and option to disable * Fix layout bug caused by merge conflict * Initialize rule with correct action for category CTA * Add rule deletions, get rules working * Complete dynamic rule form, split Stimulus controllers by resource * Fix failing tests * Change test password to avoid chromium conflicts * Update integration tests * Centralize all test password references * Add re-apply rule action * Rule confirm modal * Run migrations * Trigger rule notification after inline category updates * Clean up rule styles * Basic attribute locking for rules * Apply attribute locks on user edits * Log data enrichments, only apply rules to unlocked attributes * Fix merge errors * Additional merge conflict fixes * Form UI improvements, ignore attribute locks on manual rule application * Batch AI auto-categorization of transactions * Auto merchant detection, ai enrichment in batches * Fix Plaid merchant assignments * Plaid category matching * Cleanup 1 * Test cleanup * Remove stale route * Fix desktop chat UI issues * Fix mobile nav styling issues |
||
|
|
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 |