Skip to content

Commit 48cb669

Browse files
sokiealessiocappa
authored andcommitted
Fixes & Improvements (we-promise#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 0c95448 commit 48cb669

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
@@ -14,9 +14,8 @@ class ValidationError < StandardError; end
1414
field :enable_banking_application_id, type: :string, default: ENV["ENABLE_BANKING_APPLICATION_ID"]
1515
field :enable_banking_certificate, type: :text, default: ENV["ENABLE_BANKING_CERTIFICATE"]
1616

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

2120
# Onboarding and app settings
2221
ONBOARDING_STATES = %w[open closed invite_only].freeze
@@ -53,16 +52,16 @@ def onboarding_state=(state)
5352
end
5453

5554
# Support dynamic field access via bracket notation
56-
# First checks if it's a declared field, then falls back to dynamic_fields hash
55+
# First checks if it's a declared field, then falls back to individual dynamic entries
5756
def [](key)
5857
key_str = key.to_s
5958

6059
# Check if it's a declared field first
6160
if respond_to?(key_str)
6261
public_send(key_str)
6362
else
64-
# Fall back to dynamic_fields hash
65-
dynamic_fields[key_str]
63+
# Fall back to individual dynamic entry lookup
64+
find_by(var: dynamic_key_name(key_str))&.value
6665
end
6766
end
6867

@@ -73,38 +72,50 @@ def []=(key, value)
7372
if respond_to?("#{key_str}=")
7473
public_send("#{key_str}=", value)
7574
else
76-
# Otherwise, manage in dynamic_fields hash
77-
current_dynamic = dynamic_fields.dup
75+
# Store as individual dynamic entry
76+
dynamic_key = dynamic_key_name(key_str)
7877
if value.nil?
79-
current_dynamic.delete(key_str) # treat nil as delete
78+
where(var: dynamic_key).destroy_all
79+
clear_cache
8080
else
81-
current_dynamic[key_str] = value
81+
# Use upsert for atomic insert/update to avoid race conditions
82+
upsert({ var: dynamic_key, value: value.to_yaml }, unique_by: :var)
83+
clear_cache
8284
end
83-
self.dynamic_fields = current_dynamic # persists & busts cache
8485
end
8586
end
8687

8788
# Check if a dynamic field exists (useful to distinguish nil value vs missing key)
8889
def key?(key)
8990
key_str = key.to_s
90-
respond_to?(key_str) || dynamic_fields.key?(key_str)
91+
return true if respond_to?(key_str)
92+
93+
# Check if dynamic entry exists
94+
where(var: dynamic_key_name(key_str)).exists?
9195
end
9296

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

98-
current_dynamic = dynamic_fields.dup
99-
value = current_dynamic.delete(key_str)
100-
self.dynamic_fields = current_dynamic
102+
dynamic_key = dynamic_key_name(key_str)
103+
value = self[key_str]
104+
where(var: dynamic_key).destroy_all
105+
clear_cache
101106
value
102107
end
103108

104109
# List all dynamic field keys (excludes declared fields)
105110
def dynamic_keys
106-
dynamic_fields.keys
111+
where("var LIKE ?", "dynamic:%").pluck(:var).map { |var| var.sub(/^dynamic:/, "") }
107112
end
113+
114+
private
115+
116+
def dynamic_key_name(key_str)
117+
"dynamic:#{key_str}"
118+
end
108119
end
109120

110121
# 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)