Files
sure/app/views/reports/_period_picker.html.erb
Blaž Dular 826c4a356e feat(bank-sync): Wise integration (#2433)
* feat(wise): add Wise integration with JAR savings account and activity support

- Add WiseItem/WiseAccount models with full sync pipeline (importer, syncer, processor)
- Detect income vs expense using targetAccount == recipientId from borderless accounts API
- Support JAR (SAVINGS) accounts with totalWorth balance and savings subtype
- Fetch JAR activity via profile activities API (INTERBALANCE, BALANCE_CASHBACK, BALANCE_ASSET_FEE)
- Route INTERBALANCE activities to both JAR and STANDARD accounts and link as Transfer records
- Add provider connection status registration, routes, views, and i18n
- Add migration for wise_items and wise_accounts tables
- Add tests for WiseAccount, WiseEntry::Processor, WiseActivity::Processor, WiseItem::Importer, and WiseItem#link_jar_transfers!

* chore(lint): add ignore to security scan (false positive)

* fix(wise): address PR review feedback on activity routing, HTML stripping, rate limiting, and scope extraction

- Replace title string matching in activity_for_account? with resource.id vs balance_id comparison to avoid breakage when users rename JAR accounts on Wise
- Replace gsub(/<[^>]+>/, "") with ActionController::Base.helpers.strip_tags to safely handle user-controlled HTML-like content
- Wrap paginated API calls in with_rate_limit_retry (up to 3 attempts, exponential backoff) to handle 429 responses during fetch_jar_activities and fetch_transfers
- Extract WiseAccount.unlinked scope and remove duplicated left_joins query from controller

* fix(wise): address PR #2433 review feedback

- Wire @wise_items into AccountsController#index so linked accounts appear on /accounts
- Fix find_wise_account_for_linking to preserve .active scope via .merge instead of .then
- Fix cross-currency incoming transfer amount to use targetValue instead of sourceValue
- Add missing select_profiles.session_expired locale key
- Add missing account_name interpolation to link_existing_account success notice
- Use i18n for profile type labels in profile_display_name
- Trigger wise_item.sync_later after account linking in all three linking actions
- Isolate fee transaction failure so it no longer aborts the main transfer import
- Use bare raise in WiseActivity/WiseEntry processors to preserve original backtrace
- Re-raise in WiseItem::Unlinking to prevent silently orphaned Holdings
- Fix avatar badge to use design system tokens (bg-container-inset, text-primary)

* fix(wise): fix INTERBALANCE routing and encrypt pending token in session

* fix(wise): replace hand-rolled buttons/links with DS::Button and DS::Link

Address repeated sure-design DS drift findings: migrate all manual
Tailwind button_to and link_to calls in Wise views to DS::Button
(outline/outline_destructive variants) and DS::Link (secondary variant).
Add missing provider_panel.disconnect locale key for the disconnect button text.

* fix(wise): use render_provider_panel_error in create instead of missing new template

* fix(wise): add missing comma in PROVIDERS array

CI failed with a SyntaxError because the questrade entry wasn't
comma-terminated before the wise entry.

* chore(wise): re-add pipelock:ignore for pending token param

Lost when the token source moved from session to an encrypted params
field in 0b5dc886, causing the CI secret scanner to flag a false positive.

* fix(test): widen random ticker suffix to avoid rare collision flake

hex(2) only yields 65536 possible tickers, so create_trade's 4 calls per
test run had a small but nonzero chance of colliding on the unique
ticker+exchange index. hex(8) makes collisions practically impossible.
2026-07-14 03:16:28 +02:00

151 lines
6.0 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<%# locals: (period_type:, start_date:) %>
<%= turbo_frame_tag "reports_picker" do %>
<div class="p-3 space-y-2">
<% case period_type %>
<% when :monthly %>
<div class="flex items-center gap-2 justify-between mb-2">
<%= render DS::Link.new(
variant: "icon",
icon: "chevron-left",
href: picker_reports_path(period_type: :monthly, start_date: start_date.beginning_of_year - 1.year),
aria: { label: t("reports.index.previous_year") },
frame: "reports_picker"
) %>
<span class="w-40 text-center px-3 py-2">
<%= start_date.year %>
</span>
<% next_year_start = Date.new(start_date.year + 1, 1, 1) %>
<% if next_year_start <= Date.current %>
<%= render DS::Link.new(
variant: "icon",
icon: "chevron-right",
href: picker_reports_path(period_type: :monthly, start_date: next_year_start),
aria: { label: t("reports.index.next_year") },
frame: "reports_picker"
) %>
<% else %>
<span class="text-subdued inline-flex items-center justify-center w-9 h-9 rounded-lg">
<%= icon "chevron-right", color: "current" %>
</span>
<% end %>
</div>
<div class="grid grid-cols-3 gap-2 text-sm text-center font-medium">
<% Date::ABBR_MONTHNAMES.compact.each_with_index do |month_name, i| %>
<% month = i + 1 %>
<% date = Date.new(start_date.year, month, 1) %>
<% if date <= Date.current %>
<%= render DS::Link.new(
variant: date.month == start_date.month ? "secondary" : "ghost",
text: month_name,
href: reports_path(period_type: :monthly, start_date: date, end_date: date.end_of_month),
full_width: true,
frame: :_top
) %>
<% else %>
<span class="px-3 py-2 text-subdued rounded-md"><%= month_name %></span>
<% end %>
<% end %>
</div>
<% when :quarterly %>
<div class="flex items-center gap-2 justify-between mb-2">
<%= render DS::Link.new(
variant: "icon",
icon: "chevron-left",
href: picker_reports_path(period_type: :quarterly, start_date: Date.new(start_date.year - 1, 1, 1)),
aria: { label: t("reports.index.previous_year") },
frame: "reports_picker"
) %>
<span class="w-40 text-center px-3 py-2">
<%= start_date.year %>
</span>
<% next_year_start = Date.new(start_date.year + 1, 1, 1) %>
<% if next_year_start <= Date.current %>
<%= render DS::Link.new(
variant: "icon",
icon: "chevron-right",
href: picker_reports_path(period_type: :quarterly, start_date: next_year_start),
aria: { label: t("reports.index.next_year") },
frame: "reports_picker"
) %>
<% else %>
<span class="text-subdued inline-flex items-center justify-center w-9 h-9 rounded-lg">
<%= icon "chevron-right", color: "current" %>
</span>
<% end %>
</div>
<div class="grid grid-cols-2 gap-2 text-sm text-center font-medium">
<% (1..4).each do |q| %>
<% quarter_start = Date.new(start_date.year, (q - 1) * 3 + 1, 1) %>
<% quarter_end = quarter_start.end_of_quarter %>
<% if quarter_start <= Date.current %>
<%= render DS::Link.new(
variant: quarter_start.quarter == start_date.quarter && start_date.year == quarter_start.year ? "secondary" : "ghost",
text: t("reports.index.period_picker.quarter", quarter: q, year: start_date.year),
href: reports_path(period_type: :quarterly, start_date: quarter_start, end_date: quarter_end),
full_width: true,
frame: :_top
) %>
<% else %>
<span class="px-3 py-2 text-subdued rounded-md"><%= t("reports.index.period_picker.quarter", quarter: q, year: start_date.year) %></span>
<% end %>
<% end %>
</div>
<% when :ytd %>
<% decade_start = (start_date.year / 10) * 10 %>
<% decade_end = decade_start + 9 %>
<div class="flex items-center gap-2 justify-between mb-2">
<%= render DS::Link.new(
variant: "icon",
icon: "chevron-left",
href: picker_reports_path(period_type: :ytd, start_date: Date.new(decade_start - 1, 1, 1)),
aria: { label: t("reports.index.previous_decade") },
frame: "reports_picker"
) %>
<span class="w-40 text-center px-3 py-2">
<%= decade_start %><%= decade_end %>
</span>
<% if decade_start + 10 <= Date.current.year %>
<%= render DS::Link.new(
variant: "icon",
icon: "chevron-right",
href: picker_reports_path(period_type: :ytd, start_date: Date.new(decade_start + 10, 1, 1)),
aria: { label: t("reports.index.next_decade") },
frame: "reports_picker"
) %>
<% else %>
<span class="text-subdued inline-flex items-center justify-center w-9 h-9 rounded-lg">
<%= icon "chevron-right", color: "current" %>
</span>
<% end %>
</div>
<div class="grid grid-cols-2 gap-2 text-sm text-center font-medium">
<% (decade_start..decade_end).each do |year| %>
<% next if year > Date.current.year %>
<% year_start = Date.new(year, 1, 1) %>
<% year_end = year == Date.current.year ? Date.current : Date.new(year, 12, 31) %>
<%= render DS::Link.new(
variant: year == start_date.year ? "secondary" : "ghost",
text: year == Date.current.year ? t("reports.index.period_picker.ytd", year: year) : year.to_s,
href: reports_path(period_type: :ytd, start_date: year_start, end_date: year_end),
full_width: true,
frame: :_top
) %>
<% end %>
</div>
<% end %>
</div>
<% end %>