Skip to content

Commit

Permalink
Refactor synth provider and show UI error if not configured
Browse files Browse the repository at this point in the history
  • Loading branch information
jakubkottnauer committed May 23, 2024
1 parent 128aaad commit 7ad98f0
Show file tree
Hide file tree
Showing 9 changed files with 83 additions and 39 deletions.
9 changes: 8 additions & 1 deletion app/models/account/syncable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ def sync_later(start_date = nil)
def sync(start_date = nil)
update!(status: "syncing")

if self.foreign_currency? && !ExchangeRate.exchange_rates_provider.initialized?
logger.error("Cancelling sync of foreign account #{id}: No exchange rate provider ready")
update!(status: "error")
return
end

sync_exchange_rates

calc_start_date = start_date - 1.day if start_date.present? && self.balance_on(start_date - 1.day).present?
Expand All @@ -21,10 +27,11 @@ def sync(start_date = nil)
update!(status: "ok", last_sync_date: Date.today, balance: new_balance)
rescue => e
update!(status: "error")
Rails.logger.error("Failed to sync account #{id}: #{e.message}")
logger.error("Failed to sync account #{id}: #{e.message}")
end

def can_sync?
return false if self.foreign_currency? && !ExchangeRate.exchange_rates_provider.initialized?
# Skip account sync if account is not active or the sync process is already running
return false unless is_active
return false if syncing?
Expand Down
2 changes: 1 addition & 1 deletion app/models/concerns/providable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ module Providable

class_methods do
def exchange_rates_provider
Provider::Synth.new
Provider::Synth
end

def git_repository_provider
Expand Down
83 changes: 47 additions & 36 deletions app/models/provider/synth.rb
Original file line number Diff line number Diff line change
@@ -1,51 +1,62 @@
class Provider::Synth
include Retryable
class << self
include Retryable

def initialize(api_key = ENV["SYNTH_API_KEY"])
@api_key = api_key || ENV["SYNTH_API_KEY"]
end
attr_accessor :configuration

def fetch_exchange_rate(from:, to:, date:)
retrying Provider::Base.known_transient_errors do |on_last_attempt|
response = Faraday.get("#{base_url}/rates/historical") do |req|
req.headers["Authorization"] = "Bearer #{api_key}"
req.params["date"] = date.to_s
req.params["from"] = from
req.params["to"] = to
end
def initialized?
configuration.api_key.present?
end

def configure
self.configuration ||= Configuration.new
yield(configuration)
end

def fetch_exchange_rate(from:, to:, date:)
retrying Provider::Base.known_transient_errors do |on_last_attempt|
response = Faraday.get("#{base_url}/rates/historical") do |req|
req.headers["Authorization"] = "Bearer #{configuration.api_key}"
req.params["date"] = date.to_s
req.params["from"] = from
req.params["to"] = to
end

if response.success?
ExchangeRateResponse.new \
rate: JSON.parse(response.body).dig("data", "rates", to),
success?: true,
raw_response: response
else
if on_last_attempt
if response.success?
ExchangeRateResponse.new \
success?: false,
error: build_error(response),
rate: JSON.parse(response.body).dig("data", "rates", to),
success?: true,
raw_response: response
else
raise build_error(response)
if on_last_attempt
ExchangeRateResponse.new \
success?: false,
error: build_error(response),
raw_response: response
else
raise build_error(response)
end
end
end
end
end

private
attr_reader :api_key
private
class Configuration
attr_accessor :api_key
end

ExchangeRateResponse = Struct.new :rate, :success?, :error, :raw_response, keyword_init: true
ExchangeRateResponse = Struct.new :rate, :success?, :error, :raw_response, keyword_init: true

def base_url
"https://api.synthfinance.com"
end
def base_url
"https://api.synthfinance.com"
end

def build_error(response)
Provider::Base::ProviderError.new(<<~ERROR)
Failed to fetch exchange rate from #{self.class}
Status: #{response.status}
Body: #{response.body.inspect}
ERROR
end
def build_error(response)
Provider::Base::ProviderError.new(<<~ERROR)
Failed to fetch exchange rate from #{self}
Status: #{response.status}
Body: #{response.body.inspect}
ERROR
end
end
end
3 changes: 3 additions & 0 deletions app/views/accounts/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@
<%= turbo_frame_tag "sync_message" do %>
<%= render partial: "accounts/sync_message", locals: { is_syncing: @account.syncing? } %>
<% end %>
<% if @account.foreign_currency? && !ExchangeRate.exchange_rates_provider.initialized? %>
<%= render partial: "shared/alert", locals: { type: "error", content: t(".no_exchange_rate_provider") } %>
<% end %>
<div class="bg-white shadow-xs rounded-xl border border-alpha-black-25 rounded-lg">
<div class="p-4 flex justify-between">
<div class="space-y-2">
Expand Down
12 changes: 12 additions & 0 deletions app/views/shared/_alert.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<%# locals: (type: "error", content: "") -%>
<% color = type == "error" ? "red" : "blue" %>
<%= content_tag :div,
class: "flex justify-between bg-#{color}-50 rounded-xl p-3",
data: {controller: "element-removal" },
role: type == "error" ? "alert" : "status" do %>
<div class="flex gap-3 text-<%= color %>-500 items-center">
<%= lucide_icon("info", class: "w-5 h-5 shrink-0") %>
<p class="text-sm"><%= content %></p>
</div>
<%= content_tag :a, lucide_icon("x", class: "w-5 h-5 shrink-0 text-#{color}-500"), data: { action: "click->element-removal#remove" }, class:"flex gap-1 font-medium items-center text-gray-900 px-3 py-1.5 rounded-lg cursor-pointer" %>
<% end %>
5 changes: 5 additions & 0 deletions config/initializers/synth.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Rails.application.config.to_prepare do
Provider::Synth.configure do |config|
config.api_key = ENV["SYNTH_API_KEY"]
end
end
2 changes: 2 additions & 0 deletions config/locales/views/account/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ en:
/> <p>After deletion, there is no way you'll be able to restore the account
information because you'll need to add it as a new account.</p>"
confirm_title: Delete account?
no_exchange_rate_provider: This account cannot be synced at the moment as there
is no exchange rate provider configured.
summary:
new: New account
sync:
Expand Down
2 changes: 1 addition & 1 deletion test/models/provider/synth_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ class Provider::SynthTest < ActiveSupport::TestCase
include ExchangeRateProviderInterfaceTest

setup do
@subject = Provider::Synth.new
@subject = Provider::Synth
end
end
4 changes: 4 additions & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ class TestCase
end
end

Provider::Synth.configure do |config|
config.api_key = "api_key"
end

# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
fixtures :all

Expand Down

0 comments on commit 7ad98f0

Please sign in to comment.