Add SnapTrade brokerage integration with full trade history support (#737)

* Introduce SnapTrade integration with models, migrations, views, and activity processing logic.

* Refactor SnapTrade activities processing: improve activity fetching flow, handle pending states, and update UI elements for enhanced user feedback.

* Update Brakeman ignore file to include intentional redirect for SnapTrade OAuth portal.

* Refactor SnapTrade models, views, and processing logic: add currency extraction helper, improve pending state handling, optimize migration checks, and enhance user feedback in UI.

* Remove encryption for SnapTrade `snaptrade_user_id`, as it is an identifier, not a secret.

* Introduce `SnaptradeConnectionCleanupJob` to asynchronously handle SnapTrade connection cleanup and improve i18n for SnapTrade item status messages.

* Update SnapTrade encryption: make `snaptrade_user_secret` non-deterministic to enhance security.

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: luckyPipewrench <luckypipewrench@proton.me>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
This commit is contained in:
LPW
2026-01-22 14:52:49 -05:00
committed by GitHub
parent 179552657c
commit a83f70425f
52 changed files with 4417 additions and 25 deletions

View File

@@ -28,15 +28,22 @@
<% end %>
<%# Transactions section - shown if provider collects transaction stats %>
<% if has_transaction_stats? %>
<% if has_transaction_stats? || activities_pending? %>
<div>
<h4 class="text-primary font-medium mb-1"><%= t("provider_sync_summary.transactions.title") %></h4>
<div class="flex items-center gap-3 flex-wrap">
<span><%= t("provider_sync_summary.transactions.seen", count: tx_seen) %></span>
<span><%= t("provider_sync_summary.transactions.imported", count: tx_imported) %></span>
<span><%= t("provider_sync_summary.transactions.updated", count: tx_updated) %></span>
<span><%= t("provider_sync_summary.transactions.skipped", count: tx_skipped) %></span>
</div>
<% if activities_pending? && !has_transaction_stats? %>
<div class="flex items-center gap-2">
<%= helpers.icon "loader-circle", size: "sm", class: "animate-spin text-secondary" %>
<span class="text-secondary"><%= t("provider_sync_summary.transactions.fetching") %></span>
</div>
<% else %>
<div class="flex items-center gap-3 flex-wrap">
<span><%= t("provider_sync_summary.transactions.seen", count: tx_seen) %></span>
<span><%= t("provider_sync_summary.transactions.imported", count: tx_imported) %></span>
<span><%= t("provider_sync_summary.transactions.updated", count: tx_updated) %></span>
<span><%= t("provider_sync_summary.transactions.skipped", count: tx_skipped) %></span>
</div>
<% end %>
<%# Protected entries detail - shown when entries were skipped due to protection %>
<% if has_skipped_entries? %>
@@ -81,6 +88,26 @@
</div>
<% end %>
<%# Trades section - shown if provider collects trades stats (investment activities) %>
<% if has_trades_stats? || activities_pending? %>
<div>
<h4 class="text-primary font-medium mb-1"><%= t("provider_sync_summary.trades.title") %></h4>
<% if activities_pending? && !has_trades_stats? %>
<div class="flex items-center gap-2">
<%= helpers.icon "loader-circle", size: "sm", class: "animate-spin text-secondary" %>
<span class="text-secondary"><%= t("provider_sync_summary.trades.fetching") %></span>
</div>
<% else %>
<div class="flex items-center gap-3 flex-wrap">
<span><%= t("provider_sync_summary.trades.imported", count: trades_imported) %></span>
<% if trades_skipped > 0 %>
<span><%= t("provider_sync_summary.trades.skipped", count: trades_skipped) %></span>
<% end %>
</div>
<% end %>
</div>
<% end %>
<%# Health section - always shown %>
<div>
<h4 class="text-primary font-medium mb-1"><%= t("provider_sync_summary.health.title") %></h4>