adyen-ruby-api-library 4.0.0 → 4.3.0

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 (51) hide show
  1. checksums.yaml +5 -5
  2. data/.github/CODEOWNERS +1 -0
  3. data/.github/ISSUE_TEMPLATE/bug_report.md +27 -0
  4. data/.github/ISSUE_TEMPLATE/feature_request.md +23 -0
  5. data/.github/PULL_REQUEST_TEMPLATE.md +7 -0
  6. data/.github/dependabot.yml +8 -0
  7. data/CODE_OF_CONDUCT.md +76 -0
  8. data/Gemfile +2 -2
  9. data/README.md +16 -2
  10. data/docs/checkout.html +17 -0
  11. data/lib/adyen-ruby-api-library.rb +4 -11
  12. data/lib/adyen/client.rb +16 -3
  13. data/lib/adyen/errors.rb +44 -2
  14. data/lib/adyen/hash_with_accessors.rb +38 -0
  15. data/lib/adyen/result.rb +2 -2
  16. data/lib/adyen/services/checkout.rb +2 -1
  17. data/lib/adyen/services/data_protection.rb +17 -0
  18. data/lib/adyen/services/marketpay.rb +18 -0
  19. data/lib/adyen/services/payments.rb +2 -1
  20. data/lib/adyen/services/postfmapi.rb +19 -0
  21. data/lib/adyen/services/service.rb +10 -1
  22. data/lib/adyen/utils/hmac_validator.rb +48 -0
  23. data/lib/adyen/version.rb +2 -2
  24. data/renovate.json +5 -0
  25. data/spec/checkout_spec.rb +13 -4
  26. data/spec/checkout_utility_spec.rb +4 -2
  27. data/spec/data_protection_spec.rb +14 -0
  28. data/spec/errors_spec.rb +33 -3
  29. data/spec/hash_with_accessors_spec.rb +127 -0
  30. data/spec/hop_spec.rb +14 -0
  31. data/spec/mocks/requests/Checkout/payment_links.json +9 -0
  32. data/spec/mocks/requests/DataProtectionService/request_subject_erasure.json +5 -0
  33. data/spec/mocks/requests/Hop/get_onboarding_url.json +4 -0
  34. data/spec/mocks/requests/Payment/donate.json +10 -0
  35. data/spec/mocks/requests/Terminal/assign_terminals.json +6 -0
  36. data/spec/mocks/requests/Terminal/find_terminal.json +3 -0
  37. data/spec/mocks/requests/Terminal/get_terminals_under_account.json +4 -0
  38. data/spec/mocks/responses/Checkout/payment_links.json +9 -0
  39. data/spec/mocks/responses/DataProtectionService/request_subject_erasure.json +3 -0
  40. data/spec/mocks/responses/Hop/get_onboarding_url.json +7 -0
  41. data/spec/mocks/responses/Payment/donate.json +4 -0
  42. data/spec/mocks/responses/Terminal/assign_terminals.json +5 -0
  43. data/spec/mocks/responses/Terminal/find_terminal.json +6 -0
  44. data/spec/mocks/responses/Terminal/get_terminals_under_account.json +11 -0
  45. data/spec/payments_spec.rb +2 -1
  46. data/spec/postfmapi_spec.rb +16 -0
  47. data/spec/service_spec.rb +45 -0
  48. data/spec/spec_helper.rb +7 -4
  49. data/spec/utils/hmac_validator_spec.rb +52 -0
  50. metadata +34 -5
  51. data/lib/adyen/util.rb +0 -21
@@ -8,7 +8,8 @@ module Adyen
8
8
  service = 'Checkout'
9
9
  method_names = [
10
10
  :payment_methods,
11
- :payment_session
11
+ :payment_session,
12
+ :payment_links
12
13
  ]
13
14
 
14
15
  super(client, version, service, method_names)
@@ -0,0 +1,17 @@
1
+ require_relative 'service'
2
+
3
+ module Adyen
4
+ class DataProtection < Service
5
+ attr_accessor :version
6
+ DEFAULT_VERSION = 1
7
+
8
+ def initialize(client, version = DEFAULT_VERSION)
9
+ service = 'DataProtectionService'
10
+ method_names = [
11
+ :request_subject_erasure
12
+ ]
13
+
14
+ super(client, version, service, method_names)
15
+ end
16
+ end
17
+ end
@@ -21,6 +21,10 @@ module Adyen
21
21
  def notification
22
22
  @notification ||= Adyen::Marketpay::Notification.new(@client)
23
23
  end
24
+
25
+ def hop
26
+ @hop ||= Adyen::Marketpay::Hop.new(@client)
27
+ end
24
28
  end
25
29
 
26
30
  class Account < Service
@@ -88,5 +92,19 @@ module Adyen
88
92
  super(client, version, service, method_names)
89
93
  end
90
94
  end
95
+
96
+ class Hop < Service
97
+ attr_accessor :version
98
+ DEFAULT_VERSION = 1
99
+
100
+ def initialize(client, version = DEFAULT_VERSION)
101
+ service = 'Hop'
102
+ method_names = [
103
+ :get_onboarding_url
104
+ ]
105
+
106
+ super(client, version, service, method_names)
107
+ end
108
+ end
91
109
  end
92
110
  end
@@ -15,7 +15,8 @@ module Adyen
15
15
  :cancel,
16
16
  :refund,
17
17
  :cancel_or_refund,
18
- :adjust_authorisation
18
+ :adjust_authorisation,
19
+ :donate
19
20
  ]
20
21
 
21
22
  super(client, version, service, method_names)
@@ -0,0 +1,19 @@
1
+ require_relative 'service'
2
+
3
+ module Adyen
4
+ class PosTerminalManagement < Service
5
+ attr_accessor :version
6
+ DEFAULT_VERSION = 1
7
+
8
+ def initialize(client, version = DEFAULT_VERSION)
9
+ service = 'Terminal'
10
+ method_names = [
11
+ :assign_terminals,
12
+ :find_terminal,
13
+ :get_terminals_under_account
14
+ ]
15
+
16
+ super(client, version, service, method_names)
17
+ end
18
+ end
19
+ end
@@ -2,6 +2,15 @@ module Adyen
2
2
  class Service
3
3
  attr_accessor :service, :version
4
4
 
5
+ # add snake case to camel case converter to String
6
+ # to convert rubinic method names to Adyen API methods
7
+ #
8
+ # i.e. snake_case -> snakeCase
9
+ # note that the first letter is not capitalized as normal
10
+ def self.action_for_method_name(method_name)
11
+ method_name.to_s.gsub(/_./) { |x| x[1].upcase }
12
+ end
13
+
5
14
  def initialize(client, version, service, method_names)
6
15
  @client = client
7
16
  @version = version
@@ -10,7 +19,7 @@ module Adyen
10
19
  # dynamically create API methods
11
20
  method_names.each do |method_name|
12
21
  define_singleton_method method_name do |request, headers = {}|
13
- action = method_name.to_s.to_camel_case
22
+ action = self.class.action_for_method_name(method_name)
14
23
  @client.call_adyen_api(@service, action, request, headers, @version)
15
24
  end
16
25
  end
@@ -0,0 +1,48 @@
1
+ module Adyen
2
+ module Utils
3
+ class HmacValidator
4
+ HMAC_ALGORITHM = 'sha256'.freeze
5
+ DATA_SEPARATOR = ':'.freeze
6
+ NOTIFICATION_VALIDATION_KEYS = %w[
7
+ pspReference originalReference merchantAccountCode merchantReference
8
+ amount.value amount.currency eventCode success
9
+ ].freeze
10
+
11
+ def valid_notification_hmac?(notification_request_item, hmac_key)
12
+ expected_sign = calculate_notification_hmac(notification_request_item, hmac_key)
13
+ merchant_sign = fetch(notification_request_item, 'additionalData.hmacSignature')
14
+
15
+ expected_sign == merchant_sign
16
+ end
17
+
18
+ def calculate_notification_hmac(notification_request_item, hmac_key)
19
+ data = data_to_sign(notification_request_item)
20
+
21
+ Base64.strict_encode64(OpenSSL::HMAC.digest(HMAC_ALGORITHM, [hmac_key].pack('H*'), data))
22
+ end
23
+
24
+ def data_to_sign(notification_request_item)
25
+ NOTIFICATION_VALIDATION_KEYS.map { |key| fetch(notification_request_item, key).to_s }
26
+ .map { |value| value.gsub('\\', '\\\\').gsub(':', '\\:') }
27
+ .join(DATA_SEPARATOR)
28
+ end
29
+
30
+ private
31
+
32
+ def fetch(hash, keys)
33
+ value = hash
34
+
35
+ keys.to_s.split('.').each do |key|
36
+ value = if key.to_i.to_s == key
37
+ value[key.to_i]
38
+ else
39
+ value[key].nil? ? value[key.to_sym] : value[key]
40
+ end
41
+ break if value.nil?
42
+ end
43
+
44
+ value
45
+ end
46
+ end
47
+ end
48
+ end
@@ -1,4 +1,4 @@
1
- module Adyen
1
+ module Adyen
2
2
  NAME = "adyen-ruby-api-library"
3
- VERSION = "4.0.0".freeze
3
+ VERSION = "4.3.0".freeze
4
4
  end
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": [
3
+ "config:base"
4
+ ]
5
+ }
@@ -57,10 +57,14 @@ RSpec.describe Adyen::Checkout, service: "checkout" do
57
57
  to eq(200)
58
58
  expect(response_hash).
59
59
  to eq(JSON.parse(response_body))
60
- expect(response_hash.class).
61
- to be Hash
60
+ expect(response_hash).
61
+ to be_a Adyen::HashWithAccessors
62
+ expect(response_hash).
63
+ to be_a_kind_of Hash
62
64
  expect(response_hash["resultCode"]).
63
65
  to eq("RedirectShopper")
66
+ expect(response_hash.resultCode).
67
+ to eq("RedirectShopper")
64
68
  end
65
69
 
66
70
  # must be created manually due to payments/result format
@@ -89,10 +93,14 @@ RSpec.describe Adyen::Checkout, service: "checkout" do
89
93
  to eq(200)
90
94
  expect(response_hash).
91
95
  to eq(JSON.parse(response_body))
92
- expect(response_hash.class).
93
- to be Hash
96
+ expect(response_hash).
97
+ to be_a Adyen::HashWithAccessors
98
+ expect(response_hash).
99
+ to be_a_kind_of Hash
94
100
  expect(response_hash["resultCode"]).
95
101
  to eq("Authorised")
102
+ expect(response_hash.resultCode).
103
+ to eq("Authorised")
96
104
  end
97
105
 
98
106
  # create client for automated tests
@@ -102,6 +110,7 @@ RSpec.describe Adyen::Checkout, service: "checkout" do
102
110
  # format is defined in spec_helper
103
111
  test_sets = [
104
112
  ["payment_session", "publicKeyToken", "8115054323780109"],
113
+ ["payment_links", "url", "https://checkoutshopper-test.adyen.com"],
105
114
  ["payments", "resultCode", "Authorised"]
106
115
  ]
107
116
 
@@ -19,8 +19,10 @@ RSpec.describe Adyen::CheckoutUtility, service: "checkout utility" do
19
19
  # must be created manually because every field in the response is an array
20
20
  it "makes an origin_keys call" do
21
21
  parsed_body = create_test(@shared_values[:client], @shared_values[:service], "origin_keys", @shared_values[:client].checkout_utility)
22
- expect(parsed_body["originKeys"].class).
23
- to be Hash
22
+ expect(parsed_body["originKeys"]).
23
+ to be_a Adyen::HashWithAccessors
24
+ expect(parsed_body["originKeys"]).
25
+ to be_a_kind_of Hash
24
26
  end
25
27
  end
26
28
 
@@ -0,0 +1,14 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe Adyen::DataProtection, service: "Data Protection Service" do
4
+ # client instance to be used in dynamically generated tests
5
+ client = create_client(:basic)
6
+
7
+ # methods / values to test for
8
+ # format is defined in spec_helper
9
+ test_sets = [
10
+ ["request_subject_erasure", "result", "SUCCESS"],
11
+ ]
12
+
13
+ generate_tests(client, "DataProtectionService", test_sets, client.data_protection)
14
+ end
@@ -3,15 +3,45 @@
3
3
  require 'spec_helper'
4
4
 
5
5
  RSpec.describe Adyen::AdyenError do
6
+ before(:all) do
7
+ @shared_values = {
8
+ request: {
9
+ amount: {
10
+ currency: "USD",
11
+ value: 1000
12
+ },
13
+ reference: "Your order number",
14
+ paymentMethod: {
15
+ type: "scheme",
16
+ number: "4111111111111111",
17
+ expiryMonth: "10",
18
+ expiryYear: "2020",
19
+ holderName: "John Smith",
20
+ cvc: "737"
21
+ },
22
+ returnUrl: "https://your-company.com/",
23
+ merchantAccount: "YOUR_MERCHANT_ACCOUNT"
24
+ }
25
+ }
26
+ end
27
+
6
28
  describe '#to_s' do
7
29
  it 'describes using the error properties' do
8
- expect(Adyen::AdyenError.new('request', 'response', 'message', 'code').to_s).to eq('Adyen::AdyenError code:code, request:request, response:response, msg:message')
30
+ expect(Adyen::AdyenError.new(@shared_values[:request], 'response', 'message', 'code').to_s).to eq("Adyen::AdyenError code:code, msg:message, request:#{@shared_values[:request]}, response:response")
9
31
  end
10
32
  it 'skips the null properties' do
11
- expect(Adyen::AdyenError.new('request', nil, nil, 'code').to_s).to eq('Adyen::AdyenError code:code, request:request')
33
+ expect(Adyen::AdyenError.new(@shared_values[:request], nil, nil, 'code').to_s).to eq("Adyen::AdyenError code:code, request:#{@shared_values[:request]}")
12
34
  end
13
35
  it 'uses the proper error class name' do
14
- expect(Adyen::PermissionError.new('a message', 'a request').to_s).to eq('Adyen::PermissionError code:403, request:a request, msg:a message')
36
+ expect(Adyen::PermissionError.new('message', @shared_values[:request]).to_s).to eq("Adyen::PermissionError code:403, msg:message, request:#{@shared_values[:request]}")
37
+ end
38
+ end
39
+ describe '#masking' do
40
+ it 'masks card number when logging request in errors' do
41
+ expect(Adyen::AdyenError.new(@shared_values[:request], 'response', 'message', 'code').request[:paymentMethod][:number]).to eq('411111******1111')
42
+ end
43
+ it 'masks CVC when logging request in errors' do
44
+ expect(Adyen::AdyenError.new(@shared_values[:request], 'response', 'message', 'code').request[:paymentMethod][:cvc]).to eq('***')
15
45
  end
16
46
  end
17
47
  end
@@ -0,0 +1,127 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Adyen::HashWithAccessors do
4
+ shared_examples :hash_with_accessors do
5
+ subject do
6
+ h = described_class.new
7
+ h[key] = 1
8
+ h
9
+ end
10
+
11
+ it 'returns values of a hashes' do
12
+ expect(subject.arbitrary_accessor).to be 1
13
+ end
14
+
15
+ it 'can assign existing values' do
16
+ subject.arbitrary_accessor = 2
17
+ expect(subject.arbitrary_accessor).to be 2
18
+ expect(subject[key]).to be 2
19
+ end
20
+
21
+ it 'complains if there are arguments for the accessor' do
22
+ expect { subject.arbitrary_accessor(1) }.to raise_error(ArgumentError, 'wrong number of arguments (given 1, expected 0)')
23
+ expect { subject.arbitrary_accessor(1, 2) }.to raise_error(ArgumentError, 'wrong number of arguments (given 2, expected 0)')
24
+ end
25
+
26
+ it 'complains if there are arguments for the accessor =' do
27
+ # using send because i'm not sure how to do this wrong with normal ruby setter calling.
28
+ # just here for completeness
29
+ expect { subject.send(:arbitrary_accessor=) }.to raise_error(ArgumentError, 'wrong number of arguments (given 0, expected 1)')
30
+ expect { subject.send(:arbitrary_accessor=, 1, 2) }.to raise_error(ArgumentError, 'wrong number of arguments (given 2, expected 1)')
31
+ end
32
+
33
+ it 'responds to the accessor' do
34
+ expect(subject).to respond_to(:arbitrary_accessor)
35
+ expect(subject).to respond_to(:arbitrary_accessor=)
36
+ expect(subject).to_not respond_to(:another_accessor)
37
+ expect(subject).to_not respond_to(:another_accessor=)
38
+ end
39
+
40
+ it "raises when the key doesn't exist" do
41
+ expect { subject.another_accessor }.to raise_error(NoMethodError)
42
+ expect { subject.another_accessor = 1 }.to raise_error(NoMethodError)
43
+ end
44
+ end
45
+
46
+ context 'with a string key' do
47
+ let(:key) { 'arbitrary_accessor' }
48
+
49
+ it_behaves_like :hash_with_accessors
50
+ end
51
+
52
+ context 'with a symbol key' do
53
+ let(:key) { :arbitrary_accessor }
54
+
55
+ it_behaves_like :hash_with_accessors
56
+ end
57
+
58
+ context 'with a conflicting key' do
59
+ subject do
60
+ h = described_class.new
61
+ h['keys'] = 'not the keys'
62
+ h
63
+ end
64
+
65
+ it "does original thing if there'd be a conflict" do
66
+ expect(subject.keys).to eq ['keys'] # the default behaviour
67
+ expect(subject['keys']).to eq 'not the keys'
68
+ end
69
+
70
+ it 'still does the writer thing even if the reader is defined' do
71
+ subject.keys = 'written keys'
72
+ expect(subject['keys']).to eq 'written keys'
73
+ expect(subject.keys).to eq ['keys']
74
+ end
75
+ end
76
+
77
+ context 'with some other method missing defined' do
78
+ # this test setup is kind of janky,
79
+ # but we want to confirm super is set up correctly
80
+ # We could do a lot more house-keeping if we weren't sure Hash doesn't
81
+ # define its own method_missing and respond_to_missing? by default,
82
+ # and there was any particular reason to clean up properly and remove our
83
+ # called_super method from Hash.
84
+
85
+ before(:all) do
86
+ class Hash
87
+ def called_super(*args)
88
+ end
89
+
90
+ def method_missing(*args)
91
+ called_super(:method_missing, *args)
92
+ super
93
+ end
94
+
95
+ def respond_to_missing?(*args)
96
+ called_super(:respond_to_missing?, *args)
97
+ super
98
+ end
99
+ end
100
+ end
101
+
102
+ subject do
103
+ h = described_class.new
104
+ h[:my_accessor] = 1
105
+ h
106
+ end
107
+
108
+ it 'can fall back to another respond_to_missing?' do
109
+ expect(subject).to_not receive(:called_super).with(:respond_to_missing?, :my_accessor, false)
110
+ expect(subject).to respond_to(:my_accessor)
111
+ expect(subject).to receive(:called_super).with(:respond_to_missing?, :literally_anything, false)
112
+ expect(subject.respond_to?(:literally_anything)).to be false
113
+ end
114
+
115
+ it 'can fall back to another method_missing' do
116
+ expect(subject).to_not receive(:called_super).with(:method_missing, :my_accessor)
117
+ expect(subject.my_accessor).to be 1
118
+ expect(subject).to receive(:called_super).with(:method_missing, :something_else)
119
+ expect { subject.something_else }.to raise_error(NoMethodError)
120
+ end
121
+
122
+ end
123
+
124
+ it "doesn't modify all hashes" do
125
+ expect { {a: 1}.a }.to raise_error(NoMethodError)
126
+ end
127
+ end
@@ -0,0 +1,14 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe Adyen::Payments, service: "marketpay hop service" do
4
+ # client instance to be used in dynamically generated tests
5
+ client = create_client(:basic)
6
+
7
+ # methods / values to test for
8
+ # format is defined in spec_helper
9
+ test_sets = [
10
+ ["get_onboarding_url", "pspReference", "8815850625171183"]
11
+ ]
12
+
13
+ generate_tests(client, "Hop", test_sets, client.marketpay.hop)
14
+ end
@@ -0,0 +1,9 @@
1
+ {
2
+ "amount": {
3
+ "currency": "USD",
4
+ "value": 1000
5
+ },
6
+ "countryCode": "US",
7
+ "merchantAccount": "TestMerchant",
8
+ "reference": "Merchant Reference"
9
+ }