Mercury integration (#723)

* Initial mercury impl

* FIX both mercury and generator class

* Finish mercury integration and provider generator

* Fix schema

* Fix linter and tags

* Update routes.rb

* Avoid schema drift

---------

Signed-off-by: soky srm <sokysrm@gmail.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
This commit is contained in:
soky srm
2026-01-22 20:37:07 +01:00
committed by GitHub
parent 7842b4a044
commit 179552657c
46 changed files with 3345 additions and 30 deletions

View File

@@ -318,11 +318,11 @@ class Provider::FamilyGenerator < Rails::Generators::NamedBase
# Add section before the last closing div (at end of file)
section_content = <<~ERB
<%%= settings_section title: "#{class_name}", collapsible: true, open: false do %>
<%= settings_section title: "#{class_name}", collapsible: true, open: false do %>
<turbo-frame id="#{file_name}-providers-panel">
<%%= render "settings/providers/#{file_name}_panel" %>
<%= render "settings/providers/#{file_name}_panel" %>
</turbo-frame>
<%% end %>
<% end %>
ERB
# Insert before the final </div> at the end of file
@@ -331,6 +331,99 @@ class Provider::FamilyGenerator < Rails::Generators::NamedBase
end
end
def update_accounts_controller
controller_path = "app/controllers/accounts_controller.rb"
return unless File.exist?(controller_path)
content = File.read(controller_path)
items_var = "@#{file_name}_items"
# Check if already added
if content.include?(items_var)
say "Accounts controller already has #{items_var}", :skip
return
end
# Add to index action - find the last @*_items line and insert after it
lines = content.lines
last_items_index = nil
lines.each_with_index do |line, index|
if line =~ /@\w+_items = family\.\w+_items\.ordered/
last_items_index = index
end
end
if last_items_index
indentation = lines[last_items_index][/^\s*/]
new_line = "#{indentation}#{items_var} = family.#{file_name}_items.ordered.includes(:syncs, :#{file_name}_accounts)\n"
lines.insert(last_items_index + 1, new_line)
File.write(controller_path, lines.join)
say "Added #{items_var} to accounts controller index", :green
else
say "Could not find @*_items assignments in accounts controller", :yellow
end
# Add sync stats map
add_accounts_controller_sync_stats_map(controller_path)
end
def update_accounts_index_view
view_path = "app/views/accounts/index.html.erb"
return unless File.exist?(view_path)
content = File.read(view_path)
items_var = "@#{file_name}_items"
if content.include?(items_var)
say "Accounts index view already has #{class_name} section", :skip
return
end
# Add to empty check - find the existing pattern and append our check
content = content.gsub(
/@coinstats_items\.empty\? %>/,
"@coinstats_items.empty? && #{items_var}.empty? %>"
)
# Add provider section before manual_accounts
section = <<~ERB
<% if #{items_var}.any? %>
<%= render #{items_var}.sort_by(&:created_at) %>
<% end %>
ERB
content = content.gsub(
/<% if @manual_accounts\.any\? %>/,
"#{section.strip}\n\n <% if @manual_accounts.any? %>"
)
File.write(view_path, content)
say "Added #{class_name} section to accounts index view", :green
end
def create_locale_file
locale_dir = "config/locales/views/#{file_name}_items"
locale_path = "#{locale_dir}/en.yml"
if File.exist?(locale_path)
say "Locale file already exists: #{locale_path}", :skip
return
end
FileUtils.mkdir_p(locale_dir)
template "locale.en.yml.tt", locale_path
say "Created locale file: #{locale_path}", :green
end
def update_source_enums
# Add the new provider to the source enum in ProviderMerchant and DataEnrichment
# These enums track which provider created a merchant or enrichment record
update_source_enum("app/models/provider_merchant.rb")
update_source_enum("app/models/data_enrichment.rb")
end
def show_summary
say "\n" + "=" * 80, :green
say "Successfully generated per-family provider: #{class_name}", :green
@@ -395,6 +488,77 @@ class Provider::FamilyGenerator < Rails::Generators::NamedBase
private
def update_source_enum(model_path)
return unless File.exist?(model_path)
content = File.read(model_path)
model_name = File.basename(model_path, ".rb").camelize
# Check if provider is already in the enum
if content.include?("#{file_name}: \"#{file_name}\"")
say "#{model_name} source enum already includes #{file_name}", :skip
return
end
# Find the enum :source line and add the new provider
# Pattern: enum :source, { key: "value", ... }
if content =~ /(enum :source, \{[^}]+)(})/
# Insert the new provider before the closing brace
updated_content = content.sub(
/(enum :source, \{[^}]+)(})/,
"\\1, #{file_name}: \"#{file_name}\"\\2"
)
File.write(model_path, updated_content)
say "Added #{file_name} to #{model_name} source enum", :green
else
say "Could not find source enum in #{model_name}", :yellow
end
end
def add_accounts_controller_sync_stats_map(controller_path)
content = File.read(controller_path)
stats_var = "@#{file_name}_sync_stats_map"
if content.include?(stats_var)
say "Accounts controller already has #{stats_var}", :skip
return
end
# Find the build_sync_stats_maps method and add our stats map before the closing 'end'
sync_stats_block = <<~RUBY
# #{class_name} sync stats
#{stats_var} = {}
@#{file_name}_items.each do |item|
latest_sync = item.syncs.ordered.first
#{stats_var}[item.id] = latest_sync&.sync_stats || {}
end
RUBY
lines = content.lines
method_start = nil
method_end = nil
indent_level = 0
lines.each_with_index do |line, index|
if line.include?("def build_sync_stats_maps")
method_start = index
indent_level = line[/^\s*/].length
elsif method_start && line =~ /^#{' ' * indent_level}end\s*$/
method_end = index
break
end
end
if method_end
lines.insert(method_end, sync_stats_block)
File.write(controller_path, lines.join)
say "Added #{stats_var} to build_sync_stats_maps", :green
else
say "Could not find build_sync_stats_maps method end", :yellow
end
end
def table_name
"#{file_name}_items"
end

View File

@@ -0,0 +1,147 @@
---
en:
<%= file_name %>_items:
create:
success: <%= class_name %> connection created successfully
destroy:
success: <%= class_name %> connection removed
index:
title: <%= class_name %> Connections
loading:
loading_message: Loading <%= class_name %> accounts...
loading_title: Loading
link_accounts:
all_already_linked:
one: "The selected account (%%{names}) is already linked"
other: "All %%{count} selected accounts are already linked: %%{names}"
api_error: "API error: %%{message}"
invalid_account_names:
one: "Cannot link account with blank name"
other: "Cannot link %%{count} accounts with blank names"
link_failed: Failed to link accounts
no_accounts_selected: Please select at least one account
no_api_key: <%= class_name %> API key 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}"
success:
one: "Successfully linked %%{count} account"
other: "Successfully linked %%{count} accounts"
<%= file_name %>_item:
accounts_need_setup: Accounts need setup
delete: Delete connection
deletion_in_progress: deletion in progress...
error: Error
no_accounts_description: This connection has no linked accounts yet.
no_accounts_title: No accounts
setup_action: Set Up New Accounts
setup_description: "%%{linked} of %%{total} accounts linked. Choose account types for your newly imported <%= class_name %> accounts."
setup_needed: New accounts ready to set up
status: "Synced %%{timestamp} ago"
status_never: Never synced
status_with_summary: "Last synced %%{timestamp} ago - %%{summary}"
syncing: Syncing...
total: Total
unlinked: Unlinked
select_accounts:
accounts_selected: accounts selected
api_error: "API error: %%{message}"
cancel: Cancel
configure_name_in_provider: Cannot import - please configure account name in <%= class_name %>
description: Select the accounts you want to link to your %%{product_name} account.
link_accounts: Link selected accounts
no_accounts_found: No accounts found. Please check your API key configuration.
no_api_key: <%= class_name %> API key is not configured. Please configure it in Settings.
no_credentials_configured: Please configure your <%= class_name %> credentials first in Provider Settings.
no_name_placeholder: "(No name)"
title: Select <%= class_name %> Accounts
select_existing_account:
account_already_linked: This account is already linked to a provider
all_accounts_already_linked: All <%= class_name %> accounts are already linked
api_error: "API error: %%{message}"
cancel: Cancel
configure_name_in_provider: Cannot import - please configure account name in <%= class_name %>
description: Select a <%= class_name %> account to link with this account. Transactions will be synced and deduplicated automatically.
link_account: Link account
no_account_specified: No account specified
no_accounts_found: No <%= class_name %> accounts found. Please check your API key configuration.
no_api_key: <%= class_name %> API key is not configured. Please configure it in Settings.
no_credentials_configured: Please configure your <%= class_name %> credentials first in Provider Settings.
no_name_placeholder: "(No name)"
title: "Link %%{account_name} with <%= class_name %>"
link_existing_account:
account_already_linked: This account is already linked to a provider
api_error: "API error: %%{message}"
invalid_account_name: Cannot link account with blank name
provider_account_already_linked: This <%= class_name %> account is already linked to another account
provider_account_not_found: <%= class_name %> account not found
missing_parameters: Missing required parameters
no_api_key: <%= class_name %> API key not found. Please configure it in Provider Settings.
success: "Successfully linked %%{account_name} with <%= class_name %>"
setup_accounts:
account_type_label: "Account Type:"
all_accounts_linked: "All your <%= class_name %> accounts have already been set up."
api_error: "API error: %%{message}"
fetch_failed: "Failed to Fetch Accounts"
no_accounts_to_setup: "No Accounts to Set Up"
no_api_key: "<%= class_name %> API key is not configured. Please check your connection settings."
account_types:
skip: Skip this account
depository: Checking or Savings Account
credit_card: Credit Card
investment: Investment Account
loan: Loan or Mortgage
other_asset: Other Asset
subtype_labels:
depository: "Account Subtype:"
credit_card: ""
investment: "Investment Type:"
loan: "Loan Type:"
other_asset: ""
subtype_messages:
credit_card: "Credit cards will be automatically set up as credit card accounts."
other_asset: "No additional options needed for Other Assets."
subtypes:
depository:
checking: Checking
savings: Savings
hsa: Health Savings Account
cd: Certificate of Deposit
money_market: Money Market
investment:
brokerage: Brokerage
pension: Pension
retirement: Retirement
"401k": "401(k)"
roth_401k: "Roth 401(k)"
"403b": "403(b)"
tsp: Thrift Savings Plan
"529_plan": "529 Plan"
hsa: Health Savings Account
mutual_fund: Mutual Fund
ira: Traditional IRA
roth_ira: Roth IRA
angel: Angel
loan:
mortgage: Mortgage
student: Student Loan
auto: Auto Loan
other: Other Loan
balance: Balance
cancel: Cancel
choose_account_type: "Choose the correct account type for each <%= class_name %> account:"
create_accounts: Create Accounts
creating_accounts: Creating Accounts...
historical_data_range: "Historical Data Range:"
subtitle: Choose the correct account types for your imported accounts
sync_start_date_help: Select how far back you want to sync transaction history.
sync_start_date_label: "Start syncing transactions from:"
title: Set Up Your <%= class_name %> Accounts
complete_account_setup:
all_skipped: "All accounts were skipped. No accounts were created."
creation_failed: "Failed to create accounts: %%{error}"
no_accounts: "No accounts to set up."
success: "Successfully created %%{count} account(s)."
sync:
success: Sync started
update:
success: <%= class_name %> connection updated