Lunchflow fix (#307)

* Fix lunch flow pre-loading and UX

* Small UX fixes

- Proper closing of modal on cancel
- Preload on new account already

* Review comments

* Fix json error

* Delete .claude/settings.local.json

Signed-off-by: soky srm <sokysrm@gmail.com>

* Lunch Flow brand (again :-)

* FIX process only linked accounts

* FIX disable accounts with no name

* Fix string normalization

---------

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
2025-11-10 21:32:55 +01:00
committed by GitHub
parent eae532714b
commit c6771ebaab
15 changed files with 271 additions and 73 deletions

View File

@@ -37,7 +37,8 @@ class LunchflowItem < ApplicationRecord
return [] if lunchflow_accounts.empty?
results = []
lunchflow_accounts.joins(:account).each do |lunchflow_account|
# Only process accounts that are linked and have active status
lunchflow_accounts.joins(:account).merge(Account.visible).each do |lunchflow_account|
begin
result = LunchflowAccount::Processor.new(lunchflow_account).process
results << { lunchflow_account_id: lunchflow_account.id, success: true, result: result }
@@ -55,7 +56,8 @@ class LunchflowItem < ApplicationRecord
return [] if accounts.empty?
results = []
accounts.each do |account|
# Only schedule syncs for active accounts
accounts.visible.each do |account|
begin
account.sync_later(
parent_sync: parent_sync,

View File

@@ -24,31 +24,39 @@ class LunchflowItem::Importer
# Continue with import even if snapshot storage fails
end
# Step 2: Import accounts
accounts_imported = 0
# Step 2: Update only previously selected accounts (don't create new ones)
accounts_updated = 0
accounts_failed = 0
if accounts_data[:accounts].present?
# Get all existing lunchflow account IDs for this item (normalize to strings for comparison)
existing_account_ids = lunchflow_item.lunchflow_accounts.pluck(:account_id).map(&:to_s)
accounts_data[:accounts].each do |account_data|
account_id = account_data[:id]&.to_s
next unless account_id.present?
# Only update if this account was previously selected (exists in our DB)
next unless existing_account_ids.include?(account_id)
begin
import_account(account_data)
accounts_imported += 1
accounts_updated += 1
rescue => e
accounts_failed += 1
account_id = account_data[:id] || "unknown"
Rails.logger.error "LunchflowItem::Importer - Failed to import account #{account_id}: #{e.message}"
# Continue importing other accounts even if one fails
Rails.logger.error "LunchflowItem::Importer - Failed to update account #{account_id}: #{e.message}"
# Continue updating other accounts even if one fails
end
end
end
Rails.logger.info "LunchflowItem::Importer - Imported #{accounts_imported} accounts (#{accounts_failed} failed)"
Rails.logger.info "LunchflowItem::Importer - Updated #{accounts_updated} accounts (#{accounts_failed} failed)"
# Step 3: Fetch transactions for each account
# Step 3: Fetch transactions only for linked accounts with active status
transactions_imported = 0
transactions_failed = 0
lunchflow_item.lunchflow_accounts.each do |lunchflow_account|
lunchflow_item.lunchflow_accounts.joins(:account).merge(Account.visible).each do |lunchflow_account|
begin
result = fetch_and_store_transactions(lunchflow_account)
if result[:success]
@@ -63,11 +71,11 @@ class LunchflowItem::Importer
end
end
Rails.logger.info "LunchflowItem::Importer - Completed import for item #{lunchflow_item.id}: #{accounts_imported} accounts, #{transactions_imported} transactions"
Rails.logger.info "LunchflowItem::Importer - Completed import for item #{lunchflow_item.id}: #{accounts_updated} accounts updated, #{transactions_imported} transactions"
{
success: accounts_failed == 0 && transactions_failed == 0,
accounts_imported: accounts_imported,
accounts_updated: accounts_updated,
accounts_failed: accounts_failed,
transactions_imported: transactions_imported,
transactions_failed: transactions_failed
@@ -123,16 +131,23 @@ class LunchflowItem::Importer
account_id = account_data[:id]
# Validate required account_id to prevent duplicate creation
# Validate required account_id
if account_id.blank?
Rails.logger.warn "LunchflowItem::Importer - Skipping account with missing ID"
raise ArgumentError, "Account ID is required"
end
lunchflow_account = lunchflow_item.lunchflow_accounts.find_or_initialize_by(
# Only find existing accounts, don't create new ones during sync
lunchflow_account = lunchflow_item.lunchflow_accounts.find_by(
account_id: account_id.to_s
)
# Skip if account wasn't previously selected
unless lunchflow_account
Rails.logger.debug "LunchflowItem::Importer - Skipping unselected account #{account_id}"
return
end
begin
lunchflow_account.upsert_lunchflow_snapshot!(account_data)
lunchflow_account.save!

View File

@@ -13,7 +13,7 @@ class LunchflowItem::Syncer
# Phase 2: Check account setup status and collect sync statistics
sync.update!(status_text: "Checking account configuration...") if sync.respond_to?(:status_text)
total_accounts = lunchflow_item.lunchflow_accounts.count
linked_accounts = lunchflow_item.lunchflow_accounts.joins(:account)
linked_accounts = lunchflow_item.lunchflow_accounts.joins(:account).merge(Account.visible)
unlinked_accounts = lunchflow_item.lunchflow_accounts.includes(:account).where(accounts: { id: nil })
# Store sync statistics for display

View File

@@ -21,10 +21,10 @@ class Provider::Lunchflow
handle_response(response)
rescue SocketError, Net::OpenTimeout, Net::ReadTimeout => e
Rails.logger.error "Lunchflow API: GET /accounts failed: #{e.class}: #{e.message}"
Rails.logger.error "Lunch Flow API: GET /accounts failed: #{e.class}: #{e.message}"
raise LunchflowError.new("Exception during GET request: #{e.message}", :request_failed)
rescue => e
Rails.logger.error "Lunchflow API: Unexpected error during GET /accounts: #{e.class}: #{e.message}"
Rails.logger.error "Lunch Flow API: Unexpected error during GET /accounts: #{e.class}: #{e.message}"
raise LunchflowError.new("Exception during GET request: #{e.message}", :request_failed)
end
@@ -52,10 +52,10 @@ class Provider::Lunchflow
handle_response(response)
rescue SocketError, Net::OpenTimeout, Net::ReadTimeout => e
Rails.logger.error "Lunchflow API: GET #{path} failed: #{e.class}: #{e.message}"
Rails.logger.error "Lunch Flow API: GET #{path} failed: #{e.class}: #{e.message}"
raise LunchflowError.new("Exception during GET request: #{e.message}", :request_failed)
rescue => e
Rails.logger.error "Lunchflow API: Unexpected error during GET #{path}: #{e.class}: #{e.message}"
Rails.logger.error "Lunch Flow API: Unexpected error during GET #{path}: #{e.class}: #{e.message}"
raise LunchflowError.new("Exception during GET request: #{e.message}", :request_failed)
end
@@ -71,10 +71,10 @@ class Provider::Lunchflow
handle_response(response)
rescue SocketError, Net::OpenTimeout, Net::ReadTimeout => e
Rails.logger.error "Lunchflow API: GET #{path} failed: #{e.class}: #{e.message}"
Rails.logger.error "Lunch Flow API: GET #{path} failed: #{e.class}: #{e.message}"
raise LunchflowError.new("Exception during GET request: #{e.message}", :request_failed)
rescue => e
Rails.logger.error "Lunchflow API: Unexpected error during GET #{path}: #{e.class}: #{e.message}"
Rails.logger.error "Lunch Flow API: Unexpected error during GET #{path}: #{e.class}: #{e.message}"
raise LunchflowError.new("Exception during GET request: #{e.message}", :request_failed)
end
@@ -93,7 +93,7 @@ class Provider::Lunchflow
when 200
JSON.parse(response.body, symbolize_names: true)
when 400
Rails.logger.error "Lunchflow API: Bad request - #{response.body}"
Rails.logger.error "Lunch Flow API: Bad request - #{response.body}"
raise LunchflowError.new("Bad request to Lunchflow API: #{response.body}", :bad_request)
when 401
raise LunchflowError.new("Invalid API key", :unauthorized)
@@ -104,7 +104,7 @@ class Provider::Lunchflow
when 429
raise LunchflowError.new("Rate limit exceeded. Please try again later.", :rate_limited)
else
Rails.logger.error "Lunchflow API: Unexpected response - Code: #{response.code}, Body: #{response.body}"
Rails.logger.error "Lunch Flow API: Unexpected response - Code: #{response.code}, Body: #{response.body}"
raise LunchflowError.new("Failed to fetch data: #{response.code} #{response.message} - #{response.body}", :fetch_failed)
end
end

View File

@@ -6,12 +6,12 @@ class Provider::LunchflowAdapter < Provider::Base
# Register this adapter with the factory
Provider::Factory.register("LunchflowAccount", self)
# Configuration for Lunchflow
# Configuration for Lunch Flow
configure do
description <<~DESC
Setup instructions:
1. Visit [Lunchflow](https://www.lunchflow.app) to get your API key
2. Enter your API key below to enable Lunchflow bank data sync
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
@@ -20,21 +20,21 @@ class Provider::LunchflowAdapter < Provider::Base
required: true,
secret: true,
env_key: "LUNCHFLOW_API_KEY",
description: "Your Lunchflow API key for authentication"
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 Lunchflow API"
description: "Base URL for Lunch Flow API"
end
def provider_name
"lunchflow"
end
# Build a Lunchflow provider instance with configured credentials
# Build a Lunch Flow provider instance with configured credentials
# @return [Provider::Lunchflow, nil] Returns nil if API key is not configured
def self.build_provider
api_key = config_value(:api_key)
@@ -46,7 +46,7 @@ class Provider::LunchflowAdapter < Provider::Base
# Reload Lunchflow configuration when settings are updated
def self.reload_configuration
# Lunchflow doesn't need to configure Rails.application.config like Plaid does
# 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
@@ -65,7 +65,7 @@ class Provider::LunchflowAdapter < Provider::Base
end
def institution_domain
# Lunchflow may provide institution metadata in account data
# Lunch Flow may provide institution metadata in account data
metadata = provider_account.institution_metadata
return nil unless metadata.present?
@@ -77,7 +77,7 @@ class Provider::LunchflowAdapter < Provider::Base
begin
domain = URI.parse(url).host&.gsub(/^www\./, "")
rescue URI::InvalidURIError
Rails.logger.warn("Invalid institution URL for Lunchflow account #{provider_account.id}: #{url}")
Rails.logger.warn("Invalid institution URL for Lunch Flow account #{provider_account.id}: #{url}")
end
end