mirror of
https://github.com/we-promise/sure.git
synced 2026-05-07 12:54:04 +00:00
fix(mercury): support named multiple API connections (#1627)
* fix(mercury): support named multiple connections * fix(mercury): address multi-connection review feedback * fix(mercury): localize connection labels * fix(mercury): strip API tokens before provider calls * test(mercury): localize provider config assertions * fix(mercury): address multi-connection review * refactor(mercury): simplify connection selection failure
This commit is contained in:
@@ -13,13 +13,19 @@ class MercuryItemsController < ApplicationController
|
||||
# Preload Mercury accounts in background (async, non-blocking)
|
||||
def preload_accounts
|
||||
begin
|
||||
# Check if family has credentials
|
||||
unless Current.family.has_mercury_credentials?
|
||||
account_flow = mercury_item_account_flow_context
|
||||
mercury_item = account_flow[:mercury_item]
|
||||
unless mercury_item
|
||||
render json: mercury_item_selection_error_payload(account_flow[:credentialed_items])
|
||||
return
|
||||
end
|
||||
|
||||
unless mercury_item.credentials_configured?
|
||||
render json: { success: false, error: "no_credentials", has_accounts: false }
|
||||
return
|
||||
end
|
||||
|
||||
cache_key = "mercury_accounts_#{Current.family.id}"
|
||||
cache_key = mercury_accounts_cache_key(mercury_item)
|
||||
|
||||
# Check if already cached
|
||||
cached_accounts = Rails.cache.read(cache_key)
|
||||
@@ -29,8 +35,7 @@ class MercuryItemsController < ApplicationController
|
||||
return
|
||||
end
|
||||
|
||||
# Fetch from API
|
||||
mercury_provider = Provider::MercuryAdapter.build_provider(family: Current.family)
|
||||
mercury_provider = mercury_item.mercury_provider
|
||||
|
||||
unless mercury_provider.present?
|
||||
render json: { success: false, error: "no_api_token", has_accounts: false }
|
||||
@@ -58,28 +63,21 @@ class MercuryItemsController < ApplicationController
|
||||
# Fetch available accounts from Mercury API and show selection UI
|
||||
def select_accounts
|
||||
begin
|
||||
# Check if family has Mercury credentials configured
|
||||
unless Current.family.has_mercury_credentials?
|
||||
if turbo_frame_request?
|
||||
# Render setup modal for turbo frame requests
|
||||
render partial: "mercury_items/setup_required", layout: false
|
||||
else
|
||||
# Redirect for regular requests
|
||||
redirect_to settings_providers_path,
|
||||
alert: t(".no_credentials_configured",
|
||||
default: "Please configure your Mercury API token first in Provider Settings.")
|
||||
end
|
||||
account_flow = mercury_item_account_flow_context
|
||||
@mercury_item = account_flow[:mercury_item]
|
||||
unless @mercury_item
|
||||
render_mercury_item_selection_failure(credentialed_items: account_flow[:credentialed_items])
|
||||
return
|
||||
end
|
||||
|
||||
cache_key = "mercury_accounts_#{Current.family.id}"
|
||||
cache_key = mercury_accounts_cache_key(@mercury_item)
|
||||
|
||||
# Try to get cached accounts first
|
||||
@available_accounts = Rails.cache.read(cache_key)
|
||||
|
||||
# If not cached, fetch from API
|
||||
if @available_accounts.nil?
|
||||
mercury_provider = Provider::MercuryAdapter.build_provider(family: Current.family)
|
||||
mercury_provider = @mercury_item.mercury_provider
|
||||
|
||||
unless mercury_provider.present?
|
||||
redirect_to settings_providers_path, alert: t(".no_api_token",
|
||||
@@ -95,12 +93,8 @@ class MercuryItemsController < ApplicationController
|
||||
Rails.cache.write(cache_key, @available_accounts, expires_in: 5.minutes)
|
||||
end
|
||||
|
||||
# Filter out already linked accounts
|
||||
mercury_item = Current.family.mercury_items.first
|
||||
if mercury_item
|
||||
linked_account_ids = mercury_item.mercury_accounts.joins(:account_provider).pluck(:account_id)
|
||||
@available_accounts = @available_accounts.reject { |acc| linked_account_ids.include?(acc[:id].to_s) }
|
||||
end
|
||||
linked_account_ids = @mercury_item.mercury_accounts.joins(:account_provider).pluck(:account_id)
|
||||
@available_accounts = @available_accounts.reject { |acc| linked_account_ids.include?(acc[:id].to_s) }
|
||||
|
||||
@accountable_type = params[:accountable_type] || "Depository"
|
||||
@return_to = safe_return_to_path
|
||||
@@ -139,13 +133,16 @@ class MercuryItemsController < ApplicationController
|
||||
return
|
||||
end
|
||||
|
||||
# Create or find mercury_item for this family
|
||||
mercury_item = Current.family.mercury_items.first_or_create!(
|
||||
name: "Mercury Connection"
|
||||
)
|
||||
account_flow = mercury_item_account_flow_context
|
||||
mercury_item = account_flow[:mercury_item]
|
||||
|
||||
unless mercury_item
|
||||
redirect_to settings_providers_path, alert: t(".select_connection", default: "Choose a Mercury connection before linking accounts.")
|
||||
return
|
||||
end
|
||||
|
||||
# Fetch account details from API
|
||||
mercury_provider = Provider::MercuryAdapter.build_provider(family: Current.family)
|
||||
mercury_provider = mercury_item.mercury_provider
|
||||
unless mercury_provider.present?
|
||||
redirect_to new_account_path, alert: t(".no_api_token")
|
||||
return
|
||||
@@ -259,29 +256,22 @@ class MercuryItemsController < ApplicationController
|
||||
return
|
||||
end
|
||||
|
||||
# Check if family has Mercury credentials configured
|
||||
unless Current.family.has_mercury_credentials?
|
||||
if turbo_frame_request?
|
||||
# Render setup modal for turbo frame requests
|
||||
render partial: "mercury_items/setup_required", layout: false
|
||||
else
|
||||
# Redirect for regular requests
|
||||
redirect_to settings_providers_path,
|
||||
alert: t(".no_credentials_configured",
|
||||
default: "Please configure your Mercury API token first in Provider Settings.")
|
||||
end
|
||||
account_flow = mercury_item_account_flow_context
|
||||
@mercury_item = account_flow[:mercury_item]
|
||||
unless @mercury_item
|
||||
render_mercury_item_selection_failure(credentialed_items: account_flow[:credentialed_items])
|
||||
return
|
||||
end
|
||||
|
||||
begin
|
||||
cache_key = "mercury_accounts_#{Current.family.id}"
|
||||
cache_key = mercury_accounts_cache_key(@mercury_item)
|
||||
|
||||
# Try to get cached accounts first
|
||||
@available_accounts = Rails.cache.read(cache_key)
|
||||
|
||||
# If not cached, fetch from API
|
||||
if @available_accounts.nil?
|
||||
mercury_provider = Provider::MercuryAdapter.build_provider(family: Current.family)
|
||||
mercury_provider = @mercury_item.mercury_provider
|
||||
|
||||
unless mercury_provider.present?
|
||||
redirect_to settings_providers_path, alert: t(".no_api_token",
|
||||
@@ -302,12 +292,8 @@ class MercuryItemsController < ApplicationController
|
||||
return
|
||||
end
|
||||
|
||||
# Filter out already linked accounts
|
||||
mercury_item = Current.family.mercury_items.first
|
||||
if mercury_item
|
||||
linked_account_ids = mercury_item.mercury_accounts.joins(:account_provider).pluck(:account_id)
|
||||
@available_accounts = @available_accounts.reject { |acc| linked_account_ids.include?(acc[:id].to_s) }
|
||||
end
|
||||
linked_account_ids = @mercury_item.mercury_accounts.joins(:account_provider).pluck(:account_id)
|
||||
@available_accounts = @available_accounts.reject { |acc| linked_account_ids.include?(acc[:id].to_s) }
|
||||
|
||||
if @available_accounts.empty?
|
||||
redirect_to accounts_path, alert: t(".all_accounts_already_linked")
|
||||
@@ -343,6 +329,9 @@ class MercuryItemsController < ApplicationController
|
||||
return
|
||||
end
|
||||
|
||||
account_flow = mercury_item_account_flow_context
|
||||
mercury_item = account_flow[:mercury_item]
|
||||
|
||||
@account = Current.family.accounts.find(account_id)
|
||||
|
||||
# Check if account is already linked
|
||||
@@ -351,13 +340,13 @@ class MercuryItemsController < ApplicationController
|
||||
return
|
||||
end
|
||||
|
||||
# Create or find mercury_item for this family
|
||||
mercury_item = Current.family.mercury_items.first_or_create!(
|
||||
name: "Mercury Connection"
|
||||
)
|
||||
unless mercury_item
|
||||
redirect_to settings_providers_path, alert: t(".select_connection", default: "Choose a Mercury connection before linking accounts.")
|
||||
return
|
||||
end
|
||||
|
||||
# Fetch account details from API
|
||||
mercury_provider = Provider::MercuryAdapter.build_provider(family: Current.family)
|
||||
mercury_provider = mercury_item.mercury_provider
|
||||
unless mercury_provider.present?
|
||||
redirect_to accounts_path, alert: t(".no_api_token")
|
||||
return
|
||||
@@ -423,7 +412,7 @@ class MercuryItemsController < ApplicationController
|
||||
|
||||
if turbo_frame_request?
|
||||
flash.now[:notice] = t(".success")
|
||||
@mercury_items = Current.family.mercury_items.ordered
|
||||
@mercury_items = Current.family.mercury_items.active.ordered.includes(:syncs, :mercury_accounts)
|
||||
render turbo_stream: [
|
||||
turbo_stream.replace(
|
||||
"mercury-providers-panel",
|
||||
@@ -454,10 +443,15 @@ class MercuryItemsController < ApplicationController
|
||||
end
|
||||
|
||||
def update
|
||||
if @mercury_item.update(mercury_item_params)
|
||||
permitted_params = mercury_item_params
|
||||
expire_accounts_cache = mercury_accounts_cache_sensitive_update?(permitted_params)
|
||||
|
||||
if @mercury_item.update(permitted_params)
|
||||
Rails.cache.delete(mercury_accounts_cache_key(@mercury_item)) if expire_accounts_cache
|
||||
|
||||
if turbo_frame_request?
|
||||
flash.now[:notice] = t(".success")
|
||||
@mercury_items = Current.family.mercury_items.ordered
|
||||
@mercury_items = Current.family.mercury_items.active.ordered.includes(:syncs, :mercury_accounts)
|
||||
render turbo_stream: [
|
||||
turbo_stream.replace(
|
||||
"mercury-providers-panel",
|
||||
@@ -750,7 +744,67 @@ class MercuryItemsController < ApplicationController
|
||||
end
|
||||
|
||||
def mercury_item_params
|
||||
params.require(:mercury_item).permit(:name, :sync_start_date, :token, :base_url)
|
||||
permitted = params.require(:mercury_item).permit(:name, :sync_start_date, :token, :base_url)
|
||||
permitted.delete(:token) if @mercury_item&.persisted? && permitted[:token].blank?
|
||||
permitted
|
||||
end
|
||||
|
||||
def mercury_items_with_credentials
|
||||
Current.family.mercury_items.active.ordered.select(&:credentials_configured?)
|
||||
end
|
||||
|
||||
def mercury_item_account_flow_context
|
||||
credentialed_items = mercury_items_with_credentials
|
||||
mercury_item = nil
|
||||
|
||||
if params[:mercury_item_id].present?
|
||||
mercury_item = credentialed_items.find { |item| item.id.to_s == params[:mercury_item_id].to_s }
|
||||
elsif credentialed_items.one?
|
||||
mercury_item = credentialed_items.first
|
||||
end
|
||||
|
||||
{
|
||||
mercury_item: mercury_item,
|
||||
credentialed_items: credentialed_items
|
||||
}
|
||||
end
|
||||
|
||||
def mercury_accounts_cache_key(mercury_item)
|
||||
"mercury_accounts_#{Current.family.id}_#{mercury_item.id}"
|
||||
end
|
||||
|
||||
def mercury_accounts_cache_sensitive_update?(permitted_params)
|
||||
permitted_params.key?(:token) || permitted_params.key?(:base_url)
|
||||
end
|
||||
|
||||
def mercury_item_selection_error_payload(credentialed_items)
|
||||
if mercury_item_selection_required?(credentialed_items)
|
||||
{
|
||||
success: false,
|
||||
error: "select_connection",
|
||||
error_message: t(".select_connection", default: "Choose a Mercury connection before loading accounts."),
|
||||
has_accounts: nil
|
||||
}
|
||||
else
|
||||
{ success: false, error: "no_credentials", has_accounts: false }
|
||||
end
|
||||
end
|
||||
|
||||
def render_mercury_item_selection_failure(credentialed_items:)
|
||||
if mercury_item_selection_required?(credentialed_items)
|
||||
redirect_to settings_providers_path,
|
||||
alert: t(".select_connection", default: "Choose a Mercury connection in Provider Settings.")
|
||||
elsif turbo_frame_request?
|
||||
render partial: "mercury_items/setup_required", layout: false
|
||||
else
|
||||
redirect_to settings_providers_path,
|
||||
alert: t(".no_credentials_configured",
|
||||
default: "Please configure your Mercury API token first in Provider Settings.")
|
||||
end
|
||||
end
|
||||
|
||||
def mercury_item_selection_required?(credentialed_items)
|
||||
credentialed_items.count > 1 && params[:mercury_item_id].blank?
|
||||
end
|
||||
|
||||
# Sanitize return_to parameter to prevent XSS attacks
|
||||
|
||||
@@ -141,7 +141,7 @@ class Settings::ProvidersController < ApplicationController
|
||||
# Providers page only needs to know whether any Sophtron connections exist with valid credentials
|
||||
@sophtron_items = Current.family.sophtron_items.where.not(user_id: [ nil, "" ], access_key: [ nil, "" ]).ordered.select(:id)
|
||||
@coinstats_items = Current.family.coinstats_items.ordered # CoinStats panel needs account info for status display
|
||||
@mercury_items = Current.family.mercury_items.ordered.select(:id)
|
||||
@mercury_items = Current.family.mercury_items.active.ordered.includes(:syncs, :mercury_accounts)
|
||||
@coinbase_items = Current.family.coinbase_items.ordered # Coinbase panel needs name and sync info for status display
|
||||
@snaptrade_items = Current.family.snaptrade_items.includes(:snaptrade_accounts).ordered
|
||||
@indexa_capital_items = Current.family.indexa_capital_items.ordered.select(:id)
|
||||
|
||||
@@ -23,6 +23,6 @@ module Family::MercuryConnectable
|
||||
end
|
||||
|
||||
def has_mercury_credentials?
|
||||
mercury_items.where.not(token: nil).exists?
|
||||
mercury_items.active.any?(&:credentials_configured?)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -168,7 +168,7 @@ class MercuryItem < ApplicationRecord
|
||||
end
|
||||
|
||||
def credentials_configured?
|
||||
token.present?
|
||||
token.to_s.strip.present?
|
||||
end
|
||||
|
||||
def effective_base_url
|
||||
|
||||
@@ -15,23 +15,11 @@ class Provider::MercuryAdapter < Provider::Base
|
||||
def self.connection_configs(family:)
|
||||
return [] unless family.can_connect_mercury?
|
||||
|
||||
[ {
|
||||
key: "mercury",
|
||||
name: "Mercury",
|
||||
description: "Connect to your bank via Mercury",
|
||||
can_connect: true,
|
||||
new_account_path: ->(accountable_type, return_to) {
|
||||
Rails.application.routes.url_helpers.select_accounts_mercury_items_path(
|
||||
accountable_type: accountable_type,
|
||||
return_to: return_to
|
||||
)
|
||||
},
|
||||
existing_account_path: ->(account_id) {
|
||||
Rails.application.routes.url_helpers.select_existing_account_mercury_items_path(
|
||||
account_id: account_id
|
||||
)
|
||||
}
|
||||
} ]
|
||||
mercury_items = family.mercury_items.active.ordered.select(&:credentials_configured?)
|
||||
|
||||
return [ connection_config_for(nil) ] if mercury_items.empty?
|
||||
|
||||
mercury_items.map { |mercury_item| connection_config_for(mercury_item) }
|
||||
end
|
||||
|
||||
def provider_name
|
||||
@@ -41,19 +29,54 @@ class Provider::MercuryAdapter < Provider::Base
|
||||
# Build a Mercury provider instance with family-specific credentials
|
||||
# @param family [Family] The family to get credentials for (required)
|
||||
# @return [Provider::Mercury, nil] Returns nil if credentials are not configured
|
||||
def self.build_provider(family: nil)
|
||||
def self.build_provider(family: nil, mercury_item_id: nil)
|
||||
return nil unless family.present?
|
||||
|
||||
# Get family-specific credentials
|
||||
mercury_item = family.mercury_items.where.not(token: nil).first
|
||||
mercury_item = resolve_mercury_item(family, mercury_item_id)
|
||||
return nil unless mercury_item&.credentials_configured?
|
||||
|
||||
Provider::Mercury.new(
|
||||
mercury_item.token,
|
||||
mercury_item.token.to_s.strip,
|
||||
base_url: mercury_item.effective_base_url
|
||||
)
|
||||
end
|
||||
|
||||
def self.connection_config_for(mercury_item)
|
||||
path_params = ->(extra = {}) do
|
||||
mercury_item.present? ? extra.merge(mercury_item_id: mercury_item.id) : extra
|
||||
end
|
||||
|
||||
{
|
||||
key: mercury_item.present? ? "mercury_#{mercury_item.id}" : "mercury",
|
||||
name: mercury_item.present? ? I18n.t("mercury_items.provider_connection.name", name: mercury_item.name) : I18n.t("mercury_items.provider_connection.default_name"),
|
||||
description: mercury_item.present? ? I18n.t("mercury_items.provider_connection.description", name: mercury_item.name) : I18n.t("mercury_items.provider_connection.default_description"),
|
||||
can_connect: true,
|
||||
new_account_path: ->(accountable_type, return_to) {
|
||||
Rails.application.routes.url_helpers.select_accounts_mercury_items_path(
|
||||
path_params.call(accountable_type: accountable_type, return_to: return_to)
|
||||
)
|
||||
},
|
||||
existing_account_path: ->(account_id) {
|
||||
Rails.application.routes.url_helpers.select_existing_account_mercury_items_path(
|
||||
path_params.call(account_id: account_id)
|
||||
)
|
||||
}
|
||||
}
|
||||
end
|
||||
private_class_method :connection_config_for
|
||||
|
||||
def self.resolve_mercury_item(family, mercury_item_id)
|
||||
if mercury_item_id.present?
|
||||
item = family.mercury_items.active.find_by(id: mercury_item_id)
|
||||
return item if item&.credentials_configured?
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
family.mercury_items.active.ordered.find(&:credentials_configured?)
|
||||
end
|
||||
private_class_method :resolve_mercury_item
|
||||
|
||||
def sync_path
|
||||
Rails.application.routes.url_helpers.sync_mercury_item_path(item)
|
||||
end
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
<form action="<%= link_accounts_mercury_items_path %>" method="post" class="space-y-4" data-turbo-frame="_top">
|
||||
<%= hidden_field_tag :authenticity_token, form_authenticity_token %>
|
||||
<%= hidden_field_tag :mercury_item_id, @mercury_item.id %>
|
||||
<%= hidden_field_tag :accountable_type, @accountable_type %>
|
||||
<%= hidden_field_tag :return_to, @return_to %>
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
<form action="<%= link_existing_account_mercury_items_path %>" method="post" class="space-y-4" data-turbo-frame="_top">
|
||||
<%= hidden_field_tag :authenticity_token, form_authenticity_token %>
|
||||
<%= hidden_field_tag :mercury_item_id, @mercury_item.id %>
|
||||
<%= hidden_field_tag :account_id, @account.id %>
|
||||
<%= hidden_field_tag :return_to, @return_to %>
|
||||
|
||||
|
||||
@@ -1,24 +1,19 @@
|
||||
<div class="space-y-4">
|
||||
<div id="mercury-providers-panel" class="space-y-4">
|
||||
<% active_items = local_assigns[:mercury_items] || @mercury_items || Current.family.mercury_items.active.ordered %>
|
||||
<% credentialed_items = active_items.select(&:credentials_configured?) %>
|
||||
|
||||
<div class="prose prose-sm text-secondary">
|
||||
<p class="text-primary font-medium">Setup instructions:</p>
|
||||
<p class="text-primary font-medium"><%= t("mercury_items.provider_panel.setup_title") %></p>
|
||||
<ol>
|
||||
<li>Visit <a href="https://mercury.com" target="_blank" rel="noopener noreferrer" class="link">Mercury</a> and log in to your account</li>
|
||||
<li>Go to Settings > Developer > API Tokens</li>
|
||||
<li>Create a new API token with "Read Only" access</li>
|
||||
<li><strong>Important:</strong> Add your server's IP address to the token's whitelist</li>
|
||||
<li>Copy the <strong>full token</strong> (including the <code>secret-token:</code> prefix) and paste it below</li>
|
||||
<li>After a successful connection, go to the Accounts tab to set up new accounts</li>
|
||||
<li><%= t("mercury_items.provider_panel.instructions.sign_in_html", link: link_to("Mercury", "https://mercury.com", target: "_blank", rel: "noopener noreferrer", class: "link")) %></li>
|
||||
<li><%= t("mercury_items.provider_panel.instructions.open_tokens") %></li>
|
||||
<li><%= t("mercury_items.provider_panel.instructions.create_token") %></li>
|
||||
<li><%= t("mercury_items.provider_panel.instructions.whitelist_ip_html") %></li>
|
||||
<li><%= t("mercury_items.provider_panel.instructions.copy_token_html") %></li>
|
||||
</ol>
|
||||
|
||||
<p class="text-primary font-medium">Field descriptions:</p>
|
||||
<ul>
|
||||
<li><strong>API Token:</strong> Your full Mercury API token including the <code>secret-token:</code> prefix (required)</li>
|
||||
<li><strong>Base URL:</strong> Mercury API URL (optional, defaults to https://api.mercury.com/api/v1)</li>
|
||||
</ul>
|
||||
|
||||
<p class="text-sm text-subdued mt-2">
|
||||
<strong>Note:</strong> For sandbox testing, use <code>https://api-sandbox.mercury.com/api/v1</code> as the Base URL.
|
||||
Mercury requires IP whitelisting - make sure to add your IP in the Mercury dashboard.
|
||||
<%= t("mercury_items.provider_panel.sandbox_note_html") %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -29,45 +24,119 @@
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%
|
||||
# Get or initialize a mercury_item for this family
|
||||
# - If family has an item WITH credentials, use it (for updates)
|
||||
# - If family has an item WITHOUT credentials, use it (to add credentials)
|
||||
# - If family has no items at all, create a new one
|
||||
mercury_item = Current.family.mercury_items.first_or_initialize(name: "Mercury Connection")
|
||||
is_new_record = mercury_item.new_record?
|
||||
%>
|
||||
<% if active_items.any? %>
|
||||
<div class="space-y-3">
|
||||
<% active_items.each do |item| %>
|
||||
<details class="group bg-container p-4 shadow-border-xs rounded-xl">
|
||||
<summary class="flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center justify-center h-8 w-8 bg-blue-600/10 rounded-full">
|
||||
<p class="text-blue-600 text-xs font-medium"><%= item.name.to_s.first.to_s.upcase %></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-medium text-primary"><%= item.name %></p>
|
||||
<p class="text-xs text-secondary"><%= item.sync_status_summary %></p>
|
||||
</div>
|
||||
</div>
|
||||
</summary>
|
||||
|
||||
<%= styled_form_with model: mercury_item,
|
||||
url: is_new_record ? mercury_items_path : mercury_item_path(mercury_item),
|
||||
scope: :mercury_item,
|
||||
method: is_new_record ? :post : :patch,
|
||||
data: { turbo: true },
|
||||
class: "space-y-3" do |form| %>
|
||||
<%= form.text_field :token,
|
||||
label: "Token",
|
||||
placeholder: is_new_record ? "Paste token here" : "Enter new token to update",
|
||||
type: :password %>
|
||||
<div class="mt-4 space-y-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= button_to sync_mercury_item_path(item),
|
||||
method: :post,
|
||||
class: "inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium text-secondary hover:text-primary border border-secondary rounded-lg hover:border-primary",
|
||||
disabled: item.syncing? do %>
|
||||
<%= icon "refresh-cw", size: "sm" %>
|
||||
<%= t("mercury_items.provider_panel.sync") %>
|
||||
<% end %>
|
||||
<%= button_to mercury_item_path(item),
|
||||
method: :delete,
|
||||
class: "inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium text-destructive hover:bg-destructive/10 rounded-lg",
|
||||
data: { turbo_confirm: t("mercury_items.provider_panel.disconnect_confirm", name: item.name) } do %>
|
||||
<%= icon "trash-2", size: "sm" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= form.text_field :base_url,
|
||||
label: "Base Url (Optional)",
|
||||
placeholder: "https://api.mercury.com/api/v1 (default)",
|
||||
value: mercury_item.base_url %>
|
||||
<%= styled_form_with model: item,
|
||||
url: mercury_item_path(item),
|
||||
scope: :mercury_item,
|
||||
method: :patch,
|
||||
data: { turbo: true },
|
||||
class: "space-y-3" do |form| %>
|
||||
<%= form.text_field :name,
|
||||
label: t("mercury_items.provider_panel.connection_name_label"),
|
||||
placeholder: t("mercury_items.provider_panel.connection_name_placeholder") %>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<%= form.submit is_new_record ? "Save Configuration" : "Update Configuration",
|
||||
class: "inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-medium text-white bg-gray-900 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-gray-900 focus:ring-offset-2 transition-colors" %>
|
||||
<%= form.text_field :token,
|
||||
label: t("mercury_items.provider_panel.token_label"),
|
||||
placeholder: t("mercury_items.provider_panel.keep_token_placeholder"),
|
||||
type: :password,
|
||||
value: nil %>
|
||||
|
||||
<%= form.text_field :base_url,
|
||||
label: t("mercury_items.provider_panel.base_url_label"),
|
||||
placeholder: t("mercury_items.provider_panel.base_url_placeholder"),
|
||||
value: item.base_url %>
|
||||
|
||||
<div class="flex flex-wrap justify-end gap-2">
|
||||
<%= render DS::Link.new(
|
||||
text: t("mercury_items.provider_panel.setup_accounts"),
|
||||
icon: "settings",
|
||||
variant: "secondary",
|
||||
href: setup_accounts_mercury_item_path(item),
|
||||
frame: :modal
|
||||
) %>
|
||||
<%= form.submit t("mercury_items.provider_panel.update_connection"),
|
||||
class: "inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-medium text-inverse bg-inverse hover:bg-inverse-hover focus:outline-none focus:ring-2 focus:ring-primary transition-colors" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</details>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% items = local_assigns[:mercury_items] || @mercury_items || Current.family.mercury_items.where.not(token: [nil, ""]) %>
|
||||
<details <%= "open" unless active_items.any? %> class="group bg-container p-4 shadow-border-xs rounded-xl">
|
||||
<summary class="flex items-center gap-2 text-sm font-medium text-primary">
|
||||
<%= icon "plus" %>
|
||||
<%= t("mercury_items.provider_panel.add_connection") %>
|
||||
</summary>
|
||||
|
||||
<% mercury_item = Current.family.mercury_items.build(name: t("mercury_items.provider_panel.default_connection_name")) %>
|
||||
<%= styled_form_with model: mercury_item,
|
||||
url: mercury_items_path,
|
||||
scope: :mercury_item,
|
||||
method: :post,
|
||||
data: { turbo: true },
|
||||
class: "space-y-3 mt-4" do |form| %>
|
||||
<%= form.text_field :name,
|
||||
label: t("mercury_items.provider_panel.connection_name_label"),
|
||||
placeholder: t("mercury_items.provider_panel.connection_name_placeholder") %>
|
||||
|
||||
<%= form.text_field :token,
|
||||
label: t("mercury_items.provider_panel.token_label"),
|
||||
placeholder: t("mercury_items.provider_panel.token_placeholder"),
|
||||
type: :password,
|
||||
value: nil %>
|
||||
|
||||
<%= form.text_field :base_url,
|
||||
label: t("mercury_items.provider_panel.base_url_label"),
|
||||
placeholder: t("mercury_items.provider_panel.base_url_placeholder") %>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<%= form.submit t("mercury_items.provider_panel.add_connection"),
|
||||
class: "inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-medium text-inverse bg-inverse hover:bg-inverse-hover focus:outline-none focus:ring-2 focus:ring-primary transition-colors" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</details>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<% if items&.any? %>
|
||||
<% if credentialed_items.any? %>
|
||||
<div class="w-2 h-2 bg-success rounded-full"></div>
|
||||
<p class="text-sm text-secondary">Configured and ready to use. Visit the <a href="<%= accounts_path %>" class="link">Accounts</a> tab to manage and set up accounts.</p>
|
||||
<p class="text-sm text-secondary"><%= t("mercury_items.provider_panel.configured_html", accounts_link: link_to(t("mercury_items.provider_panel.accounts_link"), accounts_path, class: "link")) %></p>
|
||||
<% else %>
|
||||
<div class="w-2 h-2 bg-gray-400 rounded-full"></div>
|
||||
<p class="text-sm text-secondary">Not configured</p>
|
||||
<p class="text-sm text-secondary"><%= t("mercury_items.provider_panel.not_configured") %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,7 @@ en:
|
||||
no_api_token: Mercury API token not found. Please configure it in Provider Settings.
|
||||
partial_invalid: "Successfully linked %{created_count} account(s), %{already_linked_count} were already linked, %{invalid_count} account(s) had invalid names"
|
||||
partial_success: "Successfully linked %{created_count} account(s). %{already_linked_count} account(s) were already linked: %{already_linked_names}"
|
||||
select_connection: Choose a Mercury connection before linking accounts.
|
||||
success:
|
||||
one: "Successfully linked %{count} account"
|
||||
other: "Successfully linked %{count} accounts"
|
||||
@@ -42,6 +43,36 @@ en:
|
||||
syncing: Syncing...
|
||||
total: Total
|
||||
unlinked: Unlinked
|
||||
provider_panel:
|
||||
accounts_link: Accounts
|
||||
add_connection: Add Mercury connection
|
||||
base_url_label: Base URL (optional)
|
||||
base_url_placeholder: https://api.mercury.com/api/v1 (default)
|
||||
configured_html: "Configured and ready to use. Visit the %{accounts_link} tab to manage and set up accounts."
|
||||
connection_name_label: Connection name
|
||||
connection_name_placeholder: Business checking
|
||||
default_connection_name: Mercury Connection
|
||||
disconnect_confirm: "Disconnect %{name}?"
|
||||
instructions:
|
||||
copy_token_html: "Copy the <strong>full token</strong> (including the <code>secret-token:</code> prefix) and add it as a named connection below"
|
||||
create_token: Create a new API token with "Read Only" access
|
||||
open_tokens: Go to Settings > Developer > API Tokens
|
||||
sign_in_html: "Visit %{link} and log in to the account you want to connect"
|
||||
whitelist_ip_html: "<strong>Important:</strong> Add your server's IP address to the token's whitelist"
|
||||
keep_token_placeholder: Leave blank to keep the current token
|
||||
not_configured: Not configured
|
||||
sandbox_note_html: "Use a separate named connection for each Mercury login/API token you want to sync. For sandbox testing, use <code>https://api-sandbox.mercury.com/api/v1</code> as the Base URL. Mercury requires IP whitelisting - make sure to add your IP in the Mercury dashboard."
|
||||
setup_accounts: Set up accounts
|
||||
setup_title: "Setup instructions:"
|
||||
sync: Sync
|
||||
token_label: Token
|
||||
token_placeholder: Paste token here
|
||||
update_connection: Update connection
|
||||
provider_connection:
|
||||
default_description: Connect to your bank via Mercury
|
||||
default_name: Mercury
|
||||
description: "Connect using %{name}"
|
||||
name: "Mercury - %{name}"
|
||||
select_accounts:
|
||||
accounts_selected: accounts selected
|
||||
api_error: "API error: %{message}"
|
||||
@@ -53,6 +84,7 @@ en:
|
||||
no_api_token: Mercury API token is not configured. Please configure it in Settings.
|
||||
no_credentials_configured: Please configure your Mercury API token first in Provider Settings.
|
||||
no_name_placeholder: "(No name)"
|
||||
select_connection: Choose a Mercury connection in Provider Settings.
|
||||
title: Select Mercury Accounts
|
||||
select_existing_account:
|
||||
account_already_linked: This account is already linked to a provider
|
||||
@@ -67,6 +99,7 @@ en:
|
||||
no_api_token: Mercury API token is not configured. Please configure it in Settings.
|
||||
no_credentials_configured: Please configure your Mercury API token first in Provider Settings.
|
||||
no_name_placeholder: "(No name)"
|
||||
select_connection: Choose a Mercury connection in Provider Settings.
|
||||
title: "Link %{account_name} with Mercury"
|
||||
link_existing_account:
|
||||
account_already_linked: This account is already linked to a provider
|
||||
@@ -76,6 +109,7 @@ en:
|
||||
mercury_account_not_found: Mercury account not found
|
||||
missing_parameters: Missing required parameters
|
||||
no_api_token: Mercury API token not found. Please configure it in Provider Settings.
|
||||
select_connection: Choose a Mercury connection before linking accounts.
|
||||
success: "Successfully linked %{account_name} with Mercury"
|
||||
setup_accounts:
|
||||
account_type_label: "Account Type:"
|
||||
|
||||
268
test/controllers/mercury_items_controller_test.rb
Normal file
268
test/controllers/mercury_items_controller_test.rb
Normal file
@@ -0,0 +1,268 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class MercuryItemsControllerTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
sign_in users(:family_admin)
|
||||
Rails.cache.clear
|
||||
SyncJob.stubs(:perform_later)
|
||||
|
||||
@family = families(:dylan_family)
|
||||
@existing_item = mercury_items(:one)
|
||||
@second_item = MercuryItem.create!(
|
||||
family: @family,
|
||||
name: "Business Mercury",
|
||||
token: "second_mercury_token",
|
||||
base_url: "https://api.mercury.com/api/v1"
|
||||
)
|
||||
end
|
||||
|
||||
teardown do
|
||||
Rails.cache.clear
|
||||
end
|
||||
|
||||
test "create adds a new mercury connection without overwriting existing credentials" do
|
||||
existing_token = @existing_item.token
|
||||
|
||||
assert_difference "MercuryItem.count", 1 do
|
||||
post mercury_items_url, params: {
|
||||
mercury_item: {
|
||||
name: "Joint Mercury",
|
||||
token: "joint_mercury_token",
|
||||
base_url: "https://api.mercury.com/api/v1"
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
assert_redirected_to accounts_path
|
||||
assert_equal existing_token, @existing_item.reload.token
|
||||
assert_equal "joint_mercury_token", @family.mercury_items.find_by!(name: "Joint Mercury").token
|
||||
end
|
||||
|
||||
test "update changes only the selected mercury connection" do
|
||||
existing_token = @existing_item.token
|
||||
|
||||
patch mercury_item_url(@second_item), params: {
|
||||
mercury_item: {
|
||||
name: "Renamed Business Mercury",
|
||||
token: "updated_second_token",
|
||||
base_url: "https://api-sandbox.mercury.com/api/v1"
|
||||
}
|
||||
}
|
||||
|
||||
assert_redirected_to accounts_path
|
||||
assert_equal existing_token, @existing_item.reload.token
|
||||
assert_equal "Renamed Business Mercury", @second_item.reload.name
|
||||
assert_equal "updated_second_token", @second_item.token
|
||||
assert_equal "https://api-sandbox.mercury.com/api/v1", @second_item.base_url
|
||||
end
|
||||
|
||||
test "blank token update preserves the selected mercury token" do
|
||||
original_token = @second_item.token
|
||||
|
||||
patch mercury_item_url(@second_item), params: {
|
||||
mercury_item: {
|
||||
name: "Renamed Business Mercury",
|
||||
token: "",
|
||||
base_url: "https://api.mercury.com/api/v1"
|
||||
}
|
||||
}
|
||||
|
||||
assert_redirected_to accounts_path
|
||||
assert_equal "Renamed Business Mercury", @second_item.reload.name
|
||||
assert_equal original_token, @second_item.token
|
||||
end
|
||||
|
||||
test "update expires selected mercury account cache when credentials change" do
|
||||
Rails.cache.expects(:delete).with(mercury_cache_key(@existing_item)).never
|
||||
Rails.cache.expects(:delete).with(mercury_cache_key(@second_item)).once
|
||||
|
||||
patch mercury_item_url(@second_item), params: {
|
||||
mercury_item: {
|
||||
name: "Renamed Business Mercury",
|
||||
token: "updated_second_token",
|
||||
base_url: "https://api-sandbox.mercury.com/api/v1"
|
||||
}
|
||||
}
|
||||
|
||||
assert_redirected_to accounts_path
|
||||
end
|
||||
|
||||
test "update does not expire selected mercury account cache for name-only changes" do
|
||||
Rails.cache.expects(:delete).never
|
||||
|
||||
patch mercury_item_url(@second_item), params: {
|
||||
mercury_item: {
|
||||
name: "Renamed Business Mercury"
|
||||
}
|
||||
}
|
||||
|
||||
assert_redirected_to accounts_path
|
||||
assert_equal "Renamed Business Mercury", @second_item.reload.name
|
||||
end
|
||||
|
||||
test "preload accounts uses selected mercury item cache key" do
|
||||
Rails.cache.expects(:read).with(mercury_cache_key(@second_item)).returns(nil)
|
||||
Rails.cache.expects(:write).with(mercury_cache_key(@second_item), mercury_accounts_payload, expires_in: 5.minutes)
|
||||
|
||||
provider = mock("mercury_provider")
|
||||
provider.expects(:get_accounts).returns(accounts: mercury_accounts_payload)
|
||||
Provider::Mercury.expects(:new)
|
||||
.with(@second_item.token, base_url: @second_item.effective_base_url)
|
||||
.returns(provider)
|
||||
|
||||
get preload_accounts_mercury_items_url, params: { mercury_item_id: @second_item.id }, as: :json
|
||||
|
||||
assert_response :success
|
||||
response = JSON.parse(@response.body)
|
||||
assert_equal true, response["success"]
|
||||
assert_equal true, response["has_accounts"]
|
||||
end
|
||||
|
||||
test "select accounts requires an explicit connection when multiple mercury items exist" do
|
||||
get select_accounts_mercury_items_url, params: { accountable_type: "Depository" }
|
||||
|
||||
assert_redirected_to settings_providers_path
|
||||
assert_equal "Choose a Mercury connection in Provider Settings.", flash[:alert]
|
||||
end
|
||||
|
||||
test "select accounts renders the selected mercury item id" do
|
||||
Rails.cache.expects(:read).with(mercury_cache_key(@second_item)).returns(nil)
|
||||
Rails.cache.expects(:write).with(mercury_cache_key(@second_item), mercury_accounts_payload, expires_in: 5.minutes)
|
||||
|
||||
provider = mock("mercury_provider")
|
||||
provider.expects(:get_accounts).returns(accounts: mercury_accounts_payload)
|
||||
Provider::Mercury.expects(:new)
|
||||
.with(@second_item.token, base_url: @second_item.effective_base_url)
|
||||
.returns(provider)
|
||||
|
||||
get select_accounts_mercury_items_url, params: {
|
||||
mercury_item_id: @second_item.id,
|
||||
accountable_type: "Depository"
|
||||
}
|
||||
|
||||
assert_response :success
|
||||
assert_includes @response.body, %(name="mercury_item_id")
|
||||
assert_includes @response.body, %(value="#{@second_item.id}")
|
||||
end
|
||||
|
||||
test "select existing account renders the selected mercury item id" do
|
||||
account = @family.accounts.create!(
|
||||
name: "Manual Checking",
|
||||
balance: 0,
|
||||
currency: "USD",
|
||||
accountable: Depository.new
|
||||
)
|
||||
|
||||
Rails.cache.expects(:read).with(mercury_cache_key(@second_item)).returns(nil)
|
||||
Rails.cache.expects(:write).with(mercury_cache_key(@second_item), mercury_accounts_payload, expires_in: 5.minutes)
|
||||
|
||||
provider = mock("mercury_provider")
|
||||
provider.expects(:get_accounts).returns(accounts: mercury_accounts_payload)
|
||||
Provider::Mercury.expects(:new)
|
||||
.with(@second_item.token, base_url: @second_item.effective_base_url)
|
||||
.returns(provider)
|
||||
|
||||
get select_existing_account_mercury_items_url, params: {
|
||||
mercury_item_id: @second_item.id,
|
||||
account_id: account.id
|
||||
}
|
||||
|
||||
assert_response :success
|
||||
assert_includes @response.body, %(name="mercury_item_id")
|
||||
assert_includes @response.body, %(value="#{@second_item.id}")
|
||||
end
|
||||
|
||||
test "link accounts uses selected mercury item and allows duplicate upstream ids across items" do
|
||||
@existing_item.mercury_accounts.create!(
|
||||
account_id: "shared_mercury_account",
|
||||
name: "Shared Checking",
|
||||
currency: "USD",
|
||||
current_balance: 1000
|
||||
)
|
||||
|
||||
provider = mock("mercury_provider")
|
||||
provider.expects(:get_accounts).returns(accounts: mercury_accounts_payload)
|
||||
Provider::Mercury.expects(:new)
|
||||
.with(@second_item.token, base_url: @second_item.effective_base_url)
|
||||
.returns(provider)
|
||||
|
||||
assert_difference -> { @second_item.mercury_accounts.where(account_id: "shared_mercury_account").count }, 1 do
|
||||
assert_difference "AccountProvider.count", 1 do
|
||||
post link_accounts_mercury_items_url, params: {
|
||||
mercury_item_id: @second_item.id,
|
||||
account_ids: [ "shared_mercury_account" ],
|
||||
accountable_type: "Depository"
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
assert_redirected_to accounts_path
|
||||
assert_equal 1, @existing_item.mercury_accounts.where(account_id: "shared_mercury_account").count
|
||||
end
|
||||
|
||||
test "link accounts does not silently use the first connection when multiple items exist" do
|
||||
assert_no_difference "MercuryAccount.count" do
|
||||
assert_no_difference "Account.count" do
|
||||
post link_accounts_mercury_items_url, params: {
|
||||
account_ids: [ "shared_mercury_account" ],
|
||||
accountable_type: "Depository"
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
assert_redirected_to settings_providers_path
|
||||
assert_equal "Choose a Mercury connection before linking accounts.", flash[:alert]
|
||||
end
|
||||
|
||||
test "link existing account does not silently use the first connection when multiple items exist" do
|
||||
account = @family.accounts.create!(
|
||||
name: "Manual Checking",
|
||||
balance: 0,
|
||||
currency: "USD",
|
||||
accountable: Depository.new
|
||||
)
|
||||
|
||||
assert_no_difference "MercuryAccount.count" do
|
||||
assert_no_difference "AccountProvider.count" do
|
||||
post link_existing_account_mercury_items_url, params: {
|
||||
account_id: account.id,
|
||||
mercury_account_id: "shared_mercury_account"
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
assert_redirected_to settings_providers_path
|
||||
assert_equal "Choose a Mercury connection before linking accounts.", flash[:alert]
|
||||
end
|
||||
|
||||
test "sync only queues a sync for the selected mercury item" do
|
||||
assert_difference -> { Sync.where(syncable: @second_item).count }, 1 do
|
||||
assert_no_difference -> { Sync.where(syncable: @existing_item).count } do
|
||||
post sync_mercury_item_url(@second_item)
|
||||
end
|
||||
end
|
||||
|
||||
assert_response :redirect
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def mercury_accounts_payload
|
||||
[
|
||||
{
|
||||
id: "shared_mercury_account",
|
||||
nickname: "Shared Checking",
|
||||
name: "Shared Checking",
|
||||
status: "active",
|
||||
type: "checking",
|
||||
currentBalance: 1000
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
def mercury_cache_key(mercury_item)
|
||||
"mercury_accounts_#{@family.id}_#{mercury_item.id}"
|
||||
end
|
||||
end
|
||||
@@ -44,6 +44,34 @@ class MercuryAccountTest < ActiveSupport::TestCase
|
||||
end
|
||||
end
|
||||
|
||||
test "same account_id can be linked under different mercury_items in the same family" do
|
||||
item_a_2 = MercuryItem.create!(
|
||||
family: @family_a,
|
||||
name: "Family A Second Mercury",
|
||||
token: "token_a_2",
|
||||
base_url: "https://api-sandbox.mercury.com/api/v1",
|
||||
status: "good"
|
||||
)
|
||||
|
||||
MercuryAccount.create!(
|
||||
mercury_item: @item_a,
|
||||
account_id: "shared_merc_acc_1",
|
||||
name: "Checking",
|
||||
currency: "USD",
|
||||
current_balance: 5000
|
||||
)
|
||||
|
||||
assert_difference "MercuryAccount.count", 1 do
|
||||
MercuryAccount.create!(
|
||||
mercury_item: item_a_2,
|
||||
account_id: "shared_merc_acc_1",
|
||||
name: "Checking",
|
||||
currency: "USD",
|
||||
current_balance: 5000
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
test "same account_id cannot appear twice under the same mercury_item" do
|
||||
MercuryAccount.create!(
|
||||
mercury_item: @item_a,
|
||||
|
||||
@@ -22,6 +22,11 @@ class MercuryItemTest < ActiveSupport::TestCase
|
||||
assert_not @mercury_item.credentials_configured?
|
||||
end
|
||||
|
||||
test "credentials_configured returns false when token is whitespace" do
|
||||
@mercury_item.token = " "
|
||||
assert_not @mercury_item.credentials_configured?
|
||||
end
|
||||
|
||||
test "effective_base_url returns custom url when set" do
|
||||
assert_equal "https://api-sandbox.mercury.com/api/v1", @mercury_item.effective_base_url
|
||||
end
|
||||
@@ -42,6 +47,42 @@ class MercuryItemTest < ActiveSupport::TestCase
|
||||
assert_nil @mercury_item.mercury_provider
|
||||
end
|
||||
|
||||
test "family credential check ignores blank and scheduled for deletion items" do
|
||||
family = families(:empty)
|
||||
blank_item = MercuryItem.create!(
|
||||
family: family,
|
||||
name: "Blank Mercury",
|
||||
token: "temporary_token",
|
||||
base_url: "https://api-sandbox.mercury.com/api/v1"
|
||||
)
|
||||
blank_item.update_column(:token, "")
|
||||
|
||||
whitespace_item = MercuryItem.create!(
|
||||
family: family,
|
||||
name: "Whitespace Mercury",
|
||||
token: "temporary_token",
|
||||
base_url: "https://api-sandbox.mercury.com/api/v1"
|
||||
)
|
||||
whitespace_item.update_column(:token, " ")
|
||||
|
||||
deleted_item = MercuryItem.create!(
|
||||
family: family,
|
||||
name: "Deleted Mercury",
|
||||
token: "deleted_token",
|
||||
base_url: "https://api-sandbox.mercury.com/api/v1",
|
||||
scheduled_for_deletion: true
|
||||
)
|
||||
|
||||
refute family.has_mercury_credentials?
|
||||
|
||||
whitespace_item.update_column(:token, "configured_token")
|
||||
assert family.has_mercury_credentials?
|
||||
|
||||
whitespace_item.update_column(:token, " ")
|
||||
deleted_item.update!(scheduled_for_deletion: false)
|
||||
assert family.has_mercury_credentials?
|
||||
end
|
||||
|
||||
test "syncer returns MercuryItem::Syncer instance" do
|
||||
syncer = @mercury_item.send(:syncer)
|
||||
assert_instance_of MercuryItem::Syncer, syncer
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
require "uri"
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class Provider::MercuryAdapterTest < ActiveSupport::TestCase
|
||||
@@ -9,17 +11,59 @@ class Provider::MercuryAdapterTest < ActiveSupport::TestCase
|
||||
assert_not_includes Provider::MercuryAdapter.supported_account_types, "Investment"
|
||||
end
|
||||
|
||||
test "returns connection configs for any family" do
|
||||
test "returns fallback connection config when no credentials exist yet" do
|
||||
# Mercury is a per-family provider - any family can connect
|
||||
family = families(:dylan_family)
|
||||
family = families(:empty)
|
||||
configs = Provider::MercuryAdapter.connection_configs(family: family)
|
||||
|
||||
assert_equal 1, configs.length
|
||||
assert_equal "mercury", configs.first[:key]
|
||||
assert_equal "Mercury", configs.first[:name]
|
||||
assert_equal I18n.t("mercury_items.provider_connection.default_name"), configs.first[:name]
|
||||
assert configs.first[:can_connect]
|
||||
end
|
||||
|
||||
test "returns one connection config per credentialed mercury item" do
|
||||
family = families(:dylan_family)
|
||||
first_item = mercury_items(:one)
|
||||
second_item = MercuryItem.create!(
|
||||
family: family,
|
||||
name: "Business Mercury",
|
||||
token: "second_mercury_token",
|
||||
base_url: "https://api.mercury.com/api/v1"
|
||||
)
|
||||
|
||||
configs = Provider::MercuryAdapter.connection_configs(family: family)
|
||||
|
||||
assert_equal 2, configs.length
|
||||
assert_equal [ "mercury_#{second_item.id}", "mercury_#{first_item.id}" ], configs.map { |config| config[:key] }
|
||||
assert_equal [
|
||||
I18n.t("mercury_items.provider_connection.name", name: second_item.name),
|
||||
I18n.t("mercury_items.provider_connection.name", name: first_item.name)
|
||||
], configs.map { |config| config[:name] }
|
||||
|
||||
new_account_uri = URI.parse(configs.first[:new_account_path].call("Depository", "/accounts"))
|
||||
assert_equal "/mercury_items/select_accounts", new_account_uri.path
|
||||
assert_includes new_account_uri.query, "mercury_item_id=#{second_item.id}"
|
||||
|
||||
existing_account_uri = URI.parse(configs.first[:existing_account_path].call(accounts(:depository).id))
|
||||
assert_equal "/mercury_items/select_existing_account", existing_account_uri.path
|
||||
assert_includes existing_account_uri.query, "mercury_item_id=#{second_item.id}"
|
||||
end
|
||||
|
||||
test "connection configs ignore items with whitespace-only tokens" do
|
||||
family = families(:dylan_family)
|
||||
MercuryItem.create!(
|
||||
family: family,
|
||||
name: "Blank Mercury",
|
||||
token: "temporary_token",
|
||||
base_url: "https://api.mercury.com/api/v1"
|
||||
).update_column(:token, " ")
|
||||
|
||||
configs = Provider::MercuryAdapter.connection_configs(family: family)
|
||||
|
||||
assert_equal [ "mercury_#{mercury_items(:one).id}" ], configs.map { |config| config[:key] }
|
||||
end
|
||||
|
||||
test "build_provider returns nil when family is nil" do
|
||||
assert_nil Provider::MercuryAdapter.build_provider(family: nil)
|
||||
end
|
||||
@@ -35,4 +79,59 @@ class Provider::MercuryAdapterTest < ActiveSupport::TestCase
|
||||
|
||||
assert_instance_of Provider::Mercury, provider
|
||||
end
|
||||
|
||||
test "build_provider uses explicit mercury item credentials" do
|
||||
family = families(:dylan_family)
|
||||
second_item = MercuryItem.create!(
|
||||
family: family,
|
||||
name: "Business Mercury",
|
||||
token: "second_mercury_token",
|
||||
base_url: "https://api.mercury.com/api/v1"
|
||||
)
|
||||
|
||||
provider = Provider::MercuryAdapter.build_provider(family: family, mercury_item_id: second_item.id)
|
||||
|
||||
assert_instance_of Provider::Mercury, provider
|
||||
assert_equal "second_mercury_token", provider.token
|
||||
assert_equal "https://api.mercury.com/api/v1", provider.base_url
|
||||
end
|
||||
|
||||
test "build_provider strips surrounding token whitespace" do
|
||||
family = families(:dylan_family)
|
||||
second_item = MercuryItem.create!(
|
||||
family: family,
|
||||
name: "Business Mercury",
|
||||
token: " second_mercury_token \n",
|
||||
base_url: "https://api.mercury.com/api/v1"
|
||||
)
|
||||
|
||||
provider = Provider::MercuryAdapter.build_provider(family: family, mercury_item_id: second_item.id)
|
||||
|
||||
assert_equal "second_mercury_token", provider.token
|
||||
end
|
||||
|
||||
test "build_provider refuses mercury items outside the family" do
|
||||
family = families(:dylan_family)
|
||||
other_item = MercuryItem.create!(
|
||||
family: families(:empty),
|
||||
name: "Other Mercury",
|
||||
token: "other_mercury_token",
|
||||
base_url: "https://api.mercury.com/api/v1"
|
||||
)
|
||||
|
||||
assert_nil Provider::MercuryAdapter.build_provider(family: family, mercury_item_id: other_item.id)
|
||||
end
|
||||
|
||||
test "build_provider refuses explicit mercury item without usable credentials" do
|
||||
family = families(:dylan_family)
|
||||
blank_item = MercuryItem.create!(
|
||||
family: family,
|
||||
name: "Blank Mercury",
|
||||
token: "temporary_token",
|
||||
base_url: "https://api.mercury.com/api/v1"
|
||||
)
|
||||
blank_item.update_column(:token, " ")
|
||||
|
||||
assert_nil Provider::MercuryAdapter.build_provider(family: family, mercury_item_id: blank_item.id)
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user