Skip to content

Commit 1e0b8ab

Browse files
etewiahclaude
andcommitted
Add tests for currency conversion feature
- Add CurrencyHelper spec (18 examples) - Add ExchangeRateService spec (16 examples) - Add CurrenciesController request spec (8 examples) Also fix ExchangeRateService to store timestamp in exchange_rates JSON instead of requiring a separate column. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 6412e3d commit 1e0b8ab

File tree

4 files changed

+412
-4
lines changed

4 files changed

+412
-4
lines changed

app/services/pwb/exchange_rate_service.rb

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,9 @@ def update_rates(website)
4949

5050
rates = build_rates_hash(bank, base_currency, target_currencies)
5151

52+
# Store rates with timestamp in the JSON column
5253
website.update!(
53-
exchange_rates: rates,
54-
exchange_rates_updated_at: Time.current
54+
exchange_rates: rates.merge('_updated_at' => Time.current.iso8601)
5555
)
5656

5757
Rails.logger.info "[ExchangeRates] Updated #{rates.size} rates for website #{website.id}"
@@ -132,9 +132,15 @@ def convert(money, to_currency, website)
132132
# @param website [Pwb::Website] the website to check
133133
# @return [Boolean] true if rates need updating
134134
def rates_stale?(website)
135-
return true if website.exchange_rates_updated_at.nil?
135+
rates = website.exchange_rates
136+
return true if rates.blank?
137+
138+
updated_at = rates['_updated_at']
139+
return true if updated_at.nil?
136140

137-
website.exchange_rates_updated_at < 24.hours.ago
141+
Time.parse(updated_at) < 24.hours.ago
142+
rescue ArgumentError
143+
true
138144
end
139145

140146
# Get list of available currencies from ECB
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
5+
RSpec.describe CurrencyHelper, type: :helper do
6+
let(:website) { create(:pwb_website, default_currency: 'EUR', available_currencies: ['USD', 'GBP']) }
7+
8+
before do
9+
# Set up current website context
10+
allow(helper).to receive(:current_website).and_return(website)
11+
allow(helper).to receive(:session).and_return({})
12+
allow(helper).to receive(:cookies).and_return({})
13+
end
14+
15+
describe '#display_price' do
16+
let(:price) { Money.new(25000000, 'EUR') } # €250,000
17+
18+
context 'with nil price' do
19+
it 'returns nil' do
20+
expect(helper.display_price(nil)).to be_nil
21+
end
22+
end
23+
24+
context 'with zero price' do
25+
it 'returns nil' do
26+
expect(helper.display_price(Money.new(0, 'EUR'))).to be_nil
27+
end
28+
end
29+
30+
context 'when user has not selected a different currency' do
31+
it 'returns formatted price without conversion' do
32+
result = helper.display_price(price)
33+
expect(result).to include('€')
34+
expect(result).to include('250,000')
35+
expect(result).not_to include('(~')
36+
end
37+
end
38+
39+
context 'when show_conversion is false' do
40+
it 'returns only the original price' do
41+
allow(helper).to receive(:session).and_return({ preferred_currency: 'USD' })
42+
result = helper.display_price(price, show_conversion: false)
43+
expect(result).to include('€')
44+
expect(result).not_to include('(~')
45+
end
46+
end
47+
end
48+
49+
describe '#user_preferred_currency' do
50+
context 'with session preference' do
51+
it 'returns session currency' do
52+
allow(helper).to receive(:session).and_return({ preferred_currency: 'USD' })
53+
expect(helper.user_preferred_currency).to eq('USD')
54+
end
55+
end
56+
57+
context 'with cookie preference' do
58+
it 'returns cookie currency' do
59+
allow(helper).to receive(:session).and_return({})
60+
allow(helper).to receive(:cookies).and_return({ preferred_currency: 'GBP' })
61+
expect(helper.user_preferred_currency).to eq('GBP')
62+
end
63+
end
64+
65+
context 'with no preference' do
66+
it 'returns website default currency' do
67+
expect(helper.user_preferred_currency).to eq('EUR')
68+
end
69+
end
70+
end
71+
72+
describe '#available_display_currencies' do
73+
it 'includes the default currency' do
74+
expect(helper.available_display_currencies).to include('EUR')
75+
end
76+
77+
it 'includes additional currencies' do
78+
expect(helper.available_display_currencies).to include('USD', 'GBP')
79+
end
80+
81+
it 'returns unique values' do
82+
website.update(available_currencies: ['EUR', 'USD', 'EUR'])
83+
result = helper.available_display_currencies
84+
expect(result.count('EUR')).to eq(1)
85+
end
86+
end
87+
88+
describe '#multiple_currencies_available?' do
89+
context 'with multiple currencies' do
90+
it 'returns true' do
91+
expect(helper.multiple_currencies_available?).to be true
92+
end
93+
end
94+
95+
context 'with only default currency' do
96+
before do
97+
website.update(available_currencies: [])
98+
end
99+
100+
it 'returns false' do
101+
expect(helper.multiple_currencies_available?).to be false
102+
end
103+
end
104+
end
105+
106+
describe '#currency_symbol' do
107+
it 'returns € for EUR' do
108+
expect(helper.currency_symbol('EUR')).to eq('€')
109+
end
110+
111+
it 'returns $ for USD' do
112+
expect(helper.currency_symbol('USD')).to eq('$')
113+
end
114+
115+
it 'returns £ for GBP' do
116+
expect(helper.currency_symbol('GBP')).to eq('£')
117+
end
118+
119+
it 'returns code for unknown currency' do
120+
expect(helper.currency_symbol('XXX')).to eq('XXX')
121+
end
122+
end
123+
124+
describe '#currency_select_options' do
125+
it 'returns array of label/value pairs' do
126+
options = helper.currency_select_options
127+
expect(options).to be_an(Array)
128+
expect(options.first).to be_an(Array)
129+
expect(options.first.size).to eq(2)
130+
end
131+
132+
it 'includes symbols in labels' do
133+
options = helper.currency_select_options
134+
labels = options.map(&:first)
135+
expect(labels.join).to include('€')
136+
end
137+
end
138+
end
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
5+
RSpec.describe 'Currency Selection', type: :request do
6+
let(:website) do
7+
create(:pwb_website,
8+
subdomain: 'currency-test',
9+
default_currency: 'EUR',
10+
available_currencies: ['USD', 'GBP'])
11+
end
12+
let!(:agency) { create(:pwb_agency, website: website) }
13+
14+
before do
15+
# Set up tenant context
16+
host! "#{website.subdomain}.test.host"
17+
Pwb::Current.website = website
18+
ActsAsTenant.current_tenant = website
19+
end
20+
21+
describe 'POST /set_currency' do
22+
context 'with valid currency' do
23+
it 'sets the currency preference and redirects' do
24+
post '/set_currency', params: { currency: 'USD' }
25+
26+
expect(response).to have_http_status(:redirect)
27+
expect(session[:preferred_currency]).to eq('USD')
28+
end
29+
30+
it 'stores currency in cookie' do
31+
post '/set_currency', params: { currency: 'GBP' }
32+
33+
expect(cookies[:preferred_currency]).to eq('GBP')
34+
end
35+
36+
context 'with JSON format' do
37+
it 'returns success JSON' do
38+
post '/set_currency', params: { currency: 'USD' }, as: :json
39+
40+
expect(response).to have_http_status(:success)
41+
json = JSON.parse(response.body)
42+
expect(json['success']).to be true
43+
expect(json['currency']).to eq('USD')
44+
end
45+
end
46+
end
47+
48+
context 'with invalid currency' do
49+
it 'redirects with error' do
50+
post '/set_currency', params: { currency: 'XXX' }
51+
52+
expect(response).to have_http_status(:redirect)
53+
end
54+
55+
context 'with JSON format' do
56+
it 'returns error JSON' do
57+
post '/set_currency', params: { currency: 'XXX' }, as: :json
58+
59+
expect(response).to have_http_status(:unprocessable_entity)
60+
json = JSON.parse(response.body)
61+
expect(json['success']).to be false
62+
end
63+
end
64+
end
65+
66+
context 'with currency not in available list' do
67+
it 'rejects the currency' do
68+
post '/set_currency', params: { currency: 'JPY' }, as: :json
69+
70+
expect(response).to have_http_status(:unprocessable_entity)
71+
end
72+
end
73+
74+
context 'with blank currency' do
75+
it 'rejects the request' do
76+
post '/set_currency', params: { currency: '' }, as: :json
77+
78+
expect(response).to have_http_status(:unprocessable_entity)
79+
end
80+
end
81+
82+
context 'with lowercase currency code' do
83+
it 'accepts and normalizes to uppercase' do
84+
post '/set_currency', params: { currency: 'usd' }, as: :json
85+
86+
expect(response).to have_http_status(:success)
87+
json = JSON.parse(response.body)
88+
expect(json['currency']).to eq('USD')
89+
end
90+
end
91+
end
92+
end

0 commit comments

Comments
 (0)