Skip to content

Commit fad241c

Browse files
authored
Fixes & Improvements (#316)
* Some improvements - Fix issue with lunch flow accounts that were imported - Remove the period comparison section from reports * Add cleanup migration * FIX for dynamic config * Fix linter * FIX settings setter Reuse the base class’ atomic setter to leverage its locking and cache invalidation. * Make upsert atomic * Remove migration file Signed-off-by: soky srm <[email protected]> * Delete db/migrate/20251111094448_migrate_dynamic_fields_to_individual_entries.rb Signed-off-by: soky srm <[email protected]> * Fix cache reset * Revert "Remove migration file" This reverts commit 1f2a21e. * Revert "Delete db/migrate/20251111094448_migrate_dynamic_fields_to_individual_entries.rb" This reverts commit 29dcaaa. * Fix Plaid initialiser --------- Signed-off-by: soky srm <[email protected]>
1 parent 7851e96 commit fad241c

File tree

19 files changed

+215
-391
lines changed

19 files changed

+215
-391
lines changed

app/controllers/reports_controller.rb

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,6 @@ def index
2525
# Calculate summary metrics
2626
@summary_metrics = build_summary_metrics
2727

28-
# Build comparison data
29-
@comparison_data = build_comparison_data
30-
3128
# Build trend data (last 6 months)
3229
@trends_data = build_trends_data
3330

@@ -195,25 +192,6 @@ def calculate_budget_performance
195192
nil
196193
end
197194

198-
def build_comparison_data
199-
currency_symbol = Money::Currency.new(Current.family.currency).symbol
200-
201-
# Totals are BigDecimal amounts in dollars - pass directly to Money.new()
202-
{
203-
current: {
204-
income: @current_income_totals.total,
205-
expenses: @current_expense_totals.total,
206-
net: @current_income_totals.total - @current_expense_totals.total
207-
},
208-
previous: {
209-
income: @previous_income_totals.total,
210-
expenses: @previous_expense_totals.total,
211-
net: @previous_income_totals.total - @previous_expense_totals.total
212-
},
213-
currency_symbol: currency_symbol
214-
}
215-
end
216-
217195
def build_trends_data
218196
# Generate month-by-month data based on the current period filter
219197
trends = []

app/controllers/settings/providers_controller.rb

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,6 @@ def update
2828

2929
updated_fields = []
3030

31-
# This hash will store only the updates for dynamic (non-declared) fields
32-
dynamic_updates = {}
33-
3431
# Perform all updates within a transaction for consistency
3532
Setting.transaction do
3633
provider_params.each do |param_key, param_value|
@@ -57,32 +54,13 @@ def update
5754
# This is safe and uses the proper setter.
5855
Setting.public_send("#{key_str}=", value)
5956
else
60-
# If it's a dynamic field, add it to our batch hash
61-
# to avoid the Read-Modify-Write conflict.
62-
dynamic_updates[key_str] = value
57+
# If it's a dynamic field, set it as an individual entry
58+
# Each field is stored independently, preventing race conditions
59+
Setting[key_str] = value
6360
end
6461

6562
updated_fields << param_key
6663
end
67-
68-
# Now, if we have any dynamic updates, apply them all at once
69-
if dynamic_updates.any?
70-
# 1. READ the current hash once
71-
current_dynamic = Setting.dynamic_fields.dup
72-
73-
# 2. MODIFY by merging changes
74-
# Treat nil values as deletions to keep the hash clean
75-
dynamic_updates.each do |key, value|
76-
if value.nil?
77-
current_dynamic.delete(key)
78-
else
79-
current_dynamic[key] = value
80-
end
81-
end
82-
83-
# 3. WRITE the complete, merged hash back once
84-
Setting.dynamic_fields = current_dynamic
85-
end
8664
end
8765

8866
if updated_fields.any?

app/models/lunchflow_item/importer.rb

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,12 @@ def import
2929
accounts_failed = 0
3030

3131
if accounts_data[:accounts].present?
32-
# Get all existing lunchflow account IDs for this item (normalize to strings for comparison)
33-
existing_account_ids = lunchflow_item.lunchflow_accounts.pluck(:account_id).map(&:to_s)
32+
# Get only linked lunchflow account IDs (ones actually imported/used by the user)
33+
# This prevents updating orphaned accounts from old behavior that saved everything
34+
existing_account_ids = lunchflow_item.lunchflow_accounts
35+
.joins(:account_provider)
36+
.pluck(:account_id)
37+
.map(&:to_s)
3438

3539
accounts_data[:accounts].each do |account_data|
3640
account_id = account_data[:id]&.to_s

app/models/provider/configurable.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# Module for providers to declare their configuration requirements
22
#
33
# Providers can declare their own configuration fields without needing to modify
4-
# the Setting model. Settings are stored dynamically using RailsSettings::Base's
5-
# hash-style access (Setting[:key] = value).
4+
# the Setting model. Settings are stored dynamically as individual entries using
5+
# RailsSettings::Base's bracket-style access (Setting[:key] = value).
66
#
77
# Configuration fields are automatically registered and displayed in the UI at
88
# /settings/providers. The system checks Setting storage first, then ENV variables,
@@ -186,8 +186,8 @@ def setting_key
186186

187187
# Get the value for this field (Setting -> ENV -> default)
188188
def value
189-
# First try Setting using dynamic hash-style access
190-
# This works even without explicit field declarations in Setting model
189+
# First try Setting using dynamic bracket-style access
190+
# Each field is stored as an individual entry without explicit field declarations
191191
setting_value = Setting[setting_key]
192192
return normalize_value(setting_value) if setting_value.present?
193193

app/models/setting.rb

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,8 @@ class ValidationError < StandardError; end
1111
field :openai_model, type: :string, default: ENV["OPENAI_MODEL"]
1212
field :brand_fetch_client_id, type: :string, default: ENV["BRAND_FETCH_CLIENT_ID"]
1313

14-
# Single hash field for all dynamic provider credentials and other dynamic settings
15-
# This allows unlimited dynamic fields without declaring them upfront
16-
field :dynamic_fields, type: :hash, default: {}
14+
# Dynamic fields are now stored as individual entries with "dynamic:" prefix
15+
# This prevents race conditions and ensures each field is independently managed
1716

1817
# Onboarding and app settings
1918
ONBOARDING_STATES = %w[open closed invite_only].freeze
@@ -50,16 +49,16 @@ def onboarding_state=(state)
5049
end
5150

5251
# Support dynamic field access via bracket notation
53-
# First checks if it's a declared field, then falls back to dynamic_fields hash
52+
# First checks if it's a declared field, then falls back to individual dynamic entries
5453
def [](key)
5554
key_str = key.to_s
5655

5756
# Check if it's a declared field first
5857
if respond_to?(key_str)
5958
public_send(key_str)
6059
else
61-
# Fall back to dynamic_fields hash
62-
dynamic_fields[key_str]
60+
# Fall back to individual dynamic entry lookup
61+
find_by(var: dynamic_key_name(key_str))&.value
6362
end
6463
end
6564

@@ -70,38 +69,50 @@ def []=(key, value)
7069
if respond_to?("#{key_str}=")
7170
public_send("#{key_str}=", value)
7271
else
73-
# Otherwise, manage in dynamic_fields hash
74-
current_dynamic = dynamic_fields.dup
72+
# Store as individual dynamic entry
73+
dynamic_key = dynamic_key_name(key_str)
7574
if value.nil?
76-
current_dynamic.delete(key_str) # treat nil as delete
75+
where(var: dynamic_key).destroy_all
76+
clear_cache
7777
else
78-
current_dynamic[key_str] = value
78+
# Use upsert for atomic insert/update to avoid race conditions
79+
upsert({ var: dynamic_key, value: value.to_yaml }, unique_by: :var)
80+
clear_cache
7981
end
80-
self.dynamic_fields = current_dynamic # persists & busts cache
8182
end
8283
end
8384

8485
# Check if a dynamic field exists (useful to distinguish nil value vs missing key)
8586
def key?(key)
8687
key_str = key.to_s
87-
respond_to?(key_str) || dynamic_fields.key?(key_str)
88+
return true if respond_to?(key_str)
89+
90+
# Check if dynamic entry exists
91+
where(var: dynamic_key_name(key_str)).exists?
8892
end
8993

9094
# Delete a dynamic field
9195
def delete(key)
9296
key_str = key.to_s
9397
return nil if respond_to?(key_str) # Can't delete declared fields
9498

95-
current_dynamic = dynamic_fields.dup
96-
value = current_dynamic.delete(key_str)
97-
self.dynamic_fields = current_dynamic
99+
dynamic_key = dynamic_key_name(key_str)
100+
value = self[key_str]
101+
where(var: dynamic_key).destroy_all
102+
clear_cache
98103
value
99104
end
100105

101106
# List all dynamic field keys (excludes declared fields)
102107
def dynamic_keys
103-
dynamic_fields.keys
108+
where("var LIKE ?", "dynamic:%").pluck(:var).map { |var| var.sub(/^dynamic:/, "") }
104109
end
110+
111+
private
112+
113+
def dynamic_key_name(key_str)
114+
"dynamic:#{key_str}"
115+
end
105116
end
106117

107118
# Validates OpenAI configuration requires model when custom URI base is set

app/views/lunchflow_items/select_accounts.html.erb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@
1616
<div class="space-y-2">
1717
<% @available_accounts.each do |account| %>
1818
<% has_blank_name = account[:name].blank? %>
19-
<label class="flex items-start gap-3 p-3 border <%= has_blank_name ? 'border-error bg-error/5' : 'border-primary' %> rounded-lg <%= has_blank_name ? 'cursor-not-allowed opacity-60' : 'hover:bg-subtle cursor-pointer' %> transition-colors">
19+
<label class="flex items-start gap-3 p-3 border <%= has_blank_name ? "border-error bg-error/5" : "border-primary" %> rounded-lg <%= has_blank_name ? "cursor-not-allowed opacity-60" : "hover:bg-subtle cursor-pointer" %> transition-colors">
2020
<%= check_box_tag "account_ids[]", account[:id], false, disabled: has_blank_name, class: "mt-1" %>
2121
<div class="flex-1">
22-
<div class="font-medium text-sm <%= has_blank_name ? 'text-error' : 'text-primary' %>">
22+
<div class="font-medium text-sm <%= has_blank_name ? "text-error" : "text-primary" %>">
2323
<% if has_blank_name %>
2424
<%= t(".no_name_placeholder") %>
2525
<% else %>

app/views/lunchflow_items/select_existing_account.html.erb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@
1616
<div class="space-y-2">
1717
<% @available_accounts.each do |account| %>
1818
<% has_blank_name = account[:name].blank? %>
19-
<label class="flex items-start gap-3 p-3 border <%= has_blank_name ? 'border-error bg-error/5' : 'border-primary' %> rounded-lg <%= has_blank_name ? 'cursor-not-allowed opacity-60' : 'hover:bg-subtle cursor-pointer' %> transition-colors">
19+
<label class="flex items-start gap-3 p-3 border <%= has_blank_name ? "border-error bg-error/5" : "border-primary" %> rounded-lg <%= has_blank_name ? "cursor-not-allowed opacity-60" : "hover:bg-subtle cursor-pointer" %> transition-colors">
2020
<%= radio_button_tag "lunchflow_account_id", account[:id], false, disabled: has_blank_name, class: "mt-1" %>
2121
<div class="flex-1">
22-
<div class="font-medium text-sm <%= has_blank_name ? 'text-error' : 'text-primary' %>">
22+
<div class="font-medium text-sm <%= has_blank_name ? "text-error" : "text-primary" %>">
2323
<% if has_blank_name %>
2424
<%= t(".no_name_placeholder") %>
2525
<% else %>

0 commit comments

Comments
 (0)