Skip to content

Conversation

@sokie
Copy link
Collaborator

@sokie sokie commented Nov 25, 2025

1.1 Investment Dashboard Widget

  • New section on dashboard showing portfolio value, total return, and top 5 holdings
  • Located between outflows and net worth charts

2.1 InvestmentStatement Model (Parallel Tracking)

  • app/models/investment_statement.rb - Core model for investment analytics
  • app/models/investment_statement/totals.rb - SQL aggregation for trade data
  • Tracks contributions, withdrawals, portfolio value, unrealized gains

2.2 Enhanced Category System

  • Added 4 new investment categories: Investment Contributions, Dividends, Capital Gains, Investment Interest
  • Added INVESTMENT_COLOR constant and investments scope to Category model

2.3 Cashflow View Toggle

  • Checkbox on cashflow sankey: "Include investments"
  • When enabled, shows Investment Contributions (outflows) and Investment Liquidations (inflows)

1.3 Investment Performance in Reports

  • New section in Reports page showing portfolio metrics
  • Displays: portfolio value, total return, period contributions/withdrawals
  • Top 5 holdings table with return percentages
  • Investment accounts summary
Screenshot 2025-11-25 at 14 39 02 Screenshot 2025-11-25 at 14 39 14 Screenshot 2025-11-25 at 14 39 30

Summary by CodeRabbit

  • New Features

    • Dashboard: investment summary panel showing portfolio value, unrealized gains, top holdings, and activity.
    • Reports: investment performance section with portfolio metrics, top holdings and account summaries.
    • Trades: category selector in trade forms and category-aware badges; trades can be associated with categories.
  • UX

    • Improved touch support for draggable dashboard and reports sections.
    • Updated cashflow visuals and surplus presentation.
  • Localization

    • Added translations for investment UI.
  • Documentation

    • Added Investment & Holdings integration assessment.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Nov 25, 2025

Walkthrough

Adds investment analytics and UI: new InvestmentStatement service and Totals SQL helper, Family accessor, dashboard and reports partials, trade→category wiring plus migration/schema changes, IncomeStatement totals extended to include trades, and PagesController cashflow sankey signature/logic updates.

Changes

Cohort / File(s) Summary
Investment statement & totals
app/models/investment_statement.rb, app/models/investment_statement/totals.rb
New InvestmentStatement class (portfolio value, holdings, allocation, trends, period totals) and InvestmentStatement::Totals SQL aggregator for contributions/withdrawals/trades.
Family accessor
app/models/family.rb
Adds memoized def investment_statement.
Category model & defaults
app/models/category.rb
Adds has_many :trades, dependent: :nullify, INVESTMENT_COLOR, and inserts "Investment Contributions" into default_categories.
Trade model & forms
app/models/trade.rb, app/models/trade/create_form.rb
Trade gains belongs_to :category, optional: true and buy?/sell? predicates; Trade::CreateForm assigns an investment category for buy trades.
Trades params & views
app/controllers/trades_controller.rb, app/views/trades/_trade.html.erb, app/views/trades/show.html.erb, config/locales/views/trades/en.yml
Permits :category_id in nested entry params; badge falls back to `trade.category
PagesController cashflow & dashboard
app/controllers/pages_controller.rb, app/views/pages/dashboard.html.erb, app/views/pages/dashboard/_investment_summary.html.erb, config/locales/views/pages/en.yml
build_cashflow_sankey_data signature renamed to accept currency (now derives symbol internally); sankey node logic refactored (income/expense node percentages, surplus node); dashboard exposes @investment_statement and adds investment summary partial; touch/drag reorder handlers moved to section.
ReportsController & views
app/controllers/reports_controller.rb, app/views/reports/_investment_performance.html.erb, app/views/reports/index.html.erb, config/locales/views/reports/en.yml
Adds build_investment_metrics, sets @investment_metrics in index (duplicate method definitions present), and introduces investment_performance partial; touch handling adjusted for sortable sections.
IncomeStatement totals extended
app/models/income_statement/totals.rb
IncomeStatement::Totals#initialize adds include_trades: true; can combine transactions and trades via UNION ALL and extracts date range for trade queries.
DB migration & schema
db/migrate/20251125141213_add_category_to_trades.rb, db/schema.rb
Migration adds nullable category_id FK to trades; schema updated (trades.category_id index and FK, new rule_runs table).
Docs, locales & tests
docs/roadmap/investment-integration-assessment.md, config/locales/views/pages/en.yml, config/locales/views/reports/en.yml, test/controllers/categories_controller_test.rb
New investment assessment doc; new locale keys for dashboard/reports; test expectation updated for default category count.

Sequence Diagram(s)

sequenceDiagram
    participant Pages as PagesController#dashboard
    participant Reports as ReportsController#index
    participant Family as Family
    participant InvStmt as InvestmentStatement
    participant Totals as InvestmentStatement::Totals
    participant DB as Database

    Pages->>Family: request investment_statement (memoized)
    Reports->>Family: request investment_statement (via `@investment_metrics`)
    Family->>InvStmt: InvestmentStatement.new(self)
    Pages->>InvStmt: request totals/portfolio_value/top_holdings(period)
    InvStmt->>DB: query investment_accounts, holdings, balances
    DB-->>InvStmt: account & holding rows

    rect rgb(230,240,255)
    InvStmt->>Totals: initialize(trades_scope) & call
    Totals->>DB: execute aggregation SQL (contributions, withdrawals, trades_count, exchange rates)
    DB-->>Totals: aggregated results
    Totals-->>InvStmt: PeriodTotals
    end

    InvStmt-->>Pages: PeriodTotals, portfolio_value, top_holdings
    Pages->>Pages: render _investment_summary with investment data
    Reports->>Reports: build_investment_metrics -> render _investment_performance
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Potential hotspots:

  • app/models/investment_statement.rb — aggregation math, Money usage, trend logic, eager-loading and N+1 risks.
  • app/models/investment_statement/totals.rb — raw SQL, parameter sanitization, exchange-rate joins and caching.
  • app/models/income_statement/totals.rb — UNION ALL combining transactions and trades, date-range extraction heuristics.
  • app/controllers/pages_controller.rb — sankey signature/logic and currency handling.
  • Duplicate build_investment_metrics definitions in app/controllers/reports_controller.rb.
  • DB migration & schema — migration guard, FK/index correctness, and existing-data implications.

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • jjmata

Poem

🐇
I hopped through ledgers, counted seeds and trades,
Turned whispers of numbers into portfolio parades.
Holdings hum softly, gains peek and play,
Carrots of data brighten the day.
Thump—investments now find their way!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.38% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add investment tracking to expenses' accurately summarizes the main feature addition in the PR, which introduces investment tracking capabilities throughout the application including dashboard, models, categories, and reports.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 442b1b0 and 239b98b.

📒 Files selected for processing (1)
  • db/migrate/20251125141213_add_category_to_trades.rb (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • db/migrate/20251125141213_add_category_to_trades.rb
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ci / test

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.

Copy link

@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.

Actionable comments posted: 4

🧹 Nitpick comments (11)
docs/roadmap/investment-integration-assessment.md (1)

1-454: Excellent strategic documentation that grounds the PR in broader architectural context.

This roadmap document clearly articulates the current gaps in investment integration, provides a well-structured root-cause analysis, and outlines a pragmatic multi-tiered implementation plan. The technical architecture reference section is particularly valuable for understanding how Holdings, Trades, and Entries fit into the existing balance calculation flow.

The PR implementation aligns well with Tier 1: Quick Wins (dashboard widget, investment categories, cashflow toggle, InvestmentStatement model) and establishes a clear foundation for future Tier 2–4 improvements. This document should serve as the primary reference for future investment-related PRs.

Minor enhancement suggestion: Consider adding a "Status by Tier" table at the end to track which items from each tier have been completed and when, making it easier to reference implementation progress in future PRs.

app/models/category.rb (1)

23-24: Investment category wiring is sound; consider reducing name-coupling

The new scopes, constants, defaults, and investment_category? predicate are internally consistent and match each other. The only downside is the tight coupling to exact name strings:

  • Changing any of the four investment category names would silently break investments, non_investment, and investment_category?.
  • INVESTMENT_COLOR is defined but the default categories still inline the hex value.

If you expect these names to remain stable, this is fine. Otherwise, consider a dedicated boolean column or a classification enum flag for investment categories, or at least reuse INVESTMENT_COLOR in default_categories to avoid duplication.

Also applies to: 32-39, 117-122, 148-150

app/views/pages/dashboard.html.erb (1)

35-37: Ensure @investment_statement is always initialized for this view

Passing period/include_investments into the Sankey partial looks correct, and the investment summary section is well‑placed between outflows and net worth. The only thing to watch is:

  • @investment_statement.investment_accounts.any? will raise if @investment_statement is ever nil.

As long as the dashboard controller unconditionally sets @investment_statement = Current.family.investment_statement on all paths that render this view, you’re fine; otherwise consider guarding with safe navigation or an early return in the controller.

Also applies to: 49-56

app/views/pages/dashboard/_cashflow_sankey.html.erb (1)

1-1: Include‑investments toggle wiring looks good; verify param casting in controller

The locals signature and the auto‑submit group (checkbox + period select) are wired cleanly. With:

form.check_box :include_investments,
  { checked: include_investments, data: { "auto-submit-form-target": "auto" } },
  "true", "false"

Rails will submit "true" / "false" as strings. Make sure the dashboard controller casts this param explicitly (e.g., via ActiveModel::Type::Boolean.new.cast(params[:include_investments]) or a simple string comparison) so @include_investments is a real boolean and the Sankey data builder behaves as expected across requests.

Also applies to: 9-22

app/controllers/reports_controller.rb (1)

40-42: Investment metrics builder is well-structured and view-friendly

build_investment_metrics cleanly encapsulates the investment data shape for the view:

  • Early { has_investments: false } return keeps the view simple and avoids nil checks.
  • Uses the same @period as the rest of the report, so contributions/withdrawals align with other metrics.
  • Returns Money objects for monetary fields, matching the app’s money handling.

If you later see performance pressure here, this would be a good spot to layer in caching via Family#build_cache_key, but what you have now is perfectly reasonable for a first iteration.

Also applies to: 356-373

app/views/pages/dashboard/_investment_summary.html.erb (2)

19-22: Consider moving Money object creation to the model layer.

Creating Money.new(trend.value, Current.family.currency) in the view adds domain logic to the template. The unrealized_gains_trend method could return a trend with a pre-formatted money value, similar to how portfolio_value_money is exposed. As per coding guidelines, domain logic should be kept out of view templates.


42-47: Add nil guards for security logo and ticker fallback.

If holding.security is nil or holding.ticker is empty, this code could raise an error. Consider adding defensive checks.

-              <% if holding.security.logo_url.present? %>
-                <img src="<%= holding.security.logo_url %>" alt="<%= holding.ticker %>" class="w-8 h-8 rounded-full">
+              <% if holding.security&.logo_url.present? %>
+                <img src="<%= holding.security.logo_url %>" alt="<%= holding.ticker.presence || 'Security' %>" class="w-8 h-8 rounded-full">
               <% else %>
                 <div class="w-8 h-8 rounded-full bg-container-inset flex items-center justify-center text-xs font-medium text-secondary">
-                  <%= holding.ticker[0..1] %>
+                  <%= holding.ticker&.slice(0, 2) || '--' %>
                 </div>
               <% end %>
app/views/reports/_investment_performance.html.erb (2)

27-30: Same Money object creation pattern in view.

Similar to the dashboard partial, Money.new(investment_metrics[:unrealized_trend].value, Current.family.currency) creates domain objects in the view. Consider pre-computing this in build_investment_metrics in the controller.


60-60: Add nil guards for optional hash keys.

If investment_metrics[:top_holdings] or investment_metrics[:accounts] is nil rather than an empty array, calling .any? will raise a NoMethodError. Consider using safe navigation.

-    <% if investment_metrics[:top_holdings].any? %>
+    <% if investment_metrics[:top_holdings]&.any? %>
-    <% if investment_metrics[:accounts].any? %>
+    <% if investment_metrics[:accounts]&.any? %>

Also applies to: 116-116

app/controllers/pages_controller.rb (1)

118-121: Hardcoded strings should use i18n.

"Investment Liquidations" and "Investment Contributions" are user-facing strings that should be localized via t() per coding guidelines.

         inv_income_idx = add_node.call(
           "investment_liquidations",
-          "Investment Liquidations",
+          t("pages.dashboard.cashflow_sankey.investment_liquidations"),
           investment_withdrawals,
         inv_expense_idx = add_node.call(
           "investment_contributions",
-          "Investment Contributions",
+          t("pages.dashboard.cashflow_sankey.investment_contributions"),
           investment_contributions,

Also applies to: 168-171

app/models/investment_statement.rb (1)

149-159: Consider moving Data.define structs to module scope.

PeriodTotals and HoldingAllocation are returned by public methods but defined in the private section. While this works (constants aren't affected by private), moving them above the public methods would improve discoverability and align with conventional Ruby patterns.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b4331f3 and 19597b7.

📒 Files selected for processing (14)
  • app/controllers/pages_controller.rb (7 hunks)
  • app/controllers/reports_controller.rb (2 hunks)
  • app/models/category.rb (3 hunks)
  • app/models/family.rb (1 hunks)
  • app/models/investment_statement.rb (1 hunks)
  • app/models/investment_statement/totals.rb (1 hunks)
  • app/views/pages/dashboard.html.erb (2 hunks)
  • app/views/pages/dashboard/_cashflow_sankey.html.erb (1 hunks)
  • app/views/pages/dashboard/_investment_summary.html.erb (1 hunks)
  • app/views/reports/_investment_performance.html.erb (1 hunks)
  • app/views/reports/index.html.erb (1 hunks)
  • config/locales/views/pages/en.yml (2 hunks)
  • config/locales/views/reports/en.yml (1 hunks)
  • docs/roadmap/investment-integration-assessment.md (1 hunks)
🧰 Additional context used
📓 Path-based instructions (25)
{app/**/*.{erb,html},app/javascript/**/*.js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Prefer Hotwire-first approach: use Turbo + Stimulus for reactive UI without heavy JavaScript

Files:

  • app/views/pages/dashboard.html.erb
  • app/views/reports/_investment_performance.html.erb
  • app/views/pages/dashboard/_investment_summary.html.erb
  • app/views/reports/index.html.erb
  • app/views/pages/dashboard/_cashflow_sankey.html.erb
app/views/**/*.erb

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

app/views/**/*.erb: Always use the icon helper for icons in views, never use lucide_icon directly
Use Tailwind design tokens (e.g., text-primary, bg-container, border-secondary) instead of raw color names
Pass data to Stimulus controllers via data-*-value attributes instead of inline JavaScript
Prefer semantic HTML elements (dialog, details, summary) over JavaScript components
Use Turbo Frames for page sections instead of client-side solutions
Use server-side formatting for currencies, numbers, and dates instead of client-side

Views should use ERB and avoid heavy logic; prefer helpers and components instead (ERB checked by erb-lint per .erb_lint.yml)

Files:

  • app/views/pages/dashboard.html.erb
  • app/views/reports/_investment_performance.html.erb
  • app/views/pages/dashboard/_investment_summary.html.erb
  • app/views/reports/index.html.erb
  • app/views/pages/dashboard/_cashflow_sankey.html.erb
**/*.{css,erb}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use Tailwind CSS v4.x with custom design system for styling

Files:

  • app/views/pages/dashboard.html.erb
  • app/views/reports/_investment_performance.html.erb
  • app/views/pages/dashboard/_investment_summary.html.erb
  • app/views/reports/index.html.erb
  • app/views/pages/dashboard/_cashflow_sankey.html.erb
**/*.{rb,erb,html.erb}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{rb,erb,html.erb}: Use Current.user for the current user context in controllers and views. Do NOT use current_user.
Use Current.family for the current family context. Do NOT use current_family.

Files:

  • app/views/pages/dashboard.html.erb
  • app/views/reports/_investment_performance.html.erb
  • app/controllers/reports_controller.rb
  • app/models/family.rb
  • app/views/pages/dashboard/_investment_summary.html.erb
  • app/views/reports/index.html.erb
  • app/views/pages/dashboard/_cashflow_sankey.html.erb
  • app/controllers/pages_controller.rb
  • app/models/category.rb
  • app/models/investment_statement.rb
  • app/models/investment_statement/totals.rb
**/*.{erb,html,vue,jsx,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{erb,html,vue,jsx,tsx}: Use Tailwind CSS v4.x with custom design system defined in app/assets/tailwind/maybe-design-system.css. Always use functional tokens (e.g., text-primary not text-white).
Prefer semantic HTML elements over JavaScript components (e.g., use <dialog> for modals, <details><summary> for disclosures).

Files:

  • app/views/pages/dashboard.html.erb
  • app/views/reports/_investment_performance.html.erb
  • app/views/pages/dashboard/_investment_summary.html.erb
  • app/views/reports/index.html.erb
  • app/views/pages/dashboard/_cashflow_sankey.html.erb
**/*.{erb,html.erb}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{erb,html.erb}: Use icon helper for icons, never use lucide_icon directly.
Leverage Turbo frames for page sections over client-side solutions.
Pass data to Stimulus controllers via data-*-value attributes, not inline JavaScript.

**/*.{erb,html.erb}: Use ViewComponents when: element has complex logic or styling patterns, will be reused across multiple views/contexts, needs structured styling with variants/sizes, requires interactive behavior or Stimulus controllers, has configurable slots or complex APIs, or needs accessibility features or ARIA support
Use Partials when: element is primarily static HTML with minimal logic, used in only one or few specific contexts, is simple template content, doesn't need variants/sizes/complex configuration, or is more about content organization than reusable functionality
Prefer components over partials: if a component is available in app/components, use it; if not, look for a partial; if neither exists, decide between component or partial based on complexity and reusability criteria
Keep domain logic out of views: compute values like button classes, conditional logic, and data transformations in the component file, not the template file
Always use the declarative approach when integrating Stimulus controllers in views: the ERB template should declare what happens using data-* attributes, and the Stimulus controller should respond
Component controllers in app/components/ should only be used within their component templates; global controllers in app/javascript/controllers/ can be used across any view
Pass data from Rails to Stimulus using data-*-value attributes, not inline JavaScript

Files:

  • app/views/pages/dashboard.html.erb
  • app/views/reports/_investment_performance.html.erb
  • app/views/pages/dashboard/_investment_summary.html.erb
  • app/views/reports/index.html.erb
  • app/views/pages/dashboard/_cashflow_sankey.html.erb
**/*.{erb,html.erb,rb}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{erb,html.erb,rb}: All user-facing strings must use localization (i18n) via the t() helper. Update locale files for each new or changed element.
Use server-side formatting for currencies, numbers, and dates. Do not format in JavaScript.

Files:

  • app/views/pages/dashboard.html.erb
  • app/views/reports/_investment_performance.html.erb
  • app/controllers/reports_controller.rb
  • app/models/family.rb
  • app/views/pages/dashboard/_investment_summary.html.erb
  • app/views/reports/index.html.erb
  • app/views/pages/dashboard/_cashflow_sankey.html.erb
  • app/controllers/pages_controller.rb
  • app/models/category.rb
  • app/models/investment_statement.rb
  • app/models/investment_statement/totals.rb
{app/components/**/*.html.erb,app/views/**/*.{erb,html.erb}}

📄 CodeRabbit inference engine (CLAUDE.md)

Keep domain logic out of view templates. Logic belongs in component files, not template files.

Files:

  • app/views/pages/dashboard.html.erb
  • app/views/reports/_investment_performance.html.erb
  • app/views/pages/dashboard/_investment_summary.html.erb
  • app/views/reports/index.html.erb
  • app/views/pages/dashboard/_cashflow_sankey.html.erb
**/*.erb

📄 CodeRabbit inference engine (CLAUDE.md)

Run bundle exec erb_lint ./app/**/*.erb -a for ERB linting with auto-correct before pull requests.

Files:

  • app/views/pages/dashboard.html.erb
  • app/views/reports/_investment_performance.html.erb
  • app/views/pages/dashboard/_investment_summary.html.erb
  • app/views/reports/index.html.erb
  • app/views/pages/dashboard/_cashflow_sankey.html.erb
**/*.{rb,erb}

📄 CodeRabbit inference engine (.cursor/rules/general-rules.mdc)

**/*.{rb,erb}: Use Current.user for accessing the current user. Do NOT use current_user
Use Current.family for accessing the current family. Do NOT use current_family

Files:

  • app/views/pages/dashboard.html.erb
  • app/views/reports/_investment_performance.html.erb
  • app/controllers/reports_controller.rb
  • app/models/family.rb
  • app/views/pages/dashboard/_investment_summary.html.erb
  • app/views/reports/index.html.erb
  • app/views/pages/dashboard/_cashflow_sankey.html.erb
  • app/controllers/pages_controller.rb
  • app/models/category.rb
  • app/models/investment_statement.rb
  • app/models/investment_statement/totals.rb
**/*.html.erb

📄 CodeRabbit inference engine (.cursor/rules/project-conventions.mdc)

**/*.html.erb: Prefer native HTML elements over JavaScript-based components. Use semantic HTML: for modals,

for disclosures
Leverage Turbo frames to break up pages instead of JavaScript-driven client-side solutions
Use the icon helper from application_helper.rb for icons. Never use lucide_icon helper directly

Files:

  • app/views/pages/dashboard.html.erb
  • app/views/reports/_investment_performance.html.erb
  • app/views/pages/dashboard/_investment_summary.html.erb
  • app/views/reports/index.html.erb
  • app/views/pages/dashboard/_cashflow_sankey.html.erb
**/*.{rb,js,erb}

📄 CodeRabbit inference engine (.cursor/rules/project-conventions.mdc)

Format currencies, numbers, dates, and other values server-side, then pass to Stimulus controllers for display only

Files:

  • app/views/pages/dashboard.html.erb
  • app/views/reports/_investment_performance.html.erb
  • app/controllers/reports_controller.rb
  • app/models/family.rb
  • app/views/pages/dashboard/_investment_summary.html.erb
  • app/views/reports/index.html.erb
  • app/views/pages/dashboard/_cashflow_sankey.html.erb
  • app/controllers/pages_controller.rb
  • app/models/category.rb
  • app/models/investment_statement.rb
  • app/models/investment_statement/totals.rb
**/*.{rb,html.erb}

📄 CodeRabbit inference engine (.cursor/rules/project-conventions.mdc)

Use ActiveRecord validations for complex validations and business logic. Simple validations may be mirrored in ActiveRecord for form error handling convenience, but prioritize client-side form validation when possible

Files:

  • app/views/pages/dashboard.html.erb
  • app/views/reports/_investment_performance.html.erb
  • app/controllers/reports_controller.rb
  • app/models/family.rb
  • app/views/pages/dashboard/_investment_summary.html.erb
  • app/views/reports/index.html.erb
  • app/views/pages/dashboard/_cashflow_sankey.html.erb
  • app/controllers/pages_controller.rb
  • app/models/category.rb
  • app/models/investment_statement.rb
  • app/models/investment_statement/totals.rb
{app/views/**,app/helpers/**,app/javascript/controllers/**}

📄 CodeRabbit inference engine (.cursor/rules/ui-ux-design-guidelines.mdc)

{app/views/**,app/helpers/**,app/javascript/controllers/**}: Reference maybe-design-system.css for base primitives, functional tokens, and component tokens before writing styles
Prefer using functional design system tokens (e.g., text-primary, bg-container, border-primary) from maybe-design-system.css instead of raw Tailwind color values

Files:

  • app/views/pages/dashboard.html.erb
  • app/views/reports/_investment_performance.html.erb
  • app/views/pages/dashboard/_investment_summary.html.erb
  • app/views/reports/index.html.erb
  • app/views/pages/dashboard/_cashflow_sankey.html.erb
{app/views/**,app/helpers/**}

📄 CodeRabbit inference engine (.cursor/rules/ui-ux-design-guidelines.mdc)

Always generate semantic HTML

Files:

  • app/views/pages/dashboard.html.erb
  • app/views/reports/_investment_performance.html.erb
  • app/views/pages/dashboard/_investment_summary.html.erb
  • app/views/reports/index.html.erb
  • app/views/pages/dashboard/_cashflow_sankey.html.erb
config/locales/**/*.yml

📄 CodeRabbit inference engine (CLAUDE.md)

Use hierarchical i18n keys by feature: accounts.index.title, transactions.form.amount_label. Use t() helper for all user-facing strings with interpolation for dynamic content.

Files:

  • config/locales/views/reports/en.yml
  • config/locales/views/pages/en.yml
config/**/*.{rb,yml,yaml}

📄 CodeRabbit inference engine (AGENTS.md)

Configuration files should be placed in config/ directory, with environment examples in .env.local.example and .env.test.example

Files:

  • config/locales/views/reports/en.yml
  • config/locales/views/pages/en.yml
app/views/**/_*.html.erb

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use partials only for simple, static HTML with minimal logic in specific contexts

app/views/**/_*.html.erb: Use underscore prefix for partial files (e.g., _trend_change.html.erb, _form_errors.html.erb, _sync_indicator.html.erb)
Place context-specific partials in relevant controller view directory (e.g., accounts/_account_sidebar_tabs.html.erb)

Files:

  • app/views/reports/_investment_performance.html.erb
  • app/views/pages/dashboard/_investment_summary.html.erb
  • app/views/pages/dashboard/_cashflow_sankey.html.erb
app/views/**/_*.{erb,html.erb}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Partials when element is primarily static HTML with minimal logic, used in only one or few contexts, is simple template content, or doesn't need variants/sizes/complex configuration.

Files:

  • app/views/reports/_investment_performance.html.erb
  • app/views/pages/dashboard/_investment_summary.html.erb
  • app/views/pages/dashboard/_cashflow_sankey.html.erb
**/*.rb

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.rb: Application supports two modes: 'managed' and 'self_hosted' via Rails.application.config.app_mode
Use Current.user and Current.family instead of current_user / current_family for authentication context
Optimize database queries with proper indexes to prevent N+1 queries using includes/joins

**/*.rb: Use Rails pluralization in i18n: t("transactions.count", count: @transactions.count).
Run bin/rubocop -f github -a for Ruby linting with auto-correct before pull requests.

**/*.rb: Ruby code should use 2-space indent, snake_case for methods and variables, and CamelCase for classes and modules, following Rails conventions for folders and file names
Prefer environment variables over hard-coded values for configuration

Files:

  • app/controllers/reports_controller.rb
  • app/models/family.rb
  • app/controllers/pages_controller.rb
  • app/models/category.rb
  • app/models/investment_statement.rb
  • app/models/investment_statement/totals.rb
app/controllers/**/*.rb

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use Rails strong parameters and CSRF protection throughout the application

app/controllers/**/*.rb: Use strong parameters and CSRF protection throughout the application.
Use query params for state over localStorage/sessions.

Files:

  • app/controllers/reports_controller.rb
  • app/controllers/pages_controller.rb
app/**/*.{rb,js,ts,jsx,tsx,css,scss}

📄 CodeRabbit inference engine (AGENTS.md)

Code should be organized in app/ directory (Rails MVC, services, jobs, mailers, components), with JS in app/javascript/, and styles/assets in app/assets/ (Tailwind, images, fonts)

Files:

  • app/controllers/reports_controller.rb
  • app/models/family.rb
  • app/controllers/pages_controller.rb
  • app/models/category.rb
  • app/models/investment_statement.rb
  • app/models/investment_statement/totals.rb
app/**/*.rb

📄 CodeRabbit inference engine (AGENTS.md)

Run bin/brakeman security scan before major PRs to check for static analysis of common Rails issues

Files:

  • app/controllers/reports_controller.rb
  • app/models/family.rb
  • app/controllers/pages_controller.rb
  • app/models/category.rb
  • app/models/investment_statement.rb
  • app/models/investment_statement/totals.rb
app/models/**/*.rb

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

app/models/**/*.rb: Keep business logic in models using skinny controllers and fat models pattern
Store all monetary values in base currency (user's primary currency) and use Money objects for conversion
Use ActiveRecord validations for form convenience and complex business logic validations
Use Rails concerns and POROs for organizing business logic instead of services folder

app/models/**/*.rb: All monetary values stored in base currency (user's primary currency). Use Money objects for currency conversion and formatting.
Place business logic in app/models/ folder. Avoid creating app/services/ folder. Use Rails concerns and POROs for organization.
Models should answer questions about themselves using instance methods: use account.balance_series not AccountSeries.new(account).call.

Files:

  • app/models/family.rb
  • app/models/category.rb
  • app/models/investment_statement.rb
  • app/models/investment_statement/totals.rb
{app/models/**/*.rb,db/migrate/**/*.rb}

📄 CodeRabbit inference engine (CLAUDE.md)

Simple validations (null checks, unique indexes) should be in the database. ActiveRecord validations for form convenience (prefer client-side when possible). Complex validations and business logic in ActiveRecord.

Files:

  • app/models/family.rb
  • app/models/category.rb
  • app/models/investment_statement.rb
  • app/models/investment_statement/totals.rb
🧠 Learnings (13)
📚 Learning: 2025-11-24T16:55:43.046Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T16:55:43.046Z
Learning: Applies to config/locales/**/*.yml : Use hierarchical i18n keys by feature: `accounts.index.title`, `transactions.form.amount_label`. Use `t()` helper for all user-facing strings with interpolation for dynamic content.

Applied to files:

  • config/locales/views/reports/en.yml
  • config/locales/views/pages/en.yml
📚 Learning: 2025-11-24T16:57:20.307Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .cursor/rules/view_conventions.mdc:0-0
Timestamp: 2025-11-24T16:57:20.307Z
Learning: Applies to app/views/**/_*.html.erb : Place context-specific partials in relevant controller view directory (e.g., accounts/_account_sidebar_tabs.html.erb)

Applied to files:

  • app/views/reports/_investment_performance.html.erb
  • app/views/pages/dashboard/_investment_summary.html.erb
📚 Learning: 2025-09-23T22:23:22.963Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .cursor/rules/view_conventions.mdc:0-0
Timestamp: 2025-09-23T22:23:22.963Z
Learning: Applies to app/views/**/*.html.erb : Use partials when content is primarily static, simple, context-specific, and without variants or complex configuration

Applied to files:

  • app/views/reports/_investment_performance.html.erb
📚 Learning: 2025-11-24T16:54:59.185Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-24T16:54:59.185Z
Learning: Applies to app/views/**/_*.html.erb : Use partials only for simple, static HTML with minimal logic in specific contexts

Applied to files:

  • app/views/reports/_investment_performance.html.erb
📚 Learning: 2025-11-24T16:55:43.046Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T16:55:43.046Z
Learning: Applies to app/views/**/_*.{erb,html.erb} : Use Partials when element is primarily static HTML with minimal logic, used in only one or few contexts, is simple template content, or doesn't need variants/sizes/complex configuration.

Applied to files:

  • app/views/reports/_investment_performance.html.erb
📚 Learning: 2025-11-24T16:57:20.307Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .cursor/rules/view_conventions.mdc:0-0
Timestamp: 2025-11-24T16:57:20.307Z
Learning: Applies to app/views/**/_*.html.erb : Use underscore prefix for partial files (e.g., _trend_change.html.erb, _form_errors.html.erb, _sync_indicator.html.erb)

Applied to files:

  • app/views/reports/_investment_performance.html.erb
📚 Learning: 2025-11-24T16:57:20.307Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .cursor/rules/view_conventions.mdc:0-0
Timestamp: 2025-11-24T16:57:20.307Z
Learning: Applies to **/*.{erb,html.erb} : Use Partials when: element is primarily static HTML with minimal logic, used in only one or few specific contexts, is simple template content, doesn't need variants/sizes/complex configuration, or is more about content organization than reusable functionality

Applied to files:

  • app/views/reports/_investment_performance.html.erb
📚 Learning: 2025-11-24T16:55:43.046Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T16:55:43.046Z
Learning: Applies to **/*.{erb,html.erb,rb} : All user-facing strings must use localization (i18n) via the `t()` helper. Update locale files for each new or changed element.

Applied to files:

  • config/locales/views/pages/en.yml
📚 Learning: 2025-11-24T16:56:30.657Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .cursor/rules/project-design.mdc:0-0
Timestamp: 2025-11-24T16:56:30.657Z
Learning: Account sync must perform transfer auto-matching, calculate daily balance records from start_date to Date.current, calculate holdings, and optionally enrich transaction data

Applied to files:

  • app/controllers/reports_controller.rb
  • docs/roadmap/investment-integration-assessment.md
  • app/models/investment_statement.rb
📚 Learning: 2025-11-24T16:56:30.657Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .cursor/rules/project-design.mdc:0-0
Timestamp: 2025-11-24T16:56:30.657Z
Learning: Entry model is a Rails delegated type with three subtypes: Valuation (absolute account value), Transaction (balance modification), and Trade (buy/sell for investment accounts)

Applied to files:

  • app/controllers/reports_controller.rb
  • app/views/pages/dashboard/_investment_summary.html.erb
  • docs/roadmap/investment-integration-assessment.md
  • app/models/category.rb
  • app/models/investment_statement.rb
  • app/models/investment_statement/totals.rb
📚 Learning: 2025-11-24T16:55:43.046Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T16:55:43.046Z
Learning: Core domain model: User → Accounts → Transactions. Accounts support types: checking, savings, credit cards, investments, crypto, loans, properties. Transactions belong to Categories and may have Tags and Rules.

Applied to files:

  • docs/roadmap/investment-integration-assessment.md
📚 Learning: 2025-11-14T14:22:37.076Z
Learnt from: jjmata
Repo: we-promise/sure PR: 326
File: app/models/category_import.rb:10-10
Timestamp: 2025-11-14T14:22:37.076Z
Learning: In the CategoryImport model (app/models/category_import.rb), the import process implements a "last write wins" strategy where importing a category.csv file completely overrides existing category structures, including parent relationships. Setting category.parent = nil in the first pass is intentional behavior to ensure the CSV becomes the source of truth.

Applied to files:

  • app/models/category.rb
📚 Learning: 2025-11-24T16:56:30.657Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .cursor/rules/project-design.mdc:0-0
Timestamp: 2025-11-24T16:56:30.657Z
Learning: Account model is a Rails delegated type with subtypes: Depository, Investment, Crypto, Property, Vehicle, OtherAsset (assets) and CreditCard, Loan, OtherLiability (liabilities)

Applied to files:

  • app/models/category.rb
  • app/models/investment_statement.rb
🧬 Code graph analysis (4)
app/controllers/reports_controller.rb (2)
app/models/family.rb (1)
  • investment_statement (72-74)
app/models/investment_statement.rb (5)
  • investment_accounts (144-146)
  • totals (15-30)
  • portfolio_value_money (43-45)
  • unrealized_gains_trend (117-125)
  • top_holdings (88-90)
app/controllers/pages_controller.rb (2)
app/models/investment_statement.rb (1)
  • totals (15-30)
app/models/investment_statement/totals.rb (1)
  • call (7-17)
app/models/investment_statement.rb (4)
app/models/family.rb (1)
  • entries_cache_version (121-126)
app/models/concerns/monetizable.rb (1)
  • monetize (5-15)
app/models/investment_statement/totals.rb (3)
  • initialize (1-56)
  • initialize (2-5)
  • call (7-17)
app/models/period.rb (1)
  • date_range (133-135)
app/models/investment_statement/totals.rb (2)
app/models/category.rb (1)
  • initialize (52-55)
app/models/investment_statement.rb (1)
  • initialize (10-12)
🪛 markdownlint-cli2 (0.18.1)
docs/roadmap/investment-integration-assessment.md

43-43: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


163-163: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


169-169: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


398-398: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


430-430: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


444-444: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🔇 Additional comments (13)
config/locales/views/reports/en.yml (1)

109-120: Investment performance i18n block is consistent

Key structure and labels under reports.investment_performance look coherent and ready for use by the reports UI.

app/models/family.rb (1)

72-74: InvestmentStatement accessor matches existing pattern

investment_statement follows the same memoized pattern as balance_sheet and income_statement, which keeps call sites clean and avoids repeated instantiation.

app/views/reports/index.html.erb (1)

105-112: Investment performance block is correctly gated by metrics flag

Conditional rendering based on @investment_metrics[:has_investments] and passing the full metrics hash into the _investment_performance partial is a clean integration. Just ensure build_investment_metrics always returns a hash (never nil) so this guard is safe in all cases, which it currently does.

config/locales/views/pages/en.yml (1)

25-25: Dashboard investment i18n keys align with new UI

The added cashflow_sankey.include_investments label and the investment_summary block are well‑named, scoped under pages.dashboard, and match the expected dashboard investment summary fields.

Also applies to: 36-48

app/views/pages/dashboard/_investment_summary.html.erb (1)

1-100: LGTM overall - well-structured partial following conventions.

The partial correctly uses i18n via t(), server-side money formatting, and functional design tokens. The conditional rendering logic is clean and the layout follows the design system.

app/views/reports/_investment_performance.html.erb (1)

64-111: Good use of semantic table elements.

The top holdings section correctly uses semantic <table>, <thead>, <tbody>, and proper table row structure, which is better for accessibility than the flexbox approach in the dashboard partial.

app/controllers/pages_controller.rb (2)

8-21: LGTM - Clean integration of investment data into dashboard.

The investment statement integration follows good patterns: conditional data loading based on params, safe navigation for optional values, and clear separation between regular and investment-inclusive flows.


123-123: Category::INVESTMENT_COLOR constant is properly defined and correctly used.

The constant is defined in app/models/category.rb:32 with the value "#0d9488" and is correctly referenced at all specified locations in app/controllers/pages_controller.rb.

app/models/investment_statement/totals.rb (2)

1-56: LGTM - Well-structured SQL aggregation class.

The class follows good patterns: single responsibility for aggregation, proper SQL sanitization for parameters, and clear separation of query building. The comments explaining buy vs. sell logic are helpful.


30-47: No security concern identified—trades_scope is safe.

Verification confirms trades_scope always originates from controlled ActiveRecord queries within InvestmentStatement (lines 16–18). It's constructed entirely with AR builder methods (joins, where) and filtered only by internal date ranges—never from user input. When to_sql() is called on an AR relation, Rails maintains SQL parameterization internally, so the interpolation pattern is safe.

app/models/investment_statement.rb (3)

66-85: Currency filter may exclude foreign holdings.

The query filters holdings by currency: family.currency (line 74), which would exclude any holdings recorded in foreign currencies. If the portfolio contains international securities, this could underreport holdings. Verify this is intentional or if currency conversion should be applied instead.


93-107: LGTM - Clean allocation calculation.

The allocation method correctly computes weight percentages relative to total holdings value and wraps results in a structured HoldingAllocation object.


161-167: LGTM - Solid caching strategy.

The cache key combines family ID, SQL hash, and entries version, ensuring proper invalidation when data changes. Using MD5 for the SQL hash is appropriate for cache key generation.

class InvestmentStatement
include Monetizable

monetize :total_contributions, :total_dividends, :total_interest, :unrealized_gains
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify monetize macro matches actual method names.

The monetize macro references :total_contributions, :total_dividends, :total_interest, :unrealized_gains. However, I only see unrealized_gains defined as a method. The others appear to be missing, which would cause the generated *_money methods to fail.


🏁 Script executed:

#!/bin/bash
# Description: Verify total_contributions, total_dividends, total_interest methods exist
ast-grep --pattern 'def total_contributions'
ast-grep --pattern 'def total_dividends'
ast-grep --pattern 'def total_interest'

Length of output: 0



---

🏁 Script executed:

```shell
#!/bin/bash
# Description: View the investment_statement.rb file to understand current state
cat app/models/investment_statement.rb

Length of output: 4637


🏁 Script executed:

#!/bin/bash
# Description: Search for Monetizable concern to understand what monetize does
fd -e rb -path '*/concerns/*' -name '*monetizable*' -o -path '*/concerns/*' | head -20

Length of output: 168


🏁 Script executed:

#!/bin/bash
# Description: Search for total_contributions, total_dividends, total_interest usage in the file
rg -n 'total_contributions|total_dividends|total_interest' app/models/investment_statement.rb

Length of output: 87


🏁 Script executed:

#!/bin/bash
# Description: Find Monetizable concern definition
rg -n 'module Monetizable' --type rb

Length of output: 26


🏁 Script executed:

#!/bin/bash
# Description: Search for monetize macro usage in the codebase to understand patterns
rg -n 'monetize\s+:' --type rb -A2 | head -40

Length of output: 26



---

🏁 Script executed:

```shell
#!/bin/bash
# Description: Find Monetizable concern with different search approach
find . -type f -name "*.rb" | xargs grep -l "module Monetizable" | head -5

Length of output: 36


🏁 Script executed:

#!/bin/bash
# Description: Search for monetize macro usage across codebase with proper syntax
rg 'monetize\s+:' -C3 | head -60

Length of output: 2758


🏁 Script executed:

#!/bin/bash
# Description: Check if total_contributions, total_dividends, total_interest exist as methods elsewhere
rg 'def (total_contributions|total_dividends|total_interest)' -C2

Length of output: 0



---

🏁 Script executed:

```shell
#!/bin/bash
# Description: View Monetizable concern implementation
cat app/models/concerns/monetizable.rb

Length of output: 438


🏁 Script executed:

#!/bin/bash
# Description: View IncomeStatement to compare similar PORO pattern
cat app/models/income_statement.rb

Length of output: 4407


Add instance methods for total_contributions, total_dividends, and total_interest.

The monetize macro on line 6 references four methods, but only unrealized_gains is defined. The generated total_contributions_money, total_dividends_money, and total_interest_money methods will fail at runtime with NoMethodError because total_contributions, total_dividends, and total_interest don't exist as instance methods.

Compare with IncomeStatement, which correctly defines median_expense and median_income methods to match its monetize declaration. Add the three missing methods to InvestmentStatement returning numeric values, likely extracting them from the totals() method's PeriodTotals struct or as direct accessor methods.

🤖 Prompt for AI Agents
In app/models/investment_statement.rb around line 6, monetize references
total_contributions, total_dividends, and total_interest but those instance
methods are missing; add three instance methods named total_contributions,
total_dividends, and total_interest that return numeric values (e.g.,
BigDecimal/Integer/Float) so monetize can build the *_money helpers—implement
each to read the corresponding value from the existing totals() result (the
PeriodTotals struct or appropriate accessor) or compute it from available fields
and ensure they return 0 when nil to avoid NoMethodError.

Comment on lines +117 to +125
def unrealized_gains_trend
holdings = current_holdings.to_a
return nil if holdings.empty?

current = holdings.sum(&:amount)
previous = holdings.sum { |h| h.qty * h.avg_cost.amount }

Trend.new(current: current, previous: previous)
end
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential nil access on avg_cost.

Line 122 accesses h.avg_cost.amount without nil checking. If any holding has a nil avg_cost, this will raise NoMethodError.

   def unrealized_gains_trend
     holdings = current_holdings.to_a
     return nil if holdings.empty?

     current = holdings.sum(&:amount)
-    previous = holdings.sum { |h| h.qty * h.avg_cost.amount }
+    previous = holdings.sum { |h| h.avg_cost ? h.qty * h.avg_cost.amount : 0 }

     Trend.new(current: current, previous: previous)
   end
🤖 Prompt for AI Agents
In app/models/investment_statement.rb around lines 117 to 125, the calculation
for `previous` calls `h.avg_cost.amount` without guarding against `avg_cost`
being nil which can raise NoMethodError; update the sum to handle nil avg_cost
(e.g. skip holdings with nil avg_cost or treat their contribution as zero) by
using a safe check such as `h.avg_cost ? h.qty * h.avg_cost.amount : 0` or
`h.avg_cost&.amount.to_f` so `previous` always gets a numeric value and
`unrealized_gains_trend` won’t raise when any holding has nil `avg_cost`.

Comment on lines +33 to +34
COALESCE(SUM(CASE WHEN t.qty > 0 THEN ABS(ae.amount * COALESCE(er.rate, 1)) ELSE 0 END), 0) as contributions,
COALESCE(SUM(CASE WHEN t.qty < 0 THEN ABS(ae.amount * COALESCE(er.rate, 1)) ELSE 0 END), 0) as withdrawals,
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing exchange rate silently defaults to 1.

When an exchange rate is missing, COALESCE(er.rate, 1) defaults to 1, meaning no currency conversion occurs. This could produce incorrect totals for multi-currency portfolios. Consider logging or flagging when rates are missing for transactions in foreign currencies.

Also applies to: 39-43

🤖 Prompt for AI Agents
In app/models/investment_statement/totals.rb around lines 33-34 (and similarly
lines 39-43), the SQL uses COALESCE(er.rate, 1) which silently treats missing
exchange rates as 1; change the query to detect missing rates instead of
defaulting to 1 — remove the COALESCE fallback and instead multiply by er.rate
only when present (or use a CASE that sets the amount to NULL/0 and sets a flag
when er.rate IS NULL), and add logic to (a) surface a boolean/column indicating
missing_rate for those rows or (b) log/raise an explicit warning when any
transaction uses a foreign currency without an exchange rate so callers can
handle or reject the result.

Copy link

@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.

Actionable comments posted: 7

🧹 Nitpick comments (9)
app/models/income_statement/totals.rb (1)

48-91: Consider extracting shared SQL to reduce duplication.

transactions_only_query_sql and transactions_subquery_sql are nearly identical (differing only in the count column alias and trailing semicolon). This duplication means changes need to be made in two places.

You could extract a shared method with a parameter:

def transactions_sql(count_alias: "transactions_count", include_semicolon: true)
  <<~SQL
    SELECT
      c.id as category_id,
      c.parent_id as parent_category_id,
      CASE WHEN ae.amount < 0 THEN 'income' ELSE 'expense' END as classification,
      ABS(SUM(ae.amount * COALESCE(er.rate, 1))) as total,
      COUNT(ae.id) as #{count_alias}
    FROM (#{@transactions_scope.to_sql}) at
    JOIN entries ae ON ae.entryable_id = at.id AND ae.entryable_type = 'Transaction'
    LEFT JOIN categories c ON c.id = at.category_id
    LEFT JOIN exchange_rates er ON (
      er.date = ae.date AND
      er.from_currency = ae.currency AND
      er.to_currency = :target_currency
    )
    WHERE at.kind NOT IN ('funds_movement', 'one_time', 'cc_payment')
      AND ae.excluded = false
    GROUP BY c.id, c.parent_id, CASE WHEN ae.amount < 0 THEN 'income' ELSE 'expense' END#{include_semicolon ? ';' : ''}
  SQL
end
app/models/trade/create_form.rb (1)

57-64: Consider using a constant for the category name lookup.

The hardcoded string "Investment Contributions" makes this lookup fragile—if the default category is renamed, translated, or doesn't exist for a family, the lookup silently fails. Consider defining a constant (e.g., Category::INVESTMENT_CONTRIBUTIONS_NAME) or using an identifier-based lookup to make this more robust.

     def investment_category_for(trade_type)
       return nil unless trade_type == "buy"
 
-      account.family.categories.find_by(name: "Investment Contributions")
+      account.family.categories.find_by(name: Category::INVESTMENT_CONTRIBUTIONS_NAME)
     end
db/schema.rb (1)

1083-1083: Update migration to add on_delete: :nullify for consistency with transactions.

The trades category FK uses the default RESTRICT behavior, which prevents deleting categories that have associated trades. Transactions use on_delete: :nullify instead. Align trades to match:

-    add_reference :trades, :category, null: true, foreign_key: true, type: :uuid
+    add_reference :trades, :category, null: true, foreign_key: { on_delete: :nullify }, type: :uuid

This ensures both tables handle category deletion consistently: trades become uncategorized rather than blocking the deletion.

app/models/category.rb (2)

24-25: Scopes rely on hardcoded English category names — fragile for renamed or localized categories.

These scopes query by name, which is user-facing and mutable. If a user renames "Dividends" or if localization is introduced, these scopes will silently break. Consider using a dedicated investment boolean column or a classification value (e.g., "investment") instead.

If a schema change isn't feasible now, at minimum document that these category names are reserved/system-managed:

+ # Note: These scopes depend on system-managed category names.
+ # Do not allow users to rename investment categories.
  scope :investments, -> { where(name: INVESTMENT_CATEGORY_NAMES) }
  scope :non_investment, -> { where.not(name: INVESTMENT_CATEGORY_NAMES) }

35-40: Hardcoded category names create implicit coupling.

INVESTMENT_CATEGORY_NAMES uses English strings that must exactly match the default_categories entries. This creates a maintenance burden and potential for drift. Consider extracting these as i18n keys or using a dedicated attribute to mark investment categories.

app/models/cashflow_statement.rb (1)

74-79: Summary constant should be public for external consumers.

Summary is defined in the private section but is returned by the public summary method. While Ruby allows this, it's unconventional and may confuse consumers who want to type-check or pattern-match against the struct. Consider moving it above the private keyword.

+ Summary = Data.define(:operating, :investing, :financing, :net_cash_flow, :total_inflows, :total_outflows)
+
  private
-   Summary = Data.define(:operating, :investing, :financing, :net_cash_flow, :total_inflows, :total_outflows)

    def monetizable_currency
app/models/cashflow_statement/investing_activities.rb (2)

11-14: Inflows represent withdrawals (selling) — semantics are clear but document the sign convention.

The comment at line 11 helps, but the mapping of "withdrawals" → "inflows" could confuse maintainers. The qty < 0 convention for sells should be documented or referenced.


41-50: Summary has redundant fields.

contributions is identical to outflows_money and withdrawals is identical to inflows_money. This duplication could lead to drift if one is updated without the other.

Consider aliasing instead:

  def summary
    Summary.new(
      inflows: inflows_money,
      outflows: outflows_money,
      net: net_money,
-     contributions: outflows_money,
-     withdrawals: inflows_money,
+     contributions: outflows_money,  # alias for clarity in views
+     withdrawals: inflows_money,     # alias for clarity in views
      trades_count: trades_count
    )
  end

Or remove the aliases and use outflows/inflows consistently in views.

app/models/cashflow_statement/sankey_builder.rb (1)

186-230: Extract financing color to a constant.

The color "#7c3aed" is repeated 4 times. Extract to a constant for consistency and maintainability, similar to Category::INVESTMENT_COLOR.

+ FINANCING_COLOR = "#7c3aed".freeze
+
  def build_financing_outflows
    # Loan payments
    loan_payments = financing.loan_payments.to_f.round(2)
    if loan_payments.positive?
      percentage = total_outflows.zero? ? 0 : (loan_payments / total_outflows * 100).round(1)

      idx = add_node(
        "loan_payments",
        "Loan Payments",
        loan_payments,
        percentage,
-       "#7c3aed" # Purple for financing
+       FINANCING_COLOR
      )
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 19597b7 and 5d235e1.

📒 Files selected for processing (17)
  • app/controllers/pages_controller.rb (3 hunks)
  • app/controllers/trades_controller.rb (1 hunks)
  • app/models/cashflow_statement.rb (1 hunks)
  • app/models/cashflow_statement/financing_activities.rb (1 hunks)
  • app/models/cashflow_statement/investing_activities.rb (1 hunks)
  • app/models/cashflow_statement/operating_activities.rb (1 hunks)
  • app/models/cashflow_statement/sankey_builder.rb (1 hunks)
  • app/models/category.rb (4 hunks)
  • app/models/family.rb (1 hunks)
  • app/models/income_statement/totals.rb (3 hunks)
  • app/models/trade.rb (1 hunks)
  • app/models/trade/create_form.rb (2 hunks)
  • app/views/trades/_trade.html.erb (1 hunks)
  • app/views/trades/show.html.erb (1 hunks)
  • config/locales/views/trades/en.yml (2 hunks)
  • db/migrate/20251125141213_add_category_to_trades.rb (1 hunks)
  • db/schema.rb (5 hunks)
✅ Files skipped from review due to trivial changes (1)
  • config/locales/views/trades/en.yml
🧰 Additional context used
📓 Path-based instructions (26)
**/*.rb

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.rb: Application supports two modes: 'managed' and 'self_hosted' via Rails.application.config.app_mode
Use Current.user and Current.family instead of current_user / current_family for authentication context
Optimize database queries with proper indexes to prevent N+1 queries using includes/joins

**/*.rb: Use Rails pluralization in i18n: t("transactions.count", count: @transactions.count).
Run bin/rubocop -f github -a for Ruby linting with auto-correct before pull requests.

**/*.rb: Ruby code should use 2-space indent, snake_case for methods and variables, and CamelCase for classes and modules, following Rails conventions for folders and file names
Prefer environment variables over hard-coded values for configuration

Files:

  • app/controllers/trades_controller.rb
  • app/models/category.rb
  • db/migrate/20251125141213_add_category_to_trades.rb
  • app/models/family.rb
  • app/models/trade.rb
  • app/controllers/pages_controller.rb
  • app/models/income_statement/totals.rb
  • app/models/trade/create_form.rb
  • app/models/cashflow_statement/investing_activities.rb
  • app/models/cashflow_statement/operating_activities.rb
  • app/models/cashflow_statement.rb
  • app/models/cashflow_statement/financing_activities.rb
  • app/models/cashflow_statement/sankey_builder.rb
  • db/schema.rb
app/controllers/**/*.rb

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use Rails strong parameters and CSRF protection throughout the application

app/controllers/**/*.rb: Use strong parameters and CSRF protection throughout the application.
Use query params for state over localStorage/sessions.

Files:

  • app/controllers/trades_controller.rb
  • app/controllers/pages_controller.rb
**/*.{rb,erb,html.erb}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{rb,erb,html.erb}: Use Current.user for the current user context in controllers and views. Do NOT use current_user.
Use Current.family for the current family context. Do NOT use current_family.

Files:

  • app/controllers/trades_controller.rb
  • app/models/category.rb
  • app/views/trades/_trade.html.erb
  • db/migrate/20251125141213_add_category_to_trades.rb
  • app/views/trades/show.html.erb
  • app/models/family.rb
  • app/models/trade.rb
  • app/controllers/pages_controller.rb
  • app/models/income_statement/totals.rb
  • app/models/trade/create_form.rb
  • app/models/cashflow_statement/investing_activities.rb
  • app/models/cashflow_statement/operating_activities.rb
  • app/models/cashflow_statement.rb
  • app/models/cashflow_statement/financing_activities.rb
  • app/models/cashflow_statement/sankey_builder.rb
  • db/schema.rb
**/*.{erb,html.erb,rb}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{erb,html.erb,rb}: All user-facing strings must use localization (i18n) via the t() helper. Update locale files for each new or changed element.
Use server-side formatting for currencies, numbers, and dates. Do not format in JavaScript.

Files:

  • app/controllers/trades_controller.rb
  • app/models/category.rb
  • app/views/trades/_trade.html.erb
  • db/migrate/20251125141213_add_category_to_trades.rb
  • app/views/trades/show.html.erb
  • app/models/family.rb
  • app/models/trade.rb
  • app/controllers/pages_controller.rb
  • app/models/income_statement/totals.rb
  • app/models/trade/create_form.rb
  • app/models/cashflow_statement/investing_activities.rb
  • app/models/cashflow_statement/operating_activities.rb
  • app/models/cashflow_statement.rb
  • app/models/cashflow_statement/financing_activities.rb
  • app/models/cashflow_statement/sankey_builder.rb
  • db/schema.rb
**/*.{rb,erb}

📄 CodeRabbit inference engine (.cursor/rules/general-rules.mdc)

**/*.{rb,erb}: Use Current.user for accessing the current user. Do NOT use current_user
Use Current.family for accessing the current family. Do NOT use current_family

Files:

  • app/controllers/trades_controller.rb
  • app/models/category.rb
  • app/views/trades/_trade.html.erb
  • db/migrate/20251125141213_add_category_to_trades.rb
  • app/views/trades/show.html.erb
  • app/models/family.rb
  • app/models/trade.rb
  • app/controllers/pages_controller.rb
  • app/models/income_statement/totals.rb
  • app/models/trade/create_form.rb
  • app/models/cashflow_statement/investing_activities.rb
  • app/models/cashflow_statement/operating_activities.rb
  • app/models/cashflow_statement.rb
  • app/models/cashflow_statement/financing_activities.rb
  • app/models/cashflow_statement/sankey_builder.rb
  • db/schema.rb
**/*.{rb,js,erb}

📄 CodeRabbit inference engine (.cursor/rules/project-conventions.mdc)

Format currencies, numbers, dates, and other values server-side, then pass to Stimulus controllers for display only

Files:

  • app/controllers/trades_controller.rb
  • app/models/category.rb
  • app/views/trades/_trade.html.erb
  • db/migrate/20251125141213_add_category_to_trades.rb
  • app/views/trades/show.html.erb
  • app/models/family.rb
  • app/models/trade.rb
  • app/controllers/pages_controller.rb
  • app/models/income_statement/totals.rb
  • app/models/trade/create_form.rb
  • app/models/cashflow_statement/investing_activities.rb
  • app/models/cashflow_statement/operating_activities.rb
  • app/models/cashflow_statement.rb
  • app/models/cashflow_statement/financing_activities.rb
  • app/models/cashflow_statement/sankey_builder.rb
  • db/schema.rb
**/*.{rb,html.erb}

📄 CodeRabbit inference engine (.cursor/rules/project-conventions.mdc)

Use ActiveRecord validations for complex validations and business logic. Simple validations may be mirrored in ActiveRecord for form error handling convenience, but prioritize client-side form validation when possible

Files:

  • app/controllers/trades_controller.rb
  • app/models/category.rb
  • app/views/trades/_trade.html.erb
  • db/migrate/20251125141213_add_category_to_trades.rb
  • app/views/trades/show.html.erb
  • app/models/family.rb
  • app/models/trade.rb
  • app/controllers/pages_controller.rb
  • app/models/income_statement/totals.rb
  • app/models/trade/create_form.rb
  • app/models/cashflow_statement/investing_activities.rb
  • app/models/cashflow_statement/operating_activities.rb
  • app/models/cashflow_statement.rb
  • app/models/cashflow_statement/financing_activities.rb
  • app/models/cashflow_statement/sankey_builder.rb
  • db/schema.rb
app/**/*.{rb,js,ts,jsx,tsx,css,scss}

📄 CodeRabbit inference engine (AGENTS.md)

Code should be organized in app/ directory (Rails MVC, services, jobs, mailers, components), with JS in app/javascript/, and styles/assets in app/assets/ (Tailwind, images, fonts)

Files:

  • app/controllers/trades_controller.rb
  • app/models/category.rb
  • app/models/family.rb
  • app/models/trade.rb
  • app/controllers/pages_controller.rb
  • app/models/income_statement/totals.rb
  • app/models/trade/create_form.rb
  • app/models/cashflow_statement/investing_activities.rb
  • app/models/cashflow_statement/operating_activities.rb
  • app/models/cashflow_statement.rb
  • app/models/cashflow_statement/financing_activities.rb
  • app/models/cashflow_statement/sankey_builder.rb
app/**/*.rb

📄 CodeRabbit inference engine (AGENTS.md)

Run bin/brakeman security scan before major PRs to check for static analysis of common Rails issues

Files:

  • app/controllers/trades_controller.rb
  • app/models/category.rb
  • app/models/family.rb
  • app/models/trade.rb
  • app/controllers/pages_controller.rb
  • app/models/income_statement/totals.rb
  • app/models/trade/create_form.rb
  • app/models/cashflow_statement/investing_activities.rb
  • app/models/cashflow_statement/operating_activities.rb
  • app/models/cashflow_statement.rb
  • app/models/cashflow_statement/financing_activities.rb
  • app/models/cashflow_statement/sankey_builder.rb
app/models/**/*.rb

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

app/models/**/*.rb: Keep business logic in models using skinny controllers and fat models pattern
Store all monetary values in base currency (user's primary currency) and use Money objects for conversion
Use ActiveRecord validations for form convenience and complex business logic validations
Use Rails concerns and POROs for organizing business logic instead of services folder

app/models/**/*.rb: All monetary values stored in base currency (user's primary currency). Use Money objects for currency conversion and formatting.
Place business logic in app/models/ folder. Avoid creating app/services/ folder. Use Rails concerns and POROs for organization.
Models should answer questions about themselves using instance methods: use account.balance_series not AccountSeries.new(account).call.

Files:

  • app/models/category.rb
  • app/models/family.rb
  • app/models/trade.rb
  • app/models/income_statement/totals.rb
  • app/models/trade/create_form.rb
  • app/models/cashflow_statement/investing_activities.rb
  • app/models/cashflow_statement/operating_activities.rb
  • app/models/cashflow_statement.rb
  • app/models/cashflow_statement/financing_activities.rb
  • app/models/cashflow_statement/sankey_builder.rb
{app/models/**/*.rb,db/migrate/**/*.rb}

📄 CodeRabbit inference engine (CLAUDE.md)

Simple validations (null checks, unique indexes) should be in the database. ActiveRecord validations for form convenience (prefer client-side when possible). Complex validations and business logic in ActiveRecord.

Files:

  • app/models/category.rb
  • db/migrate/20251125141213_add_category_to_trades.rb
  • app/models/family.rb
  • app/models/trade.rb
  • app/models/income_statement/totals.rb
  • app/models/trade/create_form.rb
  • app/models/cashflow_statement/investing_activities.rb
  • app/models/cashflow_statement/operating_activities.rb
  • app/models/cashflow_statement.rb
  • app/models/cashflow_statement/financing_activities.rb
  • app/models/cashflow_statement/sankey_builder.rb
{app/**/*.{erb,html},app/javascript/**/*.js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Prefer Hotwire-first approach: use Turbo + Stimulus for reactive UI without heavy JavaScript

Files:

  • app/views/trades/_trade.html.erb
  • app/views/trades/show.html.erb
app/views/**/_*.html.erb

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use partials only for simple, static HTML with minimal logic in specific contexts

app/views/**/_*.html.erb: Use underscore prefix for partial files (e.g., _trend_change.html.erb, _form_errors.html.erb, _sync_indicator.html.erb)
Place context-specific partials in relevant controller view directory (e.g., accounts/_account_sidebar_tabs.html.erb)

Files:

  • app/views/trades/_trade.html.erb
app/views/**/*.erb

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

app/views/**/*.erb: Always use the icon helper for icons in views, never use lucide_icon directly
Use Tailwind design tokens (e.g., text-primary, bg-container, border-secondary) instead of raw color names
Pass data to Stimulus controllers via data-*-value attributes instead of inline JavaScript
Prefer semantic HTML elements (dialog, details, summary) over JavaScript components
Use Turbo Frames for page sections instead of client-side solutions
Use server-side formatting for currencies, numbers, and dates instead of client-side

Views should use ERB and avoid heavy logic; prefer helpers and components instead (ERB checked by erb-lint per .erb_lint.yml)

Files:

  • app/views/trades/_trade.html.erb
  • app/views/trades/show.html.erb
**/*.{css,erb}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use Tailwind CSS v4.x with custom design system for styling

Files:

  • app/views/trades/_trade.html.erb
  • app/views/trades/show.html.erb
**/*.{erb,html,vue,jsx,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{erb,html,vue,jsx,tsx}: Use Tailwind CSS v4.x with custom design system defined in app/assets/tailwind/maybe-design-system.css. Always use functional tokens (e.g., text-primary not text-white).
Prefer semantic HTML elements over JavaScript components (e.g., use <dialog> for modals, <details><summary> for disclosures).

Files:

  • app/views/trades/_trade.html.erb
  • app/views/trades/show.html.erb
**/*.{erb,html.erb}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{erb,html.erb}: Use icon helper for icons, never use lucide_icon directly.
Leverage Turbo frames for page sections over client-side solutions.
Pass data to Stimulus controllers via data-*-value attributes, not inline JavaScript.

**/*.{erb,html.erb}: Use ViewComponents when: element has complex logic or styling patterns, will be reused across multiple views/contexts, needs structured styling with variants/sizes, requires interactive behavior or Stimulus controllers, has configurable slots or complex APIs, or needs accessibility features or ARIA support
Use Partials when: element is primarily static HTML with minimal logic, used in only one or few specific contexts, is simple template content, doesn't need variants/sizes/complex configuration, or is more about content organization than reusable functionality
Prefer components over partials: if a component is available in app/components, use it; if not, look for a partial; if neither exists, decide between component or partial based on complexity and reusability criteria
Keep domain logic out of views: compute values like button classes, conditional logic, and data transformations in the component file, not the template file
Always use the declarative approach when integrating Stimulus controllers in views: the ERB template should declare what happens using data-* attributes, and the Stimulus controller should respond
Component controllers in app/components/ should only be used within their component templates; global controllers in app/javascript/controllers/ can be used across any view
Pass data from Rails to Stimulus using data-*-value attributes, not inline JavaScript

Files:

  • app/views/trades/_trade.html.erb
  • app/views/trades/show.html.erb
app/views/**/_*.{erb,html.erb}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Partials when element is primarily static HTML with minimal logic, used in only one or few contexts, is simple template content, or doesn't need variants/sizes/complex configuration.

Files:

  • app/views/trades/_trade.html.erb
{app/components/**/*.html.erb,app/views/**/*.{erb,html.erb}}

📄 CodeRabbit inference engine (CLAUDE.md)

Keep domain logic out of view templates. Logic belongs in component files, not template files.

Files:

  • app/views/trades/_trade.html.erb
  • app/views/trades/show.html.erb
**/*.erb

📄 CodeRabbit inference engine (CLAUDE.md)

Run bundle exec erb_lint ./app/**/*.erb -a for ERB linting with auto-correct before pull requests.

Files:

  • app/views/trades/_trade.html.erb
  • app/views/trades/show.html.erb
**/*.html.erb

📄 CodeRabbit inference engine (.cursor/rules/project-conventions.mdc)

**/*.html.erb: Prefer native HTML elements over JavaScript-based components. Use semantic HTML: for modals,

for disclosures
Leverage Turbo frames to break up pages instead of JavaScript-driven client-side solutions
Use the icon helper from application_helper.rb for icons. Never use lucide_icon helper directly

Files:

  • app/views/trades/_trade.html.erb
  • app/views/trades/show.html.erb
{app/views/**,app/helpers/**,app/javascript/controllers/**}

📄 CodeRabbit inference engine (.cursor/rules/ui-ux-design-guidelines.mdc)

{app/views/**,app/helpers/**,app/javascript/controllers/**}: Reference maybe-design-system.css for base primitives, functional tokens, and component tokens before writing styles
Prefer using functional design system tokens (e.g., text-primary, bg-container, border-primary) from maybe-design-system.css instead of raw Tailwind color values

Files:

  • app/views/trades/_trade.html.erb
  • app/views/trades/show.html.erb
{app/views/**,app/helpers/**}

📄 CodeRabbit inference engine (.cursor/rules/ui-ux-design-guidelines.mdc)

Always generate semantic HTML

Files:

  • app/views/trades/_trade.html.erb
  • app/views/trades/show.html.erb
db/migrate/**/*.rb

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use simple validations (null checks, unique indexes) in database layer

Files:

  • db/migrate/20251125141213_add_category_to_trades.rb
db/migrate/*.rb

📄 CodeRabbit inference engine (.cursor/rules/general-rules.mdc)

ActiveRecord migrations must inherit from ActiveRecord::Migration[7.2]. Do NOT use version 8.0 yet

Enforce null checks, unique indexes, and simple validations in the database schema for PostgreSQL

Files:

  • db/migrate/20251125141213_add_category_to_trades.rb
db/**/*.rb

📄 CodeRabbit inference engine (AGENTS.md)

Data migrations should be placed in db/ directory with test fixtures in test/fixtures/

Files:

  • db/migrate/20251125141213_add_category_to_trades.rb
  • db/schema.rb
🧠 Learnings (13)
📓 Common learnings
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .cursor/rules/project-design.mdc:0-0
Timestamp: 2025-11-24T16:56:30.657Z
Learning: Entry model is a Rails delegated type with three subtypes: Valuation (absolute account value), Transaction (balance modification), and Trade (buy/sell for investment accounts)
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .cursor/rules/project-design.mdc:0-0
Timestamp: 2025-11-24T16:56:30.657Z
Learning: Account model is a Rails delegated type with subtypes: Depository, Investment, Crypto, Property, Vehicle, OtherAsset (assets) and CreditCard, Loan, OtherLiability (liabilities)
📚 Learning: 2025-11-14T14:22:37.076Z
Learnt from: jjmata
Repo: we-promise/sure PR: 326
File: app/models/category_import.rb:10-10
Timestamp: 2025-11-14T14:22:37.076Z
Learning: In the CategoryImport model (app/models/category_import.rb), the import process implements a "last write wins" strategy where importing a category.csv file completely overrides existing category structures, including parent relationships. Setting category.parent = nil in the first pass is intentional behavior to ensure the CSV becomes the source of truth.

Applied to files:

  • app/controllers/trades_controller.rb
  • app/models/category.rb
  • db/migrate/20251125141213_add_category_to_trades.rb
  • app/views/trades/show.html.erb
  • app/models/trade.rb
  • app/models/trade/create_form.rb
  • db/schema.rb
📚 Learning: 2025-11-24T16:56:30.657Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .cursor/rules/project-design.mdc:0-0
Timestamp: 2025-11-24T16:56:30.657Z
Learning: Account model is a Rails delegated type with subtypes: Depository, Investment, Crypto, Property, Vehicle, OtherAsset (assets) and CreditCard, Loan, OtherLiability (liabilities)

Applied to files:

  • app/models/category.rb
  • app/models/cashflow_statement/investing_activities.rb
  • app/models/cashflow_statement/operating_activities.rb
  • app/models/cashflow_statement/financing_activities.rb
📚 Learning: 2025-11-24T16:56:30.657Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .cursor/rules/project-design.mdc:0-0
Timestamp: 2025-11-24T16:56:30.657Z
Learning: Entry model is a Rails delegated type with three subtypes: Valuation (absolute account value), Transaction (balance modification), and Trade (buy/sell for investment accounts)

Applied to files:

  • app/models/category.rb
  • app/models/trade.rb
  • app/models/income_statement/totals.rb
  • app/models/trade/create_form.rb
  • app/models/cashflow_statement/investing_activities.rb
📚 Learning: 2025-11-24T16:54:59.185Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-24T16:54:59.185Z
Learning: Domain model structure: User → has many Accounts → has many Transactions; Account types include checking, savings, credit cards, investments, crypto, loans, properties

Applied to files:

  • app/models/category.rb
📚 Learning: 2025-11-24T16:55:59.131Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .cursor/rules/general-rules.mdc:0-0
Timestamp: 2025-11-24T16:55:59.131Z
Learning: Applies to db/migrate/*.rb : ActiveRecord migrations must inherit from `ActiveRecord::Migration[7.2]`. Do NOT use version 8.0 yet

Applied to files:

  • db/migrate/20251125141213_add_category_to_trades.rb
  • db/schema.rb
📚 Learning: 2025-11-24T16:55:43.046Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T16:55:43.046Z
Learning: Applies to app/models/**/*.rb : Models should answer questions about themselves using instance methods: use `account.balance_series` not `AccountSeries.new(account).call`.

Applied to files:

  • app/models/family.rb
📚 Learning: 2025-11-24T16:54:59.185Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-24T16:54:59.185Z
Learning: Applies to **/*.rb : Optimize database queries with proper indexes to prevent N+1 queries using includes/joins

Applied to files:

  • app/models/income_statement/totals.rb
  • db/schema.rb
📚 Learning: 2025-09-13T11:36:45.479Z
Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: app/models/enable_banking_account/processor.rb:72-79
Timestamp: 2025-09-13T11:36:45.479Z
Learning: The balance_calculator method pattern is consistent across banking integrations in the codebase. Both PlaidAccount::Processor and EnableBankingAccount::Processor use the same approach: `balance = account.current_balance || account.available_balance || 0` followed by `OpenStruct.new(balance: balance, cash_balance: balance)` without explicit requires, and include identical comments about not distinguishing cash vs non-cash balances for non-investment accounts.

Applied to files:

  • app/models/cashflow_statement/operating_activities.rb
📚 Learning: 2025-09-13T11:36:45.479Z
Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: app/models/enable_banking_account/processor.rb:72-79
Timestamp: 2025-09-13T11:36:45.479Z
Learning: The balance_calculator method in EnableBankingAccount::Processor follows the same pattern as the Plaid integration, using OpenStruct without explicit requires and returning balance values as-is without BigDecimal coercion for consistency across banking integrations.

Applied to files:

  • app/models/cashflow_statement/operating_activities.rb
📚 Learning: 2025-11-24T16:56:13.394Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .cursor/rules/project-conventions.mdc:0-0
Timestamp: 2025-11-24T16:56:13.394Z
Learning: Applies to db/migrate/*.rb : Enforce null checks, unique indexes, and simple validations in the database schema for PostgreSQL

Applied to files:

  • db/schema.rb
📚 Learning: 2025-11-24T16:56:44.793Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .cursor/rules/self_improve.mdc:0-0
Timestamp: 2025-11-24T16:56:44.793Z
Learning: Update rules after major refactors

Applied to files:

  • db/schema.rb
📚 Learning: 2025-11-24T16:54:59.185Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-24T16:54:59.185Z
Learning: Applies to db/migrate/**/*.rb : Use simple validations (null checks, unique indexes) in database layer

Applied to files:

  • db/schema.rb
🧬 Code graph analysis (4)
app/controllers/pages_controller.rb (4)
app/models/family.rb (2)
  • investment_statement (72-74)
  • cashflow_statement (76-78)
app/models/cashflow_statement.rb (2)
  • sankey_data (70-72)
  • operating_activities (13-15)
app/models/cashflow_statement/financing_activities.rb (1)
  • outflows (23-25)
app/models/cashflow_statement/operating_activities.rb (2)
  • outflows (22-24)
  • expenses_by_category (43-45)
app/models/trade/create_form.rb (1)
app/models/income_statement.rb (1)
  • categories (62-64)
app/models/cashflow_statement/investing_activities.rb (2)
app/models/cashflow_statement/operating_activities.rb (9)
  • include (1-227)
  • inflows (12-14)
  • totals (61-69)
  • inflows_money (16-18)
  • outflows (22-24)
  • outflows_money (26-28)
  • net (30-32)
  • net_money (34-36)
  • summary (47-55)
app/models/period.rb (1)
  • date_range (133-135)
app/models/cashflow_statement/sankey_builder.rb (2)
app/models/cashflow_statement.rb (6)
  • operating_activities (13-15)
  • investing_activities (18-20)
  • financing_activities (23-25)
  • total_inflows (37-39)
  • total_outflows (46-48)
  • net_cash_flow (28-30)
app/models/cashflow_statement/investing_activities.rb (2)
  • inflows (12-14)
  • outflows (21-23)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ci / test
🔇 Additional comments (25)
app/models/income_statement/totals.rb (3)

1-6: LGTM!

The initialization cleanly adds the include_trades flag with a sensible default that aligns with the investment tracking feature.


23-28: LGTM!

Clean conditional routing between query paths with proper SQL parameter sanitization.


30-46: LGTM!

Good use of UNION ALL for combining distinct data sources. The outer aggregation correctly sums totals across both transactions and trades.

app/models/cashflow_statement/financing_activities.rb (2)

1-9: LGTM!

The class structure follows the existing Monetizable pattern well, with clean separation of concerns and proper initialization.


83-91: Currency conversion fallback silently masks missing exchange rate records across reporting.

The COALESCE(er.rate, 1) pattern is systematic throughout the codebase (9+ files, 15+ locations). When an exchange rate record doesn't exist for a transaction's currency/date combination, the query defaults to 1:1 conversion. While ExchangeRate::Importer uses a gapfill strategy, there's no validation preventing transactions without exchange rates, and tests don't exercise this fallback path.

This means cross-currency amounts could be silently misrepresented if:

  • Exchange rates haven't been imported for a transaction's currency
  • Historical transactions predate rate collection
  • Data population gaps occur despite gapfill logic

Recommend either:

  • Verify all required exchange rates are guaranteed before transactions are created (add validation if not)
  • Add explicit logging/auditing when COALESCE fallback is used to catch data quality issues
  • Filter affected transactions with missing rates from reports or raise warnings

This pattern affects: cashflow_statement/financing_activities.rb, cashflow_statement/operating_activities.rb, cashflow_statement/investing_activities.rb, investment_statement/totals.rb, income_statement/totals.rb, balance/chart_series_builder.rb, and others.

app/models/cashflow_statement/operating_activities.rb (2)

123-183: String interpolation in SQL is safe here, but worth documenting.

The #{direction_condition} interpolation (line 181) is safe since direction_condition is derived from the internal :income/:expense classification, not user input. However, a brief comment noting this is intentionally non-parameterized could help future maintainers.

The same currency conversion fallback concern applies here as in FinancingActivities.


196-220: LGTM!

The OpenStruct usage for category objects is consistent with existing balance_calculator patterns in the codebase. The Uncategorized fallback properly uses the Category::UNCATEGORIZED_COLOR constant.

app/controllers/trades_controller.rb (1)

54-59: LGTM!

The addition of :category_id to permitted entryable_attributes correctly enables category assignment for trades, aligning with the new Trade.belongs_to :category association.

app/views/trades/_trade.html.erb (1)

1-1: The _trade.html.erb partial appears to be unused dead code.

The partial exists at app/views/trades/_trade.html.erb and does reference an undefined trade_category variable on line 33. However, the partial is not rendered from any view in the codebase, so the undefined variable doesn't cause runtime errors. Verify whether this partial is:

  1. Intentionally unused and should be removed, or
  2. Planned for future use (in which case, fix the trade_category undefined variable issue)

If keeping the partial, add trade_category to the locals declaration:

-<%# locals: (entry:, balance_trend: nil, **) %>
+<%# locals: (entry:, balance_trend: nil, trade_category: nil, **) %>
db/migrate/20251125141213_add_category_to_trades.rb (1)

1-5: LGTM!

The migration correctly uses ActiveRecord::Migration[7.2], UUID type for consistency with the schema, and properly sets null: true to match the optional association in the model. The foreign key constraint is enforced at the database level as per best practices.

app/models/trade.rb (2)

12-19: LGTM! Minor edge case to consider.

The predicate methods are clean and follow Ruby conventions. The validates :qty, presence: true on line 9 protects against nil values.

Note: If qty is exactly zero, both buy? and sell? return false. This may be intentional for edge cases like cancelled trades, but verify this behavior is expected.


7-7: LGTM!

The optional association correctly mirrors the nullable foreign key in the migration and aligns with the new category-tracking feature for trades.

app/models/family.rb (1)

72-78: LGTM!

Both accessors follow the pattern established by balance_sheet and income_statement. The investment_statement is correctly memoized since it doesn't take parameters. The cashflow_statement intentionally isn't memoized since it accepts a period parameter—different calls may need different periods.

db/schema.rb (1)

739-752: Unrelated changes: rule_runs table appears outside PR scope.

The rule_runs table addition seems unrelated to the investment tracking feature described in the PR objectives. If this is intentional (perhaps bundled changes), no action needed. Otherwise, consider separating into a distinct PR for cleaner history.

app/controllers/pages_controller.rb (3)

19-23: LGTM!

The sankey data construction properly uses the unified CashflowStatement model with configurable options for including investing activities. The include_financing: true default ensures loan/credit card payments are always visible.


8-8: No action required — @investment_statement is used in the dashboard views.

The instance variable is referenced in app/views/pages/dashboard.html.erb at lines 49 and 52, where it's used to check investment accounts and passed to a component for rendering.


67-68: Now I need to check the actual pages_controller.rb file to see the full context of the code in question:Perfect! I now have all the information needed to complete the verification. The web search results confirm that using respond_to? to check if an object responds to a particular method is a standard Ruby practice to check if an object can perform an action, which is central to duck typing. Duck typing in Ruby is less concerned with the class of an object and more concerned with what methods can be called on it.

From my earlier analysis of the codebase, the respond_to?(:parent_id) check at line 67 is correct because the category_breakdown method intentionally creates synthetic "Uncategorized" category objects that don't have the parent_id attribute.

Using respond_to? is the correct approach to determine if an object responds to certain methods, rather than relying on type checking.

The review comment is incorrect.

Respond_to? check is correct defensive programming; no issue to fix.

The respond_to?(:parent_id) check at line 67 is not indicative of a type inconsistency problem. Duck typing in Ruby is concerned with what methods can be called on objects, and this code correctly uses that pattern.

The category_breakdown method intentionally creates synthetic "Uncategorized" category objects (created as hashes with limited attributes) alongside real Category objects (from the database with all attributes including parent_id). The respond_to? check ensures the code handles both variants safely, which is proper duck typing implementation.

app/models/category.rb (2)

3-3: LGTM on the trades association.

The has_many :trades with dependent: :nullify correctly mirrors the existing has_many :transactions pattern and aligns with the Trade model's optional category association mentioned in the PR.


149-151: LGTM — consistent with the constant usage.

The method correctly delegates to the INVESTMENT_CATEGORY_NAMES constant for determining investment status.

app/models/cashflow_statement.rb (3)

1-9: Clean initialization and composition pattern.

The class follows Rails PORO conventions with proper attribute readers and sensible defaults. The composition of activity types is well-structured.


54-64: LGTM — Summary composition is clean.

The Data.define pattern provides an immutable, named-field structure that's idiomatic for Ruby 3.2+.


27-52: Review comment is incorrect—all activity methods safely return numeric values.

All three activity classes (OperatingActivities, InvestingActivities, FinancingActivities) guarantee numeric returns via || 0 fallback patterns in their totals methods:

  • OperatingActivities: inflows/outflows return result["income"]&.to_d || 0 and result["expense"]&.to_d || 0
  • InvestingActivities: inflows/outflows return result["withdrawals"]&.to_d || 0 and result["contributions"]&.to_d || 0
  • FinancingActivities: inflows is hardcoded 0; outflows returns sum of two decimals with || 0 fallback

Each net method computes inflows - outflows (numeric arithmetic), ensuring numeric output. No nil values propagate to CashflowStatement aggregations. The code is correct.

Likely an incorrect or invalid review comment.

app/models/cashflow_statement/sankey_builder.rb (2)

232-260: LGTM — surplus/deficit logic handles edge cases correctly.

The positive/negative/zero cash flow handling is clean, with appropriate percentage calculations relative to inflows (surplus) and outflows (deficit).


84-109: No issues found — interface is properly implemented with correct nil handling.

The operating.income_by_category returns CategoryTotal objects with a nested category attribute (an OpenStruct with id, name, color, lucide_icon) and a total attribute. Uncategorized transactions are properly handled by creating a default category OpenStruct with id: nil, name: "Uncategorized", and a default color. The code in build_operating_inflows correctly accesses these attributes and handles the nil case for uncategorized transactions using || fallbacks.

app/models/cashflow_statement/investing_activities.rb (1)

52-55: The review concern is incorrect — the filters are aligned.

The visible scope in Account model (line 21) filters by status: [ "draft", "active" ], which is identical to the a.status IN ('draft', 'active') condition in the totals query. Both queries apply the same status filter, so there is no misalignment or risk of has_investments? returning true while totals are empty due to scope filtering differences.

Likely an incorrect or invalid review comment.

Comment on lines 71 to 99
def totals_query_sql
trades_scope = family.trades
.joins(:entry)
.where(entries: { date: period.date_range })

ActiveRecord::Base.sanitize_sql_array([
<<~SQL,
SELECT
COALESCE(SUM(CASE WHEN t.qty > 0 THEN ABS(ae.amount * COALESCE(er.rate, 1)) ELSE 0 END), 0) as contributions,
COALESCE(SUM(CASE WHEN t.qty < 0 THEN ABS(ae.amount * COALESCE(er.rate, 1)) ELSE 0 END), 0) as withdrawals,
COUNT(t.id) as trades_count
FROM (#{trades_scope.to_sql}) t
JOIN entries ae ON ae.entryable_id = t.id AND ae.entryable_type = 'Trade'
JOIN accounts a ON a.id = ae.account_id
LEFT JOIN exchange_rates er ON (
er.date = ae.date AND
er.from_currency = ae.currency AND
er.to_currency = :target_currency
)
WHERE a.family_id = :family_id
AND a.status IN ('draft', 'active')
AND ae.excluded = false
SQL
{
family_id: family.id,
target_currency: family.currency
}
])
end
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

SQL query re-joins entries unnecessarily and may miss currency conversion edge cases.

The trades_scope already joins :entry (line 73), but the raw SQL joins entries again on the subquery result. This is redundant and could cause issues.

Additionally, when er.rate is NULL (missing exchange rate), COALESCE(er.rate, 1) assumes a 1:1 conversion which is incorrect for cross-currency families. This could silently produce wrong totals.

Consider using the entry data from the initial scope:

  def totals_query_sql
-   trades_scope = family.trades
-     .joins(:entry)
-     .where(entries: { date: period.date_range })
+   trades_scope = family.trades
+     .joins(entry: :account)
+     .where(entries: { date: period.date_range })
+     .where(accounts: { status: %w[draft active] })
+     .where(entries: { excluded: false })

    ActiveRecord::Base.sanitize_sql_array([
      <<~SQL,
        SELECT
          COALESCE(SUM(CASE WHEN t.qty > 0 THEN ABS(ae.amount * COALESCE(er.rate, 1)) ELSE 0 END), 0) as contributions,
          COALESCE(SUM(CASE WHEN t.qty < 0 THEN ABS(ae.amount * COALESCE(er.rate, 1)) ELSE 0 END), 0) as withdrawals,
          COUNT(t.id) as trades_count
-       FROM (#{trades_scope.to_sql}) t
-       JOIN entries ae ON ae.entryable_id = t.id AND ae.entryable_type = 'Trade'
-       JOIN accounts a ON a.id = ae.account_id
+       FROM trades t
+       JOIN entries ae ON ae.entryable_id = t.id AND ae.entryable_type = 'Trade'

Also consider logging or raising when exchange rates are missing for material amounts.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In app/models/cashflow_statement/investing_activities.rb around lines 71–99, the
SQL rebuild re-joins entries already joined in trades_scope and uses
COALESCE(er.rate, 1) which silently assumes 1:1 conversion when rates are
missing; fix by (1) including the needed entry columns (date, account_id,
currency, amount, excluded, etc.) in the trades_scope subquery so the outer
query can reference them directly and remove the redundant JOIN entries ae ON
...; (2) stop defaulting missing rates to 1 — either require a matching
exchange_rates row (use INNER JOIN) or keep LEFT JOIN but change expressions to
treat NULL rates explicitly (e.g. CASE to mark amounts with missing rates) and
add an application-level hook to log or raise when non-zero amounts have NULL
rates so missing exchange rates are surfaced instead of silently converted.

Comment on lines 16 to 30
def build
@nodes = []
@links = []
@node_indices = {}

build_cash_flow_node
build_operating_inflows
build_investing_inflows if include_investing?
build_operating_outflows
build_investing_outflows if include_investing?
build_financing_outflows if include_financing?
build_surplus_or_deficit

{ nodes: @nodes, links: @links, currency_symbol: currency_symbol }
end
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

build method is not idempotent — calling it twice corrupts state.

The build method resets @nodes, @links, and @node_indices each call, but the memoized @total_inflows, @total_outflows, @net_cash_flow, and @cash_flow_idx are not reset. Calling build twice on the same instance will produce incorrect results.

Either document single-use or reset all memoized state:

  def build
    @nodes = []
    @links = []
    @node_indices = {}
+   @total_inflows = nil
+   @total_outflows = nil
+   @net_cash_flow = nil
+   @cash_flow_idx = nil

Or make the builder a single-use object (instantiate per call).

🤖 Prompt for AI Agents
In app/models/cashflow_statement/sankey_builder.rb around lines 16 to 30, the
build method currently clears @nodes, @links, and @node_indices but leaves
memoized state (@total_inflows, @total_outflows, @net_cash_flow, @cash_flow_idx)
intact which makes repeated calls corrupt the result; fix by resetting all
memoized instance variables at the top of build (clear @total_inflows,
@total_outflows, @net_cash_flow, @cash_flow_idx and any other cached fields used
by helper methods) or alternatively document that the builder is single-use and
change callers to instantiate a new builder per build call.

Comment on lines 111 to 133
def build_investing_inflows
# Investment liquidations (selling investments = cash inflow)
withdrawals = investing.inflows.to_f.round(2)
return unless withdrawals.positive?

percentage = total_inflows.zero? ? 0 : (withdrawals / total_inflows * 100).round(1)

idx = add_node(
"investment_liquidations",
"Investment Liquidations",
withdrawals,
percentage,
Category::INVESTMENT_COLOR
)

@links << {
source: idx,
target: cash_flow_idx,
value: withdrawals,
color: Category::INVESTMENT_COLOR,
percentage: percentage
}
end
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

"Investment Liquidations" label is not localized.

The hardcoded string "Investment Liquidations" should use i18n. Per coding guidelines, all user-facing strings must use the t() helper.

This applies to other hardcoded labels as well: "Investment Contributions", "Loan Payments", "Credit Card Payments", "Surplus", "Deficit", "Cash Flow".

Comment on lines 118 to 123
[ "Savings & Investments", "#059669", "piggy-bank", "expense" ],
# Investment-specific categories
[ "Investment Contributions", "#0d9488", "trending-up", "expense" ],
[ "Dividends", "#16a34a", "banknote", "income" ],
[ "Capital Gains", "#15803d", "trophy", "income" ],
[ "Investment Interest", "#22c55e", "percent", "income" ]
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

Missing "trending-up" in icon_codes whitelist.

The "Investment Contributions" category uses "trending-up" as its icon, but this icon is not present in the icon_codes method (lines 61-74). This could cause validation or display issues if icon codes are validated against this list.


🏁 Script executed:

#!/bin/bash
# Check if trending-up is used elsewhere or if icon_codes is used for validation
rg -n "trending-up" --type=rb
rg -n "icon_codes" --type=rb -A3

Length of output: 53


🏁 Script executed:

#!/bin/bash
# Retry: Search for trending-up icon usage without explicit type flag
rg -n "trending-up"

Length of output: 568


🏁 Script executed:

#!/bin/bash
# Search for icon_codes method definition
rg -n "def icon_codes" -A 20

Length of output: 1723


🏁 Script executed:

#!/bin/bash
# Search for icon_codes usage to check if it's used for validation
rg -n "icon_codes" -B2 -A2

Length of output: 871


🏁 Script executed:

#!/bin/bash
# Search for validations on lucide_icon field
rg -n "lucide_icon" -B2 -A2

Length of output: 14901


🏁 Script executed:

#!/bin/bash
# Search for any validation logic that uses icon_codes
rg -n "validates.*icon" -B2 -A2

Length of output: 310


🏁 Script executed:

#!/bin/bash
# Check the complete default_categories array to see all icon usages
rg -n "def default_categories" -A 50

Length of output: 3409


Add "trending-up" icon to the icon_codes whitelist.

The "Investment Contributions" category uses the "trending-up" icon (line 120), but it's not included in the icon_codes method (lines 61-75). While there's no validation error, this creates a UI inconsistency: users cannot select "trending-up" when editing categories through the form, even though the default category is created with this icon.

🤖 Prompt for AI Agents
In app/models/category.rb (around lines 118-123; icon_codes method is around
lines 61-75), the default "Investment Contributions" category uses the
"trending-up" icon but that token is missing from the icon_codes whitelist,
causing a UI mismatch; add "trending-up" to the icon_codes array (keeping the
list's format/ordering consistent) so the icon is available in the category
edit/create form and persists correctly for the default category.

Comment on lines +93 to +119
def trades_subquery_sql
# Get trades for the same family and date range as transactions
# Only include trades that have a category assigned
<<~SQL
SELECT
c.id as category_id,
c.parent_id as parent_category_id,
CASE WHEN ae.amount < 0 THEN 'income' ELSE 'expense' END as classification,
ABS(SUM(ae.amount * COALESCE(er.rate, 1))) as total,
COUNT(ae.id) as entry_count
FROM trades t
JOIN entries ae ON ae.entryable_id = t.id AND ae.entryable_type = 'Trade'
JOIN accounts a ON a.id = ae.account_id
LEFT JOIN categories c ON c.id = t.category_id
LEFT JOIN exchange_rates er ON (
er.date = ae.date AND
er.from_currency = ae.currency AND
er.to_currency = :target_currency
)
WHERE a.family_id = :family_id
AND a.status IN ('draft', 'active')
AND ae.excluded = false
AND ae.date BETWEEN :start_date AND :end_date
AND t.category_id IS NOT NULL
GROUP BY c.id, c.parent_id, CASE WHEN ae.amount < 0 THEN 'income' ELSE 'expense' END
SQL
end
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Inconsistent account status filtering between trades and transactions.

The trades subquery filters accounts by a.status IN ('draft', 'active') (line 113), but the transactions subquery (lines 71-91) has no account status filter. This means transactions from inactive/archived accounts will be included while trades from those same accounts are excluded, leading to inconsistent totals.

Consider adding the same account join and status filter to transactions_subquery_sql:

         FROM (#{@transactions_scope.to_sql}) at
         JOIN entries ae ON ae.entryable_id = at.id AND ae.entryable_type = 'Transaction'
+        JOIN accounts a ON a.id = ae.account_id
         LEFT JOIN categories c ON c.id = at.category_id
         LEFT JOIN exchange_rates er ON (
           er.date = ae.date AND
           er.from_currency = ae.currency AND
           er.to_currency = :target_currency
         )
-        WHERE at.kind NOT IN ('funds_movement', 'one_time', 'cc_payment')
+        WHERE a.status IN ('draft', 'active')
+          AND at.kind NOT IN ('funds_movement', 'one_time', 'cc_payment')
           AND ae.excluded = false

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In app/models/income_statement/totals.rb around lines 93-119 (and adjust
transactions_subquery_sql at ~lines 71-91), the trades subquery filters by
account family and status but the transactions subquery does not, causing
inconsistent totals; update transactions_subquery_sql to JOIN accounts a ON a.id
= ae.account_id (or the equivalent account join used in trades), add AND
a.family_id = :family_id and AND a.status IN ('draft', 'active') to the WHERE
clause (keep parameter names and aliases consistent with the trades subquery and
ensure no alias collisions).

Comment on lines +134 to +154
def extract_date_range
# Try to extract date range from the scope's where clauses
# This is a heuristic - the transactions_scope should have date filters
scope_sql = @transactions_scope.to_sql

# Default to current month if we can't extract dates
start_date = Date.current.beginning_of_month
end_date = Date.current.end_of_month

# Try to find date conditions in the SQL
# Look for patterns like "date >= '2024-01-01'" or "date BETWEEN"
if scope_sql =~ /entries.*date.*>=.*'(\d{4}-\d{2}-\d{2})'/i
start_date = Date.parse($1) rescue start_date
end

if scope_sql =~ /entries.*date.*<=.*'(\d{4}-\d{2}-\d{2})'/i
end_date = Date.parse($1) rescue end_date
end

{ start_date: start_date, end_date: end_date }
end
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fragile regex-based SQL parsing is error-prone and could silently produce incorrect results.

Parsing SQL strings with regex to extract dates is brittle:

  1. The regex pattern entries.*date.*>= is greedy and may match unintended fragments
  2. Parameterized dates (:start_date) or BETWEEN clauses won't be extracted
  3. Silent fallback to current month means incorrect data without any warning
  4. SQL structure changes (e.g., table aliases, quoting styles) will break extraction

Consider passing the date range explicitly to avoid parsing SQL:

-  def initialize(family, transactions_scope:, include_trades: true)
+  def initialize(family, transactions_scope:, include_trades: true, date_range: nil)
     @family = family
     @transactions_scope = transactions_scope
     @include_trades = include_trades
+    @date_range = date_range
   end
-    def extract_date_range
-      # Try to extract date range from the scope's where clauses
-      # This is a heuristic - the transactions_scope should have date filters
-      scope_sql = @transactions_scope.to_sql
-
-      # Default to current month if we can't extract dates
-      start_date = Date.current.beginning_of_month
-      end_date = Date.current.end_of_month
-
-      # Try to find date conditions in the SQL
-      # Look for patterns like "date >= '2024-01-01'" or "date BETWEEN"
-      if scope_sql =~ /entries.*date.*>=.*'(\d{4}-\d{2}-\d{2})'/i
-        start_date = Date.parse($1) rescue start_date
-      end
-
-      if scope_sql =~ /entries.*date.*<=.*'(\d{4}-\d{2}-\d{2})'/i
-        end_date = Date.parse($1) rescue end_date
-      end
-
-      { start_date: start_date, end_date: end_date }
-    end
+    def date_range
+      @date_range || {
+        start_date: Date.current.beginning_of_month,
+        end_date: Date.current.end_of_month
+      }
+    end

Then update callers to pass the date range they already have when constructing the scope.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In app/models/income_statement/totals.rb around lines 134–154, the method
extract_date_range currently parses SQL with fragile regexes and silently falls
back to the current month; instead change the interface so the date range is
provided explicitly (e.g., accept start_date and end_date or a date_range option
in the initializer or method), remove the regex-based SQL parsing, and make the
fallback explicit by raising or logging an error if no dates are supplied; then
update all callers to pass the existing date range they already have when
constructing or calling this class so no SQL string parsing is necessary.

Copy link

@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.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5d235e1 and 4c6dcde.

📒 Files selected for processing (4)
  • app/controllers/pages_controller.rb (2 hunks)
  • app/models/family.rb (1 hunks)
  • app/views/pages/dashboard.html.erb (1 hunks)
  • config/locales/views/pages/en.yml (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/views/pages/dashboard.html.erb
  • app/models/family.rb
🧰 Additional context used
📓 Path-based instructions (11)
**/*.rb

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.rb: Application supports two modes: 'managed' and 'self_hosted' via Rails.application.config.app_mode
Use Current.user and Current.family instead of current_user / current_family for authentication context
Optimize database queries with proper indexes to prevent N+1 queries using includes/joins

**/*.rb: Use Rails pluralization in i18n: t("transactions.count", count: @transactions.count).
Run bin/rubocop -f github -a for Ruby linting with auto-correct before pull requests.

**/*.rb: Ruby code should use 2-space indent, snake_case for methods and variables, and CamelCase for classes and modules, following Rails conventions for folders and file names
Prefer environment variables over hard-coded values for configuration

Files:

  • app/controllers/pages_controller.rb
app/controllers/**/*.rb

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use Rails strong parameters and CSRF protection throughout the application

app/controllers/**/*.rb: Use strong parameters and CSRF protection throughout the application.
Use query params for state over localStorage/sessions.

Files:

  • app/controllers/pages_controller.rb
**/*.{rb,erb,html.erb}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{rb,erb,html.erb}: Use Current.user for the current user context in controllers and views. Do NOT use current_user.
Use Current.family for the current family context. Do NOT use current_family.

Files:

  • app/controllers/pages_controller.rb
**/*.{erb,html.erb,rb}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{erb,html.erb,rb}: All user-facing strings must use localization (i18n) via the t() helper. Update locale files for each new or changed element.
Use server-side formatting for currencies, numbers, and dates. Do not format in JavaScript.

Files:

  • app/controllers/pages_controller.rb
**/*.{rb,erb}

📄 CodeRabbit inference engine (.cursor/rules/general-rules.mdc)

**/*.{rb,erb}: Use Current.user for accessing the current user. Do NOT use current_user
Use Current.family for accessing the current family. Do NOT use current_family

Files:

  • app/controllers/pages_controller.rb
**/*.{rb,js,erb}

📄 CodeRabbit inference engine (.cursor/rules/project-conventions.mdc)

Format currencies, numbers, dates, and other values server-side, then pass to Stimulus controllers for display only

Files:

  • app/controllers/pages_controller.rb
**/*.{rb,html.erb}

📄 CodeRabbit inference engine (.cursor/rules/project-conventions.mdc)

Use ActiveRecord validations for complex validations and business logic. Simple validations may be mirrored in ActiveRecord for form error handling convenience, but prioritize client-side form validation when possible

Files:

  • app/controllers/pages_controller.rb
app/**/*.{rb,js,ts,jsx,tsx,css,scss}

📄 CodeRabbit inference engine (AGENTS.md)

Code should be organized in app/ directory (Rails MVC, services, jobs, mailers, components), with JS in app/javascript/, and styles/assets in app/assets/ (Tailwind, images, fonts)

Files:

  • app/controllers/pages_controller.rb
app/**/*.rb

📄 CodeRabbit inference engine (AGENTS.md)

Run bin/brakeman security scan before major PRs to check for static analysis of common Rails issues

Files:

  • app/controllers/pages_controller.rb
config/locales/**/*.yml

📄 CodeRabbit inference engine (CLAUDE.md)

Use hierarchical i18n keys by feature: accounts.index.title, transactions.form.amount_label. Use t() helper for all user-facing strings with interpolation for dynamic content.

Files:

  • config/locales/views/pages/en.yml
config/**/*.{rb,yml,yaml}

📄 CodeRabbit inference engine (AGENTS.md)

Configuration files should be placed in config/ directory, with environment examples in .env.local.example and .env.test.example

Files:

  • config/locales/views/pages/en.yml
🧠 Learnings (2)
📚 Learning: 2025-11-24T16:56:13.394Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .cursor/rules/project-conventions.mdc:0-0
Timestamp: 2025-11-24T16:56:13.394Z
Learning: Applies to **/*.{rb,js,erb} : Format currencies, numbers, dates, and other values server-side, then pass to Stimulus controllers for display only

Applied to files:

  • app/controllers/pages_controller.rb
📚 Learning: 2025-11-24T16:55:43.046Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T16:55:43.046Z
Learning: Applies to config/locales/**/*.yml : Use hierarchical i18n keys by feature: `accounts.index.title`, `transactions.form.amount_label`. Use `t()` helper for all user-facing strings with interpolation for dynamic content.

Applied to files:

  • config/locales/views/pages/en.yml
🧬 Code graph analysis (1)
app/controllers/pages_controller.rb (3)
app/models/family.rb (1)
  • investment_statement (72-74)
app/models/investment_statement.rb (1)
  • total_income (154-156)
app/models/investment_statement/totals.rb (1)
  • call (7-17)
🪛 ast-grep (0.40.0)
app/controllers/pages_controller.rb

[warning] 80-80: Found the use of an hardcoded passphrase for RSA. The passphrase can be easily discovered, and therefore should not be stored in source-code. It is recommended to remove the passphrase from source-code, and use system environment variables or a restricted configuration file.
Context: add_node.call("income_#{ct.category.id}", ct.category.name, val, percentage, color)
Note: [CWE-798]: Use of Hard-coded Credentials [OWASP A07:2021]: Identification and Authentication Failures [REFERENCES]
https://cwe.mitre.org/data/definitions/522.html

(hardcoded-secret-rsa-passphrase-ruby)


[warning] 94-94: Found the use of an hardcoded passphrase for RSA. The passphrase can be easily discovered, and therefore should not be stored in source-code. It is recommended to remove the passphrase from source-code, and use system environment variables or a restricted configuration file.
Context: add_node.call("expense_#{ct.category.id}", ct.category.name, val, percentage, color)
Note: [CWE-798]: Use of Hard-coded Credentials [OWASP A07:2021]: Identification and Authentication Failures [REFERENCES]
https://cwe.mitre.org/data/definitions/522.html

(hardcoded-secret-rsa-passphrase-ruby)

🔇 Additional comments (2)
config/locales/views/pages/en.yml (1)

35-47: Locale keys are well-structured and follow hierarchical conventions.

The new investment_summary block adheres to the coding guidelines with proper feature-based hierarchy (en.pages.dashboard.investment_summary.*), consistent naming, and correct interpolation syntax for dynamic content. The keys align with their consumption in the investment_summary view partial.

app/controllers/pages_controller.rb (1)

6-18: Dashboard investments wiring looks consistent with new models

Using Current.family.investment_statement and Current.family.income_statement here keeps context-scoped and aligns with the new investment analytics objects. The currency is derived from Current.family.currency and later converted to a symbol server‑side, which matches the guideline of doing formatting on the server before passing data to the frontend. Based on learnings, this is the right place to hook in the investment statement for the dashboard.

Comment on lines +53 to +175
def build_cashflow_sankey_data(income_totals, expense_totals, currency)
nodes = []
links = []
node_indices = {} # Memoize node indices by a unique key: "type_categoryid"
node_indices = {}

# Helper to add/find node and return its index
add_node = ->(unique_key, display_name, value, percentage, color) {
node_indices[unique_key] ||= begin
nodes << { name: display_name, value: value.to_f.round(2), percentage: percentage.to_f.round(1), color: color }
nodes.size - 1
end
}

total_income_val = income_totals.total.to_f.round(2)
total_expense_val = expense_totals.total.to_f.round(2)
total_income = income_totals.total.to_f.round(2)
total_expense = expense_totals.total.to_f.round(2)

# --- Create Central Cash Flow Node ---
cash_flow_idx = add_node.call("cash_flow_node", "Cash Flow", total_income_val, 0, "var(--color-success)")
# Central Cash Flow node
cash_flow_idx = add_node.call("cash_flow_node", "Cash Flow", total_income, 100.0, "var(--color-success)")

# --- Process Income Side (Top-level categories only) ---
# Income side (top-level categories only)
income_totals.category_totals.each do |ct|
# Skip subcategories – only include root income categories
next if ct.category.parent_id.present?

val = ct.total.to_f.round(2)
next if val.zero?

percentage_of_total_income = total_income_val.zero? ? 0 : (val / total_income_val * 100).round(1)

node_display_name = ct.category.name
node_color = ct.category.color.presence || Category::COLORS.sample

current_cat_idx = add_node.call(
"income_#{ct.category.id}",
node_display_name,
val,
percentage_of_total_income,
node_color
)

links << {
source: current_cat_idx,
target: cash_flow_idx,
value: val,
color: node_color,
percentage: percentage_of_total_income
}
percentage = total_income.zero? ? 0 : (val / total_income * 100).round(1)
color = ct.category.color.presence || Category::COLORS.sample

idx = add_node.call("income_#{ct.category.id}", ct.category.name, val, percentage, color)
links << { source: idx, target: cash_flow_idx, value: val, color: color, percentage: percentage }
end

# --- Process Expense Side (Top-level categories only) ---
# Expense side (top-level categories only)
expense_totals.category_totals.each do |ct|
# Skip subcategories – only include root expense categories to keep Sankey shallow
next if ct.category.parent_id.present?

val = ct.total.to_f.round(2)
next if val.zero?

percentage_of_total_expense = total_expense_val.zero? ? 0 : (val / total_expense_val * 100).round(1)

node_display_name = ct.category.name
node_color = ct.category.color.presence || Category::UNCATEGORIZED_COLOR

current_cat_idx = add_node.call(
"expense_#{ct.category.id}",
node_display_name,
val,
percentage_of_total_expense,
node_color
)

links << {
source: cash_flow_idx,
target: current_cat_idx,
value: val,
color: node_color,
percentage: percentage_of_total_expense
}
end
percentage = total_expense.zero? ? 0 : (val / total_expense * 100).round(1)
color = ct.category.color.presence || Category::UNCATEGORIZED_COLOR

# --- Process Surplus ---
leftover = (total_income_val - total_expense_val).round(2)
if leftover.positive?
percentage_of_total_income_for_surplus = total_income_val.zero? ? 0 : (leftover / total_income_val * 100).round(1)
surplus_idx = add_node.call("surplus_node", "Surplus", leftover, percentage_of_total_income_for_surplus, "var(--color-success)")
links << { source: cash_flow_idx, target: surplus_idx, value: leftover, color: "var(--color-success)", percentage: percentage_of_total_income_for_surplus }
idx = add_node.call("expense_#{ct.category.id}", ct.category.name, val, percentage, color)
links << { source: cash_flow_idx, target: idx, value: val, color: color, percentage: percentage }
end

# Update Cash Flow and Income node percentages (relative to total income)
if node_indices["cash_flow_node"]
nodes[node_indices["cash_flow_node"]][:percentage] = 100.0
# Surplus/Deficit
net = (total_income - total_expense).round(2)
if net.positive?
percentage = total_income.zero? ? 0 : (net / total_income * 100).round(1)
idx = add_node.call("surplus_node", "Surplus", net, percentage, "var(--color-success)")
links << { source: cash_flow_idx, target: idx, value: net, color: "var(--color-success)", percentage: percentage }
end
# No primary income node anymore, percentages are on individual income cats relative to total_income_val

{ nodes: nodes, links: links, currency_symbol: Money::Currency.new(currency_symbol).symbol }
{ nodes: nodes, links: links, currency_symbol: Money::Currency.new(currency).symbol }
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Sankey helper: add i18n for labels and decide how to represent deficits

Two things to tighten up here:

  1. i18n for user‑facing labels
    "Cash Flow" and "Surplus" are likely rendered directly in the UI. Per guidelines, all user‑facing strings should go through t(). Consider something like:

  • cash_flow_idx = add_node.call("cash_flow_node", "Cash Flow", total_income, 100.0, "var(--color-success)")
  • cash_flow_idx = add_node.call(
  • "cash_flow_node",
  • t("dashboard.cashflow.sankey.cash_flow"),
  • total_income,
  • 100.0,
  • "var(--color-success)"
  • )
    ...
  • idx = add_node.call("surplus_node", "Surplus", net, percentage, "var(--color-success)")
  • idx = add_node.call(
  • "surplus_node",
  • t("dashboard.cashflow.sankey.surplus"),
  • net,
  • percentage,
  • "var(--color-success)"
  • )
    …plus corresponding keys in the locale files. This keeps the chart labels localizable. As per coding guidelines, all new user‑facing strings should be localized.
    
    
  1. Deficit case currently not surfaced
    The comment says # Surplus/Deficit, but the implementation only adds a node when net.positive?. When expenses exceed income, the chart will show more flow out of the central node than into it, with no explicit “Deficit” node. If you want symmetry with the surplus case, you could add a branch for net.negative? to create a “Deficit” node (also localized) and link it appropriately, or update the comment to reflect that only surplus is visualized.

These are behavioral/UX refinements rather than correctness bugs, but worth addressing now while this helper is fresh.

🧰 Tools
🪛 ast-grep (0.40.0)

[warning] 80-80: Found the use of an hardcoded passphrase for RSA. The passphrase can be easily discovered, and therefore should not be stored in source-code. It is recommended to remove the passphrase from source-code, and use system environment variables or a restricted configuration file.
Context: add_node.call("income_#{ct.category.id}", ct.category.name, val, percentage, color)
Note: [CWE-798]: Use of Hard-coded Credentials [OWASP A07:2021]: Identification and Authentication Failures [REFERENCES]
https://cwe.mitre.org/data/definitions/522.html

(hardcoded-secret-rsa-passphrase-ruby)


[warning] 94-94: Found the use of an hardcoded passphrase for RSA. The passphrase can be easily discovered, and therefore should not be stored in source-code. It is recommended to remove the passphrase from source-code, and use system environment variables or a restricted configuration file.
Context: add_node.call("expense_#{ct.category.id}", ct.category.name, val, percentage, color)
Note: [CWE-798]: Use of Hard-coded Credentials [OWASP A07:2021]: Identification and Authentication Failures [REFERENCES]
https://cwe.mitre.org/data/definitions/522.html

(hardcoded-secret-rsa-passphrase-ruby)

Copy link

@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.

Actionable comments posted: 1

♻️ Duplicate comments (8)
docs/roadmap/investment-integration-assessment.md (1)

43-43: Resolve previously flagged markdown linting violations.

These markdown formatting issues were identified in the prior review and must be fixed before merge. Static analysis confirms violations remain unresolved:

  1. Lines 43, 398, 430, 444: Code blocks lack language identifiers (MD040)
  2. Lines 163, 169: Emphasis used as headings instead of markdown syntax (MD036)

Apply this comprehensive diff to fix all violations:

@@ line 43
-```
+```ruby

@@ line 163
-**Option A: Shadow Transactions**
+### Option A: Shadow Transactions

@@ line 169
-**Option B: Parallel Tracking**
+### Option B: Parallel Tracking

@@ line 398
-```
+```

@@ line 430
- +


@@ line 444
-```
+```





Also applies to: 163-163, 169-169, 398-398, 430-430, 444-444

</blockquote></details>
<details>
<summary>app/models/income_statement/totals.rb (2)</summary><blockquote>

`93-119`: **Inconsistent account status filtering between trades and transactions.**

This concern was raised in a previous review. The trades subquery filters by `a.status IN ('draft', 'active')` (line 113), but the transactions subquery has no account status filter. This leads to inconsistent totals where transactions from inactive/archived accounts are included while trades from those accounts are excluded.

---

`134-154`: **Fragile regex-based SQL parsing is error-prone.**

This concern was raised in a previous review. Parsing SQL strings with regex to extract dates is brittle and can silently produce incorrect results. The regex patterns may not match parameterized dates or different SQL structures, and the fallback to current month happens silently without warning.

</blockquote></details>
<details>
<summary>app/models/investment_statement/totals.rb (1)</summary><blockquote>

`30-47`: **Exchange rate handling silently defaults to 1.**

The SQL query uses `COALESCE(er.rate, 1)` on lines 33-34, which silently treats missing exchange rates as 1:1 conversion. This could produce incorrect totals for multi-currency portfolios without any indication that data is missing.

</blockquote></details>
<details>
<summary>app/controllers/pages_controller.rb (2)</summary><blockquote>

`121-138`: **Hardcoded user-facing strings need i18n.**

Line 137 contains the hardcoded string `"Cash Flow"` that will be displayed in the UI. Per coding guidelines, all user-facing strings should use `t()` for localization.

---

`167-176`: **Hardcoded "Surplus" string needs i18n.**

Line 171 contains the hardcoded string `"Surplus"` that will be displayed in the UI. Per coding guidelines, all user-facing strings should use `t()` for localization.

</blockquote></details>
<details>
<summary>app/models/investment_statement.rb (2)</summary><blockquote>

`6-6`: **Missing instance methods for monetize macro.**

The `monetize` macro references `:total_contributions`, `:total_dividends`, and `:total_interest`, but these instance methods don't exist. Only `unrealized_gains` is defined (lines 109-115). The generated `*_money` methods will fail with `NoMethodError` at runtime.

---

`117-125`: **Potential nil access on avg_cost.**

Line 122 accesses `h.avg_cost.amount` without checking if `avg_cost` is nil. If any holding has a nil `avg_cost`, this will raise `NoMethodError`.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>app/models/income_statement/totals.rb (1)</summary><blockquote>

`48-91`: **Reduce duplication between transactions queries.**

`transactions_only_query_sql` (lines 48-69) and `transactions_subquery_sql` (lines 71-91) are nearly identical, differing only in the column alias (`transactions_count` vs `entry_count`). Extract a shared method with a parameter for the alias name.



```diff
-    # Original transactions-only query (for backwards compatibility)
-    def transactions_only_query_sql
+    def transactions_query_sql(count_alias: "transactions_count")
       <<~SQL
         SELECT
           c.id as category_id,
           c.parent_id as parent_category_id,
           CASE WHEN ae.amount < 0 THEN 'income' ELSE 'expense' END as classification,
           ABS(SUM(ae.amount * COALESCE(er.rate, 1))) as total,
-          COUNT(ae.id) as transactions_count
+          COUNT(ae.id) as #{count_alias}
         FROM (#{@transactions_scope.to_sql}) at
         JOIN entries ae ON ae.entryable_id = at.id AND ae.entryable_type = 'Transaction'
         LEFT JOIN categories c ON c.id = at.category_id
         LEFT JOIN exchange_rates er ON (
           er.date = ae.date AND
           er.from_currency = ae.currency AND
           er.to_currency = :target_currency
         )
         WHERE at.kind NOT IN ('funds_movement', 'one_time', 'cc_payment')
           AND ae.excluded = false
         GROUP BY c.id, c.parent_id, CASE WHEN ae.amount < 0 THEN 'income' ELSE 'expense' END;
       SQL
     end
+
+    def transactions_only_query_sql
+      transactions_query_sql(count_alias: "transactions_count")
+    end
+
+    def transactions_subquery_sql
+      transactions_query_sql(count_alias: "entry_count").chomp(";")
+    end
app/views/pages/dashboard.html.erb (1)

82-116: New sections are not integrated with the sortable/collapsible dashboard UI.

The existing dashboard sections (lines 32-81) use a sortable, collapsible infrastructure with drag-and-drop reordering. The new sections (cashflow_sankey, outflows_donut, investment_summary, net_worth_chart, balance_sheet) are rendered outside this loop and lack:

  • Collapse/expand functionality
  • Drag-to-reorder capability
  • Consistent section headers with toggle buttons

This creates an inconsistent user experience. Consider adding these sections to the @dashboard_sections array in the controller so they participate in the same sortable/collapsible behavior.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d4d5d66 and 4ff1773.

📒 Files selected for processing (23)
  • app/controllers/pages_controller.rb (2 hunks)
  • app/controllers/reports_controller.rb (2 hunks)
  • app/controllers/trades_controller.rb (1 hunks)
  • app/models/category.rb (3 hunks)
  • app/models/family.rb (1 hunks)
  • app/models/income_statement/totals.rb (3 hunks)
  • app/models/investment_statement.rb (1 hunks)
  • app/models/investment_statement/totals.rb (1 hunks)
  • app/models/trade.rb (1 hunks)
  • app/models/trade/create_form.rb (2 hunks)
  • app/views/pages/dashboard.html.erb (1 hunks)
  • app/views/pages/dashboard/_investment_summary.html.erb (1 hunks)
  • app/views/reports/_investment_performance.html.erb (1 hunks)
  • app/views/reports/index.html.erb (1 hunks)
  • app/views/trades/_trade.html.erb (1 hunks)
  • app/views/trades/show.html.erb (1 hunks)
  • config/locales/views/pages/en.yml (1 hunks)
  • config/locales/views/reports/en.yml (1 hunks)
  • config/locales/views/trades/en.yml (2 hunks)
  • db/migrate/20251125141213_add_category_to_trades.rb (1 hunks)
  • db/schema.rb (4 hunks)
  • docs/roadmap/investment-integration-assessment.md (1 hunks)
  • test/controllers/categories_controller_test.rb (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (11)
  • app/models/family.rb
  • config/locales/views/trades/en.yml
  • app/controllers/trades_controller.rb
  • app/views/trades/_trade.html.erb
  • test/controllers/categories_controller_test.rb
  • app/views/trades/show.html.erb
  • config/locales/views/pages/en.yml
  • app/models/category.rb
  • app/views/reports/_investment_performance.html.erb
  • app/models/trade.rb
  • app/models/trade/create_form.rb
🧰 Additional context used
📓 Path-based instructions (28)
{app/**/*.{erb,html},app/javascript/**/*.js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Prefer Hotwire-first approach: use Turbo + Stimulus for reactive UI without heavy JavaScript

Files:

  • app/views/pages/dashboard.html.erb
  • app/views/pages/dashboard/_investment_summary.html.erb
  • app/views/reports/index.html.erb
app/views/**/*.erb

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

app/views/**/*.erb: Always use the icon helper for icons in views, never use lucide_icon directly
Use Tailwind design tokens (e.g., text-primary, bg-container, border-secondary) instead of raw color names
Pass data to Stimulus controllers via data-*-value attributes instead of inline JavaScript
Prefer semantic HTML elements (dialog, details, summary) over JavaScript components
Use Turbo Frames for page sections instead of client-side solutions
Use server-side formatting for currencies, numbers, and dates instead of client-side

Views should use ERB and avoid heavy logic; prefer helpers and components instead (ERB checked by erb-lint per .erb_lint.yml)

Files:

  • app/views/pages/dashboard.html.erb
  • app/views/pages/dashboard/_investment_summary.html.erb
  • app/views/reports/index.html.erb
**/*.{css,erb}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use Tailwind CSS v4.x with custom design system for styling

Files:

  • app/views/pages/dashboard.html.erb
  • app/views/pages/dashboard/_investment_summary.html.erb
  • app/views/reports/index.html.erb
**/*.{rb,erb,html.erb}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{rb,erb,html.erb}: Use Current.user for the current user context in controllers and views. Do NOT use current_user.
Use Current.family for the current family context. Do NOT use current_family.

Files:

  • app/views/pages/dashboard.html.erb
  • app/models/income_statement/totals.rb
  • app/views/pages/dashboard/_investment_summary.html.erb
  • app/models/investment_statement/totals.rb
  • app/controllers/reports_controller.rb
  • db/schema.rb
  • app/views/reports/index.html.erb
  • app/models/investment_statement.rb
  • db/migrate/20251125141213_add_category_to_trades.rb
  • app/controllers/pages_controller.rb
**/*.{erb,html,vue,jsx,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{erb,html,vue,jsx,tsx}: Use Tailwind CSS v4.x with custom design system defined in app/assets/tailwind/maybe-design-system.css. Always use functional tokens (e.g., text-primary not text-white).
Prefer semantic HTML elements over JavaScript components (e.g., use <dialog> for modals, <details><summary> for disclosures).

Files:

  • app/views/pages/dashboard.html.erb
  • app/views/pages/dashboard/_investment_summary.html.erb
  • app/views/reports/index.html.erb
**/*.{erb,html.erb}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{erb,html.erb}: Use icon helper for icons, never use lucide_icon directly.
Leverage Turbo frames for page sections over client-side solutions.
Pass data to Stimulus controllers via data-*-value attributes, not inline JavaScript.

**/*.{erb,html.erb}: Use ViewComponents when: element has complex logic or styling patterns, will be reused across multiple views/contexts, needs structured styling with variants/sizes, requires interactive behavior or Stimulus controllers, has configurable slots or complex APIs, or needs accessibility features or ARIA support
Use Partials when: element is primarily static HTML with minimal logic, used in only one or few specific contexts, is simple template content, doesn't need variants/sizes/complex configuration, or is more about content organization than reusable functionality
Prefer components over partials: if a component is available in app/components, use it; if not, look for a partial; if neither exists, decide between component or partial based on complexity and reusability criteria
Keep domain logic out of views: compute values like button classes, conditional logic, and data transformations in the component file, not the template file
Always use the declarative approach when integrating Stimulus controllers in views: the ERB template should declare what happens using data-* attributes, and the Stimulus controller should respond
Component controllers in app/components/ should only be used within their component templates; global controllers in app/javascript/controllers/ can be used across any view
Pass data from Rails to Stimulus using data-*-value attributes, not inline JavaScript

Files:

  • app/views/pages/dashboard.html.erb
  • app/views/pages/dashboard/_investment_summary.html.erb
  • app/views/reports/index.html.erb
**/*.{erb,html.erb,rb}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{erb,html.erb,rb}: All user-facing strings must use localization (i18n) via the t() helper. Update locale files for each new or changed element.
Use server-side formatting for currencies, numbers, and dates. Do not format in JavaScript.

Files:

  • app/views/pages/dashboard.html.erb
  • app/models/income_statement/totals.rb
  • app/views/pages/dashboard/_investment_summary.html.erb
  • app/models/investment_statement/totals.rb
  • app/controllers/reports_controller.rb
  • db/schema.rb
  • app/views/reports/index.html.erb
  • app/models/investment_statement.rb
  • db/migrate/20251125141213_add_category_to_trades.rb
  • app/controllers/pages_controller.rb
{app/components/**/*.html.erb,app/views/**/*.{erb,html.erb}}

📄 CodeRabbit inference engine (CLAUDE.md)

Keep domain logic out of view templates. Logic belongs in component files, not template files.

Files:

  • app/views/pages/dashboard.html.erb
  • app/views/pages/dashboard/_investment_summary.html.erb
  • app/views/reports/index.html.erb
**/*.erb

📄 CodeRabbit inference engine (CLAUDE.md)

Run bundle exec erb_lint ./app/**/*.erb -a for ERB linting with auto-correct before pull requests.

Files:

  • app/views/pages/dashboard.html.erb
  • app/views/pages/dashboard/_investment_summary.html.erb
  • app/views/reports/index.html.erb
**/*.{rb,erb}

📄 CodeRabbit inference engine (.cursor/rules/general-rules.mdc)

**/*.{rb,erb}: Use Current.user for accessing the current user. Do NOT use current_user
Use Current.family for accessing the current family. Do NOT use current_family

Files:

  • app/views/pages/dashboard.html.erb
  • app/models/income_statement/totals.rb
  • app/views/pages/dashboard/_investment_summary.html.erb
  • app/models/investment_statement/totals.rb
  • app/controllers/reports_controller.rb
  • db/schema.rb
  • app/views/reports/index.html.erb
  • app/models/investment_statement.rb
  • db/migrate/20251125141213_add_category_to_trades.rb
  • app/controllers/pages_controller.rb
**/*.html.erb

📄 CodeRabbit inference engine (.cursor/rules/project-conventions.mdc)

**/*.html.erb: Prefer native HTML elements over JavaScript-based components. Use semantic HTML: for modals,

for disclosures
Leverage Turbo frames to break up pages instead of JavaScript-driven client-side solutions
Use the icon helper from application_helper.rb for icons. Never use lucide_icon helper directly

Files:

  • app/views/pages/dashboard.html.erb
  • app/views/pages/dashboard/_investment_summary.html.erb
  • app/views/reports/index.html.erb
**/*.{rb,js,erb}

📄 CodeRabbit inference engine (.cursor/rules/project-conventions.mdc)

Format currencies, numbers, dates, and other values server-side, then pass to Stimulus controllers for display only

Files:

  • app/views/pages/dashboard.html.erb
  • app/models/income_statement/totals.rb
  • app/views/pages/dashboard/_investment_summary.html.erb
  • app/models/investment_statement/totals.rb
  • app/controllers/reports_controller.rb
  • db/schema.rb
  • app/views/reports/index.html.erb
  • app/models/investment_statement.rb
  • db/migrate/20251125141213_add_category_to_trades.rb
  • app/controllers/pages_controller.rb
**/*.{rb,html.erb}

📄 CodeRabbit inference engine (.cursor/rules/project-conventions.mdc)

Use ActiveRecord validations for complex validations and business logic. Simple validations may be mirrored in ActiveRecord for form error handling convenience, but prioritize client-side form validation when possible

Files:

  • app/views/pages/dashboard.html.erb
  • app/models/income_statement/totals.rb
  • app/views/pages/dashboard/_investment_summary.html.erb
  • app/models/investment_statement/totals.rb
  • app/controllers/reports_controller.rb
  • db/schema.rb
  • app/views/reports/index.html.erb
  • app/models/investment_statement.rb
  • db/migrate/20251125141213_add_category_to_trades.rb
  • app/controllers/pages_controller.rb
{app/views/**,app/helpers/**,app/javascript/controllers/**}

📄 CodeRabbit inference engine (.cursor/rules/ui-ux-design-guidelines.mdc)

{app/views/**,app/helpers/**,app/javascript/controllers/**}: Reference maybe-design-system.css for base primitives, functional tokens, and component tokens before writing styles
Prefer using functional design system tokens (e.g., text-primary, bg-container, border-primary) from maybe-design-system.css instead of raw Tailwind color values

Files:

  • app/views/pages/dashboard.html.erb
  • app/views/pages/dashboard/_investment_summary.html.erb
  • app/views/reports/index.html.erb
{app/views/**,app/helpers/**}

📄 CodeRabbit inference engine (.cursor/rules/ui-ux-design-guidelines.mdc)

Always generate semantic HTML

Files:

  • app/views/pages/dashboard.html.erb
  • app/views/pages/dashboard/_investment_summary.html.erb
  • app/views/reports/index.html.erb
config/locales/**/*.yml

📄 CodeRabbit inference engine (CLAUDE.md)

Use hierarchical i18n keys by feature: accounts.index.title, transactions.form.amount_label. Use t() helper for all user-facing strings with interpolation for dynamic content.

Files:

  • config/locales/views/reports/en.yml
config/**/*.{rb,yml,yaml}

📄 CodeRabbit inference engine (AGENTS.md)

Configuration files should be placed in config/ directory, with environment examples in .env.local.example and .env.test.example

Files:

  • config/locales/views/reports/en.yml
**/*.rb

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.rb: Application supports two modes: 'managed' and 'self_hosted' via Rails.application.config.app_mode
Use Current.user and Current.family instead of current_user / current_family for authentication context
Optimize database queries with proper indexes to prevent N+1 queries using includes/joins

**/*.rb: Use Rails pluralization in i18n: t("transactions.count", count: @transactions.count).
Run bin/rubocop -f github -a for Ruby linting with auto-correct before pull requests.

**/*.rb: Ruby code should use 2-space indent, snake_case for methods and variables, and CamelCase for classes and modules, following Rails conventions for folders and file names
Prefer environment variables over hard-coded values for configuration

Files:

  • app/models/income_statement/totals.rb
  • app/models/investment_statement/totals.rb
  • app/controllers/reports_controller.rb
  • db/schema.rb
  • app/models/investment_statement.rb
  • db/migrate/20251125141213_add_category_to_trades.rb
  • app/controllers/pages_controller.rb
app/models/**/*.rb

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

app/models/**/*.rb: Keep business logic in models using skinny controllers and fat models pattern
Store all monetary values in base currency (user's primary currency) and use Money objects for conversion
Use ActiveRecord validations for form convenience and complex business logic validations
Use Rails concerns and POROs for organizing business logic instead of services folder

app/models/**/*.rb: All monetary values stored in base currency (user's primary currency). Use Money objects for currency conversion and formatting.
Place business logic in app/models/ folder. Avoid creating app/services/ folder. Use Rails concerns and POROs for organization.
Models should answer questions about themselves using instance methods: use account.balance_series not AccountSeries.new(account).call.

Files:

  • app/models/income_statement/totals.rb
  • app/models/investment_statement/totals.rb
  • app/models/investment_statement.rb
{app/models/**/*.rb,db/migrate/**/*.rb}

📄 CodeRabbit inference engine (CLAUDE.md)

Simple validations (null checks, unique indexes) should be in the database. ActiveRecord validations for form convenience (prefer client-side when possible). Complex validations and business logic in ActiveRecord.

Files:

  • app/models/income_statement/totals.rb
  • app/models/investment_statement/totals.rb
  • app/models/investment_statement.rb
  • db/migrate/20251125141213_add_category_to_trades.rb
app/**/*.{rb,js,ts,jsx,tsx,css,scss}

📄 CodeRabbit inference engine (AGENTS.md)

Code should be organized in app/ directory (Rails MVC, services, jobs, mailers, components), with JS in app/javascript/, and styles/assets in app/assets/ (Tailwind, images, fonts)

Files:

  • app/models/income_statement/totals.rb
  • app/models/investment_statement/totals.rb
  • app/controllers/reports_controller.rb
  • app/models/investment_statement.rb
  • app/controllers/pages_controller.rb
app/**/*.rb

📄 CodeRabbit inference engine (AGENTS.md)

Run bin/brakeman security scan before major PRs to check for static analysis of common Rails issues

Files:

  • app/models/income_statement/totals.rb
  • app/models/investment_statement/totals.rb
  • app/controllers/reports_controller.rb
  • app/models/investment_statement.rb
  • app/controllers/pages_controller.rb
app/views/**/_*.html.erb

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use partials only for simple, static HTML with minimal logic in specific contexts

app/views/**/_*.html.erb: Use underscore prefix for partial files (e.g., _trend_change.html.erb, _form_errors.html.erb, _sync_indicator.html.erb)
Place context-specific partials in relevant controller view directory (e.g., accounts/_account_sidebar_tabs.html.erb)

Files:

  • app/views/pages/dashboard/_investment_summary.html.erb
app/views/**/_*.{erb,html.erb}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Partials when element is primarily static HTML with minimal logic, used in only one or few contexts, is simple template content, or doesn't need variants/sizes/complex configuration.

Files:

  • app/views/pages/dashboard/_investment_summary.html.erb
app/controllers/**/*.rb

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use Rails strong parameters and CSRF protection throughout the application

app/controllers/**/*.rb: Use strong parameters and CSRF protection throughout the application.
Use query params for state over localStorage/sessions.

Files:

  • app/controllers/reports_controller.rb
  • app/controllers/pages_controller.rb
db/**/*.rb

📄 CodeRabbit inference engine (AGENTS.md)

Data migrations should be placed in db/ directory with test fixtures in test/fixtures/

Files:

  • db/schema.rb
  • db/migrate/20251125141213_add_category_to_trades.rb
db/migrate/**/*.rb

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use simple validations (null checks, unique indexes) in database layer

Files:

  • db/migrate/20251125141213_add_category_to_trades.rb
db/migrate/*.rb

📄 CodeRabbit inference engine (.cursor/rules/general-rules.mdc)

ActiveRecord migrations must inherit from ActiveRecord::Migration[7.2]. Do NOT use version 8.0 yet

Enforce null checks, unique indexes, and simple validations in the database schema for PostgreSQL

Files:

  • db/migrate/20251125141213_add_category_to_trades.rb
🧠 Learnings (26)
📓 Common learnings
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .cursor/rules/project-design.mdc:0-0
Timestamp: 2025-11-24T16:56:30.669Z
Learning: Entry model is a Rails delegated type with three subtypes: Valuation (absolute account value), Transaction (balance modification), and Trade (buy/sell for investment accounts)
📚 Learning: 2025-11-24T16:57:20.318Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .cursor/rules/view_conventions.mdc:0-0
Timestamp: 2025-11-24T16:57:20.318Z
Learning: Applies to app/views/**/_*.html.erb : Place context-specific partials in relevant controller view directory (e.g., accounts/_account_sidebar_tabs.html.erb)

Applied to files:

  • app/views/pages/dashboard.html.erb
📚 Learning: 2025-11-24T16:54:59.198Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-24T16:54:59.198Z
Learning: Applies to app/views/**/*.erb : Use Turbo Frames for page sections instead of client-side solutions

Applied to files:

  • app/views/pages/dashboard.html.erb
  • app/views/reports/index.html.erb
📚 Learning: 2025-11-24T16:55:43.069Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T16:55:43.069Z
Learning: Applies to config/locales/**/*.yml : Use hierarchical i18n keys by feature: `accounts.index.title`, `transactions.form.amount_label`. Use `t()` helper for all user-facing strings with interpolation for dynamic content.

Applied to files:

  • config/locales/views/reports/en.yml
  • app/controllers/pages_controller.rb
📚 Learning: 2025-11-24T16:55:43.069Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T16:55:43.069Z
Learning: Applies to **/*.{erb,html.erb,rb} : All user-facing strings must use localization (i18n) via the `t()` helper. Update locale files for each new or changed element.

Applied to files:

  • config/locales/views/reports/en.yml
  • app/controllers/pages_controller.rb
📚 Learning: 2025-09-13T12:23:31.324Z
Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: db/migrate/20250825224717_add_enable_banking_domain.rb:3-34
Timestamp: 2025-09-13T12:23:31.324Z
Learning: When suggesting database schema improvements for banking integrations, consider consistency across all similar integrations (Plaid, SimpleFin, Enable Banking) and propose comprehensive changes in a separate PR rather than making changes to individual integrations in isolation.

Applied to files:

  • app/models/income_statement/totals.rb
📚 Learning: 2025-11-24T16:56:30.669Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .cursor/rules/project-design.mdc:0-0
Timestamp: 2025-11-24T16:56:30.669Z
Learning: Account sync must perform transfer auto-matching, calculate daily balance records from start_date to Date.current, calculate holdings, and optionally enrich transaction data

Applied to files:

  • app/models/income_statement/totals.rb
  • app/controllers/reports_controller.rb
  • docs/roadmap/investment-integration-assessment.md
  • app/models/investment_statement.rb
📚 Learning: 2025-09-13T11:36:45.479Z
Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: app/models/enable_banking_account/processor.rb:72-79
Timestamp: 2025-09-13T11:36:45.479Z
Learning: The balance_calculator method pattern is consistent across banking integrations in the codebase. Both PlaidAccount::Processor and EnableBankingAccount::Processor use the same approach: `balance = account.current_balance || account.available_balance || 0` followed by `OpenStruct.new(balance: balance, cash_balance: balance)` without explicit requires, and include identical comments about not distinguishing cash vs non-cash balances for non-investment accounts.

Applied to files:

  • app/models/income_statement/totals.rb
  • app/models/investment_statement.rb
📚 Learning: 2025-11-24T16:56:30.669Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .cursor/rules/project-design.mdc:0-0
Timestamp: 2025-11-24T16:56:30.669Z
Learning: Entry model is a Rails delegated type with three subtypes: Valuation (absolute account value), Transaction (balance modification), and Trade (buy/sell for investment accounts)

Applied to files:

  • app/models/income_statement/totals.rb
  • app/models/investment_statement/totals.rb
  • app/controllers/reports_controller.rb
  • docs/roadmap/investment-integration-assessment.md
  • app/models/investment_statement.rb
📚 Learning: 2025-11-24T16:54:59.198Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-24T16:54:59.198Z
Learning: Applies to **/*.rb : Optimize database queries with proper indexes to prevent N+1 queries using includes/joins

Applied to files:

  • app/models/income_statement/totals.rb
  • db/schema.rb
📚 Learning: 2025-11-24T16:55:43.069Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T16:55:43.069Z
Learning: Applies to app/models/**/*.rb : All monetary values stored in base currency (user's primary currency). Use `Money` objects for currency conversion and formatting.

Applied to files:

  • app/models/investment_statement/totals.rb
  • app/models/investment_statement.rb
📚 Learning: 2025-11-24T16:54:59.198Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-24T16:54:59.198Z
Learning: Applies to app/models/**/*.rb : Store all monetary values in base currency (user's primary currency) and use Money objects for conversion

Applied to files:

  • app/models/investment_statement/totals.rb
📚 Learning: 2025-11-24T16:56:30.669Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .cursor/rules/project-design.mdc:0-0
Timestamp: 2025-11-24T16:56:30.669Z
Learning: Financial data is normalized to a single family currency via ExchangeRate records to enable metrics, historical graphs, and cross-currency insights

Applied to files:

  • app/models/investment_statement/totals.rb
📚 Learning: 2025-11-24T16:55:43.069Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T16:55:43.069Z
Learning: Core domain model: User → Accounts → Transactions. Accounts support types: checking, savings, credit cards, investments, crypto, loans, properties. Transactions belong to Categories and may have Tags and Rules.

Applied to files:

  • docs/roadmap/investment-integration-assessment.md
📚 Learning: 2025-11-24T16:56:44.818Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .cursor/rules/self_improve.mdc:0-0
Timestamp: 2025-11-24T16:56:44.818Z
Learning: Update rules after major refactors

Applied to files:

  • db/schema.rb
📚 Learning: 2025-11-24T16:56:13.406Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .cursor/rules/project-conventions.mdc:0-0
Timestamp: 2025-11-24T16:56:13.406Z
Learning: Applies to db/migrate/*.rb : Enforce null checks, unique indexes, and simple validations in the database schema for PostgreSQL

Applied to files:

  • db/schema.rb
📚 Learning: 2025-11-24T16:56:44.818Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .cursor/rules/self_improve.mdc:0-0
Timestamp: 2025-11-24T16:56:44.818Z
Learning: Document breaking changes in rules

Applied to files:

  • db/schema.rb
📚 Learning: 2025-11-24T16:54:59.198Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-24T16:54:59.198Z
Learning: Applies to db/migrate/**/*.rb : Use simple validations (null checks, unique indexes) in database layer

Applied to files:

  • db/schema.rb
📚 Learning: 2025-11-14T14:22:37.076Z
Learnt from: jjmata
Repo: we-promise/sure PR: 326
File: app/models/category_import.rb:10-10
Timestamp: 2025-11-14T14:22:37.076Z
Learning: In the CategoryImport model (app/models/category_import.rb), the import process implements a "last write wins" strategy where importing a category.csv file completely overrides existing category structures, including parent relationships. Setting category.parent = nil in the first pass is intentional behavior to ensure the CSV becomes the source of truth.

Applied to files:

  • db/schema.rb
  • db/migrate/20251125141213_add_category_to_trades.rb
📚 Learning: 2025-11-24T16:56:13.406Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .cursor/rules/project-conventions.mdc:0-0
Timestamp: 2025-11-24T16:56:13.406Z
Learning: Use Rails concerns for code organization around model 'traits' and shared functionality, not just for moving code to another location. Prefer models answering questions about themselves with methods like account.balance_series over service-like patterns

Applied to files:

  • app/models/investment_statement.rb
📚 Learning: 2025-11-24T16:55:43.069Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T16:55:43.069Z
Learning: Applies to app/models/**/*.rb : Models should answer questions about themselves using instance methods: use `account.balance_series` not `AccountSeries.new(account).call`.

Applied to files:

  • app/models/investment_statement.rb
📚 Learning: 2025-11-04T15:16:23.341Z
Learnt from: sokie
Repo: we-promise/sure PR: 276
File: app/controllers/reports_controller.rb:159-169
Timestamp: 2025-11-04T15:16:23.341Z
Learning: In the Budget model, the methods `actual_spending` and `allocated_spending` return BigDecimal values (not Money objects), so division operations like `budget.actual_spending / budget.allocated_spending` work correctly without needing conversion to fractional units.

Applied to files:

  • app/models/investment_statement.rb
📚 Learning: 2025-09-13T11:36:45.479Z
Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: app/models/enable_banking_account/processor.rb:72-79
Timestamp: 2025-09-13T11:36:45.479Z
Learning: The balance_calculator method in EnableBankingAccount::Processor follows the same pattern as the Plaid integration, using OpenStruct without explicit requires and returning balance values as-is without BigDecimal coercion for consistency across banking integrations.

Applied to files:

  • app/models/investment_statement.rb
📚 Learning: 2025-11-24T16:56:30.669Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .cursor/rules/project-design.mdc:0-0
Timestamp: 2025-11-24T16:56:30.669Z
Learning: Account model is a Rails delegated type with subtypes: Depository, Investment, Crypto, Property, Vehicle, OtherAsset (assets) and CreditCard, Loan, OtherLiability (liabilities)

Applied to files:

  • app/models/investment_statement.rb
📚 Learning: 2025-11-24T16:55:59.156Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .cursor/rules/general-rules.mdc:0-0
Timestamp: 2025-11-24T16:55:59.156Z
Learning: Applies to db/migrate/*.rb : ActiveRecord migrations must inherit from `ActiveRecord::Migration[7.2]`. Do NOT use version 8.0 yet

Applied to files:

  • db/migrate/20251125141213_add_category_to_trades.rb
📚 Learning: 2025-10-07T09:51:02.265Z
Learnt from: jjmata
Repo: we-promise/sure PR: 189
File: app/views/password_resets/new.html.erb:14-14
Timestamp: 2025-10-07T09:51:02.265Z
Learning: In the we-promise/sure repository, use i18n methods (like `t()` or `I18n.t`) for all user-facing strings to maintain proper internationalization infrastructure. While translations should be provided, extensive localization work is not a priority at this stage of development.

Applied to files:

  • app/controllers/pages_controller.rb
🧬 Code graph analysis (2)
app/models/investment_statement.rb (4)
app/models/family.rb (1)
  • entries_cache_version (121-126)
app/models/concerns/monetizable.rb (1)
  • monetize (5-15)
app/models/investment_statement/totals.rb (3)
  • initialize (1-56)
  • initialize (2-5)
  • call (7-17)
app/models/period.rb (1)
  • date_range (139-141)
app/controllers/pages_controller.rb (4)
app/models/family.rb (1)
  • investment_statement (72-74)
app/models/income_statement.rb (2)
  • income_totals (33-35)
  • expense_totals (29-31)
app/models/investment_statement.rb (1)
  • total_income (154-156)
app/models/investment_statement/totals.rb (1)
  • call (7-17)
🪛 ast-grep (0.40.0)
app/controllers/pages_controller.rb

[warning] 148-148: Found the use of an hardcoded passphrase for RSA. The passphrase can be easily discovered, and therefore should not be stored in source-code. It is recommended to remove the passphrase from source-code, and use system environment variables or a restricted configuration file.
Context: add_node.call("income_#{ct.category.id}", ct.category.name, val, percentage, color)
Note: [CWE-798]: Use of Hard-coded Credentials [OWASP A07:2021]: Identification and Authentication Failures [REFERENCES]
https://cwe.mitre.org/data/definitions/522.html

(hardcoded-secret-rsa-passphrase-ruby)


[warning] 162-162: Found the use of an hardcoded passphrase for RSA. The passphrase can be easily discovered, and therefore should not be stored in source-code. It is recommended to remove the passphrase from source-code, and use system environment variables or a restricted configuration file.
Context: add_node.call("expense_#{ct.category.id}", ct.category.name, val, percentage, color)
Note: [CWE-798]: Use of Hard-coded Credentials [OWASP A07:2021]: Identification and Authentication Failures [REFERENCES]
https://cwe.mitre.org/data/definitions/522.html

(hardcoded-secret-rsa-passphrase-ruby)

🪛 markdownlint-cli2 (0.18.1)
docs/roadmap/investment-integration-assessment.md

43-43: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


163-163: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


169-169: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


398-398: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


430-430: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


444-444: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ci / test
🔇 Additional comments (23)
docs/roadmap/investment-integration-assessment.md (2)

163-174: Verify architectural approach aligns with actual implementation.

The documentation recommends "Option A: Shadow Transactions" (line 163-174) as simpler and better-aligned with existing architecture. However, the PR summary indicates InvestmentStatement model is being added, which corresponds to "Option B: Parallel Tracking" (line 169).

This is a significant architectural choice with different tradeoffs. Please clarify:

  • Which approach (A or B) does this PR actually implement?
  • If Option B was chosen despite the recommendation for Option A, what changed the decision?
  • Should the documentation be updated to reflect the actual implementation?

1-454: Document provides comprehensive investment integration assessment and roadmap.

The strategic content and analysis are well-executed. The document effectively identifies architecture gaps, proposes tiered solutions with clear tradeoffs, and provides implementation guidance. This serves as valuable reference for current and future development.

Once the markdown linting violations are resolved, this will be ready for merge.

db/migrate/20251125141213_add_category_to_trades.rb (1)

1-5: LGTM!

The migration correctly uses ActiveRecord::Migration[7.2], adds a nullable UUID foreign key reference, and aligns with the optional: true association expected in the Trade model.

config/locales/views/reports/en.yml (1)

111-122: LGTM!

The investment performance translations are properly structured with hierarchical keys under reports.investment_performance. All necessary labels for the investment UI are defined.

app/controllers/reports_controller.rb (2)

43-44: LGTM on integration.

The investment metrics are properly loaded via build_investment_metrics and follow the existing pattern of building data structures in the controller for view consumption.


413-430: Verify investment_statement API availability.

The code calls several methods on investment_statement:

  • investment_accounts
  • portfolio_value_money
  • unrealized_gains_trend
  • totals(period:) returning an object with .contributions and .withdrawals
  • top_holdings(limit:)

Ensure these methods are implemented in InvestmentStatement and related classes.

db/schema.rb (2)

986-988: LGTM on trades schema changes.

The category_id column with proper UUID type, index, and foreign key constraint aligns with the migration and supports the investment category tracking feature.


787-800: Unrelated rule_runs table included in schema diff.

The rule_runs table and its foreign key (line 1121) appear unrelated to the investment tracking PR objectives. If this was added by a separate migration that was merged, this is fine. Otherwise, consider splitting unrelated schema changes into separate PRs.

app/views/pages/dashboard.html.erb (1)

98-105: Use safe navigation or verify @investment_statement is always initialized.

The conditional if @investment_statement.investment_accounts.any? assumes @investment_statement is never nil. If the controller doesn't guarantee initialization, this will raise a NoMethodError. Either add a safe navigation operator (@investment_statement&.investment_accounts&.any?) or confirm the controller always sets this variable before rendering this view.

app/views/pages/dashboard/_investment_summary.html.erb (3)

1-26: LGTM! Clean i18n and formatting.

The header section properly uses localized strings, server-side formatting helpers, and guards against missing data. The inline Money construction on line 20 is appropriate since trend.value is a numeric amount that needs currency context.


28-76: LGTM! Well-structured holdings table.

The holdings display uses proper semantic HTML, Tailwind design tokens, server-side formatting, and graceful fallbacks for missing logos and trend data. The logo fallback to initials (lines 44-47) is a nice UX touch.


78-98: LGTM! Activity summary with proper i18n.

The investment activity section correctly uses i18n with interpolation (line 82), server-side money formatting, and conditional rendering based on data availability.

app/models/investment_statement/totals.rb (2)

1-17: LGTM! Clean initialization and result handling.

The initialization and call method properly handle nil results with safe navigation and defaults. The comments on lines 13-14 clearly explain why dividends and interest are zero (they come through as transactions, not trades), which aligns with the Entry model's delegated type design.


19-25: LGTM! Proper SQL sanitization.

The query properly uses sanitize_sql_array to prevent SQL injection, with clean separation of SQL template and parameters.

app/controllers/pages_controller.rb (3)

6-23: LGTM! Dashboard setup follows consistent patterns.

The addition of @investment_statement (line 8) follows the same pattern as @balance_sheet, and the comment on line 13 clearly documents that IncomeStatement now includes categorized trades for unified cashflow data.


153-165: Verify Category::UNCATEGORIZED_COLOR constant exists.

Line 161 references Category::UNCATEGORIZED_COLOR as a fallback for missing category colors. Ensure this constant is defined in the Category model.


139-151: Verify Category::COLORS constant exists.

Line 147 references Category::COLORS.sample as a fallback for missing category colors. Ensure this constant is defined in the Category model.

app/models/investment_statement.rb (6)

1-12: LGTM! Clean class initialization.

The class structure is clean with proper concern inclusion and initialization. The require "digest/md5" is appropriately used for cache key generation later in the class.


14-36: LGTM! Proper Money object usage in totals.

The totals method correctly constructs Money objects with the family currency and returns a well-structured PeriodTotals data object. The net_contributions method cleanly calculates the difference between contributions and withdrawals.


38-63: LGTM! Clean value accessor methods.

The portfolio value, cash balance, and holdings value methods follow a consistent pattern with both raw value and Money-wrapped versions, all using the family currency correctly.


65-90: LGTM! Well-optimized holdings query.

The current_holdings query properly uses PostgreSQL's DISTINCT ON to fetch the latest holding per account+security combination, with appropriate guards, currency filtering, and eager loading of associations.


92-115: LGTM! Safe allocation and gains calculations.

The allocation method correctly calculates portfolio weights as percentages with proper zero-total guards. The unrealized_gains method safely handles nil trends with a ternary operator.


143-172: LGTM! Well-structured private methods and data objects.

The private methods are cleanly organized with appropriate caching using entries_cache_version, and the Data.define structures (PeriodTotals, HoldingAllocation) provide clear, immutable data containers with helper methods.

Copy link

@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.

Actionable comments posted: 0

♻️ Duplicate comments (2)
app/controllers/pages_controller.rb (2)

144-145: Hardcoded "Cash Flow" label should use i18n.

The string "Cash Flow" is user-facing and should be localized via t() per coding guidelines.


175-181: Hardcoded "Surplus" label and missing deficit handling.

The "Surplus" string should use i18n, and the deficit case (net.negative?) is not surfaced in the visualization.

🧹 Nitpick comments (1)
app/controllers/pages_controller.rb (1)

147-159: Consider deterministic fallback color for income categories.

The income side logic correctly filters to top-level categories and handles zero values. However, Category::COLORS.sample on line 155 produces non-deterministic colors on each request, which could cause visual inconsistency when the dashboard re-renders.

Consider using a deterministic fallback based on the category ID:

-        color = ct.category.color.presence || Category::COLORS.sample
+        color = ct.category.color.presence || Category::COLORS[ct.category.id % Category::COLORS.size]
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2309489 and 442b1b0.

📒 Files selected for processing (2)
  • app/controllers/pages_controller.rb (3 hunks)
  • app/views/pages/dashboard.html.erb (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/views/pages/dashboard.html.erb
🧰 Additional context used
📓 Path-based instructions (9)
**/*.rb

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.rb: Application supports two modes: 'managed' and 'self_hosted' via Rails.application.config.app_mode
Use Current.user and Current.family instead of current_user / current_family for authentication context
Optimize database queries with proper indexes to prevent N+1 queries using includes/joins

**/*.rb: Use Rails pluralization in i18n: t("transactions.count", count: @transactions.count).
Run bin/rubocop -f github -a for Ruby linting with auto-correct before pull requests.

**/*.rb: Ruby code should use 2-space indent, snake_case for methods and variables, and CamelCase for classes and modules, following Rails conventions for folders and file names
Prefer environment variables over hard-coded values for configuration

Files:

  • app/controllers/pages_controller.rb
app/controllers/**/*.rb

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use Rails strong parameters and CSRF protection throughout the application

app/controllers/**/*.rb: Use strong parameters and CSRF protection throughout the application.
Use query params for state over localStorage/sessions.

Files:

  • app/controllers/pages_controller.rb
**/*.{rb,erb,html.erb}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{rb,erb,html.erb}: Use Current.user for the current user context in controllers and views. Do NOT use current_user.
Use Current.family for the current family context. Do NOT use current_family.

Files:

  • app/controllers/pages_controller.rb
**/*.{erb,html.erb,rb}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{erb,html.erb,rb}: All user-facing strings must use localization (i18n) via the t() helper. Update locale files for each new or changed element.
Use server-side formatting for currencies, numbers, and dates. Do not format in JavaScript.

Files:

  • app/controllers/pages_controller.rb
**/*.{rb,erb}

📄 CodeRabbit inference engine (.cursor/rules/general-rules.mdc)

**/*.{rb,erb}: Use Current.user for accessing the current user. Do NOT use current_user
Use Current.family for accessing the current family. Do NOT use current_family

Files:

  • app/controllers/pages_controller.rb
**/*.{rb,js,erb}

📄 CodeRabbit inference engine (.cursor/rules/project-conventions.mdc)

Format currencies, numbers, dates, and other values server-side, then pass to Stimulus controllers for display only

Files:

  • app/controllers/pages_controller.rb
**/*.{rb,html.erb}

📄 CodeRabbit inference engine (.cursor/rules/project-conventions.mdc)

Use ActiveRecord validations for complex validations and business logic. Simple validations may be mirrored in ActiveRecord for form error handling convenience, but prioritize client-side form validation when possible

Files:

  • app/controllers/pages_controller.rb
app/**/*.{rb,js,ts,jsx,tsx,css,scss}

📄 CodeRabbit inference engine (AGENTS.md)

Code should be organized in app/ directory (Rails MVC, services, jobs, mailers, components), with JS in app/javascript/, and styles/assets in app/assets/ (Tailwind, images, fonts)

Files:

  • app/controllers/pages_controller.rb
app/**/*.rb

📄 CodeRabbit inference engine (AGENTS.md)

Run bin/brakeman security scan before major PRs to check for static analysis of common Rails issues

Files:

  • app/controllers/pages_controller.rb
🧠 Learnings (4)
📓 Common learnings
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .cursor/rules/project-design.mdc:0-0
Timestamp: 2025-11-24T16:56:30.669Z
Learning: Entry model is a Rails delegated type with three subtypes: Valuation (absolute account value), Transaction (balance modification), and Trade (buy/sell for investment accounts)
📚 Learning: 2025-11-24T16:55:43.069Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T16:55:43.069Z
Learning: Applies to config/locales/**/*.yml : Use hierarchical i18n keys by feature: `accounts.index.title`, `transactions.form.amount_label`. Use `t()` helper for all user-facing strings with interpolation for dynamic content.

Applied to files:

  • app/controllers/pages_controller.rb
📚 Learning: 2025-10-07T09:51:02.265Z
Learnt from: jjmata
Repo: we-promise/sure PR: 189
File: app/views/password_resets/new.html.erb:14-14
Timestamp: 2025-10-07T09:51:02.265Z
Learning: In the we-promise/sure repository, use i18n methods (like `t()` or `I18n.t`) for all user-facing strings to maintain proper internationalization infrastructure. While translations should be provided, extensive localization work is not a priority at this stage of development.

Applied to files:

  • app/controllers/pages_controller.rb
📚 Learning: 2025-11-24T16:55:43.069Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T16:55:43.069Z
Learning: Applies to **/*.{erb,html.erb,rb} : All user-facing strings must use localization (i18n) via the `t()` helper. Update locale files for each new or changed element.

Applied to files:

  • app/controllers/pages_controller.rb
🧬 Code graph analysis (1)
app/controllers/pages_controller.rb (4)
app/models/family.rb (1)
  • investment_statement (72-74)
app/models/investment_statement.rb (2)
  • investment_accounts (144-146)
  • total_income (154-156)
app/models/income_statement.rb (2)
  • income_totals (33-35)
  • expense_totals (29-31)
app/models/investment_statement/totals.rb (1)
  • call (7-17)
🪛 ast-grep (0.40.0)
app/controllers/pages_controller.rb

[warning] 156-156: Found the use of an hardcoded passphrase for RSA. The passphrase can be easily discovered, and therefore should not be stored in source-code. It is recommended to remove the passphrase from source-code, and use system environment variables or a restricted configuration file.
Context: add_node.call("income_#{ct.category.id}", ct.category.name, val, percentage, color)
Note: [CWE-798]: Use of Hard-coded Credentials [OWASP A07:2021]: Identification and Authentication Failures [REFERENCES]
https://cwe.mitre.org/data/definitions/522.html

(hardcoded-secret-rsa-passphrase-ruby)


[warning] 170-170: Found the use of an hardcoded passphrase for RSA. The passphrase can be easily discovered, and therefore should not be stored in source-code. It is recommended to remove the passphrase from source-code, and use system environment variables or a restricted configuration file.
Context: add_node.call("expense_#{ct.category.id}", ct.category.name, val, percentage, color)
Note: [CWE-798]: Use of Hard-coded Credentials [OWASP A07:2021]: Identification and Authentication Failures [REFERENCES]
https://cwe.mitre.org/data/definitions/522.html

(hardcoded-secret-rsa-passphrase-ruby)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ci / test
🔇 Additional comments (4)
app/controllers/pages_controller.rb (4)

6-18: LGTM!

The dashboard action correctly uses Current.family for accessing the investment statement and income statement. The @investment_statement assignment aligns with the memoized investment_statement method in the Family model.


85-92: LGTM!

The investment summary section follows the established pattern for dashboard sections with proper i18n key usage for the title and appropriate visibility conditions. The investment_accounts method is memoized in InvestmentStatement, preventing repeated queries.


161-173: LGTM!

The expense side processing correctly mirrors the income side pattern, filtering to top-level categories and using Category::UNCATEGORIZED_COLOR as a deterministic fallback color.


183-183: LGTM!

The currency symbol derivation using Money::Currency.new(currency).symbol is correct and aligns with the signature change to accept currency instead of currency_symbol.

@jjmata jjmata modified the milestones: v0.7.0, v0.6.6 Dec 5, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants