Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ Gera is a Rails engine for generating and managing currency exchange rates for c
- **CurrencyPair** - Utility class for currency pair operations
- **Universe** - Central repository pattern for accessing rate data

### Worker Architecture
- **RatesWorker** concern for fetching external rates
- Individual workers for each rate source (ExmoRatesWorker, BitfinexRatesWorker, etc.)
- **CurrencyRatesWorker** - Builds currency rate matrix from external rates
- **DirectionsRatesWorker** - Calculates final direction rates with commissions
- **CreateHistory_intervalsWorker** - Aggregates historical data
### Job Architecture (ActiveJob/SolidQueue)
- **RatesJob** concern for fetching external rates
- Individual jobs for each rate source (ExmoRatesJob, BitfinexRatesJob, etc.)
- **CurrencyRatesJob** - Builds currency rate matrix from external rates
- **DirectionsRatesJob** - Calculates final direction rates with commissions
- **CreateHistoryIntervalsJob** - Aggregates historical data

## Development Commands

Expand Down Expand Up @@ -100,7 +100,7 @@ RUB, USD, BTC, LTC, ETH, DSH, KZT, XRP, ETC, XMR, BCH, EUR, NEO, ZEC
## File Organization

- `app/models/gera/` - Core domain models
- `app/workers/gera/` - Background job workers
- `app/jobs/gera/` - Background jobs (ActiveJob/SolidQueue)
- `lib/gera/` - Core engine logic and utilities
- `lib/builders/` - Rate calculation builders
- `spec/` - Test suite with dummy app
57 changes: 43 additions & 14 deletions app/jobs/gera/directions_rates_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module Gera
class DirectionsRatesJob < ApplicationJob
include ActiveSupport::Callbacks
include AutoLogger
include Mathematic

Error = Class.new StandardError

Expand All @@ -19,31 +20,59 @@ def perform(*_args) # exchange_rate_id: nil)

run_callbacks :perform do
Gera::DirectionRateSnapshot.transaction do
Gera::ExchangeRate.includes(:payment_system_from, :payment_system_to).find_each do |exchange_rate|
safe_create(exchange_rate)
end
records = build_direction_rate_records
Gera::DirectionRate.insert_all!(records) if records.any?
end
end
logger.info 'finish'
end

private

delegate :direction_rates, to: :snapshot

def snapshot
@snapshot ||= Gera::DirectionRateSnapshot.create!
end

def safe_create(exchange_rate)
direction_rates.create!(
snapshot: snapshot,
exchange_rate: exchange_rate,
currency_rate: Gera::Universe.currency_rates_repository.find_currency_rate_by_pair(exchange_rate.currency_pair)
)
rescue Gera::CurrencyRatesRepository::UnknownPair => err
rescue Gera::DirectionRate::UnknownExchangeRate, ActiveRecord::RecordInvalid => err
logger.error err
def currency_rates_cache
@currency_rates_cache ||= Gera::Universe.currency_rates_repository
.snapshot
.rates
.index_by(&:currency_pair)
end

def build_direction_rate_records
current_time = Time.current
exchange_rates = Gera::ExchangeRate.includes(:payment_system_from, :payment_system_to).to_a

exchange_rates.filter_map do |exchange_rate|
build_direction_rate_hash(exchange_rate, current_time)
end
end

def build_direction_rate_hash(exchange_rate, current_time)
currency_rate = currency_rates_cache[exchange_rate.currency_pair]
return nil unless currency_rate

rate_percent = exchange_rate.final_rate_percents
return nil if rate_percent.nil?

base_rate_value = currency_rate.rate_value
rate_value = calculate_finite_rate(base_rate_value, rate_percent)

{
snapshot_id: snapshot.id,
exchange_rate_id: exchange_rate.id,
currency_rate_id: currency_rate.id,
ps_from_id: exchange_rate.income_payment_system_id,
ps_to_id: exchange_rate.outcome_payment_system_id,
base_rate_value: base_rate_value,
rate_percent: rate_percent,
rate_value: rate_value,
created_at: current_time
}
rescue StandardError => e
logger.error "Failed to build direction rate for exchange_rate #{exchange_rate.id}: #{e.message}"
nil
end
end
end
6 changes: 5 additions & 1 deletion app/services/gera/rate_comission_calculator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,11 @@ def auto_rate_by_reserve_to
def current_base_rate
return 1.0 if same_currencies?

@current_base_rate ||= Gera::CurrencyRateHistoryInterval.where(cur_from_id: in_currency.local_id, cur_to_id: out_currency.local_id).last.avg_rate
@current_base_rate ||= Gera::CurrencyRateHistoryInterval
.where(cur_from_id: in_currency.local_id, cur_to_id: out_currency.local_id)
.order(:interval_from)
.last
.avg_rate
end

def average_base_rate
Expand Down
52 changes: 52 additions & 0 deletions spec/jobs/gera/directions_rates_job_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,58 @@ def rows_without_kassa; []; end
expect(subject).to receive(:logger).at_least(:twice).and_return(double(info: nil))
subject.perform
end

it 'creates direction rate with correct attributes' do
subject.perform
direction_rate = DirectionRate.last

expect(direction_rate.exchange_rate_id).to eq(exchange_rate.id)
expect(direction_rate.currency_rate_id).to eq(currency_rate.id)
expect(direction_rate.ps_from_id).to eq(payment_system_from.id)
expect(direction_rate.ps_to_id).to eq(payment_system_to.id)
expect(direction_rate.base_rate_value).to eq(currency_rate.rate_value)
expect(direction_rate.rate_percent).to be_present
expect(direction_rate.rate_value).to be_present
end

context 'with multiple exchange rates' do
let!(:payment_system_eur) { create(:gera_payment_system, currency: Money::Currency.find('EUR')) }
let!(:exchange_rate2) do
create(:gera_exchange_rate,
payment_system_from: payment_system_to,
payment_system_to: payment_system_eur)
end
let!(:currency_rate2) do
create(:currency_rate,
snapshot: currency_rate_snapshot,
cur_from: Money::Currency.find('RUB'),
cur_to: Money::Currency.find('EUR'),
currency_pair: Gera::CurrencyPair.new(Money::Currency.find('RUB'), Money::Currency.find('EUR')))
end

it 'creates direction rates for all exchange rates with matching currency rates' do
expect { subject.perform }.to change(DirectionRate, :count).by(2)
end

it 'uses batch insert (single SQL INSERT)' do
# Verify that insert_all! is called instead of individual creates
expect(DirectionRate).to receive(:insert_all!).once.and_call_original
subject.perform
end
end

context 'when currency rate is missing for an exchange rate' do
let!(:payment_system_btc) { create(:gera_payment_system, currency: Money::Currency.find('BTC')) }
let!(:exchange_rate_no_currency_rate) do
create(:gera_exchange_rate,
payment_system_from: payment_system_from,
payment_system_to: payment_system_btc)
end

it 'skips exchange rates without matching currency rates' do
expect { subject.perform }.to change(DirectionRate, :count).by(1)
end
end
end

describe 'queue configuration' do
Expand Down