Skip to content

feat(splits): add exclusion support for splits and improve rendering#1661

Merged
jjmata merged 11 commits intowe-promise:mainfrom
CrossDrain:split-children-exclusion-support-and-improced-split-group-rendering
May 9, 2026
Merged

feat(splits): add exclusion support for splits and improve rendering#1661
jjmata merged 11 commits intowe-promise:mainfrom
CrossDrain:split-children-exclusion-support-and-improced-split-group-rendering

Conversation

@CrossDrain
Copy link
Copy Markdown
Contributor

@CrossDrain CrossDrain commented May 3, 2026

Split Children Exclusion & Rendering Fixes

Overview

This PR introduces two major improvements to split transaction handling:

  1. Exclusion function for split children - Individual split children can now be marked as excluded, allowing them to be hidden from balance calculations while still showing the full split breakdown.
  2. Rendering bug fixes - Fixes issues where split children lose indentation and context when navigating or editing, ensuring proper visual hierarchy and maintaining the grouped state across the UI.

Feature 1: Exclusion Function for Split Children

Problem

Previously, when splitting a transaction, all child entries were always included in balance calculations. There was no way to exclude specific parts of a split (e.g., a personal expense from a shared bill that you want to track separately but not count toward your totals).

Solution

Added an excluded boolean attribute to split child entries. This allows users to:

  • Mark individual split children as excluded from balance calculations
  • Still view the full split breakdown for reference
  • The parent split entry remains excluded (as it's just a container)

Implementation

Screenshots: Exclusion Function for Split Children

🔄 Before

Before: No exclusion option

All split children always included in balances. No exclusion toggle available.

✅ After

After: Individual child exclusion

Each child has an "Exclude" toggle. Excluded children are dimmed and filtered from balance calculations.

⚠️ Note on Split Creation UI

The exclusion toggle is currently available only in the transaction detail drawer (after a split has been created). The split creation/editing modal does not yet include exclusion controls.

This is intentional. This PR focuses on establishing the core exclusion architecture (model, controller, detail drawer)

Adding exclusion to the split editor may create duplication later, as this functionality could become obsolete if a proper reimbursements feature is developed.

Current user flow: Create split → Open any child transaction → Toggle "Exclude" in settings panel.


Feature 2: Split Transaction Rendering Fixes

Problem

Split children were losing their visual indentation and context in several scenarios:

  1. When clicking into a single split child's transaction page, the grouped parameter wasn't preserved, causing children to lose their indentation when returning to the list
  2. Category dropdown and other UI elements didn't maintain split group context
  3. The exclusion settings for split children were blocked by a condition that prevented split children from accessing their own settings

Solution

  • Introduced in_split_group local parameter propagated through all transaction rendering partials
  • Applied conditional indentation (pl-8 lg:pl-12) when in_split_group is true
  • Preserved grouped parameter in all links and forms within split children
  • Fixed condition to allow split children to render their own exclusion toggles

Changes

Screenshots: Split Transaction Rendering Fixes

🔄 Before

Before: Lost Indentation & Context

Indentation lost on navigation. Category dropdown loses context.

✅ After

After: Indentation preserved

Indentation preserved throughout. All links/forms maintain grouped state


Technical Details

Data Model

# Entry#split! now accepts :excluded key
entry.split!([
  { name: "Groceries", amount: 70, excluded: true },    # Hidden from balances
  { name: "Household", amount: 30, excluded: false }    # Included in balances
])

Balance Calculations

Excluded split children are automatically filtered:

Entry.where(excluded: false) # Excludes both parent AND child entries with excluded: true

Testing

Summary by CodeRabbit

  • New Features

    • Split transactions can mark individual split entries as excluded when creating or updating.
  • UI Improvements

    • Split-grouped view state is preserved across forms, menus, and navigation.
    • Category menus, transaction rows, entry views, and notes now respect and propagate grouped state for consistent behavior across mobile and desktop.
  • Tests

    • Added coverage verifying per-split excluded behavior, casting, and filtering in balance queries.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 3, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Threads a grouped display flag through controllers, views, and forms and adds per-child excluded handling: permitted params and controller payloads include excluded, Entry#split! applies per-child excluded values, views propagate/preserve grouped/in_split_group, and tests cover behavior.

Changes

Excluded Splits with Grouped Context

Layer / File(s) Summary
Data Shape
app/models/entry.rb
Entry#split! docs and implementation updated to accept an optional excluded: per split and set each child’s excluded using boolean-casting (TRUTHY_VALUES).
Parameter & Permission Layer
app/controllers/splits_controller.rb
split_params now permits splits: [:name, :amount, :category_id, :excluded]; create/update include excluded: s[:excluded] when building split hashes passed to split!.
Helper / Controller Computation
app/helpers/transactions_helper.rb, app/controllers/transactions_controller.rb, app/controllers/transaction_categories_controller.rb
Added in_split_group?(entry, params_grouped); controllers compute in_split_group = helpers.in_split_group?(@entry, params[:grouped]) and pass it into turbo-stream/partial locals.
Entry & Transaction Partials
app/views/entries/_entry.html.erb, app/views/transactions/_transaction.html.erb, app/views/transactions/_transaction_category.html.erb
Partials declare in_split_group (default false) and propagate it to nested renders; non-transfer entry_path includes grouped: in_split_group.
Category Menu & Dropdown
app/views/categories/_menu.html.erb, app/views/category/dropdowns/_row.html.erb, app/views/category/dropdowns/show.html.erb
Category dropdown frame src and category action URLs now include grouped/in_split_group; clear action and excluded-toggle form include grouped param or hidden field when appropriate.
Form State Preservation
app/views/transactions/_notes.html.erb, app/views/transactions/show.html.erb
Hidden grouped fields added to notes and several show-page forms; exclude-toggle visibility adjusted to render for split children (hidden only for split parents).
Tests
test/models/entry_split_test.rb, test/controllers/splits_controller_test.rb
New tests assert per-child excluded persistence and boolean casting, and that excluded children are omitted from balance-related queries.

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant Browser
  participant SplitsController
  participant EntryModel
  participant TransactionsController
  participant Views

  User->>Browser: submit split form (splits with excluded, grouped)
  Browser->>SplitsController: POST /splits (params + grouped)
  SplitsController->>EntryModel: Entry#split!(splits including excluded)
  EntryModel-->>SplitsController: creates child entries with excluded flags
  SplitsController->>Browser: redirect to entry view
  Browser->>TransactionsController: request/update (grouped param preserved)
  TransactionsController->>Views: compute in_split_group and render turbo-stream partials (pass in_split_group)
  Views-->>Browser: turbo-stream replacements and forms (hidden grouped fields)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

contributor:verified

Suggested reviewers

  • jjmata
  • sokie

Poem

🐇 I nudged a tiny flag into the burrowed split,
children wear their tags where amounts may sit.
Grouped threads hop through form and stream,
hidden fields whisper the same dream.
🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary changes: adding exclusion support for split children and preserving grouped-view rendering state across interactions.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@CrossDrain CrossDrain marked this pull request as ready for review May 3, 2026 11:43
@brin-security-scanner brin-security-scanner Bot added contributor:flagged Contributor flagged for review by trust analysis. pr:verified PR passed security analysis. labels May 3, 2026
@dosubot
Copy link
Copy Markdown

dosubot Bot commented May 3, 2026

Documentation Updates

1 document(s) were updated by changes in this PR:

How split transaction child exclusion was implemented

renamed from "Why can't individual parts of a split transaction be excluded from analytics, and what changes are needed to support this?"

View Changes
@@ -1,31 +1,34 @@
-Currently, individual parts of a split transaction cannot be excluded from analytics due to intentional UI and controller restrictions — not a database limitation.
+Individual parts of a split transaction can now be excluded from analytics. This functionality was implemented in [PR #1661](https://github.com/we-promise/sure/pull/1661).
 
-## Root Cause
+## Implementation
 
-1. **View restriction** — The exclusion toggle is hidden for both split parents and split children in the transaction detail view:
+The feature required three key changes, all of which have been completed:
+
+1. **View restriction removed** — The exclusion toggle was previously hidden for both split parents and split children. The conditional has been updated to allow the toggle for split children while still hiding it for split parents:
    ```erb
-   <% unless @entry.split_parent? || @entry.split_child? %>
+   <% unless @entry.split_parent? %>
      <%= f.toggle :excluded, { data: { auto_submit_form_target: "auto" } } %>
    <% end %>
    ```
 
-2. **Controller restriction** — The `SplitsController` only permits `name`, `amount`, and `category_id` in its strong parameters — the `excluded` field is not accepted.
+2. **Controller parameter permitted** — The `SplitsController` now permits the `excluded` field in strong parameters, allowing it to be passed through during split creation and updates:
+   ```ruby
+   def split_params
+     params.require(:split).permit(splits: [ :name, :amount, :category_id, :excluded ])
+   end
+   ```
 
-## What Already Works
+3. **Model support added** — The `Entry#split!` method now accepts and handles the `excluded` key, properly casting truthy string values (`"true"`, `"1"`) to boolean:
+   ```ruby
+   excluded: TRUTHY_VALUES.include?(split_attrs[:excluded])
+   ```
 
-- The **database already supports exclusion** per split child, as each split child is a real `Entry` record with its own `excluded` boolean flag.
-- Analytics queries already filter on `ae.excluded = false`, so the underlying logic is in place.
+## How It Works
 
-## Required Changes
+- The **database already supported exclusion** per split child, as each split child is a real `Entry` record with its own `excluded` boolean flag.
+- Analytics queries filter on `ae.excluded = false`, so excluded split children are automatically removed from balance calculations.
+- Users can toggle exclusion on individual split children via the transaction detail drawer.
 
-1. **View** — Update the conditional in the form partial to allow the exclusion toggle for `split_child?` entries (while still hiding it for `split_parent?`).
+## User Flow
 
-2. **Controller** — Allow the `excluded` parameter through the `SplitsController`'s strong params, or route split child updates through `TransactionsController`, which already permits `excluded` in `entry_params`.
-
-3. **Validation** — No model-level changes are needed, as the existing `cannot_unexclude_split_parent` validation only locks exclusion on **parent** entries, not children.
-
-4. **Split editor (optional)** — Add an exclusion checkbox per row in the split creation/edit form so users can set exclusion at split-creation time.
-
-## Known Related Issue
-
-There is a noted issue where `Balance::SyncCache` does not filter `excluded: true` entries in `flows_for_date()`, which could cause double-counting in cash flow calculations. This would also affect excluded split children if not resolved.
+The exclusion toggle is available in the **transaction detail drawer** after a split has been created. The split creation/editing modal does not include exclusion controls — users create the split first, then open any child transaction to toggle exclusion in the settings panel.

How did I do? Any feedback?  Join Discord

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/models/entry.rb (1)

389-397: ⚠️ Potential issue | 🟠 Major

split_attrs[:excluded] can be nil, overriding the column default and storing NULL in the database.

The excluded column has default: false (no explicit NOT NULL constraint), so the database will accept the NULL value. However, this is still a bug: unchecked HTML checkboxes don't submit a value, causing s[:excluded] to be nil in the controller's split params (line 22 in splits_controller.rb). This nil then reaches line 395 and overrides the intended default, storing NULL instead of false. Boolean columns with defaults should never be NULL—this will cause issues with boolean predicates like excluded? and make the value indistinguishable from an intentional false.

All test cases (and any other callers of split!) omit the :excluded key entirely, so they will be affected by this bug once users interact with the form.

Proposed fix
         child_entries.create!(
           account: account,
           date: date,
           name: split_attrs[:name],
           amount: split_attrs[:amount],
           currency: currency,
-          excluded: split_attrs[:excluded],
+          excluded: split_attrs[:excluded] || false,
           entryable: child_transaction
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/models/entry.rb` around lines 389 - 397, The child_entries.create! call
is passing split_attrs[:excluded] which can be nil and override the DB default;
change the argument to ensure a non-nil boolean (e.g. use
split_attrs.fetch(:excluded, false) or otherwise coerce to a boolean) so
child_entries.create! (in Entry#split! / the block creating child entries)
always writes true/false rather than NULL for the excluded column.
🧹 Nitpick comments (2)
app/views/category/dropdowns/show.html.erb (1)

88-88: 💤 Low value

Use hidden_field_tag for consistency with every other grouped field in this PR.

f.hidden_field :grouped in a form_with url: (no model/scope) does produce name="grouped" — so this is functionally correct — but the entire PR consistently uses hidden_field_tag :grouped, params[:grouped] for this purpose (see show.html.erb lines 57, 99, 273, 290, 316, and _notes.html.erb line 6).

♻️ Proposed change
-              <%= f.hidden_field :grouped, value: params[:grouped] %>
+              <%= hidden_field_tag :grouped, params[:grouped] %>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/views/category/dropdowns/show.html.erb` at line 88, Replace the form
builder hidden field call f.hidden_field :grouped with the standalone helper
hidden_field_tag :grouped, params[:grouped] to match the rest of the PR; locate
the occurrence where f.hidden_field :grouped is used (inside the form_with
block) and change it to hidden_field_tag :grouped, params[:grouped] so the
generated input is consistent with other uses like the existing hidden_field_tag
:grouped, params[:grouped] instances.
app/views/transactions/_transaction.html.erb (1)

74-74: 💤 Low value

Optional: avoid appending grouped=false to every non-split-group transaction URL.

entry_path(entry, grouped: in_split_group) always passes the param, so normal-context transaction links get ?grouped=false appended even though the controller treats absent and false identically (params[:grouped] == "true").

♻️ Proposed fix
-                            entry_path(entry, grouped: in_split_group),
+                            in_split_group ? entry_path(entry, grouped: true) : entry_path(entry),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/views/transactions/_transaction.html.erb` at line 74, The link helper
always emits grouped=false because entry_path(entry, grouped: in_split_group)
passes the param even when in_split_group is false; change the view so the
grouped param is only included when in_split_group is true (i.e., call
entry_path without the grouped arg for the normal case and only pass grouped:
true when in_split_group is truthy) so URLs don't get ?grouped=false; update the
usage of entry_path in _transaction.html.erb to conditionally include the
grouped parameter based on in_split_group.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@app/models/entry.rb`:
- Around line 389-397: The child_entries.create! call is passing
split_attrs[:excluded] which can be nil and override the DB default; change the
argument to ensure a non-nil boolean (e.g. use split_attrs.fetch(:excluded,
false) or otherwise coerce to a boolean) so child_entries.create! (in
Entry#split! / the block creating child entries) always writes true/false rather
than NULL for the excluded column.

---

Nitpick comments:
In `@app/views/category/dropdowns/show.html.erb`:
- Line 88: Replace the form builder hidden field call f.hidden_field :grouped
with the standalone helper hidden_field_tag :grouped, params[:grouped] to match
the rest of the PR; locate the occurrence where f.hidden_field :grouped is used
(inside the form_with block) and change it to hidden_field_tag :grouped,
params[:grouped] so the generated input is consistent with other uses like the
existing hidden_field_tag :grouped, params[:grouped] instances.

In `@app/views/transactions/_transaction.html.erb`:
- Line 74: The link helper always emits grouped=false because entry_path(entry,
grouped: in_split_group) passes the param even when in_split_group is false;
change the view so the grouped param is only included when in_split_group is
true (i.e., call entry_path without the grouped arg for the normal case and only
pass grouped: true when in_split_group is truthy) so URLs don't get
?grouped=false; update the usage of entry_path in _transaction.html.erb to
conditionally include the grouped parameter based on in_split_group.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b8a2e6e9-3255-4b2a-a88c-4ae9d398cdb0

📥 Commits

Reviewing files that changed from the base of the PR and between 5093600 and 726c631.

📒 Files selected for processing (12)
  • app/controllers/splits_controller.rb
  • app/controllers/transaction_categories_controller.rb
  • app/controllers/transactions_controller.rb
  • app/models/entry.rb
  • app/views/categories/_menu.html.erb
  • app/views/category/dropdowns/_row.html.erb
  • app/views/category/dropdowns/show.html.erb
  • app/views/entries/_entry.html.erb
  • app/views/transactions/_notes.html.erb
  • app/views/transactions/_transaction.html.erb
  • app/views/transactions/_transaction_category.html.erb
  • app/views/transactions/show.html.erb

@jjmata jjmata removed the contributor:flagged Contributor flagged for review by trust analysis. label May 3, 2026
@jjmata jjmata self-requested a review May 3, 2026 16:57
Copy link
Copy Markdown
Collaborator

@jjmata jjmata left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Thanks for this PR — the UX problem is real and the overall approach is sensible. A few issues worth addressing before merge.


🔴 Bug: string "false" is truthy — boolean coercion is broken

In app/models/entry.rb:

excluded: split_attrs[:excluded] || false,

split_attrs[:excluded] comes from a form param, so it will be the string "false" when unchecked — which is truthy in Ruby. "false" || false evaluates to "false", and ActiveRecord will cast that string as true for a boolean column. Every split child will be marked excluded unless the field is absent entirely.

The fix is explicit casting:

excluded: ActiveModel::Type::Boolean.new.cast(split_attrs[:excluded]) || false,

Or at the controller layer before the hash is built.


🔴 Missing test coverage for the new excluded feature

test/models/entry_split_test.rb and test/controllers/splits_controller_test.rb both exist but neither tests the new excluded parameter on split children. At minimum, add:

  • A model test asserting that split! with excluded: true on a child creates that child with excluded: true.
  • A controller test that passes excluded: "true" in the split params and asserts the resulting child entry is excluded.

Without these, the string-coercion bug above (or any future regression) would not be caught.


🟡 DRY violation: in_split_group computed identically in two controllers

transactions_controller.rb:143 and transaction_categories_controller.rb:24 both compute:

in_split_group = @entry.split_child? && Current.user.show_split_grouped? && params[:grouped] == "true"

This belongs in a shared helper (e.g. TransactionsHelper or a before_action on a shared concern). One change to the logic currently requires two edits.


🟡 Redundant local variable default in views

Several partials now have both a locals: magic comment with a default and an explicit local_assigns.fetch call:

<%# locals: (entry:, balance_trend: nil, view_ctx: "global", in_split_group: false) %>
<% in_split_group = local_assigns.fetch(:in_split_group, false) %>

The magic comment (locals:) is the Rails 7.1 way to declare and default locals — the fetch line is redundant when the comment is present. Pick one approach and be consistent. The other partials in this codebase only use the magic comment.


🟡 Ternary can be simplified

In app/views/transactions/_transaction.html.erb:

in_split_group ? entry_path(entry, grouped: true) : entry_path(entry)

Since in_split_group is already a boolean, this simplifies to:

entry_path(entry, grouped: in_split_group)

🟢 Behavioural concern: what happens when an excluded split child is summed?

The PR enables marking individual split children as excluded. The parent is always excluded (that's how splits work — the parent is excluded and its children represent the breakdown). If a child is also excluded, does the balance/account calculation still hold? The Entry.where(excluded: false) scope used for balance queries (e.g. entry.rb:102) would then drop the child too, potentially under-counting. Please confirm the intent here and add a test if this is a supported use case.


🟢 hidden_field_tag :grouped, nil sends grouped= rather than omitting the param

When params[:grouped] is nil (the common case on non-split pages), every form that does:

<%= hidden_field_tag :grouped, params[:grouped] %>

will submit grouped= (empty string) rather than omitting the param entirely. The controller guard params[:grouped] == "true" handles this correctly (empty string ≠ "true"), but the extra param adds noise. A guard like hidden_field_tag :grouped, "true" if params[:grouped] == "true" would be cleaner.


Generated by Claude Code

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
@brin-security-scanner brin-security-scanner Bot added the contributor:flagged Contributor flagged for review by trust analysis. label May 3, 2026
@CrossDrain CrossDrain requested a review from jjmata May 3, 2026 18:41
@jjmata jjmata requested a review from sokie May 3, 2026 19:34
@jjmata jjmata removed the contributor:flagged Contributor flagged for review by trust analysis. label May 3, 2026
@jjmata
Copy link
Copy Markdown
Collaborator

jjmata commented May 3, 2026

Can you review @sokie? 🙏

Copy link
Copy Markdown
Collaborator

jjmata commented May 4, 2026

The approach is clean and the test coverage hits the important cases (boolean casting, per-child persistence, filtering). A couple of things worth noting:

Split editor UI gap
The PR wires up excluded support through the model, controller strong params, and the transaction detail drawer — but I don't see changes to the split creation/edit form (wherever split rows are rendered during initial split creation). This means a user can only exclude a split child after creating it, not at creation time. Is that intentional? If so, it might be worth a note in the PR description so reviewers don't assume the full flow is wired up.

ActiveModel::Type::Boolean.new per call
In Entry#split!:

excluded: ActiveModel::Type::Boolean.new.cast(split_attrs[:excluded]) || false

This allocates a new type object for every split row in the loop. Not a meaningful performance issue at typical split counts, but ActiveRecord::Type::Boolean.new is usually used as a singleton. Could use a frozen constant or simply [true, "true", "1", 1].include?(split_attrs[:excluded]) inline. Very minor.

grouped as a boolean in URL helpers
entry_path(entry, grouped: in_split_group) passes a Ruby false/true boolean. Rails will serialize this as "false"/"true" in the URL, and the receiving in_split_group? helper checks params_grouped == "true" — so the round-trip works correctly. Just worth a quick mental note if this check is ever refactored.

The grouped hidden-field threading through forms is idiomatic for Rails/Hotwire and the right pattern for preserving state across Turbo Stream updates.


Generated by Claude Code

@brin-security-scanner brin-security-scanner Bot added the contributor:flagged Contributor flagged for review by trust analysis. label May 4, 2026
@CrossDrain
Copy link
Copy Markdown
Contributor Author

  1. Split editor UI gap: I've thought about implementing this, but I ultimately decided it shouldn't be in this PR. Not only does leaving it out keep the scope focused on the core exclusion architecture and the detail drawer, but it also provides little long-term utility if a proper reimbursements function is implemented in the future. I will update the PR description with a note about this so other reviewers don't assume the full initial creation flow is wired up.

  2. ActiveModel::Type::Boolean.new per call: I've removed the instantiation and replaced it with a simple inline check: [true, "true", "1", 1].include?(split_attrs[:excluded]). (a50eea7)

Let me know if anything else is needed before merging!

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
app/models/entry.rb (1)

395-395: ⚡ Quick win

Minor: hoist the truthy-values array to a frozen constant.

[true, "true", "1", 1] is a new heap object on every splits.map iteration. For typical split counts this is negligible, but extracting it to a frozen constant removes the repeated allocation and makes the intent self-documenting.

♻️ Proposed refactor

Add a private constant near the top of the private section (or alongside the class constants):

  private

+   BOOLEAN_TRUE_VALUES = [ true, "true", "1", 1 ].freeze
+
    def cannot_unexclude_split_parent

Then reference it in split!:

-          excluded: [true, "true", "1", 1].include?(split_attrs[:excluded]),
+          excluded: BOOLEAN_TRUE_VALUES.include?(split_attrs[:excluded]),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/models/entry.rb` at line 395, Hoist the repeated truthy array into a
frozen private constant (e.g. TRUTHY_VALUES = [true, "true", "1", 1].freeze)
declared near the other private/class constants, then replace the inline array
in the split handling (the line building excluded: ... inside the split!/splits
mapping) with TRUTHY_VALUES.include?(split_attrs[:excluded]) to avoid allocating
the array on each iteration and make intent clearer.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/models/entry.rb`:
- Line 395: Hoist the repeated truthy array into a frozen private constant (e.g.
TRUTHY_VALUES = [true, "true", "1", 1].freeze) declared near the other
private/class constants, then replace the inline array in the split handling
(the line building excluded: ... inside the split!/splits mapping) with
TRUTHY_VALUES.include?(split_attrs[:excluded]) to avoid allocating the array on
each iteration and make intent clearer.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7b2f1f66-b282-4fa5-a638-2735a0050c6b

📥 Commits

Reviewing files that changed from the base of the PR and between fb5bff4 and a50eea7.

📒 Files selected for processing (1)
  • app/models/entry.rb

CrossDrain added 2 commits May 4, 2026 07:13
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.
@jjmata jjmata removed the contributor:flagged Contributor flagged for review by trust analysis. label May 4, 2026
@jjmata
Copy link
Copy Markdown
Collaborator

jjmata commented May 4, 2026

Thank you, @CrossDrain ... can you include before/after screenshot in the first PR comment to help reviewers? 🙏

@CrossDrain
Copy link
Copy Markdown
Contributor Author

CrossDrain commented May 4, 2026

@jjmata
Sure! I've added before/after GIFs to the first PR comment.

The screenshots show:

  1. Exclusion feature Before: split children all included; After: individual child with exclusion toggle and dimmed state
  2. Rendering fixes Before: lost indentation after navigation; After: proper indentation preserved throughout

Let me know if there's anything final I need to touch on.

@brin-security-scanner brin-security-scanner Bot added the contributor:flagged Contributor flagged for review by trust analysis. label May 4, 2026
@jjmata jjmata removed the contributor:flagged Contributor flagged for review by trust analysis. label May 4, 2026
@jjmata jjmata added this to the v0.7.1 milestone May 4, 2026
@jjmata jjmata removed their request for review May 4, 2026 23:03
@brin-security-scanner brin-security-scanner Bot added the contributor:flagged Contributor flagged for review by trust analysis. label May 5, 2026
@CrossDrain
Copy link
Copy Markdown
Contributor Author

I've resolved a merge conflict which was caused by changes from PR #1671 (feat/search-highlight) that was merged into main. The conflict was in the split transaction rendering code that was modified by both PRs. The changes from this PR have been preserved and are now compatible with the merged PR. Please review the resolution when you have a chance to ensure all changes are correctly merged. Thank you!

@jjmata
Copy link
Copy Markdown
Collaborator

jjmata commented May 5, 2026

I've resolved a merge conflict which was caused by changes from PR #1671 (feat/search-highlight) that was merged into main. The conflict was in the split transaction rendering code that was modified by both PRs. The changes from this PR have been preserved and are now compatible with the merged PR. Please review the resolution when you have a chance to ensure all changes are correctly merged. Thank you!

So that was actually a "no human interaction" merge! 🙄

I'm backing it out and you will probably have to update here as well, @CrossDrain. Sorry.

@jjmata jjmata removed the contributor:flagged Contributor flagged for review by trust analysis. label May 5, 2026
@CrossDrain
Copy link
Copy Markdown
Contributor Author

@jjmata Oh okay, thanks for letting me know, no worries! Should I roll back my merge as well, or should I just let it as is for it to be ready to merge if the feat/search-highlight PR gets re-opened and merged first?

@jjmata
Copy link
Copy Markdown
Collaborator

jjmata commented May 6, 2026

@jjmata Oh okay, thanks for letting me know, no worries! Should I roll back my merge as well, or should I just let it as is for it to be ready to merge if the feat/search-highlight PR gets re-opened and merged first?

Roll it back, please. Not planning on taking those other PRs until they are changed for Design System-type support. That way you can also take care of the current conflict with main

Copy link
Copy Markdown

@sure-design sure-design left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review — independent read of the current diff

The feedback from the previous round was substantially addressed. Here's what I confirmed is fixed and what still needs attention before merge.


✅ Previously-raised issues — confirmed resolved

Issue Fix confirmed
"false" || false evaluates to truthy string (every child marked excluded) TRUTHY_VALUES.include?(split_attrs[:excluded]) — correctly returns false for nil, "false", 0, etc.
nil stored as NULL for excluded column Same fix; TRUTHY_VALUES.include?(nil)false, always writes true/false
ActiveModel::Type::Boolean.new allocated per loop iteration Hoisted to frozen TRUTHY_VALUES constant with private_constant
Missing tests for excluded 3 model tests (boolean persistence, string casting, balance filtering) + 1 controller test — all present
in_split_group logic duplicated across two controllers Extracted to TransactionsHelper#in_split_group?; both controllers call helpers.in_split_group?
hidden_field_tag :grouped, nil submitting empty param All hidden fields now use hidden_field_tag :grouped, "true" if params[:grouped] == "true"
f.hidden_field :grouped inconsistency Changed to hidden_field_tag throughout

🔴 Blocker — merge conflict, rollback requested

mergeable_state is dirty. @jjmata explicitly asked on 2026-05-06 for the feat/search-highlight merge to be rolled back and the conflict with main resolved properly. That hasn't happened yet — this PR can't merge as-is.


🟡 update action untested for excluded

splits_controller.rb threads excluded through both create (line 22) and update (line 54), but only the create path has a test. A regression in the update path would go undetected. Please add a parallel test alongside the existing create test:

test "update with excluded parameter sets child as excluded" do
  @entry.split!([
    { name: "Groceries", amount: 70, category_id: nil },
    { name: "Household", amount: 30, category_id: nil }
  ])

  patch transaction_split_path(@entry), params: {
    split: {
      splits: [
        { name: "Groceries", amount: "-70", category_id: "", excluded: "true" },
        { name: "Household", amount: "-30", category_id: "", excluded: "false" }
      ]
    }
  }

  assert_redirected_to transactions_url
  children = @entry.child_entries.order(:amount)
  refute children.first.excluded?
  assert children.last.excluded?
end

🟢 ?grouped=false appended to every non-split transaction URL

In app/views/transactions/_transaction.html.erb:74:

entry_path(entry, grouped: in_split_group)

When in_split_group is false (the common case), this produces ?grouped=false on every transaction link. Behavior is correct (the controller checks params[:grouped] == "true"), but it's URL noise. Quick fix:

in_split_group ? entry_path(entry, grouped: true) : entry_path(entry)

🟢 Comment on helper violates project convention

# Returns true if the entry is a split child and the user has split grouping enabled.
# Used to determine whether to show grouped split display.
def in_split_group?(entry, params_grouped)

Per CLAUDE.md: "Default to writing no comments. Only add one when the WHY is non-obvious." This comment explains what the method does (which the name already communicates), not why it exists. Please remove it.


Balance math — design confirmed sound

Excluding an individual split child intentionally under-counts that portion of the transaction from balances. This is the stated use case ("track separately but not count toward totals") and is confirmed by the new model test. No issue here.


Generated by Claude Code


Generated by Claude Code

@brin-security-scanner brin-security-scanner Bot added the contributor:verified Contributor passed trust analysis. label May 6, 2026
@CrossDrain CrossDrain force-pushed the split-children-exclusion-support-and-improced-split-group-rendering branch from f16caf5 to 0d514d7 Compare May 6, 2026 21:39
@CrossDrain CrossDrain force-pushed the split-children-exclusion-support-and-improced-split-group-rendering branch from e475dd9 to 2e6d8de Compare May 6, 2026 22:03
@CrossDrain
Copy link
Copy Markdown
Contributor Author

@jjmata

Apologies for all the confusion and the messy commit history!

I got a little confused with all the merges, which led to an even more confusing loop of conflict resolutions and reverts. To clean everything up and ensure the branch is in a perfect state, I force-pushed it back to 0d514d7, which was the exact clean state BEFORE resolving conflicts with the bot's rogue merge. I then addressed @sure-design's points.

So, currently all of my original changes are intact, and I have just pushed one new commit (2e6d8de) that addresses the three points raised in the review:

  1. Added update action test for excluded splits: Updated splits_controller_test.rb with a parallel test to ensure that the excluded parameter correctly sets the excluded flag on child entries during the update action, preventing any undetected regressions.
  2. Cleaned up ?grouped=false URL noise: Updated the transaction partial (_transaction.html.erb) to use a ternary operator (in_split_group ? entry_path(entry, grouped: true) : entry_path(entry)), so ?grouped=false is no longer needlessly appended to the URL for standard transactions.
  3. Removed unnecessary helper comment: Removed the comment above the in_split_group? helper in transactions_helper.rb to adhere strictly to the project convention (CLAUDE.md) of not writing comments that simply restate what the method does.

I also merged main to this branch and it is now completely in sync and ready to be merged! If you need any further clarification feel free to reach out to me here on GitHub or on Discord.

@CrossDrain CrossDrain requested review from jjmata and sure-design May 6, 2026 22:19
@jjmata jjmata merged commit 0b7fa73 into we-promise:main May 9, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor:verified Contributor passed trust analysis. pr:verified PR passed security analysis.

Development

Successfully merging this pull request may close these issues.

3 participants