Files
sure/app/views/trading212_items/setup_accounts.html.erb
jdcdp 849578a84a Add support for trading212 integration in investments sync (#2513)
* Add support for trading212 integration in investments sync

* Respect Trading 212 history rate limits

* Replace hex colors with design tokens

* Changed withdrawal to withdraw

* Address PR review feedback for Trading 212 integration

- Replace all Rails.logger calls with DebugLogEntry.capture across
  syncer, importer, processor, provider, controller, and unlinking
- Fix destroy action to surface unlink errors via DebugLogEntry
- Replace hardcoded 'GBP' currency fallback with Current.family.currency
- Replace hardcoded English status strings in Syncer with i18n
- Add config/initializers/trading212.rb with DEBUG_RAW ENV toggle
- Add comprehensive test coverage: item, account, data_helpers,
  holdings_processor, activities_processor, importer, syncer,
  and controller tests
- Rewrite syncer and controller tests to match actual interfaces

* Fix tests and bugs found during local test execution

- Fix standard_ticker: empty string caused nil.upcase via [].first
- Fix parse_date: DateTime < Date, so when Date caught DateTime first
- Fix ActivitiesProcessor/HoldingsProcessor: missing instruments_map
  in DataHelpers caused NameError when processing dividends
- Fix test: security fixture ticker collision with test data
- Fix test: sync_status_summary i18n matching in assertions
- Fix test: trading212_provider ConfigurationError test path
- Fix test: controller invalid params needs Turbo-Frame header
- Fix test: syncer test uses stubs instead of strict expects

116 tests, 257 assertions, 0 failures, 0 errors

* Keep raw provider response bodies out of exception messages.

* Fix Secrets leak into page HTML, prevents the API key/secret from appearing in the HTML source while keeping the "leave blank to keep existing" UX.

* Address review findings: env gate, sync test, uniqueness test, destroy flow

- Gate TRADING212_DEBUG_RAW behind Rails.env.local? so staging/production
  cannot accidentally enable raw payload logging
- Assert SyncJob enqueue in sync controller test, not just redirect
- Fix cross-item uniqueness test to actually use two different items
- Remove rescue in destroy so unlink failures stop the flow (matching
  Brex/Akahu pattern) instead of proceeding to destroy_later silently

* Add Trading212 tables to db/schema.rb for CI test database

CI runs db:test:prepare which loads db/schema.rb, not migrations.
Without these table definitions, fixture loading fails with
PG::UndefinedTable: relation "trading212_accounts" does not exist.

* Removed lint complaint

* Register Trading212 in ProviderConnectionStatus::PROVIDERS

Fixes CI failure: test_provider_registry_covers_syncable_family_provider_item_associations

* Fixed bad merge

---------

Signed-off-by: jdcdp <47483528+jdcdp@users.noreply.github.com>
Co-authored-by: jdcdp <jdcdp@cdm4.net>
2026-07-25 04:54:30 +02:00

171 lines
7.8 KiB
Plaintext

<% content_for :title, t(".page_title") %>
<%= render DS::Dialog.new(disable_click_outside: true) do |dialog| %>
<% dialog.with_header(title: t(".dialog_title")) do %>
<div class="flex items-center gap-2">
<%= icon "chart-line", class: "text-primary" %>
<span class="text-primary"><%= t(".subtitle") %></span>
</div>
<% end %>
<% dialog.with_body do %>
<div class="space-y-6">
<div class="bg-surface border border-primary p-4 rounded-lg">
<div class="flex items-start gap-3">
<%= icon "info", size: "sm", class: "text-primary mt-0.5 flex-shrink-0" %>
<div>
<p class="text-sm text-primary mb-2"><strong><%= t(".info_box.title") %></strong></p>
<ul class="text-xs text-secondary space-y-1 list-disc list-inside">
<li><%= t(".info_box.items.item_1") %></li>
<li><%= t(".info_box.items.item_2") %></li>
<li><%= t(".info_box.items.item_3") %></li>
</ul>
</div>
</div>
</div>
<% if @waiting_for_sync %>
<div class="flex flex-col items-center justify-center py-6 space-y-3">
<div class="animate-spin rounded-full h-6 w-6 border-b-2 border-primary"></div>
<p class="text-secondary text-center"><%= t(".status.fetching_accounts") %></p>
</div>
<div class="flex gap-3 justify-center">
<%= render DS::Link.new(
text: t(".buttons.refresh"),
variant: "secondary",
icon: "refresh-cw",
href: setup_accounts_trading212_item_path(@trading212_item),
frame: "_top"
) %>
<%= render DS::Link.new(
text: t(".buttons.cancel"),
variant: "ghost",
href: accounts_path,
frame: "_top"
) %>
</div>
<% elsif @no_accounts_found %>
<div class="flex flex-col items-center justify-center py-6 space-y-3">
<%= icon "alert-circle", size: "lg", class: "text-warning" %>
<p class="text-primary text-center font-medium"><%= t(".status.no_accounts_found_title") %></p>
<p class="text-secondary text-center text-sm"><%= t(".status.no_accounts_found_description") %></p>
</div>
<div class="flex gap-3 justify-center">
<%= render DS::Link.new(
text: t(".buttons.back_to_settings"),
variant: "secondary",
href: settings_providers_path,
frame: "_top"
) %>
</div>
<% else %>
<%= form_with url: complete_account_setup_trading212_item_path(@trading212_item), method: :post, data: { turbo_frame: "_top" } do %>
<% if @unlinked_accounts.any? %>
<div class="space-y-4">
<h3 class="font-medium text-primary"><%= t(".available_accounts.title") %></h3>
<% @unlinked_accounts.each do |t212_account| %>
<div class="border border-primary rounded-lg p-4 hover:bg-surface transition-colors">
<div class="flex items-center gap-3">
<input type="checkbox"
id="account_<%= t212_account.id %>"
name="account_ids[]"
value="<%= t212_account.id %>"
checked
class="cursor-pointer">
<label for="account_<%= t212_account.id %>" class="flex-1 cursor-pointer">
<h4 class="font-medium text-primary"><%= t212_account.name %></h4>
<p class="text-sm text-secondary">
<%= t(
".available_accounts.account_summary",
account_type: t(".available_accounts.account_type_investment"),
balance: number_to_currency(t212_account.current_balance || 0, unit: Money::Currency.new(t212_account.currency.presence || Current.family.currency).symbol)
) %>
</p>
<% if t212_account.trading212_account_id.present? %>
<p class="text-xs text-secondary"><%= t(".available_accounts.account_id", account_id: t212_account.trading212_account_id) %></p>
<% end %>
</label>
</div>
</div>
<% end %>
</div>
<div class="flex gap-3 mt-6">
<%= render DS::Button.new(
text: t(".buttons.create_selected_accounts"),
variant: "primary",
icon: "plus",
type: "submit",
class: "flex-1"
) %>
<%= render DS::Link.new(
text: t(".buttons.cancel"),
variant: "secondary",
href: accounts_path,
frame: "_top"
) %>
</div>
<% end %>
<% end %>
<% if @unlinked_accounts.any? && @linkable_accounts.any? %>
<div class="border-t border-secondary pt-4 mt-2">
<p class="text-xs text-secondary mb-3"><%= t(".link_existing.description") %></p>
<div class="space-y-2">
<% @unlinked_accounts.each do |t212_account| %>
<%= form_with url: link_existing_account_trading212_items_path, method: :post, data: { turbo_frame: "_top" } do |link_form| %>
<%= link_form.hidden_field :trading212_account_id, value: t212_account.id %>
<p class="text-xs text-primary mb-1"><%= t212_account.name %></p>
<div class="flex items-center gap-2">
<%= link_form.select :account_id,
options_for_select(@linkable_accounts.map { |a| [ t(".link_existing.manual_account_option", name: a.name, balance: number_to_currency(a.balance, unit: Money::Currency.new(a.currency.presence || Current.family.currency).symbol)), a.id ] }),
{ prompt: t(".link_existing.select_prompt") },
class: "bg-container border border-primary rounded px-2 py-1 text-sm text-primary flex-1 min-w-0" %>
<%= render DS::Button.new(
text: t(".buttons.link"),
variant: "secondary",
size: "sm",
type: "submit"
) %>
</div>
<% end %>
<% end %>
</div>
</div>
<% end %>
<% if @linked_accounts.any? %>
<div class="<%= 'border-t border-primary pt-6 mt-4' if @unlinked_accounts.any? %>">
<h3 class="font-medium text-primary mb-4"><%= t(".linked_accounts.title") %></h3>
<% @linked_accounts.each do |t212_account| %>
<div class="border border-success/20 bg-success/5 rounded-lg p-4 mb-2">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<%= icon "check-circle", class: "text-success" %>
<div>
<h4 class="font-medium text-primary"><%= t212_account.name %></h4>
<p class="text-sm text-secondary"><%= t(".linked_accounts.linked_to_html", account: link_to(t212_account.current_account.name, account_path(t212_account.current_account), class: "link")) %></p>
</div>
</div>
</div>
</div>
<% end %>
<% if @unlinked_accounts.blank? %>
<div class="flex justify-end mt-4">
<%= render DS::Link.new(
text: t(".buttons.done"),
variant: "primary",
href: accounts_path,
frame: "_top"
) %>
</div>
<% end %>
</div>
<% end %>
<% end %>
</div>
<% end %>
<% end %>