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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c3d01c436b1a1da0688c30dce2f273fef5b15ee9
4
- data.tar.gz: a4cc4a7719b2c9de95d0628c9b9c2a7f6a332fc5
3
+ metadata.gz: 9d6cc6e85c0ee4e1d64341df25e70cc470f27e2a
4
+ data.tar.gz: fb7dc0195ec0f39ff88d35739dee63e0c5b5a02a
5
5
  SHA512:
6
- metadata.gz: c312578d3bb0aa6f831ab88567c987943a342d995419deb63d5ed36a32b0f251c867114fa5dc8870d71c4d996657382939f26b9e382040d77bc28ce3a925ed4a
7
- data.tar.gz: 42cd53dcffc54a796656728a3cdab39083fc4149bcc225d6e2d42d99b9d2e6e655c5c25516f1a1aa9642dcb62b265fb168f4ee3a50697a7156e800a123cec720
6
+ metadata.gz: f00785918f0a998f708a2dfde29b667f804c019afbb25aae4c2d0af8e37bbd7cbe627f4ee328dd789da163d8670b7e09a37241cb21808dae2302d9f5a5a58d59
7
+ data.tar.gz: cc23502771aaa44dfa7ec5610af08279dde6de96cb7ed3df47a526624acbc1f344ad57c9a38cdf38568a535be09f2d3945a2dd3d7b346d6fb92cef01ef528747
data/.gitignore CHANGED
@@ -1 +1,2 @@
1
1
  .DS_Store
2
+ *.gem
data/README.md CHANGED
@@ -1,10 +1,153 @@
1
- [![Build Status](https://travis-ci.org/CurrencyCloud/currencycloud-ruby.png?branch=master)](https://travis-ci.org/CurrencyCloud/currencycloud-ruby)
1
+ [![Gem Version](https://badge.fury.io/rb/currency_cloud.svg)](http://badge.fury.io/rb/currency_cloud) [![Build Status](https://travis-ci.org/CurrencyCloud/currencycloud-ruby.png?branch=master)](https://travis-ci.org/CurrencyCloud/currencycloud-ruby)
2
2
 
3
- ### TODO
3
+ # Currency Cloud
4
4
 
5
- - Additional debug logging
6
- - Update from object (.save method)
7
- - Delete from object (.delete method)
8
- - Relationships: has_many, has_one, belongs_to
9
- - pagination - .next_page, .previous_page
10
- - On behalf of
5
+ This is the official Ruby SDK for v2 of Currency Cloud's API. Additional documentation
6
+ for each API endpoint can be found at [connect.currencycloud.com][connect]
7
+
8
+ ## Installation
9
+
10
+ You don't need this source code unless you want to modify the gem. If
11
+ you just want to use the library in your application, you should run:
12
+
13
+ `gem install currency_cloud`
14
+
15
+ If you want to build the gem from source:
16
+
17
+ `gem build currency_cloud.gemspec`
18
+
19
+ ## Supported Ruby versions
20
+
21
+ This library aims to support and is [tested against][travis] the following Ruby
22
+ implementations:
23
+
24
+ * MRI 1.9.3
25
+ * MRI 2.0.0
26
+ * MRI 2.1.0
27
+ * MRI 2.2.0
28
+ * [JRuby][jruby]
29
+ * [Rubinius][rubinius]
30
+
31
+ # Usage
32
+
33
+ ```ruby
34
+ require 'currency_cloud'
35
+
36
+ ## Configure ##
37
+ CurrencyCloud.login_id = '<your login id>'
38
+ CurrencyCloud.api_key = '<your api key>'
39
+ CurrencyCloud.environment = :demonstration # use :production when ready
40
+
41
+ ## Make API calls ##
42
+ currencies = CurrencyCloud::Reference.currencies
43
+ #[#<CurrencyCloud::Currency:0x3fcb6d03960c {"code"=>"AED", "decimal_places"=>2, "name"=>"United Arab Emirates Dirham"}>,
44
+ # #<CurrencyCloud::Currency:0x3fcb6d0390f8 {"code"=>"AUD", "decimal_places"=>2, "name"=>"Australian Dollar"}>,
45
+ # #<CurrencyCloud::Currency:0x3fcb6d038d24 {"code"=>"CAD", "decimal_places"=>2, "name"=>"Canadian Dollar"}>,
46
+ # <snip .../>
47
+ # #<CurrencyCloud::Currency:0x3fcb6d0295b8 {"code"=>"TRY", "decimal_places"=>2, "name"=>"Turkish Lira"}>,
48
+ # #<CurrencyCloud::Currency:0x3fcb6d028fc8 {"code"=>"USD", "decimal_places"=>2, "name"=>"United States Dollar"}>,
49
+ # #<CurrencyCloud::Currency:0x3fcb6d0288d4 {"code"=>"ZAR", "decimal_places"=>2, "name"=>"South African Rand"}>]
50
+
51
+ balances = CurrencyCloud::Balance.find
52
+
53
+ #<CurrencyCloud::Balances:0x007f96da812838
54
+ # @collection=
55
+ # [#<CurrencyCloud::Balance:0x3fcb6d4093b8 {"id"=>"5a998e06-3eb7-46d6-ba58-f749864159ce", "account_id"=>"e7483671-5dc6-0132-e126-002219414986", "currency"=>"GBP", "amount"=>"20034.78", "created_at"=>"2014-12-04T09:50:35+00:00", "updated_at"=>"2015-03-23T14:33:37+00:00"}>,
56
+ # #<CurrencyCloud::Balance:0x3fcb6d4089b8 {"id"=>"7b3796d0-35f3-4f47-8554-8020750a8f9d", "account_id"=>"e7483671-5dc6-0132-e126-002219414986", "currency"=>"EUR", "amount"=>"3012.16", "created_at"=>"2014-12-04T09:50:41+00:00", "updated_at"=>"2015-04-13T08:22:50+00:00"}>,
57
+ # @pagination=
58
+ # #<CurrencyCloud::Pagination total_entries=5, total_pages=1, current_page=1, per_page=25, previous_page=-1, next_page=-1, order="created_at", order_asc_desc="asc">>
59
+ ```
60
+
61
+ ## On Behalf Of
62
+ If you want to make calls on behalf of another user (e.g. someone who has a sub-account with you), you
63
+ can execute certain commands 'on behalf of' the user's contact_id. Here is an example:
64
+
65
+ ```ruby
66
+ CurrencyCloud.on_behalf_of("c6ece846-6df1-461d-acaa-b42a6aa74045") do
67
+ beneficiary = CurrencyCloud::Beneficiary.create(<params>)
68
+ conversion = CurrencyCloud::Conversion.create(<params>)
69
+ payment = CurrencyCloud::Payment.create(<params>)
70
+ end
71
+ ```
72
+
73
+ Alternatively, you can just merge it with an existing params `Hash`, for example:
74
+
75
+ ```ruby
76
+ CurrencyCloud::Account.create(account_name: 'My Test User', on_behalf_of: "c6ece846-6df1-461d-acaa-b42a6aa74045")
77
+ ```
78
+
79
+ Each of the above transactions will be executed in scope of the limits for that contact and linked to that contact. Note
80
+ that the real user who executed the transaction will also be stored.
81
+
82
+
83
+ ## Errors
84
+ When an error occurs in the API, the library aims to give us much information
85
+ as possible. Here is an example:
86
+
87
+ ```yaml
88
+ CurrencyCloud::BadRequestError
89
+ ---
90
+ platform: Ruby 1.9.3
91
+ request:
92
+ parameters:
93
+ login_id: non-existent-login-id
94
+ api_key: ef0fd50fca1fb14c1fab3a8436b9ecb57528f0
95
+ verb: post
96
+ url: https://devapi.thecurrencycloud.com/v2/authenticate/api
97
+ response:
98
+ status_code: 400
99
+ date: Wed, 29 Apr 2015 22:46:53 GMT
100
+ request_id: 2775253392756800903
101
+ errors:
102
+ - field: api_key
103
+ code: api_key_length_is_invalid
104
+ message: api_key should be 64 character(s) long
105
+ params:
106
+ length: 64
107
+ ```
108
+
109
+ This is split into 5 sections:
110
+
111
+ 1. Error Type: In this case `BadRequestError` represents an HTTP 400 error
112
+ 2. Platform: The Ruby implementation that was used e.g. 'Ruby 2.2' or 'Ruby 1.9.3 (JRuby 1.7.8)''
113
+ 3. Request: Details about the HTTP request that was made e.g. the POST parameters
114
+ 4. Response: Details about the HTTP response that was returned e.g. HTTP status code
115
+ 5. Errors: A list of errors that provide additional information
116
+
117
+ The final section contains valuable information:
118
+
119
+ - Field: The parameter that the error is linked to
120
+ - Code: A code representing this error
121
+ - Message: A human readable message that explains the error
122
+ - Params: A hash that contains dynamic parts of the error message for building custom error messages
123
+
124
+ When troubleshooting API calls with Currency Cloud support, including the full
125
+ error in any correspondence can be very helpful.
126
+
127
+ # Development
128
+
129
+ Test cases can be run with: `bundle exec rspec`. There is also a `Guardfile` that
130
+ is useful for continuously rerunning tests while you develop.
131
+
132
+ ## Dependencies
133
+ * [HTTParty][httparty]
134
+ * [json][json]
135
+
136
+ ## Versioning
137
+
138
+ This project uses [semantic versioning][semver]. You can safely
139
+ express a dependency on a major version and expect all minor and patch versions
140
+ to be backwards compatible.
141
+
142
+ # Copyright
143
+
144
+ Copyright (c) 2015 Currency Cloud. See [LICENSE][license] for details.
145
+
146
+ [connect]: https://connect.currencycloud.com/documentation/getting-started/introduction
147
+ [travis]: https://travis-ci.org/CurrencyCloud/currencycloud-ruby
148
+ [jruby]: http://jruby.org/
149
+ [rubinius]: http://rubini.us/
150
+ [httparty]: https://github.com/jnunemaker/httparty
151
+ [json]: https://github.com/intridea/multi_json
152
+ [semver]: http://semver.org/
153
+ [license]: LICENSE.md
@@ -11,6 +11,7 @@ spec = Gem::Specification.new do |s|
11
11
  s.email = ['liam.mcandrew@currencycloud.com', 'richard.nienaber@currencycloud.com']
12
12
  s.homepage = 'https://connect.currencycloud.com/documentation/getting-started/introduction'
13
13
  s.licenses = ['MIT']
14
+ s.required_ruby_version = '>= 1.9'
14
15
 
15
16
  s.add_dependency('httparty', '~> 0.13')
16
17
  s.add_dependency('json', '~> 1.8')
@@ -3,10 +3,13 @@ require 'httparty'
3
3
  require 'json'
4
4
 
5
5
  require 'currency_cloud/resource'
6
+ require 'currency_cloud/errors/error_utils'
6
7
  lib_path = File.join(File.dirname(__FILE__), '**/*.*')
7
8
  Dir[lib_path].sort.each { |f| require f}
8
9
 
9
10
  module CurrencyCloud
11
+ UUID_REGEX = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i
12
+
10
13
  class << self
11
14
  attr_accessor :environment, :login_id, :api_key, :token
12
15
  end
@@ -22,5 +25,16 @@ module CurrencyCloud
22
25
 
23
26
  def self.reset_session
24
27
  @session = nil
28
+ @token = nil
29
+ end
30
+
31
+ def self.on_behalf_of(contact_id)
32
+ raise CurrencyCloud::GeneralError, '#on_behalf_of has already been set' unless session.on_behalf_of.nil?
33
+ raise CurrencyCloud::GeneralError, 'contact id for on behalf of is not a UUID' unless UUID_REGEX.match(contact_id)
34
+
35
+ session.on_behalf_of = contact_id
36
+ yield
37
+ ensure
38
+ session.on_behalf_of = nil
25
39
  end
26
40
  end
@@ -4,12 +4,18 @@ module CurrencyCloud
4
4
  module Delete
5
5
 
6
6
  # TODO: Set state to be deleted
7
- # TODO: Add .delete instance method
8
7
  # TODO: Disable all actionable methods / freeze?
9
8
 
10
9
  def delete(id)
11
10
  post("#{id}/delete")
12
11
  end
13
12
  end
13
+
14
+ module InstanceDelete
15
+ def delete
16
+ self.class.delete(id)
17
+ self
18
+ end
19
+ end
14
20
  end
15
21
  end
@@ -0,0 +1,14 @@
1
+ module CurrencyCloud
2
+ module Actions
3
+ module Save
4
+ def save
5
+ return self if changed_attributes.empty?
6
+
7
+ params = Hash[changed_attributes.map { |k| [k, self.send(k)] }]
8
+ post(id, params)
9
+ changed_attributes.clear
10
+ self
11
+ end
12
+ end
13
+ end
14
+ end
@@ -1,5 +1,6 @@
1
1
  module CurrencyCloud
2
2
  class ApiErrorMessage
3
+
3
4
  attr_reader :field, :code, :message, :params
4
5
 
5
6
  def initialize(field, error)
@@ -8,13 +9,26 @@ module CurrencyCloud
8
9
  @message = error['message']
9
10
  @params = error['params']
10
11
  end
11
- end
12
12
 
13
+ def to_h
14
+ {
15
+ 'field' => field,
16
+ 'code' => code,
17
+ 'message' => message,
18
+ 'params' => params
19
+ }
20
+ end
21
+ end
13
22
 
14
23
  class ApiError < StandardError
24
+ include ErrorUtils
25
+
15
26
  attr_reader :code, :messages, :raw_response, :status_code
16
27
 
17
- def initialize(raw_response)
28
+ def initialize(verb, route, params, raw_response)
29
+ @verb = verb
30
+ @route = route
31
+ @params = params
18
32
  @raw_response = raw_response
19
33
  @status_code = raw_response.code
20
34
  errors = raw_response.parsed_response
@@ -27,6 +41,28 @@ module CurrencyCloud
27
41
  end
28
42
  end
29
43
  end
44
+
45
+ def to_s
46
+ class_name = super
47
+
48
+ string_params = Hash[@params.map { |k, v| [k.to_s, v.to_s]}]
49
+
50
+ error_details = {
51
+ 'platform' => platform,
52
+ 'request' => {
53
+ 'parameters' => string_params,
54
+ 'verb' => @verb.to_s,
55
+ 'url' => @route
56
+ },
57
+ 'response' => {
58
+ 'status_code' => status_code,
59
+ 'date' => raw_response.headers['Date'],
60
+ 'request_id' => (raw_response.headers['x-request-id'] || 0).to_i
61
+ },
62
+ 'errors' => messages.map(&:to_h)
63
+ }
64
+ "#{class_name}#{$/}#{YAML.dump(error_details)}"
65
+ end
30
66
  end
31
67
 
32
68
  class BadRequestError < ApiError; end
@@ -0,0 +1,14 @@
1
+ module CurrencyCloud
2
+ module ErrorUtils
3
+ def platform
4
+ base = "ruby-#{RUBY_VERSION}"
5
+ implementation = case RUBY_ENGINE
6
+ when 'ruby' then ''
7
+ when 'jruby' then ' (jruby-#{JRUBY_VERSION})"'
8
+ when 'rbx' then ' (rbx-#{Rubinius::VERSION})'
9
+ else ' (other)'
10
+ end
11
+ "#{base}#{implementation}"
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,5 @@
1
+ module CurrencyCloud
2
+
3
+ class GeneralError < StandardError; end
4
+
5
+ end
@@ -1,10 +1,32 @@
1
1
  module CurrencyCloud
2
2
 
3
3
  class UnexpectedError < StandardError
4
+ include ErrorUtils
5
+
4
6
  attr_reader :inner_error
5
7
 
6
- def initialize(e)
8
+ def initialize(verb, route, params, e)
9
+ @verb = verb
10
+ @route = route
11
+ @params = params
7
12
  @inner_error = e
8
13
  end
14
+
15
+ def to_s
16
+ class_name = super
17
+
18
+ string_params = Hash[@params.map { |k, v| [k.to_s, v.to_s]}]
19
+
20
+ error_details = {
21
+ 'platform' => platform,
22
+ 'request' => {
23
+ 'parameters' => string_params,
24
+ 'verb' => @verb.to_s,
25
+ 'url' => @route
26
+ },
27
+ 'inner_error' => inner_error.to_s
28
+ }
29
+ "#{class_name}#{$/}#{YAML.dump(error_details)}"
30
+ end
9
31
  end
10
32
  end
@@ -9,13 +9,15 @@ module CurrencyCloud
9
9
  end
10
10
 
11
11
  def get(route, params={}, opts={})
12
- retry_authenticate('get', route, params, opts) do |url, options|
12
+ retry_authenticate('get', route, params, opts) do |url, params, options|
13
+ options.merge!(:query => params)
13
14
  HTTParty.get(url, options)
14
15
  end
15
16
  end
16
17
 
17
18
  def post(route, params={}, opts={})
18
- retry_authenticate('post', route, params, opts) do |url, options|
19
+ retry_authenticate('post', route, params, opts) do |url, params, options|
20
+ options.merge!(:body => params)
19
21
  HTTParty.post(url, options)
20
22
  end
21
23
  end
@@ -23,46 +25,51 @@ module CurrencyCloud
23
25
  private
24
26
  def retry_authenticate(verb, route, params, opts)
25
27
  should_retry = opts[:should_retry].nil? ? true : opts.delete(:should_retry)
26
- description = "to #{verb}: #{route}"
27
28
 
28
- options = process_options(verb, params, opts)
29
+ params = process_params(params)
30
+ options = process_options(verb, opts)
31
+ full_url = build_url(route)
29
32
 
30
33
  response = nil
31
34
  retry_count = should_retry ? 0 : 2
32
35
  while retry_count < 3
33
- response = yield(full_url(route), options)
36
+ response = yield(full_url, params, options)
34
37
  break unless response.code == 401 && should_retry
35
38
  session.reauthenticate
36
39
  retry_count += 1
37
40
  end
38
-
39
- process_response(response)
41
+
42
+ response_handler = CurrencyCloud::ResponseHandler.new(verb, full_url, params, response)
43
+ response_handler.process
44
+ rescue ApiError, UnexpectedError
45
+ raise
40
46
  rescue => e
41
- raise if e.class.ancestors.include?(ApiError) || e.is_a?(UnexpectedError)
42
- raise UnexpectedError.new(e)
47
+ raise UnexpectedError.new(verb, full_url, params, e)
43
48
  end
44
49
 
45
- def process_options(verb, params, opts)
50
+ def process_options(verb, opts)
46
51
  options = {:headers => headers }
47
- params_key = verb == :get ? :query : :body
48
- options[params_key] = params
49
- options.merge!(opts)
50
52
  # options[:debug_output] = $stdout
51
- options
53
+ options.merge(opts)
54
+ # options
55
+ end
56
+
57
+ def process_params(params)
58
+ if session && session.on_behalf_of && CurrencyCloud::UUID_REGEX.match(session.on_behalf_of)
59
+ params.merge!(on_behalf_of: session.on_behalf_of)
60
+ end
61
+
62
+ params
52
63
  end
53
64
 
54
65
  def headers
55
66
  headers = {}
56
- headers['X-Auth-Token'] = session.token if session.token
67
+ headers['X-Auth-Token'] = session.token if session && session.token
57
68
  headers
58
69
  end
59
70
 
60
- def full_url(route)
71
+ def build_url(route)
61
72
  "#{session.environment_url}/#{CurrencyCloud::ApiVersion}/" + route
62
73
  end
63
-
64
- def process_response(response)
65
- CurrencyCloud::ResponseHandler.process(response)
66
- end
67
74
  end
68
75
  end