Skip to content
This repository was archived by the owner on Jul 27, 2025. It is now read-only.

Commit fe199f2

Browse files
authored
Add account data enrichment (#1532)
* Add data enrichment * Make data enrichment optional for self-hosters * Add categories to data enrichment * Only update category and merchant if nil * Fix name overrides * Lint fixes
1 parent bac2e64 commit fe199f2

File tree

16 files changed

+182
-10
lines changed

16 files changed

+182
-10
lines changed

app/controllers/settings/hostings_controller.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ def update
2626
Setting.synth_api_key = hosting_params[:synth_api_key]
2727
end
2828

29+
if hosting_params.key?(:data_enrichment_enabled)
30+
Setting.data_enrichment_enabled = hosting_params[:data_enrichment_enabled]
31+
end
32+
2933
redirect_to settings_hosting_path, notice: t(".success")
3034
rescue ActiveRecord::RecordInvalid => error
3135
flash.now[:alert] = t(".failure")
@@ -34,7 +38,7 @@ def update
3438

3539
private
3640
def hosting_params
37-
params.require(:setting).permit(:render_deploy_hook, :upgrades_setting, :require_invite_for_signup, :synth_api_key)
41+
params.require(:setting).permit(:render_deploy_hook, :upgrades_setting, :require_invite_for_signup, :synth_api_key, :data_enrichment_enabled)
3842
end
3943

4044
def raise_if_not_self_hosted

app/jobs/enrich_data_job.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
class EnrichDataJob < ApplicationJob
2+
queue_as :default
3+
4+
def perform(account)
5+
account.enrich_data
6+
end
7+
end

app/models/account.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,14 @@ def favorable_direction
126126
classification == "asset" ? "up" : "down"
127127
end
128128

129+
def enrich_data
130+
DataEnricher.new(self).run
131+
end
132+
133+
def enrich_data_later
134+
EnrichDataJob.perform_later(self)
135+
end
136+
129137
def update_with_sync!(attributes)
130138
transaction do
131139
update!(attributes)
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
class Account::DataEnricher
2+
include Providable
3+
4+
attr_reader :account
5+
6+
def initialize(account)
7+
@account = account
8+
end
9+
10+
def run
11+
enrich_transactions
12+
end
13+
14+
private
15+
def enrich_transactions
16+
candidates = account.entries.account_transactions.includes(entryable: [ :merchant, :category ])
17+
18+
Rails.logger.info("Enriching #{candidates.count} transactions for account #{account.id}")
19+
20+
merchants = {}
21+
categories = {}
22+
23+
candidates.each do |entry|
24+
if entry.enriched_at.nil? || entry.entryable.merchant_id.nil? || entry.entryable.category_id.nil?
25+
begin
26+
info = self.class.synth_provider.enrich_transaction(entry.name).info
27+
28+
next unless info.present?
29+
30+
if info.name.present?
31+
merchant = merchants[info.name] ||= account.family.merchants.find_or_create_by(name: info.name)
32+
33+
if info.icon_url.present?
34+
merchant.icon_url = info.icon_url
35+
end
36+
end
37+
38+
if info.category.present?
39+
category = categories[info.category] ||= account.family.categories.find_or_create_by(name: info.category)
40+
end
41+
42+
entryable_attributes = { id: entry.entryable_id }
43+
entryable_attributes[:merchant_id] = merchant.id if merchant.present? && entry.entryable.merchant_id.nil?
44+
entryable_attributes[:category_id] = category.id if category.present? && entry.entryable.category_id.nil?
45+
46+
Account.transaction do
47+
merchant.save! if merchant.present?
48+
category.save! if category.present?
49+
entry.update!(
50+
enriched_at: Time.current,
51+
name: entry.enriched_at.nil? ? info.name : entry.name,
52+
entryable_attributes: entryable_attributes
53+
)
54+
end
55+
rescue => e
56+
Rails.logger.warn("Error enriching transaction #{entry.id}: #{e.message}")
57+
end
58+
end
59+
end
60+
end
61+
end

app/models/account/syncer.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ def run
1010
account.reload
1111
update_account_info(balances, holdings) unless account.plaid_account_id.present?
1212
convert_records_to_family_currency(balances, holdings) unless account.currency == account.family.currency
13+
14+
if Setting.data_enrichment_enabled || Rails.configuration.app_mode.managed?
15+
account.enrich_data_later
16+
else
17+
Rails.logger.info("Data enrichment is disabled, skipping enrichment for account #{account.id}")
18+
end
1319
end
1420

1521
private

app/models/concerns/providable.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@ def git_repository_provider
2323
end
2424

2525
def synth_provider
26-
api_key = self_hosted? ? Setting.synth_api_key : ENV["SYNTH_API_KEY"]
27-
api_key.present? ? Provider::Synth.new(api_key) : nil
26+
@synth_provider ||= begin
27+
api_key = self_hosted? ? Setting.synth_api_key : ENV["SYNTH_API_KEY"]
28+
api_key.present? ? Provider::Synth.new(api_key) : nil
29+
end
2830
end
2931

3032
private

app/models/provider/synth.rb

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,35 @@ def fetch_security_info(ticker:, mic_code:)
167167
raw_response: response
168168
end
169169

170+
def enrich_transaction(description, amount: nil, date: nil, city: nil, state: nil, country: nil)
171+
params = {
172+
description: description,
173+
amount: amount,
174+
date: date,
175+
city: city,
176+
state: state,
177+
country: country
178+
}.compact
179+
180+
response = client.get("#{base_url}/enrich", params)
181+
182+
parsed = JSON.parse(response.body)
183+
184+
EnrichTransactionResponse.new \
185+
info: EnrichTransactionInfo.new(
186+
name: parsed.dig("merchant"),
187+
icon_url: parsed.dig("icon"),
188+
category: parsed.dig("category")
189+
),
190+
success?: true,
191+
raw_response: response
192+
rescue StandardError => error
193+
EnrichTransactionResponse.new \
194+
success?: false,
195+
error: error,
196+
raw_response: error
197+
end
198+
170199
private
171200

172201
attr_reader :api_key
@@ -177,6 +206,8 @@ def fetch_security_info(ticker:, mic_code:)
177206
UsageResponse = Struct.new :used, :limit, :utilization, :plan, :success?, :error, :raw_response, keyword_init: true
178207
SearchSecuritiesResponse = Struct.new :securities, :success?, :error, :raw_response, keyword_init: true
179208
SecurityInfoResponse = Struct.new :info, :success?, :error, :raw_response, keyword_init: true
209+
EnrichTransactionResponse = Struct.new :info, :success?, :error, :raw_response, keyword_init: true
210+
EnrichTransactionInfo = Struct.new :name, :icon_url, :category, keyword_init: true
180211

181212
def base_url
182213
ENV["SYNTH_URL"] || "https://api.synthfinance.com"

app/models/setting.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ class Setting < RailsSettings::Base
1717
default: ENV.fetch("UPGRADES_TARGET", "release"),
1818
validates: { inclusion: { in: %w[release commit] } }
1919

20+
field :data_enrichment_enabled,
21+
type: :boolean,
22+
default: true
23+
2024
field :synth_api_key, type: :string, default: ENV["SYNTH_API_KEY"]
2125

2226
field :require_invite_for_signup, type: :boolean, default: false

app/views/account/transactions/_transaction.html.erb

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,17 @@
1111

1212
<div class="max-w-full">
1313
<%= content_tag :div, class: ["flex items-center gap-2"] do %>
14-
<div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-gray-600/5 text-gray-600">
15-
<%= transaction.name.first.upcase %>
16-
</div>
14+
<% if entry.account_transaction.merchant&.icon_url %>
15+
<%= image_tag entry.account_transaction.merchant.icon_url, class: "w-6 h-6 rounded-full" %>
16+
<% else %>
17+
<%= render "shared/circle_logo", name: entry.name, size: "sm" %>
18+
<% end %>
1719

1820
<div class="truncate">
1921
<% if entry.new_record? %>
20-
<%= content_tag :p, transaction.name %>
22+
<%= content_tag :p, entry.name %>
2123
<% else %>
22-
<%= link_to transaction.name,
24+
<%= link_to entry.name,
2325
entry.transfer.present? ? account_transfer_path(entry.transfer) : account_entry_path(entry),
2426
data: { turbo_frame: "drawer", turbo_prefetch: false },
2527
class: "hover:underline hover:text-gray-800" %>

app/views/merchants/_merchant.html.erb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@
22

33
<div class="flex justify-between items-center p-4 bg-white">
44
<div class="flex w-full items-center gap-2.5">
5-
<%= render partial: "shared/color_avatar", locals: { name: merchant.name, color: merchant.color } %>
5+
<% if merchant.icon_url %>
6+
<div class="w-8 h-8 rounded-full flex justify-center items-center">
7+
<%= image_tag merchant.icon_url, class: "w-8 h-8 rounded-full" %>
8+
</div>
9+
<% else %>
10+
<%= render partial: "shared/color_avatar", locals: { name: merchant.name, color: merchant.color } %>
11+
<% end %>
12+
613
<p class="text-gray-900 text-sm truncate">
714
<%= merchant.name %>
815
</p>

0 commit comments

Comments
 (0)