currency_cloud 0.5 → 0.7

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 (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