currency_cloud 0.5 → 0.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/README.md +151 -8
  4. data/currency_cloud.gemspec +1 -0
  5. data/lib/currency_cloud.rb +14 -0
  6. data/lib/currency_cloud/actions/delete.rb +7 -1
  7. data/lib/currency_cloud/actions/save.rb +14 -0
  8. data/lib/currency_cloud/errors/api_error.rb +38 -2
  9. data/lib/currency_cloud/errors/error_utils.rb +14 -0
  10. data/lib/currency_cloud/errors/general_error.rb +5 -0
  11. data/lib/currency_cloud/errors/unexpected_error.rb +23 -1
  12. data/lib/currency_cloud/request_handler.rb +27 -20
  13. data/lib/currency_cloud/resource.rb +7 -1
  14. data/lib/currency_cloud/resources/balance.rb +3 -1
  15. data/lib/currency_cloud/resources/beneficiary.rb +2 -3
  16. data/lib/currency_cloud/resources/payment.rb +0 -2
  17. data/lib/currency_cloud/resources/settlement.rb +30 -9
  18. data/lib/currency_cloud/response_handler.rb +14 -15
  19. data/lib/currency_cloud/session.rb +4 -4
  20. data/lib/currency_cloud/version.rb +1 -1
  21. data/spec/currency_cloud/request_handler_spec.rb +32 -0
  22. data/spec/currency_cloud/resource_spec.rb +37 -0
  23. data/spec/currency_cloud_spec.rb +49 -5
  24. data/spec/integration/actions_spec.rb +28 -0
  25. data/spec/integration/errors_spec.rb +47 -0
  26. data/spec/integration/rates_spec.rb +1 -1
  27. data/spec/integration/settlements_spec.rb +67 -0
  28. data/spec/spec_helper.rb +3 -2
  29. data/spec/support/vcr_cassettes/Actions/can_first.yml +0 -36
  30. data/spec/support/vcr_cassettes/Actions/can_use_currency_to_retrieve_balance.yml +36 -0
  31. data/spec/support/vcr_cassettes/Actions/can_validate_beneficiaries.yml +38 -0
  32. data/spec/support/vcr_cassettes/Error/contains_full_details_for_api_error.yml +35 -0
  33. data/spec/support/vcr_cassettes/Error/is_raised_on_an_internal_server_error.yml +0 -34
  34. data/spec/support/vcr_cassettes/Rates/can_find.yml +3 -3
  35. data/spec/support/vcr_cassettes/Rates/can_provided_detailed_rate.yml +3 -3
  36. data/spec/support/vcr_cassettes/Reference/can_retrieve_beneficiary_required_details.yml +3 -3
  37. data/spec/support/vcr_cassettes/Reference/can_retrieve_conversion_dates.yml +3 -3
  38. data/spec/support/vcr_cassettes/Reference/can_retrieve_settlement_accounts.yml +3 -3
  39. data/spec/support/vcr_cassettes/Settlements/can_add_conversion.yml +102 -0
  40. data/spec/support/vcr_cassettes/Settlements/can_release.yml +69 -0
  41. data/spec/support/vcr_cassettes/Settlements/can_remove_conversion.yml +69 -0
  42. data/spec/support/vcr_cassettes/Settlements/can_unrelease.yml +69 -0
  43. metadata +26 -5
  44. data/Gemfile.lock +0 -99
  45. data/lib/currency_cloud/errors/config_error.rb +0 -5
@@ -11,13 +11,19 @@ module CurrencyCloud
11
11
  def self.actions(*actions)
12
12
  @actions ||= actions
13
13
  @actions.each do |action|
14
- self.class_eval do # self.class.instance_eval # metaclass.instance_eval
14
+ self.class_eval do
15
15
  action_module = CurrencyCloud::Actions.const_get(action.to_s.capitalize)
16
16
  self.extend(action_module)
17
17
  end
18
+
19
+
20
+ self.send(:include, CurrencyCloud::Actions::Save) if action == :update
21
+ self.send(:include, CurrencyCloud::Actions::InstanceDelete) if action == :delete
18
22
  end
19
23
  end
20
24
 
25
+ attr_reader :changed_attributes
26
+
21
27
  def initialize(object)
22
28
  @object = object
23
29
  @changed_attributes = Set.new
@@ -3,6 +3,8 @@ module CurrencyCloud
3
3
  resource :balances
4
4
  actions :find
5
5
 
6
- #TODO: currency
6
+ def self.currency(ccy)
7
+ get(ccy)
8
+ end
7
9
  end
8
10
  end
@@ -7,8 +7,7 @@ module CurrencyCloud
7
7
  actions :create, :retrieve, :find, :update, :delete
8
8
 
9
9
  def self.validate(params)
10
- new(request.get("#{self.resource}/validate", params))
11
- end
12
-
10
+ post('validate', params)
11
+ end
13
12
  end
14
13
  end
@@ -5,7 +5,5 @@ module CurrencyCloud
5
5
  resource :payments
6
6
 
7
7
  actions :create, :retrieve, :find, :delete, :update
8
-
9
- #TODO: authorize, authorizations
10
8
  end
11
9
  end
@@ -5,26 +5,47 @@ module CurrencyCloud
5
5
  resource :settlements
6
6
 
7
7
  actions :create, :retrieve, :find, :delete
8
-
8
+
9
9
  def add_conversion(conversion_id)
10
- # TODO: Should just update state of current object using a refresh method?
11
- new(request.post("#{self.resource}/#{self.id}/add_conversion", conversion_id: conversion_id))
10
+ update_attributes(Settlement.add_conversion(id, conversion_id))
12
11
  end
13
12
 
14
13
  def remove_conversion(conversion_id)
15
- # TODO: Should just update state of current object using a refresh method?
16
- new(request.post("#{self.resource}/#{self.id}/remove_conversion", conversion_id: conversion_id))
14
+ update_attributes(Settlement.remove_conversion(id, conversion_id))
17
15
  end
18
16
 
19
17
  def release
20
- # TODO: Should just update state of current object using a refresh method?
21
- new(request.post("#{self.resource}/#{self.id}/release"))
18
+ update_attributes(Settlement.release(id))
22
19
  end
23
20
 
24
21
  def unrelease
25
- # TODO: Should just update state of current object using a refresh method?
26
- new(request.post("#{self.resource}/#{self.id}/unrelease"))
22
+ update_attributes(Settlement.unrelease(id))
23
+ end
24
+
25
+ def self.add_conversion(settlement_id, conversion_id)
26
+ post("#{settlement_id}/add_conversion", conversion_id: conversion_id)
27
+ end
28
+
29
+ def self.remove_conversion(settlement_id, conversion_id)
30
+ post("#{settlement_id}/remove_conversion", conversion_id: conversion_id)
27
31
  end
28
32
 
33
+ def self.release(settlement_id)
34
+ post("#{settlement_id}/release")
35
+ end
36
+
37
+ def self.unrelease(settlement_id)
38
+ post("#{settlement_id}/unrelease")
39
+ end
40
+
41
+ private
42
+ def update_attributes(settlement)
43
+ self.conversion_ids = settlement.conversion_ids
44
+ self.status = settlement.status
45
+ self.entries = settlement.entries
46
+ self.updated_at = settlement.updated_at
47
+ self.released_at = settlement.released_at
48
+ self
49
+ end
29
50
  end
30
51
  end
@@ -2,17 +2,16 @@ module CurrencyCloud
2
2
 
3
3
  class ResponseHandler
4
4
 
5
- attr_reader :response
5
+ attr_reader :verb, :route, :params, :response
6
6
 
7
- def self.process(response)
8
- new(response).data
9
- end
10
-
11
- def initialize(response)
7
+ def initialize(verb, route, params, response)
8
+ @verb = verb
9
+ @route = route
10
+ @params = params
12
11
  @response = response
13
12
  end
14
13
 
15
- def data
14
+ def process
16
15
  if success?
17
16
  return parsed_response
18
17
  else
@@ -28,15 +27,15 @@ module CurrencyCloud
28
27
 
29
28
  def handle_failure
30
29
  error_class = case response.code
31
- when 400 then CurrencyCloud::BadRequestError
32
- when 401 then CurrencyCloud::AuthenticationError
33
- when 403 then CurrencyCloud::ForbiddenError
34
- when 404 then CurrencyCloud::NotFoundError
35
- when 429 then CurrencyCloud::TooManyRequestsError
36
- when 500 then CurrencyCloud::InternalApplicationError
37
- else CurrencyCloud::UnexpectedError
30
+ when 400 then BadRequestError
31
+ when 401 then AuthenticationError
32
+ when 403 then ForbiddenError
33
+ when 404 then NotFoundError
34
+ when 429 then TooManyRequestsError
35
+ when 500 then InternalApplicationError
38
36
  end
39
- raise error_class.new(response)
37
+ raise error_class.new(verb, route, params, response) if error_class
38
+ raise UnexpectedError.new(verb, route, params, response)
40
39
  end
41
40
 
42
41
  def parsed_response
@@ -7,11 +7,11 @@ module CurrencyCloud
7
7
  :uat => 'https://api-uat1.ccycloud.com'}
8
8
 
9
9
  attr_reader :environment, :login_id, :api_key
10
- attr_accessor :token
10
+ attr_accessor :token, :on_behalf_of
11
11
 
12
12
  def self.validate_environment(environment)
13
13
  unless Environments.keys.include?(environment)
14
- raise CurrencyCloud::ConfigError, "'#{environment}' is not a valid environment, must be one of: #{Environments.keys.join(", ")}"
14
+ raise CurrencyCloud::GeneralError, "'#{environment}' is not a valid environment, must be one of: #{Environments.keys.join(", ")}"
15
15
  end
16
16
  end
17
17
 
@@ -51,8 +51,8 @@ module CurrencyCloud
51
51
  private
52
52
  def validate
53
53
  self.class.validate_environment(environment)
54
- raise CurrencyCloud::ConfigError, "login_id must be set using CurrencyCloud.login_id=" unless login_id
55
- raise CurrencyCloud::ConfigError, "api_key must be set using CurrencyCloud.api_key=" unless api_key
54
+ raise CurrencyCloud::GeneralError, "login_id must be set using CurrencyCloud.login_id=" unless login_id
55
+ raise CurrencyCloud::GeneralError, "api_key must be set using CurrencyCloud.api_key=" unless api_key
56
56
  end
57
57
 
58
58
  def request
@@ -3,6 +3,6 @@
3
3
  module CurrencyCloud
4
4
 
5
5
  ApiVersion = 'v2' # API Version
6
- Version = '0.5' # Gem Version
6
+ Version = '0.7' # Gem Version
7
7
 
8
8
  end
@@ -0,0 +1,32 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'RequestHandler' do
4
+ let(:session) { CurrencyCloud::Session.new(:demonstration, nil, nil, '4df5b3e5882a412f148dcd08fa4e5b73')}
5
+ let(:response) { double('Response', code: 200, body: '{}') }
6
+
7
+ subject { CurrencyCloud::RequestHandler.new(session) }
8
+
9
+ describe 'with on_behalf_of parameter' do
10
+ it "adds it to parameters in HTTP call" do
11
+ session.on_behalf_of = "d1f7f5c2-4187-41da-88fc-b3ae40fa958f"
12
+
13
+ allow(HTTParty).to receive(:post) do |_, params|
14
+ expect(params).to include(:body)
15
+ expect(params[:body]).to include(on_behalf_of: "d1f7f5c2-4187-41da-88fc-b3ae40fa958f")
16
+ end.and_return(response)
17
+
18
+ subject.post('accounts/create', account_name: "Test Account")
19
+ end
20
+
21
+ it "ignores it if it's not a uuid" do
22
+ session.on_behalf_of = "nonsense variable"
23
+
24
+ allow(HTTParty).to receive(:post) do |_, params|
25
+ expect(params).to include(:body)
26
+ expect(params[:body]).to eq(account_name: "Test Account")
27
+ end.and_return(response)
28
+
29
+ subject.post('accounts/create', account_name: "Test Account")
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,37 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Resource' do
4
+ before(:all) do
5
+ class Person < CurrencyCloud::Resource
6
+ resource :people
7
+ actions :update, :delete
8
+ end
9
+ end
10
+
11
+ describe "#save" do
12
+ it "only updates changed records" do
13
+ person = Person.new(id: 1, name: 'Richard', surname: 'Nienaber')
14
+ allow(person).to receive(:post).with(1, name: 'John')
15
+ person.name = 'John'
16
+
17
+ expect(person.save).to eq(person)
18
+ expect(person.changed_attributes).to eq(Set.new)
19
+ end
20
+
21
+ it "does nothing if nothing has changed" do
22
+ person = Person.new(id: 1, name: 'Richard', surname: 'Nienaber')
23
+
24
+ expect(person.save).to eq(person)
25
+ expect(person.changed_attributes).to eq(Set.new)
26
+ end
27
+ end
28
+
29
+ describe "#delete" do
30
+ it 'removes resource' do
31
+ person = Person.new(id: 1, name: 'Richard', surname: 'Nienaber')
32
+ allow(Person).to receive(:post).with('1/delete')
33
+
34
+ expect(person.delete).to eq(person)
35
+ end
36
+ end
37
+ end
@@ -2,7 +2,7 @@ require 'spec_helper'
2
2
 
3
3
  describe CurrencyCloud do
4
4
 
5
- before(:each) do
5
+ before do
6
6
  CurrencyCloud.environment = nil
7
7
  CurrencyCloud.login_id = nil
8
8
  CurrencyCloud.api_key = nil
@@ -53,26 +53,70 @@ describe CurrencyCloud do
53
53
  it "raises an error if the environment is not set" do
54
54
  CurrencyCloud.environment = nil
55
55
  expect { CurrencyCloud.session }
56
- .to raise_error(CurrencyCloud::ConfigError, "'' is not a valid environment, must be one of: production, demonstration, uat")
56
+ .to raise_error(CurrencyCloud::GeneralError, "'' is not a valid environment, must be one of: production, demonstration, uat")
57
57
  end
58
58
 
59
59
  it "raises an error if the environment is invalid" do
60
60
  CurrencyCloud.environment = :invalid
61
61
  expect { CurrencyCloud.session }
62
- .to raise_error(CurrencyCloud::ConfigError, "'invalid' is not a valid environment, must be one of: production, demonstration, uat")
62
+ .to raise_error(CurrencyCloud::GeneralError, "'invalid' is not a valid environment, must be one of: production, demonstration, uat")
63
63
  end
64
64
 
65
65
  it "raises an error if the login_id is not set" do
66
66
  CurrencyCloud.environment = :demonstration
67
67
  expect { CurrencyCloud.session }
68
- .to raise_error(CurrencyCloud::ConfigError, "login_id must be set using CurrencyCloud.login_id=")
68
+ .to raise_error(CurrencyCloud::GeneralError, "login_id must be set using CurrencyCloud.login_id=")
69
69
  end
70
70
 
71
71
  it "raises an error if the api_key is not set" do
72
72
  CurrencyCloud.environment = :demonstration
73
73
  CurrencyCloud.login_id = 'test@example.com'
74
74
  expect { CurrencyCloud.session }
75
- .to raise_error(CurrencyCloud::ConfigError, "api_key must be set using CurrencyCloud.api_key=")
75
+ .to raise_error(CurrencyCloud::GeneralError, "api_key must be set using CurrencyCloud.api_key=")
76
76
  end
77
77
  end
78
+
79
+ describe "#on_behalf_of" do
80
+ before do
81
+ CurrencyCloud.environment = :demonstration
82
+ CurrencyCloud.token = '4df5b3e5882a412f148dcd08fa4e5b73'
83
+ CurrencyCloud.session.on_behalf_of = nil
84
+ end
85
+
86
+ it "sets the value on the session, and removes it when done" do
87
+ CurrencyCloud.on_behalf_of('c6ece846-6df1-461d-acaa-b42a6aa74045') do
88
+ expect(CurrencyCloud.session.on_behalf_of).to eq('c6ece846-6df1-461d-acaa-b42a6aa74045')
89
+ end
90
+ expect(CurrencyCloud.session.on_behalf_of).to be_nil
91
+ end
92
+
93
+ it "still removes the value from the session on error" do
94
+ expect do
95
+ CurrencyCloud.on_behalf_of('c6ece846-6df1-461d-acaa-b42a6aa74045') do
96
+ expect(CurrencyCloud.session.on_behalf_of).to eq('c6ece846-6df1-461d-acaa-b42a6aa74045')
97
+ raise 'Completed Expected error'
98
+ end
99
+ end.to raise_error('Completed Expected error')
100
+
101
+ expect(CurrencyCloud.session.on_behalf_of).to be_nil
102
+ end
103
+
104
+ it 'prevents reentrant usage' do
105
+ expect do
106
+ CurrencyCloud.on_behalf_of('c6ece846-6df1-461d-acaa-b42a6aa74045') do
107
+ CurrencyCloud.on_behalf_of('f57b2d33-652c-4589-a8ff-7762add2706d') do
108
+ raise "Should raise exception"
109
+ end
110
+ end
111
+ end.to raise_error(CurrencyCloud::GeneralError, "#on_behalf_of has already been set")
112
+ end
113
+
114
+ it 'prevents reentrant usage' do
115
+ expect do
116
+ CurrencyCloud.on_behalf_of('Richard Nienaber') do
117
+ raise "Should raise exception"
118
+ end
119
+ end.to raise_error(CurrencyCloud::GeneralError, "contact id for on behalf of is not a UUID")
120
+ end
121
+ end
78
122
  end
@@ -98,4 +98,32 @@ describe 'Actions', :vcr => true do
98
98
  expect(account.created_at).to eq('2015-04-24T15:57:55+00:00')
99
99
  expect(account.updated_at).to eq('2015-04-24T15:57:55+00:00')
100
100
  end
101
+
102
+ it "can #validate beneficiaries" do
103
+ params = {
104
+ :bank_country => 'GB',
105
+ :currency => 'GBP',
106
+ :account_number => '12345678',
107
+ :routing_code_type_1 => 'sort_code',
108
+ :routing_code_value_1 => '123456',
109
+ :payment_types => ['regular']
110
+ }
111
+
112
+ beneficiary = CurrencyCloud::Beneficiary.validate(params)
113
+ expect(beneficiary).to be_a_kind_of(CurrencyCloud::Beneficiary)
114
+
115
+ expect(beneficiary.account_number).to include('12345678')
116
+ expect(beneficiary.payment_types).to include('regular')
117
+ end
118
+
119
+ it "can use #currency to retrieve balance" do
120
+ balance = CurrencyCloud::Balance.currency('GBP')
121
+
122
+ expect(balance).to be_a_kind_of(CurrencyCloud::Balance)
123
+
124
+ expect(balance.id).to eq("5a998e06-3eb7-46d6-ba58-f749864159ce")
125
+ expect(balance.amount).to eq('999866.78')
126
+ expect(balance.created_at).to eq('2014-12-04T09:50:35+00:00')
127
+ expect(balance.updated_at).to eq('2015-03-23T14:33:37+00:00')
128
+ end
101
129
  end
@@ -10,6 +10,40 @@ describe 'Error', :vcr => true do
10
10
  CurrencyCloud.token = nil
11
11
  end
12
12
 
13
+ it 'contains full details for api error' do
14
+ CurrencyCloud.login_id = 'non-existent-login-id'
15
+ CurrencyCloud.api_key = 'ef0fd50fca1fb14c1fab3a8436b9ecb57528f0'
16
+
17
+ error = nil
18
+ begin
19
+ CurrencyCloud.session
20
+ raise 'Should have failed'
21
+ rescue CurrencyCloud::BadRequestError => error
22
+ end
23
+
24
+ expected_error = %Q{CurrencyCloud::BadRequestError
25
+ ---
26
+ platform: #{error.platform}
27
+ request:
28
+ parameters:
29
+ login_id: non-existent-login-id
30
+ api_key: ef0fd50fca1fb14c1fab3a8436b9ecb57528f0
31
+ verb: post
32
+ url: https://devapi.thecurrencycloud.com/v2/authenticate/api
33
+ response:
34
+ status_code: 400
35
+ date: Wed, 29 Apr 2015 22:46:53 GMT
36
+ request_id: 2775253392756800903
37
+ errors:
38
+ - field: api_key
39
+ code: api_key_length_is_invalid
40
+ message: api_key should be 64 character(s) long
41
+ params:
42
+ length: 64
43
+ }
44
+ expect(error.to_s).to eq(expected_error)
45
+ end
46
+
13
47
  it 'is raised on a bad request' do
14
48
  CurrencyCloud.login_id = 'non-existent-login-id'
15
49
  CurrencyCloud.api_key = 'ef0fd50fca1fb14c1fab3a8436b9ecb57528f0'
@@ -66,6 +100,19 @@ describe 'Error', :vcr => true do
66
100
  rescue CurrencyCloud::UnexpectedError => error
67
101
  end
68
102
 
103
+ expected_error = %Q{CurrencyCloud::UnexpectedError
104
+ ---
105
+ platform: #{error.platform}
106
+ request:
107
+ parameters:
108
+ login_id: rjnienaber@gmail.com
109
+ api_key: ef0fd50fca1fb14c1fab3a8436b9ecb65f02f129fd87eafa45ded8ae257528f0
110
+ verb: post
111
+ url: https://devapi.thecurrencycloud.com/v2/authenticate/api
112
+ inner_error: Timeout::Error
113
+ }
114
+
115
+ expect(error.to_s).to eq(expected_error)
69
116
  expect(error.inner_error).to_not be_nil
70
117
  expect(error.inner_error.class).to eq(Timeout::Error)
71
118
  end
@@ -4,7 +4,7 @@ describe 'Rates', :vcr => true do
4
4
  before do
5
5
  CurrencyCloud.reset_session
6
6
  CurrencyCloud.environment = :demonstration
7
- CurrencyCloud.token = '242993ca94b9d1c6c1d8f7d3275a6f36'
7
+ CurrencyCloud.token = 'bbdd421bdda373ea69670c9101fa9197'
8
8
  end
9
9
 
10
10
  it 'can #find' do