# Build a Currency Exchange Service in Ruby on Rails

Working with exchange rates in Rails applications can be tricky. After implementing currency handling in several production apps, I've developed a robust pattern that I'll share with you today. We'll build a service that fetches live exchange rates from Currency Layer API and integrates smoothly with Rails.

🎯 What We'll Cover:
- Setting up a reusable currency exchange service
- Implementing background rate updates
- Handling API integration gracefully
- Testing our implementation

## Project Setup

First, let's add the necessary gems to our `Gemfile`:

```ruby
gem 'httparty'  # For API requests
gem 'solid_queue'  # For background jobs
```

## Building the Currency Exchange Service

I prefer using Plain Old Ruby Objects (POROs) for API integrations. Here's why - they're easy to test, maintain, and modify. Let's build our service:

```ruby
# app/services/currency_exchange.rb
class CurrencyExchange
  include HTTParty
  base_uri 'https://api.currencylayer.com'

  def initialize
    @options = { 
      query: { 
        access_key: Rails.application.credentials.currency_layer.api_key 
      } 
    }
  end

  def self.list
    new.list
  end

  def self.live
    new.live
  end
end
```

>  **Pro Tip 💡**
	I'm using class methods (`self.list` and `self.live`) as convenience wrappers around instance methods. This gives us flexibility - we can instantiate the class directly when we need to customize behavior, or use the class methods for quick access.

## Handling API Responses

Let's build clean value objects for our data:

```ruby
class CurrencyExchange
  # ...
  
  GlobalCurrency = Struct.new(:code, :name)
  Conversion = Struct.new(:from, :to, :rate)

  def list
    res = self.class.get('/list', @options)
    return res.parsed_response['currencies'].map { |code, name| 
      GlobalCurrency.new(code, name) 
    } if res.success?
    []
  end

  def live
    res = self.class.get('/live', @options)
    return [] unless res.success?

    res.parsed_response['quotes'].map do |code, rate|
      Conversion.new(code[0..2], code[3..], rate.to_f.round(4))
    end
  end
end
```

### Why This Approach Works 🎯
1. Value objects provide a clean interface
2. Empty arrays as fallbacks prevent nil checking
3. Response parsing is encapsulated
4. Rate rounding handles floating-point precision

## Database Integration

We need to store our currency data. Here's our migration:

```ruby
class CreateCurrencies < ActiveRecord::Migration[7.2]
  def change
    create_table :currencies do |t|
      t.string :code, null: false
      t.decimal :amount, precision: 14, scale: 4, default: 1.0
      t.string :name, null: false
      t.datetime :converted_at
      t.datetime :deleted_at

      t.timestamps
    end
    
    add_index :currencies, :code, unique: true
    add_index :currencies, :name
    add_index :currencies, :deleted_at
  end
end
```

### Model Implementation

```ruby
class Currency < ApplicationRecord
  acts_as_paranoid  # Soft deletes

  has_many :accounts, dependent: :nullify

  validates :name, presence: true
  validates :code, presence: true, uniqueness: true
  validates :amount, presence: true, 
            numericality: { greater_than_or_equal_to: 0.0 }
end
```

## Automated Rate Updates

Here's where it gets interesting. We'll use a background job to update rates:

```ruby
class UpdateCurrencyRatesJob < ApplicationJob
  def perform
    Currencies::UpdateRatesService.call
  end
end
```

The actual update service:

```ruby
module Currencies
  class UpdateRatesService < ApplicationService
    def call
      CurrencyExchange.live.each do |conversion|
        update_rate(conversion)
      end
    end

    private

    def update_rate(conversion)
      Currency.find_by(code: conversion.to)&.update(
        amount: conversion.rate,
        converted_at: Time.current
      )
    end
  end
end
```

### Scheduling Updates

Configure your scheduler in `config/recurring.yml`:

```yaml
staging:
  update_currency_rates:
    class: UpdateCurrencyRatesJob
    queue: background
    schedule: '0 1 */2 * *'  # Every 2 days at 1 AM
```

## Testing Strategy

Here's how I test this setup:

```ruby
RSpec.describe CurrencyExchange do
  describe '.list' do
    it 'returns structured currency data' do
      VCR.use_cassette('currency_layer_list') do # https://github.com/vcr/vcr
        currencies = described_class.list
        expect(currencies).to all(be_a(described_class::GlobalCurrency))
      end
    end

    it 'handles API failures gracefully' do
      allow(described_class).to receive(:get).and_return(
        double(success?: false)
      )
      expect(described_class.list).to eq([])
    end
  end
end
```

### Model Tests

```ruby
RSpec.describe Currency do
  describe 'validations' do
    it 'requires a valid exchange rate' do
      currency = build(:currency, amount: -1)
      expect(currency).not_to be_valid
    end

    it 'enforces unique currency codes' do
      create(:currency, code: 'USD')
      duplicate = build(:currency, code: 'USD')
      expect(duplicate).not_to be_valid
    end
  end
end
```

## Usage in Your Application

Here's how you'd use this in your app:

```ruby
# Fetch available currencies
currencies = CurrencyExchange.list
puts "Available currencies: #{currencies.map(&:code).join(', ')}"

# Get current rates
rates = CurrencyExchange.live
rates.each do |conversion|
  puts "1 #{conversion.from} = #{conversion.rate} #{conversion.to}"
end
```

## Common Pitfalls to Avoid ⚠️

1. **Don't** store sensitive API keys in your codebase. Use Rails credentials:
```bash
EDITOR="code --wait" bin/rails credentials:edit
```

2. **Don't** update rates synchronously during user requests. Always use background jobs.

3. **Don't** forget to handle API rate limits. Currency Layer has different limits for different plans.

## Production Considerations 🚀

1. **Error Monitoring**: Add Sentry or similar error tracking:
```ruby
Sentry.capture_exception(e) if defined?(Sentry)
```

2. **Rate Limiting**: Implement exponential backoff for API failures:
```ruby
def with_retry
  retries ||= 0
  yield
rescue StandardError => e
  retry if (retries += 1) < 3
  raise e
end
```

3. **Logging**: Add structured logging for debugging:
```ruby
Rails.logger.info(
  event: 'currency_rate_update',
  currency: conversion.to,
  rate: conversion.rate
)
```

> Remember: Currency exchange rates are critical financial data. Always validate your implementation thoroughly and consider using paid API tiers for production use.
