currency_cloud 0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +1 -0
  3. data/.ruby-gemset +1 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +12 -0
  6. data/Gemfile +7 -0
  7. data/Gemfile.lock +99 -0
  8. data/Guardfile +14 -0
  9. data/LICENSE.md +22 -0
  10. data/README.md +10 -0
  11. data/Rakefile +30 -0
  12. data/currency_cloud.gemspec +27 -0
  13. data/examples/server.rb +15 -0
  14. data/lib/currency_cloud.rb +26 -0
  15. data/lib/currency_cloud/actions/create.rb +9 -0
  16. data/lib/currency_cloud/actions/current.rb +9 -0
  17. data/lib/currency_cloud/actions/delete.rb +15 -0
  18. data/lib/currency_cloud/actions/find.rb +27 -0
  19. data/lib/currency_cloud/actions/retrieve.rb +9 -0
  20. data/lib/currency_cloud/actions/update.rb +12 -0
  21. data/lib/currency_cloud/errors/api_error.rb +38 -0
  22. data/lib/currency_cloud/errors/config_error.rb +5 -0
  23. data/lib/currency_cloud/errors/unexpected_error.rb +10 -0
  24. data/lib/currency_cloud/pagination.rb +7 -0
  25. data/lib/currency_cloud/request_handler.rb +68 -0
  26. data/lib/currency_cloud/resource.rb +73 -0
  27. data/lib/currency_cloud/resourceful_collection.rb +15 -0
  28. data/lib/currency_cloud/resources/account.rb +6 -0
  29. data/lib/currency_cloud/resources/balance.rb +8 -0
  30. data/lib/currency_cloud/resources/beneficiary.rb +14 -0
  31. data/lib/currency_cloud/resources/contact.rb +6 -0
  32. data/lib/currency_cloud/resources/conversion.rb +10 -0
  33. data/lib/currency_cloud/resources/payer.rb +6 -0
  34. data/lib/currency_cloud/resources/payment.rb +11 -0
  35. data/lib/currency_cloud/resources/rate.rb +21 -0
  36. data/lib/currency_cloud/resources/reference.rb +29 -0
  37. data/lib/currency_cloud/resources/settlement.rb +30 -0
  38. data/lib/currency_cloud/resources/transaction.rb +7 -0
  39. data/lib/currency_cloud/response_handler.rb +46 -0
  40. data/lib/currency_cloud/session.rb +62 -0
  41. data/lib/currency_cloud/version.rb +8 -0
  42. data/spec/currency_cloud_spec.rb +78 -0
  43. data/spec/integration/actions_spec.rb +101 -0
  44. data/spec/integration/authentication_spec.rb +37 -0
  45. data/spec/integration/errors_spec.rb +157 -0
  46. data/spec/integration/rates_spec.rb +39 -0
  47. data/spec/integration/reference_spec.rb +57 -0
  48. data/spec/spec_helper.rb +11 -0
  49. data/spec/support/vcr_cassettes/Actions/can_create.yml +38 -0
  50. data/spec/support/vcr_cassettes/Actions/can_current.yml +37 -0
  51. data/spec/support/vcr_cassettes/Actions/can_delete.yml +39 -0
  52. data/spec/support/vcr_cassettes/Actions/can_find.yml +38 -0
  53. data/spec/support/vcr_cassettes/Actions/can_first.yml +75 -0
  54. data/spec/support/vcr_cassettes/Actions/can_retrieve.yml +38 -0
  55. data/spec/support/vcr_cassettes/Actions/can_update.yml +39 -0
  56. data/spec/support/vcr_cassettes/Authentication/can_be_closed.yml +67 -0
  57. data/spec/support/vcr_cassettes/Authentication/can_use_just_a_token.yml +36 -0
  58. data/spec/support/vcr_cassettes/Authentication/handles_session_timeout_error.yml +101 -0
  59. data/spec/support/vcr_cassettes/Authentication/happens_lazily.yml +34 -0
  60. data/spec/support/vcr_cassettes/Error/is_raised_on_a_bad_request.yml +39 -0
  61. data/spec/support/vcr_cassettes/Error/is_raised_on_a_forbidden_request.yml +35 -0
  62. data/spec/support/vcr_cassettes/Error/is_raised_on_an_internal_server_error.yml +69 -0
  63. data/spec/support/vcr_cassettes/Error/is_raised_on_incorrect_authentication_details.yml +39 -0
  64. data/spec/support/vcr_cassettes/Error/is_raised_when_a_resource_is_not_found.yml +37 -0
  65. data/spec/support/vcr_cassettes/Error/is_raised_when_too_many_requests_have_been_issued.yml +34 -0
  66. data/spec/support/vcr_cassettes/Rates/can_find.yml +36 -0
  67. data/spec/support/vcr_cassettes/Rates/can_provided_detailed_rate.yml +36 -0
  68. data/spec/support/vcr_cassettes/Reference/can_retrieve_beneficiary_required_details.yml +36 -0
  69. data/spec/support/vcr_cassettes/Reference/can_retrieve_conversion_dates.yml +46 -0
  70. data/spec/support/vcr_cassettes/Reference/can_retrieve_currencies.yml +47 -0
  71. data/spec/support/vcr_cassettes/Reference/can_retrieve_settlement_accounts.yml +39 -0
  72. metadata +230 -0
@@ -0,0 +1,7 @@
1
+ module CurrencyCloud
2
+
3
+ class Pagination < OpenStruct
4
+
5
+ end
6
+
7
+ end
@@ -0,0 +1,68 @@
1
+ module CurrencyCloud
2
+
3
+ class RequestHandler
4
+
5
+ attr_reader :session
6
+
7
+ def initialize(session = CurrencyCloud.session)
8
+ @session = session
9
+ end
10
+
11
+ def get(route, params={}, opts={})
12
+ retry_authenticate('get', route, params, opts) do |url, options|
13
+ HTTParty.get(url, options)
14
+ end
15
+ end
16
+
17
+ def post(route, params={}, opts={})
18
+ retry_authenticate('post', route, params, opts) do |url, options|
19
+ HTTParty.post(url, options)
20
+ end
21
+ end
22
+
23
+ private
24
+ def retry_authenticate(verb, route, params, opts)
25
+ should_retry = opts[:should_retry].nil? ? true : opts.delete(:should_retry)
26
+ description = "to #{verb}: #{route}"
27
+
28
+ options = process_options(verb, params, opts)
29
+
30
+ response = nil
31
+ retry_count = should_retry ? 0 : 2
32
+ while retry_count < 3
33
+ response = yield(full_url(route), options)
34
+ break unless response.code == 401 && should_retry
35
+ session.reauthenticate
36
+ retry_count += 1
37
+ end
38
+
39
+ process_response(response)
40
+ rescue => e
41
+ raise if e.class.ancestors.include?(ApiError) || e.is_a?(UnexpectedError)
42
+ raise UnexpectedError.new(e)
43
+ end
44
+
45
+ def process_options(verb, params, opts)
46
+ options = {:headers => headers }
47
+ params_key = verb == :get ? :query : :body
48
+ options[params_key] = params
49
+ options.merge!(opts)
50
+ # options[:debug_output] = $stdout
51
+ options
52
+ end
53
+
54
+ def headers
55
+ headers = {}
56
+ headers['X-Auth-Token'] = session.token if session.token
57
+ headers
58
+ end
59
+
60
+ def full_url(route)
61
+ "#{session.environment_url}/#{CurrencyCloud::ApiVersion}/" + route
62
+ end
63
+
64
+ def process_response(response)
65
+ CurrencyCloud::ResponseHandler.process(response)
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,73 @@
1
+ require 'set'
2
+
3
+ module CurrencyCloud
4
+
5
+ class Resource
6
+
7
+ def self.resource(resource=nil)
8
+ @resource ||= resource
9
+ end
10
+
11
+ def self.actions(*actions)
12
+ @actions ||= actions
13
+ @actions.each do |action|
14
+ self.class_eval do # self.class.instance_eval # metaclass.instance_eval
15
+ action_module = CurrencyCloud::Actions.const_get(action.to_s.capitalize)
16
+ self.extend(action_module)
17
+ end
18
+ end
19
+ end
20
+
21
+ def initialize(object)
22
+ @object = object
23
+ @changed_attributes = Set.new
24
+ set_accessors(keys)
25
+ end
26
+
27
+ def keys
28
+ @object.keys
29
+ end
30
+
31
+ def inspect
32
+ "#<#{self.class}:0x#{self.object_id.to_s(16)} #{@object.inspect}>"
33
+ end
34
+
35
+ private
36
+
37
+ def changed?
38
+ !@changed_attributes.empty?
39
+ end
40
+
41
+ def metaclass
42
+ class << self; self; end
43
+ end
44
+
45
+ def set_accessors(keys)
46
+ metaclass.instance_eval do
47
+ keys.each do |key|
48
+ define_method(key) { @object[key] }
49
+ define_method("#{key}=".to_sym) do |value|
50
+ @object[key] = value
51
+ @changed_attributes << key
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+ def self.get(url, params={})
58
+ new(request.get(build_url(url), params))
59
+ end
60
+
61
+ def self.post(url, params={})
62
+ new(request.post(build_url(url), params))
63
+ end
64
+
65
+ def self.build_url(url)
66
+ "#{self.resource}/#{url}"
67
+ end
68
+
69
+ def self.request
70
+ RequestHandler.new
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,15 @@
1
+ module CurrencyCloud
2
+ class ResourcefulCollection
3
+ include Enumerable
4
+
5
+ extend Forwardable
6
+ def_delegators :@collection, :[], :length, :empty?, :each
7
+
8
+ attr_reader :pagination
9
+
10
+ def initialize(resource, klass, collection)
11
+ @collection = collection[resource.to_s].map { |object| klass.new(object) }
12
+ @pagination = Pagination.new(collection['pagination'])
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,6 @@
1
+ module CurrencyCloud
2
+ class Account < Resource
3
+ resource :accounts
4
+ actions :create, :retrieve, :find, :update, :current
5
+ end
6
+ end
@@ -0,0 +1,8 @@
1
+ module CurrencyCloud
2
+ class Balance < Resource
3
+ resource :balances
4
+ actions :find
5
+
6
+ #TODO: currency
7
+ end
8
+ end
@@ -0,0 +1,14 @@
1
+ module CurrencyCloud
2
+
3
+ class Beneficiary < Resource
4
+
5
+ resource :beneficiaries
6
+
7
+ actions :create, :retrieve, :find, :update, :delete
8
+
9
+ def self.validate(params)
10
+ new(request.get("#{self.resource}/validate", params))
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,6 @@
1
+ module CurrencyCloud
2
+ class Contact < Resource
3
+ resource :contacts
4
+ actions :create, :retrieve, :find, :update, :current
5
+ end
6
+ end
@@ -0,0 +1,10 @@
1
+ module CurrencyCloud
2
+
3
+ class Conversion < Resource
4
+
5
+ resource :conversions
6
+
7
+ actions :create, :retrieve, :find
8
+
9
+ end
10
+ end
@@ -0,0 +1,6 @@
1
+ module CurrencyCloud
2
+ class Payer < Resource
3
+ resource :payers
4
+ actions :retrieve
5
+ end
6
+ end
@@ -0,0 +1,11 @@
1
+ module CurrencyCloud
2
+
3
+ class Payment < Resource
4
+
5
+ resource :payments
6
+
7
+ actions :create, :retrieve, :find, :delete, :update
8
+
9
+ #TODO: authorize, authorizations
10
+ end
11
+ end
@@ -0,0 +1,21 @@
1
+ module CurrencyCloud
2
+ class Rates < Resource; end
3
+
4
+ class Rate < Resource
5
+ resource :rates
6
+
7
+ def self.find(params)
8
+ response = request.get("#{self.resource}/find", params)
9
+
10
+ rates = response['rates'].map do |currency_pair, bid_offer|
11
+ new(currency_pair: currency_pair, bid: bid_offer[0], offer: bid_offer[1])
12
+ end
13
+
14
+ Rates.new(currencies: rates, unavailable: response['unavailable'])
15
+ end
16
+
17
+ def self.detailed(params)
18
+ get('detailed', params)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,29 @@
1
+ module CurrencyCloud
2
+
3
+ class Currency < Resource; end
4
+ class ConversionDates < Resource; end
5
+ class SettlementAccount < Resource; end
6
+
7
+ class Reference < Resource
8
+
9
+ resource :reference
10
+
11
+ def self.currencies
12
+ response = request.get(build_url("currencies"))
13
+ response['currencies'].map { |c| Currency.new(c)}
14
+ end
15
+
16
+ def self.beneficiary_required_details(params={})
17
+ request.get(build_url("beneficiary_required_details"), params)['details']
18
+ end
19
+
20
+ def self.conversion_dates(params)
21
+ ConversionDates.new(request.get(build_url("conversion_dates"), params))
22
+ end
23
+
24
+ def self.settlement_accounts(params={})
25
+ response = request.get(build_url("settlement_accounts"), params)
26
+ response['settlement_accounts'].map { |s| SettlementAccount.new(s)}
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,30 @@
1
+ module CurrencyCloud
2
+
3
+ class Settlement < Resource
4
+
5
+ resource :settlements
6
+
7
+ actions :create, :retrieve, :find, :delete
8
+
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))
12
+ end
13
+
14
+ 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))
17
+ end
18
+
19
+ def release
20
+ # TODO: Should just update state of current object using a refresh method?
21
+ new(request.post("#{self.resource}/#{self.id}/release"))
22
+ end
23
+
24
+ def unrelease
25
+ # TODO: Should just update state of current object using a refresh method?
26
+ new(request.post("#{self.resource}/#{self.id}/unrelease"))
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,7 @@
1
+ module CurrencyCloud
2
+ class Transaction < Resource
3
+ resource :transactions
4
+
5
+ actions :retrieve, :find
6
+ end
7
+ end
@@ -0,0 +1,46 @@
1
+ module CurrencyCloud
2
+
3
+ class ResponseHandler
4
+
5
+ attr_reader :response
6
+
7
+ def self.process(response)
8
+ new(response).data
9
+ end
10
+
11
+ def initialize(response)
12
+ @response = response
13
+ end
14
+
15
+ def data
16
+ if success?
17
+ return parsed_response
18
+ else
19
+ handle_failure
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def success?
26
+ [200, 202].include?(response.code)
27
+ end
28
+
29
+ def handle_failure
30
+ 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
38
+ end
39
+ raise error_class.new(response)
40
+ end
41
+
42
+ def parsed_response
43
+ @parsed_response ||= JSON.parse(response.body)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,62 @@
1
+ module CurrencyCloud
2
+
3
+ class Session
4
+
5
+ Environments = { :production => 'https://api.thecurrencycloud.com',
6
+ :demonstration => 'https://devapi.thecurrencycloud.com',
7
+ :uat => 'https://api-uat1.ccycloud.com'}
8
+
9
+ attr_reader :environment, :login_id, :api_key
10
+ attr_accessor :token
11
+
12
+ def self.validate_environment(environment)
13
+ unless Environments.keys.include?(environment)
14
+ raise CurrencyCloud::ConfigError, "'#{environment}' is not a valid environment, must be one of: #{Environments.keys.join(", ")}"
15
+ end
16
+ end
17
+
18
+ def initialize(environment, login_id, api_key, token)
19
+ @environment = environment
20
+ @login_id = login_id
21
+ @api_key = api_key
22
+
23
+ if token
24
+ self.class.validate_environment(environment)
25
+ @token = token
26
+ else
27
+ authenticate
28
+ end
29
+ end
30
+
31
+ def environment_url
32
+ Environments[environment]
33
+ end
34
+
35
+ def close
36
+ request.post('authenticate/close_session')
37
+ end
38
+
39
+
40
+ def authenticate
41
+ validate
42
+ params = {:login_id => login_id, :api_key => api_key}
43
+ @token = request.post('authenticate/api', params, :should_retry => false)['auth_token']
44
+ end
45
+
46
+ def reauthenticate
47
+ token = nil
48
+ authenticate
49
+ end
50
+
51
+ private
52
+ def validate
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
56
+ end
57
+
58
+ def request
59
+ RequestHandler.new(self)
60
+ end
61
+ end
62
+ end