Skip to content

Commit 36b1da9

Browse files
authored
Merge branch 'main' into simplefin-enhancements-v2
Signed-off-by: Juan José Mata <[email protected]>
2 parents b8fe20d + fad241c commit 36b1da9

27 files changed

+256
-391
lines changed

.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ OIDC_REDIRECT_URI=
8080
PRODUCT_NAME=
8181
BRAND_NAME=
8282

83+
# PostHog configuration
84+
POSTHOG_KEY=
85+
POSTHOG_HOST=
86+
8387
# Disable enforcing SSL connections
8488
# DISABLE_SSL=true
8589

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ gem "rack-mini-profiler"
4040
gem "sentry-ruby"
4141
gem "sentry-rails"
4242
gem "sentry-sidekiq"
43+
gem "posthog-ruby"
4344
gem "logtail-rails"
4445
gem "skylight", groups: [ :production ]
4546

Gemfile.lock

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,8 @@ GEM
425425
platform_agent (1.0.1)
426426
activesupport (>= 5.2.0)
427427
useragent (~> 0.16.3)
428+
posthog-ruby (3.3.3)
429+
concurrent-ruby (~> 1)
428430
pp (0.6.2)
429431
prettyprint
430432
prettyprint (0.2.0)
@@ -736,6 +738,7 @@ DEPENDENCIES
736738
pagy
737739
pg (~> 1.5)
738740
plaid
741+
posthog-ruby
739742
propshaft
740743
puma (>= 5.0)
741744
rack-attack (~> 6.6)

app/assets/tailwind/maybe-design-system.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,11 @@
367367
}
368368
}
369369
}
370+
371+
textarea.form-field__input {
372+
@apply whitespace-normal overflow-auto;
373+
text-overflow: clip;
374+
}
370375

371376
select.form-field__input {
372377
@apply pr-10 appearance-none;

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
@@ -26,9 +26,6 @@ def update
2626

2727
updated_fields = []
2828

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

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

8664
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/layouts/shared/_head.html.erb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,9 @@
2727
<link rel="apple-touch-icon" href="/logo-pwa.png">
2828
<link rel="apple-touch-icon" sizes="512x512" href="/logo-pwa.png">
2929

30+
<% if Rails.env.production? && (posthog_config = Rails.configuration.x.posthog).try(:api_key).present? %>
31+
<%= render "shared/posthog", posthog_api_key: posthog_config.api_key, posthog_host: posthog_config.host %>
32+
<% end %>
33+
3034
<%= yield :head %>
3135
</head>

0 commit comments

Comments
 (0)