mirror of
https://github.com/we-promise/sure.git
synced 2026-04-07 14:31:25 +00:00
Lunchflow settings family (#363)
* Move provider config to family * remove global settings * Remove turbo auto submit * Fix flash location * Fix mssing syncer for lunchflow * Update schema.rb * FIX tests and encryption config * FIX make rabbit happy * FIX run migration in SQL * FIX turbo frame modal * Branding fixes * FIX rabbit * OCD with product names * More OCD * No other console.log|warn in codebase --------- Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
This commit is contained in:
@@ -12,6 +12,12 @@ class LunchflowItemsController < ApplicationController
|
||||
# Preload Lunchflow accounts in background (async, non-blocking)
|
||||
def preload_accounts
|
||||
begin
|
||||
# Check if family has credentials
|
||||
unless Current.family.has_lunchflow_credentials?
|
||||
render json: { success: false, error: "no_credentials", has_accounts: false }
|
||||
return
|
||||
end
|
||||
|
||||
cache_key = "lunchflow_accounts_#{Current.family.id}"
|
||||
|
||||
# Check if already cached
|
||||
@@ -23,7 +29,7 @@ class LunchflowItemsController < ApplicationController
|
||||
end
|
||||
|
||||
# Fetch from API
|
||||
lunchflow_provider = Provider::LunchflowAdapter.build_provider
|
||||
lunchflow_provider = Provider::LunchflowAdapter.build_provider(family: Current.family)
|
||||
|
||||
unless lunchflow_provider.present?
|
||||
render json: { success: false, error: "no_api_key", has_accounts: false }
|
||||
@@ -39,16 +45,32 @@ class LunchflowItemsController < ApplicationController
|
||||
render json: { success: true, has_accounts: available_accounts.any?, cached: false }
|
||||
rescue Provider::Lunchflow::LunchflowError => e
|
||||
Rails.logger.error("Lunchflow preload error: #{e.message}")
|
||||
render json: { success: false, error: e.message, has_accounts: false }
|
||||
# API error (bad key, network issue, etc) - keep button visible, show error when clicked
|
||||
render json: { success: false, error: "api_error", error_message: e.message, has_accounts: nil }
|
||||
rescue StandardError => e
|
||||
Rails.logger.error("Unexpected error preloading Lunchflow accounts: #{e.class}: #{e.message}")
|
||||
render json: { success: false, error: "unexpected_error", has_accounts: false }
|
||||
# Unexpected error - keep button visible, show error when clicked
|
||||
render json: { success: false, error: "unexpected_error", error_message: e.message, has_accounts: nil }
|
||||
end
|
||||
end
|
||||
|
||||
# Fetch available accounts from Lunchflow API and show selection UI
|
||||
def select_accounts
|
||||
begin
|
||||
# Check if family has Lunchflow credentials configured
|
||||
unless Current.family.has_lunchflow_credentials?
|
||||
if turbo_frame_request?
|
||||
# Render setup modal for turbo frame requests
|
||||
render partial: "lunchflow_items/setup_required", layout: false
|
||||
else
|
||||
# Redirect for regular requests
|
||||
redirect_to settings_providers_path,
|
||||
alert: t(".no_credentials_configured",
|
||||
default: "Please configure your Lunch Flow API key first in Provider Settings.")
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
cache_key = "lunchflow_accounts_#{Current.family.id}"
|
||||
|
||||
# Try to get cached accounts first
|
||||
@@ -56,10 +78,11 @@ class LunchflowItemsController < ApplicationController
|
||||
|
||||
# If not cached, fetch from API
|
||||
if @available_accounts.nil?
|
||||
lunchflow_provider = Provider::LunchflowAdapter.build_provider
|
||||
lunchflow_provider = Provider::LunchflowAdapter.build_provider(family: Current.family)
|
||||
|
||||
unless lunchflow_provider.present?
|
||||
redirect_to new_account_path, alert: t(".no_api_key")
|
||||
redirect_to settings_providers_path, alert: t(".no_api_key",
|
||||
default: "Lunch Flow API key not found. Please configure it in Provider Settings.")
|
||||
return
|
||||
end
|
||||
|
||||
@@ -71,6 +94,13 @@ class LunchflowItemsController < ApplicationController
|
||||
Rails.cache.write(cache_key, @available_accounts, expires_in: 5.minutes)
|
||||
end
|
||||
|
||||
# Filter out already linked accounts
|
||||
lunchflow_item = Current.family.lunchflow_items.first
|
||||
if lunchflow_item
|
||||
linked_account_ids = lunchflow_item.lunchflow_accounts.joins(:account_provider).pluck(:account_id)
|
||||
@available_accounts = @available_accounts.reject { |acc| linked_account_ids.include?(acc[:id].to_s) }
|
||||
end
|
||||
|
||||
@accountable_type = params[:accountable_type] || "Depository"
|
||||
@return_to = safe_return_to_path
|
||||
|
||||
@@ -81,7 +111,19 @@ class LunchflowItemsController < ApplicationController
|
||||
|
||||
render layout: false
|
||||
rescue Provider::Lunchflow::LunchflowError => e
|
||||
redirect_to new_account_path, alert: t(".api_error", message: e.message)
|
||||
Rails.logger.error("Lunch flow API error in select_accounts: #{e.message}")
|
||||
@error_message = e.message
|
||||
@return_path = safe_return_to_path
|
||||
render partial: "lunchflow_items/api_error",
|
||||
locals: { error_message: @error_message, return_path: @return_path },
|
||||
layout: false
|
||||
rescue StandardError => e
|
||||
Rails.logger.error("Unexpected error in select_accounts: #{e.class}: #{e.message}")
|
||||
@error_message = "An unexpected error occurred. Please try again later."
|
||||
@return_path = safe_return_to_path
|
||||
render partial: "lunchflow_items/api_error",
|
||||
locals: { error_message: @error_message, return_path: @return_path },
|
||||
layout: false
|
||||
end
|
||||
end
|
||||
|
||||
@@ -102,7 +144,7 @@ class LunchflowItemsController < ApplicationController
|
||||
)
|
||||
|
||||
# Fetch account details from API
|
||||
lunchflow_provider = Provider::LunchflowAdapter.build_provider
|
||||
lunchflow_provider = Provider::LunchflowAdapter.build_provider(family: Current.family)
|
||||
unless lunchflow_provider.present?
|
||||
redirect_to new_account_path, alert: t(".no_api_key")
|
||||
return
|
||||
@@ -210,6 +252,20 @@ class LunchflowItemsController < ApplicationController
|
||||
return
|
||||
end
|
||||
|
||||
# Check if family has Lunchflow credentials configured
|
||||
unless Current.family.has_lunchflow_credentials?
|
||||
if turbo_frame_request?
|
||||
# Render setup modal for turbo frame requests
|
||||
render partial: "lunchflow_items/setup_required", layout: false
|
||||
else
|
||||
# Redirect for regular requests
|
||||
redirect_to settings_providers_path,
|
||||
alert: t(".no_credentials_configured",
|
||||
default: "Please configure your Lunch Flow API key first in Provider Settings.")
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
begin
|
||||
cache_key = "lunchflow_accounts_#{Current.family.id}"
|
||||
|
||||
@@ -218,10 +274,11 @@ class LunchflowItemsController < ApplicationController
|
||||
|
||||
# If not cached, fetch from API
|
||||
if @available_accounts.nil?
|
||||
lunchflow_provider = Provider::LunchflowAdapter.build_provider
|
||||
lunchflow_provider = Provider::LunchflowAdapter.build_provider(family: Current.family)
|
||||
|
||||
unless lunchflow_provider.present?
|
||||
redirect_to accounts_path, alert: t(".no_api_key")
|
||||
redirect_to settings_providers_path, alert: t(".no_api_key",
|
||||
default: "Lunch Flow API key not found. Please configure it in Provider Settings.")
|
||||
return
|
||||
end
|
||||
|
||||
@@ -254,7 +311,17 @@ class LunchflowItemsController < ApplicationController
|
||||
|
||||
render layout: false
|
||||
rescue Provider::Lunchflow::LunchflowError => e
|
||||
redirect_to accounts_path, alert: t(".api_error", message: e.message)
|
||||
Rails.logger.error("Lunch flow API error in select_existing_account: #{e.message}")
|
||||
@error_message = e.message
|
||||
render partial: "lunchflow_items/api_error",
|
||||
locals: { error_message: @error_message, return_path: accounts_path },
|
||||
layout: false
|
||||
rescue StandardError => e
|
||||
Rails.logger.error("Unexpected error in select_existing_account: #{e.class}: #{e.message}")
|
||||
@error_message = "An unexpected error occurred. Please try again later."
|
||||
render partial: "lunchflow_items/api_error",
|
||||
locals: { error_message: @error_message, return_path: accounts_path },
|
||||
layout: false
|
||||
end
|
||||
end
|
||||
|
||||
@@ -283,7 +350,7 @@ class LunchflowItemsController < ApplicationController
|
||||
)
|
||||
|
||||
# Fetch account details from API
|
||||
lunchflow_provider = Provider::LunchflowAdapter.build_provider
|
||||
lunchflow_provider = Provider::LunchflowAdapter.build_provider(family: Current.family)
|
||||
unless lunchflow_provider.present?
|
||||
redirect_to accounts_path, alert: t(".no_api_key")
|
||||
return
|
||||
@@ -338,16 +405,38 @@ class LunchflowItemsController < ApplicationController
|
||||
|
||||
def create
|
||||
@lunchflow_item = Current.family.lunchflow_items.build(lunchflow_params)
|
||||
@lunchflow_item.name = "Lunch Flow Connection"
|
||||
@lunchflow_item.name ||= "Lunch Flow Connection"
|
||||
|
||||
if @lunchflow_item.save
|
||||
# Trigger initial sync to fetch accounts
|
||||
@lunchflow_item.sync_later
|
||||
|
||||
redirect_to accounts_path, notice: t(".success")
|
||||
if turbo_frame_request?
|
||||
flash.now[:notice] = t(".success")
|
||||
@lunchflow_items = Current.family.lunchflow_items.ordered
|
||||
render turbo_stream: [
|
||||
turbo_stream.replace(
|
||||
"lunchflow-providers-panel",
|
||||
partial: "settings/providers/lunchflow_panel",
|
||||
locals: { lunchflow_items: @lunchflow_items }
|
||||
),
|
||||
*flash_notification_stream_items
|
||||
]
|
||||
else
|
||||
redirect_to accounts_path, notice: t(".success"), status: :see_other
|
||||
end
|
||||
else
|
||||
@error_message = @lunchflow_item.errors.full_messages.join(", ")
|
||||
render :new, status: :unprocessable_entity
|
||||
|
||||
if turbo_frame_request?
|
||||
render turbo_stream: turbo_stream.replace(
|
||||
"lunchflow-providers-panel",
|
||||
partial: "settings/providers/lunchflow_panel",
|
||||
locals: { error_message: @error_message }
|
||||
), status: :unprocessable_entity
|
||||
else
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -356,10 +445,32 @@ class LunchflowItemsController < ApplicationController
|
||||
|
||||
def update
|
||||
if @lunchflow_item.update(lunchflow_params)
|
||||
redirect_to accounts_path, notice: t(".success")
|
||||
if turbo_frame_request?
|
||||
flash.now[:notice] = t(".success")
|
||||
@lunchflow_items = Current.family.lunchflow_items.ordered
|
||||
render turbo_stream: [
|
||||
turbo_stream.replace(
|
||||
"lunchflow-providers-panel",
|
||||
partial: "settings/providers/lunchflow_panel",
|
||||
locals: { lunchflow_items: @lunchflow_items }
|
||||
),
|
||||
*flash_notification_stream_items
|
||||
]
|
||||
else
|
||||
redirect_to accounts_path, notice: t(".success"), status: :see_other
|
||||
end
|
||||
else
|
||||
@error_message = @lunchflow_item.errors.full_messages.join(", ")
|
||||
render :edit, status: :unprocessable_entity
|
||||
|
||||
if turbo_frame_request?
|
||||
render turbo_stream: turbo_stream.replace(
|
||||
"lunchflow-providers-panel",
|
||||
partial: "settings/providers/lunchflow_panel",
|
||||
locals: { error_message: @error_message }
|
||||
), status: :unprocessable_entity
|
||||
else
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -385,7 +496,7 @@ class LunchflowItemsController < ApplicationController
|
||||
end
|
||||
|
||||
def lunchflow_params
|
||||
params.require(:lunchflow_item).permit(:name, :sync_start_date)
|
||||
params.require(:lunchflow_item).permit(:name, :sync_start_date, :api_key, :base_url)
|
||||
end
|
||||
|
||||
# Sanitize return_to parameter to prevent XSS attacks
|
||||
|
||||
@@ -120,11 +120,14 @@ class Settings::ProvidersController < ApplicationController
|
||||
|
||||
# Prepares instance vars needed by the show view and partials
|
||||
def prepare_show_context
|
||||
# Load all provider configurations (exclude SimpleFin, which has its own unified panel below)
|
||||
# Load all provider configurations (exclude SimpleFin and Lunchflow, which have their own family-specific panels below)
|
||||
Provider::Factory.ensure_adapters_loaded
|
||||
@provider_configurations = Provider::ConfigurationRegistry.all.reject { |config| config.provider_key.to_s.casecmp("simplefin").zero? }
|
||||
@provider_configurations = Provider::ConfigurationRegistry.all.reject do |config|
|
||||
config.provider_key.to_s.casecmp("simplefin").zero? || config.provider_key.to_s.casecmp("lunchflow").zero?
|
||||
end
|
||||
|
||||
# Providers page only needs to know whether any SimpleFin connections exist
|
||||
# Providers page only needs to know whether any SimpleFin/Lunchflow connections exist
|
||||
@simplefin_items = Current.family.simplefin_items.ordered.select(:id)
|
||||
@lunchflow_items = Current.family.lunchflow_items.ordered.select(:id)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -93,12 +93,16 @@ class SimplefinItemsController < ApplicationController
|
||||
)
|
||||
|
||||
if turbo_frame_request?
|
||||
flash.now[:notice] = t(".success")
|
||||
@simplefin_items = Current.family.simplefin_items.ordered
|
||||
render turbo_stream: turbo_stream.replace(
|
||||
"simplefin-providers-panel",
|
||||
partial: "settings/providers/simplefin_panel",
|
||||
locals: { simplefin_items: @simplefin_items }
|
||||
)
|
||||
render turbo_stream: [
|
||||
turbo_stream.replace(
|
||||
"simplefin-providers-panel",
|
||||
partial: "settings/providers/simplefin_panel",
|
||||
locals: { simplefin_items: @simplefin_items }
|
||||
),
|
||||
*flash_notification_stream_items
|
||||
]
|
||||
else
|
||||
redirect_to accounts_path, notice: t(".success"), status: :see_other
|
||||
end
|
||||
|
||||
@@ -52,13 +52,23 @@ export default class extends Controller {
|
||||
if (this.hasLinkTarget) {
|
||||
this.hideLoading();
|
||||
}
|
||||
} else if (!data.has_accounts) {
|
||||
// No accounts available, hide the link entirely
|
||||
} else if (data.error === "no_credentials") {
|
||||
// No credentials configured - keep link visible so user can see setup message
|
||||
if (this.hasLinkTarget) {
|
||||
this.hideLoading();
|
||||
}
|
||||
} else if (data.has_accounts === false) {
|
||||
// Credentials configured and API works, but no accounts available - hide the link
|
||||
if (this.hasLinkTarget) {
|
||||
this.linkTarget.style.display = "none";
|
||||
}
|
||||
} else if (data.has_accounts === null || data.error === "api_error" || data.error === "unexpected_error") {
|
||||
// API error (bad credentials, network issue, etc) - keep link visible, user will see error when clicked
|
||||
if (this.hasLinkTarget) {
|
||||
this.hideLoading();
|
||||
}
|
||||
} else {
|
||||
// Error occurred
|
||||
// Other error - keep link visible
|
||||
if (this.hasLinkTarget) {
|
||||
this.hideLoading();
|
||||
}
|
||||
|
||||
@@ -6,7 +6,23 @@ module Family::LunchflowConnectable
|
||||
end
|
||||
|
||||
def can_connect_lunchflow?
|
||||
# Check if the API key is configured
|
||||
Provider::LunchflowAdapter.configured?
|
||||
# Families can now configure their own Lunchflow credentials
|
||||
true
|
||||
end
|
||||
|
||||
def create_lunchflow_item!(api_key:, base_url: nil, item_name: nil)
|
||||
lunchflow_item = lunchflow_items.create!(
|
||||
name: item_name || "Lunch Flow Connection",
|
||||
api_key: api_key,
|
||||
base_url: base_url
|
||||
)
|
||||
|
||||
lunchflow_item.sync_later
|
||||
|
||||
lunchflow_item
|
||||
end
|
||||
|
||||
def has_lunchflow_credentials?
|
||||
lunchflow_items.where.not(api_key: nil).exists?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -26,6 +26,6 @@ class Family::Syncer
|
||||
|
||||
private
|
||||
def child_syncables
|
||||
family.plaid_items + family.simplefin_items.active + family.accounts.manual
|
||||
family.plaid_items + family.simplefin_items.active + family.lunchflow_items.active + family.accounts.manual
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,7 +3,22 @@ class LunchflowItem < ApplicationRecord
|
||||
|
||||
enum :status, { good: "good", requires_update: "requires_update" }, default: :good
|
||||
|
||||
# Helper to detect if ActiveRecord Encryption is configured for this app
|
||||
def self.encryption_ready?
|
||||
creds_ready = Rails.application.credentials.active_record_encryption.present?
|
||||
env_ready = ENV["ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY"].present? &&
|
||||
ENV["ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY"].present? &&
|
||||
ENV["ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT"].present?
|
||||
creds_ready || env_ready
|
||||
end
|
||||
|
||||
# Encrypt sensitive credentials if ActiveRecord encryption is configured (credentials OR env vars)
|
||||
if encryption_ready?
|
||||
encrypts :api_key, deterministic: true
|
||||
end
|
||||
|
||||
validates :name, presence: true
|
||||
validates :api_key, presence: true, on: :create
|
||||
|
||||
belongs_to :family
|
||||
has_one_attached :logo
|
||||
@@ -146,4 +161,12 @@ class LunchflowItem < ApplicationRecord
|
||||
"#{institutions.count} institutions"
|
||||
end
|
||||
end
|
||||
|
||||
def credentials_configured?
|
||||
api_key.present?
|
||||
end
|
||||
|
||||
def effective_base_url
|
||||
base_url.presence || "https://lunchflow.app/api/v1"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -100,10 +100,10 @@ class LunchflowItem::Importer
|
||||
Rails.logger.error "LunchflowItem::Importer - Failed to update item status: #{update_error.message}"
|
||||
end
|
||||
end
|
||||
Rails.logger.error "LunchflowItem::Importer - Lunchflow API error: #{e.message}"
|
||||
Rails.logger.error "LunchflowItem::Importer - Lunch flow API error: #{e.message}"
|
||||
return nil
|
||||
rescue JSON::ParserError => e
|
||||
Rails.logger.error "LunchflowItem::Importer - Failed to parse Lunchflow API response: #{e.message}"
|
||||
Rails.logger.error "LunchflowItem::Importer - Failed to parse Lunch flow API response: #{e.message}"
|
||||
return nil
|
||||
rescue => e
|
||||
Rails.logger.error "LunchflowItem::Importer - Unexpected error fetching accounts: #{e.class} - #{e.message}"
|
||||
|
||||
@@ -2,6 +2,8 @@ module LunchflowItem::Provided
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def lunchflow_provider
|
||||
Provider::LunchflowAdapter.build_provider
|
||||
return nil unless credentials_configured?
|
||||
|
||||
Provider::Lunchflow.new(api_key, base_url: effective_base_url)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,11 +4,22 @@ class PlaidItem < ApplicationRecord
|
||||
enum :plaid_region, { us: "us", eu: "eu" }
|
||||
enum :status, { good: "good", requires_update: "requires_update" }, default: :good
|
||||
|
||||
if Rails.application.credentials.active_record_encryption.present?
|
||||
# Helper to detect if ActiveRecord Encryption is configured for this app
|
||||
def self.encryption_ready?
|
||||
creds_ready = Rails.application.credentials.active_record_encryption.present?
|
||||
env_ready = ENV["ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY"].present? &&
|
||||
ENV["ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY"].present? &&
|
||||
ENV["ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT"].present?
|
||||
creds_ready || env_ready
|
||||
end
|
||||
|
||||
# Encrypt sensitive credentials if ActiveRecord encryption is configured (credentials OR env vars)
|
||||
if encryption_ready?
|
||||
encrypts :access_token, deterministic: true
|
||||
end
|
||||
|
||||
validates :name, :access_token, presence: true
|
||||
validates :name, presence: true
|
||||
validates :access_token, presence: true, on: :create
|
||||
|
||||
before_destroy :remove_plaid_item
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
class Provider::Lunchflow
|
||||
include HTTParty
|
||||
|
||||
headers "User-Agent" => "Sure Finance Lunchflow Client"
|
||||
headers "User-Agent" => "Sure Finance Lunch Flow Client"
|
||||
default_options.merge!(verify: true, ssl_verify_mode: OpenSSL::SSL::VERIFY_PEER, timeout: 120)
|
||||
|
||||
attr_reader :api_key, :base_url
|
||||
@@ -94,7 +94,7 @@ class Provider::Lunchflow
|
||||
JSON.parse(response.body, symbolize_names: true)
|
||||
when 400
|
||||
Rails.logger.error "Lunch Flow API: Bad request - #{response.body}"
|
||||
raise LunchflowError.new("Bad request to Lunchflow API: #{response.body}", :bad_request)
|
||||
raise LunchflowError.new("Bad request to Lunch Flow API: #{response.body}", :bad_request)
|
||||
when 401
|
||||
raise LunchflowError.new("Invalid API key", :unauthorized)
|
||||
when 403
|
||||
|
||||
@@ -1,55 +1,29 @@
|
||||
class Provider::LunchflowAdapter < Provider::Base
|
||||
include Provider::Syncable
|
||||
include Provider::InstitutionMetadata
|
||||
include Provider::Configurable
|
||||
|
||||
# Register this adapter with the factory
|
||||
Provider::Factory.register("LunchflowAccount", self)
|
||||
|
||||
# Configuration for Lunch Flow
|
||||
configure do
|
||||
description <<~DESC
|
||||
Setup instructions:
|
||||
1. Visit [Lunch Flow](https://www.lunchflow.app) to get your API key
|
||||
2. Enter your API key below to enable Lunch Flow bank data sync
|
||||
3. Choose the appropriate environment (production or staging)
|
||||
DESC
|
||||
|
||||
field :api_key,
|
||||
label: "API Key",
|
||||
required: true,
|
||||
secret: true,
|
||||
env_key: "LUNCHFLOW_API_KEY",
|
||||
description: "Your Lunch Flow API key for authentication"
|
||||
|
||||
field :base_url,
|
||||
label: "Base URL",
|
||||
required: false,
|
||||
env_key: "LUNCHFLOW_BASE_URL",
|
||||
default: "https://lunchflow.app/api/v1",
|
||||
description: "Base URL for Lunch Flow API"
|
||||
end
|
||||
|
||||
def provider_name
|
||||
"lunchflow"
|
||||
end
|
||||
|
||||
# Build a Lunch Flow provider instance with configured credentials
|
||||
# Build a Lunch Flow provider instance with family-specific credentials
|
||||
# Lunchflow is now fully per-family - no global credentials supported
|
||||
# @param family [Family] The family to get credentials for (required)
|
||||
# @return [Provider::Lunchflow, nil] Returns nil if API key is not configured
|
||||
def self.build_provider
|
||||
api_key = config_value(:api_key)
|
||||
return nil unless api_key.present?
|
||||
def self.build_provider(family: nil)
|
||||
return nil unless family.present?
|
||||
|
||||
base_url = config_value(:base_url).presence || "https://lunchflow.app/api/v1"
|
||||
Provider::Lunchflow.new(api_key, base_url: base_url)
|
||||
end
|
||||
# Get family-specific credentials
|
||||
lunchflow_item = family.lunchflow_items.where.not(api_key: nil).first
|
||||
return nil unless lunchflow_item&.credentials_configured?
|
||||
|
||||
# Reload Lunchflow configuration when settings are updated
|
||||
def self.reload_configuration
|
||||
# Lunch Flow doesn't need to configure Rails.application.config like Plaid does
|
||||
# The configuration is read dynamically via config_value(:api_key) and config_value(:base_url)
|
||||
# This method exists to be called by the settings controller after updates
|
||||
# No action needed here since values are fetched on-demand
|
||||
Provider::Lunchflow.new(
|
||||
lunchflow_item.api_key,
|
||||
base_url: lunchflow_item.effective_base_url
|
||||
)
|
||||
end
|
||||
|
||||
def sync_path
|
||||
|
||||
@@ -1,27 +1,10 @@
|
||||
class Provider::SimplefinAdapter < Provider::Base
|
||||
include Provider::Syncable
|
||||
include Provider::InstitutionMetadata
|
||||
include Provider::Configurable
|
||||
|
||||
# Register this adapter with the factory
|
||||
Provider::Factory.register("SimplefinAccount", self)
|
||||
|
||||
# Configuration for SimpleFIN
|
||||
configure do
|
||||
description <<~DESC
|
||||
Setup instructions:
|
||||
1. Visit [SimpleFIN Bridge](https://bridge.simplefin.org/simplefin/create) to get a setup token
|
||||
2. This token is optional and only needed if you want to provide a default setup token for users
|
||||
DESC
|
||||
|
||||
field :setup_token,
|
||||
label: "Setup Token",
|
||||
required: false,
|
||||
secret: true,
|
||||
env_key: "SIMPLEFIN_SETUP_TOKEN",
|
||||
description: "Optional: SimpleFIN setup token from your SimpleFIN Bridge account (one-time use)"
|
||||
end
|
||||
|
||||
def provider_name
|
||||
"simplefin"
|
||||
end
|
||||
|
||||
@@ -7,10 +7,6 @@ class SimplefinItem < ApplicationRecord
|
||||
# Virtual attribute for the setup token form field
|
||||
attr_accessor :setup_token
|
||||
|
||||
if Rails.application.credentials.active_record_encryption.present?
|
||||
encrypts :access_url, deterministic: true
|
||||
end
|
||||
|
||||
# Helper to detect if ActiveRecord Encryption is configured for this app
|
||||
def self.encryption_ready?
|
||||
creds_ready = Rails.application.credentials.active_record_encryption.present?
|
||||
@@ -20,7 +16,13 @@ class SimplefinItem < ApplicationRecord
|
||||
creds_ready || env_ready
|
||||
end
|
||||
|
||||
validates :name, :access_url, presence: true
|
||||
# Encrypt sensitive credentials if ActiveRecord encryption is configured (credentials OR env vars)
|
||||
if encryption_ready?
|
||||
encrypts :access_url, deterministic: true
|
||||
end
|
||||
|
||||
validates :name, presence: true
|
||||
validates :access_url, presence: true, on: :create
|
||||
|
||||
before_destroy :remove_simplefin_item
|
||||
|
||||
|
||||
35
app/views/lunchflow_items/_api_error.html.erb
Normal file
35
app/views/lunchflow_items/_api_error.html.erb
Normal file
@@ -0,0 +1,35 @@
|
||||
<%# locals: (error_message:, return_path:) %>
|
||||
<%= turbo_frame_tag "modal" do %>
|
||||
<%= render DS::Dialog.new do |dialog| %>
|
||||
<% dialog.with_header(title: "Lunch Flow Connection Error") %>
|
||||
<% dialog.with_body do %>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-start gap-3">
|
||||
<%= icon("alert-circle", class: "text-destructive w-5 h-5 shrink-0 mt-0.5") %>
|
||||
<div class="text-sm">
|
||||
<p class="font-medium text-primary mb-2">Unable to connect to Lunch Flow</p>
|
||||
<p class="text-secondary"><%= error_message %></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-surface rounded-lg p-4 space-y-2 text-sm">
|
||||
<p class="font-medium text-primary">Common Issues:</p>
|
||||
<ul class="list-disc list-inside space-y-1 text-secondary">
|
||||
<li><strong>Invalid API Key:</strong> Check your API key in Provider Settings</li>
|
||||
<li><strong>Expired Credentials:</strong> Generate a new API key from Lunch Flow</li>
|
||||
<li><strong>Network Issue:</strong> Check your internet connection</li>
|
||||
<li><strong>Service Down:</strong> Lunch Flow API may be temporarily unavailable</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<%= link_to settings_providers_path,
|
||||
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",
|
||||
data: { turbo: false } do %>
|
||||
Check Provider Settings
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
34
app/views/lunchflow_items/_setup_required.html.erb
Normal file
34
app/views/lunchflow_items/_setup_required.html.erb
Normal file
@@ -0,0 +1,34 @@
|
||||
<%= turbo_frame_tag "modal" do %>
|
||||
<%= render DS::Dialog.new do |dialog| %>
|
||||
<% dialog.with_header(title: "Lunch Flow Setup Required") %>
|
||||
<% dialog.with_body do %>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-start gap-3">
|
||||
<%= icon("alert-circle", class: "text-warning w-5 h-5 shrink-0 mt-0.5") %>
|
||||
<div class="text-sm text-secondary">
|
||||
<p class="font-medium text-primary mb-2">API Key Not Configured</p>
|
||||
<p>Before you can link Lunch Flow accounts, you need to configure your Lunch Flow API key.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-surface rounded-lg p-4 space-y-2 text-sm">
|
||||
<p class="font-medium text-primary">Setup Steps:</p>
|
||||
<ol class="list-decimal list-inside space-y-1 text-secondary">
|
||||
<li>Go to <strong>Settings → Bank Sync Providers</strong></li>
|
||||
<li>Find the <strong>Lunch Flow</strong> section</li>
|
||||
<li>Enter your Lunch Flow API key</li>
|
||||
<li>Return here to link your accounts</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<%= link_to settings_providers_path,
|
||||
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",
|
||||
data: { turbo: false } do %>
|
||||
Go to Provider Settings
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
62
app/views/settings/providers/_lunchflow_panel.html.erb
Normal file
62
app/views/settings/providers/_lunchflow_panel.html.erb
Normal file
@@ -0,0 +1,62 @@
|
||||
<div class="space-y-4">
|
||||
<div class="prose prose-sm text-secondary">
|
||||
<p class="text-primary font-medium">Setup instructions:</p>
|
||||
<ol>
|
||||
<li>Visit <a href="https://www.lunchflow.app" target="_blank" rel="noopener noreferrer" class="link">Lunch Flow</a> to get your API key</li>
|
||||
<li>Paste your API key below and click the Save button</li>
|
||||
<li>After a successful connection, go to the Accounts tab to set up new accounts and link them to your existing ones</li>
|
||||
</ol>
|
||||
|
||||
<p class="text-primary font-medium">Field descriptions:</p>
|
||||
<ul>
|
||||
<li><strong>API Key:</strong> Your Lunch Flow API key for authentication (required)</li>
|
||||
<li><strong>Base URL:</strong> Base URL for Lunch Flow API (optional, defaults to https://lunchflow.app/api/v1)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<% error_msg = local_assigns[:error_message] || @error_message %>
|
||||
<% if error_msg.present? %>
|
||||
<div class="p-2 rounded-md bg-destructive/10 text-destructive text-sm">
|
||||
<%= error_msg %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%
|
||||
# Get or initialize a lunchflow_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
|
||||
lunchflow_item = Current.family.lunchflow_items.first_or_initialize(name: "Lunch Flow Connection")
|
||||
is_new_record = lunchflow_item.new_record?
|
||||
%>
|
||||
|
||||
<%= styled_form_with model: lunchflow_item,
|
||||
url: is_new_record ? lunchflow_items_path : lunchflow_item_path(lunchflow_item),
|
||||
scope: :lunchflow_item,
|
||||
method: is_new_record ? :post : :patch,
|
||||
data: { turbo: true },
|
||||
class: "space-y-3" do |form| %>
|
||||
<%= form.text_field :api_key,
|
||||
label: "API Key",
|
||||
placeholder: is_new_record ? "Paste API key here" : "Enter new API key to update",
|
||||
type: :password %>
|
||||
|
||||
<%= form.text_field :base_url,
|
||||
label: "Base URL (Optional)",
|
||||
placeholder: "https://lunchflow.app/api/v1 (default)",
|
||||
value: lunchflow_item.base_url %>
|
||||
|
||||
<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" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% items = local_assigns[:lunchflow_items] || @lunchflow_items || Current.family.lunchflow_items.where.not(api_key: nil) %>
|
||||
<% if items&.any? %>
|
||||
<div class="flex items-center gap-2">
|
||||
<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>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -32,11 +32,7 @@
|
||||
|
||||
<%= styled_form_with model: Setting.new,
|
||||
url: settings_providers_path,
|
||||
method: :patch,
|
||||
data: {
|
||||
controller: "auto-submit-form",
|
||||
"auto-submit-form-trigger-event-value": "blur"
|
||||
} do |form| %>
|
||||
method: :patch do |form| %>
|
||||
<div class="space-y-4">
|
||||
<% configuration.fields.each do |field| %>
|
||||
<%
|
||||
@@ -67,9 +63,13 @@
|
||||
type: input_type,
|
||||
placeholder: field.default || (field.required ? "" : "Optional"),
|
||||
value: display_value,
|
||||
disabled: disabled,
|
||||
data: { "auto-submit-form-target": "auto" } %>
|
||||
disabled: disabled %>
|
||||
<% end %>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<%= form.submit "Save 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" %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
<div class="prose prose-sm text-secondary">
|
||||
<p class="text-primary font-medium">Setup instructions:</p>
|
||||
<ol>
|
||||
<li>Visit <a href="https://beta-bridge.simplefin.org" target="_blank" rel="noopener noreferrer" class="link">SimpleFin Bridge</a> to get your one-time setup token</li>
|
||||
<li>Paste the token below to enable SimpleFin bank data sync</li>
|
||||
<li>Visit <a href="https://beta-bridge.simplefin.org" target="_blank" rel="noopener noreferrer" class="link">SimpleFIN Bridge</a> to get your one-time setup token</li>
|
||||
<li>Paste the token below and click the Save button to enable SimpleFIN bank data sync</li>
|
||||
<li>After a successful connection, go to the Accounts tab to set up new accounts and link them to your existing ones</li>
|
||||
</ol>
|
||||
|
||||
<p class="text-primary font-medium">Field descriptions:</p>
|
||||
<ul>
|
||||
<li><strong>Setup Token:</strong> Your SimpleFin one-time setup token from SimpleFin Bridge (consumed on first use)</li>
|
||||
<li><strong>Setup Token:</strong> Your SimpleFIN one-time setup token from SimpleFIN Bridge (consumed on first use)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -23,13 +23,17 @@
|
||||
url: simplefin_items_path,
|
||||
scope: :simplefin_item,
|
||||
method: :post,
|
||||
data: { controller: "auto-submit", action: "keydown.enter->auto-submit#submit blur->auto-submit#submit", turbo: true },
|
||||
data: { turbo: true },
|
||||
class: "space-y-3" do |form| %>
|
||||
<%= form.text_field :setup_token,
|
||||
label: "Setup Token",
|
||||
placeholder: "Paste SimpleFin setup token and press Enter",
|
||||
type: :password,
|
||||
data: { auto_submit_target: "input" } %>
|
||||
placeholder: "Paste SimpleFIN setup token",
|
||||
type: :password %>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<%= form.submit "Save 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" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if @simplefin_items&.any? %>
|
||||
@@ -38,6 +42,6 @@
|
||||
<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>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="text-sm text-secondary">No SimpleFin connections yet.</div>
|
||||
<div class="text-sm text-secondary">No SimpleFIN connections yet.</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,13 @@
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= settings_section title: "Simplefin" do %>
|
||||
<%= settings_section title: "Lunch Flow" do %>
|
||||
<turbo-frame id="lunchflow-providers-panel">
|
||||
<%= render "settings/providers/lunchflow_panel" %>
|
||||
</turbo-frame>
|
||||
<% end %>
|
||||
|
||||
<%= settings_section title: "SimpleFIN" do %>
|
||||
<turbo-frame id="simplefin-providers-panel">
|
||||
<%= render "settings/providers/simplefin_panel" %>
|
||||
</turbo-frame>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
class AddCredentialsToLunchflowItems < ActiveRecord::Migration[7.2]
|
||||
def change
|
||||
add_column :lunchflow_items, :api_key, :text
|
||||
add_column :lunchflow_items, :base_url, :string
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,41 @@
|
||||
class MigrateGlobalLunchflowCredentialsToFamilies < ActiveRecord::Migration[7.2]
|
||||
def up
|
||||
# Get global Lunchflow credentials from settings table
|
||||
global_api_key = execute(<<~SQL).to_a.first&.dig("value")
|
||||
SELECT value FROM settings WHERE var = 'lunchflow_api_key' LIMIT 1
|
||||
SQL
|
||||
|
||||
global_base_url = execute(<<~SQL).to_a.first&.dig("value")
|
||||
SELECT value FROM settings WHERE var = 'lunchflow_base_url' LIMIT 1
|
||||
SQL
|
||||
|
||||
# Only proceed if global API key exists
|
||||
if global_api_key.present?
|
||||
say "Found global Lunchflow API key, migrating to family-specific configuration..."
|
||||
|
||||
# Update lunchflow_items that don't have credentials yet
|
||||
rows_updated = execute(<<~SQL).cmd_tuples
|
||||
UPDATE lunchflow_items
|
||||
SET api_key = #{connection.quote(global_api_key)},
|
||||
base_url = #{connection.quote(global_base_url)}
|
||||
WHERE api_key IS NULL
|
||||
SQL
|
||||
|
||||
say "Migrated credentials to #{rows_updated} lunchflow_items"
|
||||
|
||||
# Remove global settings as they're no longer used
|
||||
execute("DELETE FROM settings WHERE var = 'lunchflow_api_key'")
|
||||
execute("DELETE FROM settings WHERE var = 'lunchflow_base_url'")
|
||||
|
||||
say "Removed global Lunchflow settings (now per-family)"
|
||||
else
|
||||
say "No global Lunchflow credentials found, skipping migration"
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
# This migration is not reversible because we don't know which families
|
||||
# had credentials before vs. which received them from global settings
|
||||
say "This migration cannot be reversed - credentials are now per-family"
|
||||
end
|
||||
end
|
||||
11
db/schema.rb
generated
11
db/schema.rb
generated
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.2].define(version: 2025_11_15_194500) do
|
||||
ActiveRecord::Schema[7.2].define(version: 2025_11_21_140453) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pgcrypto"
|
||||
enable_extension "plpgsql"
|
||||
@@ -39,7 +39,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_11_15_194500) do
|
||||
t.uuid "accountable_id"
|
||||
t.decimal "balance", precision: 19, scale: 4
|
||||
t.string "currency"
|
||||
t.virtual "classification", type: :string, as: "\nCASE\n WHEN ((accountable_type)::text = ANY ((ARRAY['Loan'::character varying, 'CreditCard'::character varying, 'OtherLiability'::character varying])::text[])) THEN 'liability'::text\n ELSE 'asset'::text\nEND", stored: true
|
||||
t.virtual "classification", type: :string, as: "\nCASE\n WHEN ((accountable_type)::text = ANY (ARRAY[('Loan'::character varying)::text, ('CreditCard'::character varying)::text, ('OtherLiability'::character varying)::text])) THEN 'liability'::text\n ELSE 'asset'::text\nEND", stored: true
|
||||
t.uuid "import_id"
|
||||
t.uuid "plaid_account_id"
|
||||
t.decimal "cash_balance", precision: 19, scale: 4, default: "0.0"
|
||||
@@ -503,6 +503,8 @@ ActiveRecord::Schema[7.2].define(version: 2025_11_15_194500) do
|
||||
t.jsonb "raw_institution_payload"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.text "api_key"
|
||||
t.string "base_url"
|
||||
t.index ["family_id"], name: "index_lunchflow_items_on_family_id"
|
||||
t.index ["status"], name: "index_lunchflow_items_on_status"
|
||||
end
|
||||
@@ -695,7 +697,8 @@ ActiveRecord::Schema[7.2].define(version: 2025_11_15_194500) do
|
||||
t.decimal "expected_amount_min", precision: 19, scale: 4
|
||||
t.decimal "expected_amount_max", precision: 19, scale: 4
|
||||
t.decimal "expected_amount_avg", precision: 19, scale: 4
|
||||
t.index ["family_id", "merchant_id", "amount", "currency"], name: "idx_recurring_txns_on_family_merchant_amount_currency", unique: true
|
||||
t.index ["family_id", "merchant_id", "amount", "currency"], name: "idx_recurring_txns_merchant", unique: true, where: "(merchant_id IS NOT NULL)"
|
||||
t.index ["family_id", "name", "amount", "currency"], name: "idx_recurring_txns_name", unique: true, where: "((name IS NOT NULL) AND (merchant_id IS NULL))"
|
||||
t.index ["family_id", "status"], name: "index_recurring_transactions_on_family_id_and_status"
|
||||
t.index ["family_id"], name: "index_recurring_transactions_on_family_id"
|
||||
t.index ["merchant_id"], name: "index_recurring_transactions_on_merchant_id"
|
||||
@@ -976,10 +979,12 @@ ActiveRecord::Schema[7.2].define(version: 2025_11_15_194500) do
|
||||
t.datetime "set_onboarding_preferences_at"
|
||||
t.datetime "set_onboarding_goals_at"
|
||||
t.string "default_account_order", default: "name_asc"
|
||||
t.jsonb "preferences", default: {}, null: false
|
||||
t.index ["email"], name: "index_users_on_email", unique: true
|
||||
t.index ["family_id"], name: "index_users_on_family_id"
|
||||
t.index ["last_viewed_chat_id"], name: "index_users_on_last_viewed_chat_id"
|
||||
t.index ["otp_secret"], name: "index_users_on_otp_secret", unique: true, where: "(otp_secret IS NOT NULL)"
|
||||
t.index ["preferences"], name: "index_users_on_preferences", using: :gin
|
||||
end
|
||||
|
||||
create_table "valuations", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
|
||||
@@ -170,4 +170,80 @@ namespace :data_migration do
|
||||
|
||||
puts "✅ Balance component migration complete."
|
||||
end
|
||||
|
||||
desc "Migrate global provider settings to family-specific"
|
||||
# 2025-11-21: Move global Lunchflow API credentials to family-specific lunchflow_items
|
||||
# Global settings are NO LONGER SUPPORTED as of this migration.
|
||||
# This improves security and enables proper multi-tenant isolation where each family
|
||||
# can have their own Lunchflow credentials instead of sharing global ones.
|
||||
task migrate_provider_settings_to_family: :environment do
|
||||
puts "==> Migrating global provider settings to family-specific..."
|
||||
puts "NOTE: Global Lunch flow/SimpleFIN credentials are NO LONGER SUPPORTED after this migration."
|
||||
puts
|
||||
|
||||
# Check if global Lunchflow API key exists
|
||||
global_api_key = Setting[:lunchflow_api_key]
|
||||
global_base_url = Setting[:lunchflow_base_url]
|
||||
|
||||
if global_api_key.blank?
|
||||
puts "No global Lunchflow API key found. Nothing to migrate."
|
||||
puts
|
||||
puts "ℹ️ If you need to configure Lunchflow:"
|
||||
puts " 1. Go to /settings/providers"
|
||||
puts " 2. Configure Lunchflow credentials per-family"
|
||||
puts
|
||||
puts "✅ Migration complete."
|
||||
return
|
||||
end
|
||||
|
||||
puts "Found global Lunchflow API key. Migrating to family-specific settings..."
|
||||
|
||||
families_updated = 0
|
||||
families_with_existing = 0
|
||||
families_with_items = 0
|
||||
|
||||
Family.find_each do |family|
|
||||
# Check if this family has any lunchflow_items
|
||||
has_lunchflow_items = family.lunchflow_items.exists?
|
||||
|
||||
if has_lunchflow_items
|
||||
families_with_items += 1
|
||||
|
||||
# Check if any of the family's lunchflow_items already have credentials
|
||||
has_credentials = family.lunchflow_items.where.not(api_key: nil).exists?
|
||||
|
||||
if has_credentials
|
||||
families_with_existing += 1
|
||||
puts " Family #{family.id} (#{family.name}): Already has credentials, skipping"
|
||||
else
|
||||
# Assign global credentials to the first lunchflow_item
|
||||
lunchflow_item = family.lunchflow_items.first
|
||||
lunchflow_item.update!(
|
||||
api_key: global_api_key,
|
||||
base_url: global_base_url
|
||||
)
|
||||
families_updated += 1
|
||||
puts " Family #{family.id} (#{family.name}): Migrated credentials to lunchflow_item #{lunchflow_item.id}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
puts
|
||||
puts "Migration Summary:"
|
||||
puts " Families with Lunchflow items: #{families_with_items}"
|
||||
puts " Families with existing credentials: #{families_with_existing}"
|
||||
puts " Families updated with global credentials: #{families_updated}"
|
||||
puts
|
||||
|
||||
if families_updated > 0
|
||||
puts "✅ Global credentials have been copied to #{families_updated} families."
|
||||
puts
|
||||
puts "⚠️ IMPORTANT: You should now remove the global settings:"
|
||||
puts " rails runner \"Setting[:lunchflow_api_key] = nil; Setting[:lunchflow_base_url] = nil\""
|
||||
puts
|
||||
puts " Global credentials are NO LONGER USED by the application."
|
||||
end
|
||||
|
||||
puts "✅ Provider settings migration complete."
|
||||
end
|
||||
end
|
||||
|
||||
@@ -87,15 +87,13 @@ class Settings::ProvidersControllerTest < ActionDispatch::IntegrationTest
|
||||
Setting["plaid_secret"] = nil
|
||||
Setting["plaid_eu_client_id"] = nil
|
||||
Setting["plaid_eu_secret"] = nil
|
||||
Setting["simplefin_setup_token"] = nil
|
||||
|
||||
patch settings_providers_url, params: {
|
||||
setting: {
|
||||
plaid_client_id: "plaid_client",
|
||||
plaid_secret: "plaid_secret",
|
||||
plaid_eu_client_id: "plaid_eu_client",
|
||||
plaid_eu_secret: "plaid_eu_secret",
|
||||
simplefin_setup_token: "simplefin_token"
|
||||
plaid_eu_secret: "plaid_eu_secret"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +104,6 @@ class Settings::ProvidersControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_equal "plaid_secret", Setting["plaid_secret"]
|
||||
assert_equal "plaid_eu_client", Setting["plaid_eu_client_id"]
|
||||
assert_equal "plaid_eu_secret", Setting["plaid_eu_secret"]
|
||||
assert_equal "simplefin_token", Setting["simplefin_setup_token"]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -202,10 +199,10 @@ class Settings::ProvidersControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_equal "client_id_1", Setting["plaid_client_id"]
|
||||
assert_equal "secret_1", Setting["plaid_secret"]
|
||||
|
||||
# Simulate second request updating simplefin fields
|
||||
# Simulate second request updating different plaid fields
|
||||
patch settings_providers_url, params: {
|
||||
setting: {
|
||||
simplefin_setup_token: "token_1"
|
||||
plaid_environment: "production"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,7 +210,7 @@ class Settings::ProvidersControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_equal "existing_value", Setting["existing_field"]
|
||||
assert_equal "client_id_1", Setting["plaid_client_id"]
|
||||
assert_equal "secret_1", Setting["plaid_secret"]
|
||||
assert_equal "token_1", Setting["simplefin_setup_token"]
|
||||
assert_equal "production", Setting["plaid_environment"]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -252,14 +249,14 @@ class Settings::ProvidersControllerTest < ActionDispatch::IntegrationTest
|
||||
|
||||
test "reloads configuration for multiple providers when updated" do
|
||||
with_self_hosting do
|
||||
# Both providers should have their configuration reloaded
|
||||
# Both Plaid providers (US and EU) should have their configuration reloaded
|
||||
Provider::PlaidAdapter.expects(:reload_configuration).once
|
||||
Provider::SimplefinAdapter.expects(:reload_configuration).once
|
||||
Provider::PlaidEuAdapter.expects(:reload_configuration).once
|
||||
|
||||
patch settings_providers_url, params: {
|
||||
setting: {
|
||||
plaid_client_id: "plaid_client",
|
||||
simplefin_setup_token: "simplefin_token"
|
||||
plaid_eu_client_id: "plaid_eu_client"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user