Files
sure/app/controllers/lunchflow_items_controller.rb
Claude ddab962d76 Fetch transaction counts during provider setup
Add transaction count validation for all banking providers (SimpleFIN,
Lunch Flow, and Enable Banking) during the account setup process.

This change fetches transaction data for each bank account immediately
after provider credentials are configured, allowing users to see
warnings about accounts with no transaction history before completing
the setup.

Key changes:
- SimpleFIN: Fetch accounts and check transaction counts after token setup
- Lunch Flow: Check transaction availability after API key configuration
- Enable Banking: Validate transaction data after OAuth authorization
- Display warning messages in provider panels when issues are detected
- Warnings show accounts with 0 transactions in the last 90 days

The warnings appear in the /settings/providers screen before the
"Configured and ready to use" message, giving users early visibility
into potential data availability issues.
2025-11-30 21:27:32 +00:00

816 lines
29 KiB
Ruby

class LunchflowItemsController < ApplicationController
before_action :set_lunchflow_item, only: [ :show, :edit, :update, :destroy, :sync, :setup_accounts, :complete_account_setup ]
def index
@lunchflow_items = Current.family.lunchflow_items.active.ordered
render layout: "settings"
end
def show
end
# 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
cached_accounts = Rails.cache.read(cache_key)
if cached_accounts.present?
render json: { success: true, has_accounts: cached_accounts.any?, cached: true }
return
end
# Fetch from API
lunchflow_provider = Provider::LunchflowAdapter.build_provider(family: Current.family)
unless lunchflow_provider.present?
render json: { success: false, error: "no_api_key", has_accounts: false }
return
end
accounts_data = lunchflow_provider.get_accounts
available_accounts = accounts_data[:accounts] || []
# Cache the accounts for 5 minutes
Rails.cache.write(cache_key, available_accounts, expires_in: 5.minutes)
render json: { success: true, has_accounts: available_accounts.any?, cached: false }
rescue Provider::Lunchflow::LunchflowError => e
Rails.logger.error("Lunchflow preload error: #{e.message}")
# 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}")
# 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
@available_accounts = Rails.cache.read(cache_key)
# If not cached, fetch from API
if @available_accounts.nil?
lunchflow_provider = Provider::LunchflowAdapter.build_provider(family: Current.family)
unless lunchflow_provider.present?
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
accounts_data = lunchflow_provider.get_accounts
@available_accounts = accounts_data[:accounts] || []
# Cache the accounts for 5 minutes
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
if @available_accounts.empty?
redirect_to new_account_path, alert: t(".no_accounts_found")
return
end
render layout: false
rescue Provider::Lunchflow::LunchflowError => e
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
# Create accounts from selected Lunchflow accounts
def link_accounts
selected_account_ids = params[:account_ids] || []
accountable_type = params[:accountable_type] || "Depository"
return_to = safe_return_to_path
if selected_account_ids.empty?
redirect_to new_account_path, alert: t(".no_accounts_selected")
return
end
# Create or find lunchflow_item for this family
lunchflow_item = Current.family.lunchflow_items.first_or_create!(
name: "Lunch Flow Connection"
)
# Fetch account details from API
lunchflow_provider = Provider::LunchflowAdapter.build_provider(family: Current.family)
unless lunchflow_provider.present?
redirect_to new_account_path, alert: t(".no_api_key")
return
end
accounts_data = lunchflow_provider.get_accounts
created_accounts = []
already_linked_accounts = []
invalid_accounts = []
selected_account_ids.each do |account_id|
# Find the account data from API response
account_data = accounts_data[:accounts].find { |acc| acc[:id].to_s == account_id.to_s }
next unless account_data
# Validate account name is not blank (required by Account model)
if account_data[:name].blank?
invalid_accounts << account_id
Rails.logger.warn "LunchflowItemsController - Skipping account #{account_id} with blank name"
next
end
# Create or find lunchflow_account
lunchflow_account = lunchflow_item.lunchflow_accounts.find_or_initialize_by(
account_id: account_id.to_s
)
lunchflow_account.upsert_lunchflow_snapshot!(account_data)
lunchflow_account.save!
# Check if this lunchflow_account is already linked
if lunchflow_account.account_provider.present?
already_linked_accounts << account_data[:name]
next
end
# Create the internal Account with proper balance initialization
account = Account.create_and_sync(
family: Current.family,
name: account_data[:name],
balance: 0, # Initial balance will be set during sync
currency: account_data[:currency] || "USD",
accountable_type: accountable_type,
accountable_attributes: {}
)
# Link account to lunchflow_account via account_providers join table
AccountProvider.create!(
account: account,
provider: lunchflow_account
)
created_accounts << account
end
# Trigger sync to fetch transactions if any accounts were created
lunchflow_item.sync_later if created_accounts.any?
# Build appropriate flash message
if invalid_accounts.any? && created_accounts.empty? && already_linked_accounts.empty?
# All selected accounts were invalid (blank names)
redirect_to new_account_path, alert: t(".invalid_account_names", count: invalid_accounts.count)
elsif invalid_accounts.any? && (created_accounts.any? || already_linked_accounts.any?)
# Some accounts were created/already linked, but some had invalid names
redirect_to return_to || accounts_path,
alert: t(".partial_invalid",
created_count: created_accounts.count,
already_linked_count: already_linked_accounts.count,
invalid_count: invalid_accounts.count)
elsif created_accounts.any? && already_linked_accounts.any?
redirect_to return_to || accounts_path,
notice: t(".partial_success",
created_count: created_accounts.count,
already_linked_count: already_linked_accounts.count,
already_linked_names: already_linked_accounts.join(", "))
elsif created_accounts.any?
redirect_to return_to || accounts_path,
notice: t(".success", count: created_accounts.count)
elsif already_linked_accounts.any?
redirect_to return_to || accounts_path,
alert: t(".all_already_linked",
count: already_linked_accounts.count,
names: already_linked_accounts.join(", "))
else
redirect_to new_account_path, alert: t(".link_failed")
end
rescue Provider::Lunchflow::LunchflowError => e
redirect_to new_account_path, alert: t(".api_error", message: e.message)
end
# Fetch available Lunchflow accounts to link with an existing account
def select_existing_account
account_id = params[:account_id]
unless account_id.present?
redirect_to accounts_path, alert: t(".no_account_specified")
return
end
@account = Current.family.accounts.find(account_id)
# Check if account is already linked
if @account.account_providers.exists?
redirect_to accounts_path, alert: t(".account_already_linked")
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}"
# Try to get cached accounts first
@available_accounts = Rails.cache.read(cache_key)
# If not cached, fetch from API
if @available_accounts.nil?
lunchflow_provider = Provider::LunchflowAdapter.build_provider(family: Current.family)
unless lunchflow_provider.present?
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
accounts_data = lunchflow_provider.get_accounts
@available_accounts = accounts_data[:accounts] || []
# Cache the accounts for 5 minutes
Rails.cache.write(cache_key, @available_accounts, expires_in: 5.minutes)
end
if @available_accounts.empty?
redirect_to accounts_path, alert: t(".no_accounts_found")
return
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
if @available_accounts.empty?
redirect_to accounts_path, alert: t(".all_accounts_already_linked")
return
end
@return_to = safe_return_to_path
render layout: false
rescue Provider::Lunchflow::LunchflowError => e
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
# Link a selected Lunchflow account to an existing account
def link_existing_account
account_id = params[:account_id]
lunchflow_account_id = params[:lunchflow_account_id]
return_to = safe_return_to_path
unless account_id.present? && lunchflow_account_id.present?
redirect_to accounts_path, alert: t(".missing_parameters")
return
end
@account = Current.family.accounts.find(account_id)
# Check if account is already linked
if @account.account_providers.exists?
redirect_to accounts_path, alert: t(".account_already_linked")
return
end
# Create or find lunchflow_item for this family
lunchflow_item = Current.family.lunchflow_items.first_or_create!(
name: "Lunch Flow Connection"
)
# Fetch account details from API
lunchflow_provider = Provider::LunchflowAdapter.build_provider(family: Current.family)
unless lunchflow_provider.present?
redirect_to accounts_path, alert: t(".no_api_key")
return
end
accounts_data = lunchflow_provider.get_accounts
# Find the selected Lunchflow account data
account_data = accounts_data[:accounts].find { |acc| acc[:id].to_s == lunchflow_account_id.to_s }
unless account_data
redirect_to accounts_path, alert: t(".lunchflow_account_not_found")
return
end
# Validate account name is not blank (required by Account model)
if account_data[:name].blank?
redirect_to accounts_path, alert: t(".invalid_account_name")
return
end
# Create or find lunchflow_account
lunchflow_account = lunchflow_item.lunchflow_accounts.find_or_initialize_by(
account_id: lunchflow_account_id.to_s
)
lunchflow_account.upsert_lunchflow_snapshot!(account_data)
lunchflow_account.save!
# Check if this lunchflow_account is already linked to another account
if lunchflow_account.account_provider.present?
redirect_to accounts_path, alert: t(".lunchflow_account_already_linked")
return
end
# Link account to lunchflow_account via account_providers join table
AccountProvider.create!(
account: @account,
provider: lunchflow_account
)
# Trigger sync to fetch transactions
lunchflow_item.sync_later
redirect_to return_to || accounts_path,
notice: t(".success", account_name: @account.name)
rescue Provider::Lunchflow::LunchflowError => e
redirect_to accounts_path, alert: t(".api_error", message: e.message)
end
def new
@lunchflow_item = Current.family.lunchflow_items.build
end
def create
@lunchflow_item = Current.family.lunchflow_items.build(lunchflow_params)
@lunchflow_item.name ||= "Lunch Flow Connection"
if @lunchflow_item.save
# Trigger initial sync to fetch accounts
@lunchflow_item.sync_later
# Fetch transaction counts for validation
@transaction_warnings = fetch_transaction_counts(@lunchflow_item)
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, transaction_warnings: @transaction_warnings }
),
*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(", ")
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
def edit
end
def update
if @lunchflow_item.update(lunchflow_params)
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(", ")
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
def destroy
# Ensure we detach provider links before scheduling deletion
begin
@lunchflow_item.unlink_all!(dry_run: false)
rescue => e
Rails.logger.warn("LunchFlow unlink during destroy failed: #{e.class} - #{e.message}")
end
@lunchflow_item.destroy_later
redirect_to accounts_path, notice: t(".success")
end
def sync
unless @lunchflow_item.syncing?
@lunchflow_item.sync_later
end
respond_to do |format|
format.html { redirect_back_or_to accounts_path }
format.json { head :ok }
end
end
# Show unlinked Lunchflow accounts for setup (similar to SimpleFIN setup_accounts)
def setup_accounts
# First, ensure we have the latest accounts from the API
@api_error = fetch_lunchflow_accounts_from_api
# Get Lunchflow accounts that are not linked (no AccountProvider)
@lunchflow_accounts = @lunchflow_item.lunchflow_accounts
.left_joins(:account_provider)
.where(account_providers: { id: nil })
# Get supported account types from the adapter
supported_types = Provider::LunchflowAdapter.supported_account_types
# Map of account type keys to their internal values
account_type_keys = {
"depository" => "Depository",
"credit_card" => "CreditCard",
"investment" => "Investment",
"loan" => "Loan",
"other_asset" => "OtherAsset"
}
# Build account type options using i18n, filtering to supported types
all_account_type_options = account_type_keys.filter_map do |key, type|
next unless supported_types.include?(type)
[ t(".account_types.#{key}"), type ]
end
# Add "Skip" option at the beginning
@account_type_options = [ [ t(".account_types.skip"), "skip" ] ] + all_account_type_options
# Helper to translate subtype options
translate_subtypes = ->(type_key, subtypes_hash) {
subtypes_hash.keys.map { |k| [ t(".subtypes.#{type_key}.#{k}"), k ] }
}
# Subtype options for each account type (only include supported types)
all_subtype_options = {
"Depository" => {
label: t(".subtype_labels.depository"),
options: translate_subtypes.call("depository", Depository::SUBTYPES)
},
"CreditCard" => {
label: t(".subtype_labels.credit_card"),
options: [],
message: t(".subtype_messages.credit_card")
},
"Investment" => {
label: t(".subtype_labels.investment"),
options: translate_subtypes.call("investment", Investment::SUBTYPES)
},
"Loan" => {
label: t(".subtype_labels.loan"),
options: translate_subtypes.call("loan", Loan::SUBTYPES)
},
"OtherAsset" => {
label: t(".subtype_labels.other_asset").presence,
options: [],
message: t(".subtype_messages.other_asset")
}
}
@subtype_options = all_subtype_options.slice(*supported_types)
end
def complete_account_setup
account_types = params[:account_types] || {}
account_subtypes = params[:account_subtypes] || {}
# Valid account types for this provider
valid_types = Provider::LunchflowAdapter.supported_account_types
created_accounts = []
skipped_count = 0
begin
ActiveRecord::Base.transaction do
account_types.each do |lunchflow_account_id, selected_type|
# Skip accounts marked as "skip"
if selected_type == "skip" || selected_type.blank?
skipped_count += 1
next
end
# Validate account type is supported
unless valid_types.include?(selected_type)
Rails.logger.warn("Invalid account type '#{selected_type}' submitted for LunchFlow account #{lunchflow_account_id}")
next
end
# Find account - scoped to this item to prevent cross-item manipulation
lunchflow_account = @lunchflow_item.lunchflow_accounts.find_by(id: lunchflow_account_id)
unless lunchflow_account
Rails.logger.warn("LunchFlow account #{lunchflow_account_id} not found for item #{@lunchflow_item.id}")
next
end
# Skip if already linked (race condition protection)
if lunchflow_account.account_provider.present?
Rails.logger.info("LunchFlow account #{lunchflow_account_id} already linked, skipping")
next
end
selected_subtype = account_subtypes[lunchflow_account_id]
# Default subtype for CreditCard since it only has one option
selected_subtype = "credit_card" if selected_type == "CreditCard" && selected_subtype.blank?
# Create account with user-selected type and subtype (raises on failure)
account = Account.create_and_sync(
family: Current.family,
name: lunchflow_account.name,
balance: lunchflow_account.current_balance || 0,
currency: lunchflow_account.currency || "USD",
accountable_type: selected_type,
accountable_attributes: selected_subtype.present? ? { subtype: selected_subtype } : {}
)
# Link account to lunchflow_account via account_providers join table (raises on failure)
AccountProvider.create!(
account: account,
provider: lunchflow_account
)
created_accounts << account
end
end
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e
Rails.logger.error("LunchFlow account setup failed: #{e.class} - #{e.message}")
Rails.logger.error(e.backtrace.first(10).join("\n"))
flash[:alert] = t(".creation_failed", error: e.message)
redirect_to accounts_path, status: :see_other
return
rescue StandardError => e
Rails.logger.error("LunchFlow account setup failed unexpectedly: #{e.class} - #{e.message}")
Rails.logger.error(e.backtrace.first(10).join("\n"))
flash[:alert] = t(".creation_failed", error: "An unexpected error occurred")
redirect_to accounts_path, status: :see_other
return
end
# Trigger a sync to process transactions
@lunchflow_item.sync_later if created_accounts.any?
# Set appropriate flash message
if created_accounts.any?
flash[:notice] = t(".success", count: created_accounts.count)
elsif skipped_count > 0
flash[:notice] = t(".all_skipped")
else
flash[:notice] = t(".no_accounts")
end
if turbo_frame_request?
# Recompute data needed by Accounts#index partials
@manual_accounts = Account.uncached {
Current.family.accounts
.visible_manual
.order(:name)
.to_a
}
@lunchflow_items = Current.family.lunchflow_items.ordered
manual_accounts_stream = if @manual_accounts.any?
turbo_stream.update(
"manual-accounts",
partial: "accounts/index/manual_accounts",
locals: { accounts: @manual_accounts }
)
else
turbo_stream.replace("manual-accounts", view_context.tag.div(id: "manual-accounts"))
end
render turbo_stream: [
manual_accounts_stream,
turbo_stream.replace(
ActionView::RecordIdentifier.dom_id(@lunchflow_item),
partial: "lunchflow_items/lunchflow_item",
locals: { lunchflow_item: @lunchflow_item }
)
] + Array(flash_notification_stream_items)
else
redirect_to accounts_path, status: :see_other
end
end
private
# Fetch Lunchflow accounts from the API and store them locally
# Returns nil on success, or an error message string on failure
def fetch_lunchflow_accounts_from_api
# Skip if we already have accounts cached
return nil unless @lunchflow_item.lunchflow_accounts.empty?
# Validate API key is configured
unless @lunchflow_item.credentials_configured?
return t("lunchflow_items.setup_accounts.no_api_key")
end
# Use the specific lunchflow_item's provider (scoped to this family's item)
lunchflow_provider = @lunchflow_item.lunchflow_provider
unless lunchflow_provider.present?
return t("lunchflow_items.setup_accounts.no_api_key")
end
begin
accounts_data = lunchflow_provider.get_accounts
available_accounts = accounts_data[:accounts] || []
if available_accounts.empty?
Rails.logger.info("LunchFlow API returned no accounts for item #{@lunchflow_item.id}")
return nil
end
available_accounts.each do |account_data|
next if account_data[:name].blank?
lunchflow_account = @lunchflow_item.lunchflow_accounts.find_or_initialize_by(
account_id: account_data[:id].to_s
)
lunchflow_account.upsert_lunchflow_snapshot!(account_data)
lunchflow_account.save!
end
nil # Success
rescue Provider::Lunchflow::LunchflowError => e
Rails.logger.error("LunchFlow API error: #{e.message}")
t("lunchflow_items.setup_accounts.api_error", message: e.message)
rescue StandardError => e
Rails.logger.error("Unexpected error fetching LunchFlow accounts: #{e.class}: #{e.message}")
t("lunchflow_items.setup_accounts.api_error", message: e.message)
end
end
def set_lunchflow_item
@lunchflow_item = Current.family.lunchflow_items.find(params[:id])
end
def lunchflow_params
params.require(:lunchflow_item).permit(:name, :sync_start_date, :api_key, :base_url)
end
# Fetch transaction counts for all accounts in the Lunchflow item
# Returns an array of warning messages if any accounts have issues
def fetch_transaction_counts(lunchflow_item)
warnings = []
begin
provider = lunchflow_item.lunchflow_provider
return warnings unless provider
accounts_data = provider.get_accounts
accounts = accounts_data[:accounts] || []
if accounts.empty?
warnings << "No bank accounts found. Please check your Lunch Flow configuration."
else
# Check transaction counts for each account (last 90 days)
accounts.each do |account_data|
account_name = account_data[:name] || "Unknown Account"
account_id = account_data[:id]
begin
transactions_data = provider.get_account_transactions(
account_id,
start_date: 90.days.ago,
end_date: Date.today
)
transactions = transactions_data[:transactions] || []
if transactions.empty?
warnings << "Account '#{account_name}' has 0 transactions available in the last 90 days."
end
rescue Provider::Lunchflow::LunchflowError => e
Rails.logger.warn("Lunchflow transaction count check failed for account #{account_id}: #{e.message}")
warnings << "Unable to fetch transactions for '#{account_name}': #{e.message}"
end
end
end
rescue Provider::Lunchflow::LunchflowError => e
Rails.logger.warn("Lunchflow accounts fetch failed: #{e.message}")
warnings << "Unable to fetch account information: #{e.message}"
rescue => e
Rails.logger.warn("Unexpected error checking Lunchflow transactions: #{e.message}")
warnings << "Unable to verify transaction availability."
end
warnings
end
# Sanitize return_to parameter to prevent XSS attacks
# Only allow internal paths, reject external URLs and javascript: URIs
def safe_return_to_path
return nil if params[:return_to].blank?
return_to = params[:return_to].to_s
# Parse the URL to check if it's external
begin
uri = URI.parse(return_to)
# Reject absolute URLs with schemes (http:, https:, javascript:, etc.)
# Only allow relative paths
return nil if uri.scheme.present?
# Ensure the path starts with / (is a relative path)
return nil unless return_to.start_with?("/")
return_to
rescue URI::InvalidURIError
# If the URI is invalid, reject it
nil
end
end
end