yp-ruby 0.1.3 → 0.1.4a

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c0e8f0cb22b7a4f03a355e460984e1ac50da242b
4
- data.tar.gz: fa63594396cabc343e2699290b1223c762c903fb
3
+ metadata.gz: 9f28691645bfdb90373e2524028d93a1e711bb46
4
+ data.tar.gz: cd6b81278539fbea582bf11dca079e7131f45357
5
5
  SHA512:
6
- metadata.gz: 9e96e8bfbb983d3336d1b81798087b6d81c64b1631f748488fccaa8206210aa325d1b4c5341523192a141bdb5c80d61882247fad4767a8d1e548a2145c487eff
7
- data.tar.gz: 01ec3ed018db1179656697a4ea112b493e04021d3453ff58519f45041abc6d9623b6bd348fe267bb4f1206ee501c32590912cc84fae86fd7af495694740a5ac2
6
+ metadata.gz: 4f933e44afc37aa9a3a244e89c78f6a7ef4f75e6ca20cb8e95dbf57df2557b6fa73afe5ea6b37aa5d0cfefd84ab625bdec24f105c09d16e8e7d712873d766a97
7
+ data.tar.gz: 299c5520a8dd0234828004f7e5d16e1b4ad404f977deddf700010606b02fb4423e475c933c201964e9c9d36852bfe57b631e433565032dc27a80a4250468f4da
data/README.md CHANGED
@@ -20,7 +20,86 @@ Or install it yourself as:
20
20
 
21
21
  ## Usage
22
22
 
23
- TODO: Write usage instructions here
23
+ Different classes are available for the Gateway actions:
24
+
25
+ - Yp::Sale
26
+ - Yp::Preauth
27
+ - Yp::Verify
28
+
29
+ If you need support for more actions, please [open an issue](https://github.com/andytango/yp-ruby/issues)!
30
+
31
+ ### Authentication and params
32
+
33
+ You will need to pass in the correct parameters according to the Yorkshire Payments integration guide:
34
+
35
+ ```ruby
36
+
37
+ signature_key = 'Engine0Milk12Next'
38
+
39
+ params = {
40
+ merchantID: '101381',
41
+ countryCode: 826,
42
+ currencyCode: 826,
43
+ cardNumber: '4012001037141112',
44
+ cardExpiryMonth: 12,
45
+ cardExpiryYear: 15,
46
+ cardCVV: '083',
47
+ customerName: 'Yorkshire Payments',
48
+ customerEmail: 'support@yorkshirepayments.com',
49
+ customerAddress: '16 Test Street',
50
+ customerPostCode: 'TE15 5ST',
51
+ orderRef: 'Test purchase'
52
+ }
53
+
54
+ transaction = Yp::Sale.new(signature, params)
55
+
56
+ ```
57
+
58
+ The library will *not* validate your parameters before sending. This feature is coming soon.
59
+
60
+ You pass in a callback method as a block, or access the response after the transaction is sent:
61
+
62
+ ```ruby
63
+
64
+ transaction.send! do |response|
65
+ puts response.state # --> 'captured'
66
+ end
67
+
68
+ # OR:
69
+
70
+ response = transaction.send!
71
+ puts response.state # --> 'captured'
72
+
73
+ ```
74
+
75
+ ### Error Handling
76
+
77
+ The client will raise a number of different types of errors:
78
+
79
+ #### Validation Errors
80
+
81
+ These occur if we could not perform any validation. This means the response is
82
+ malformed and can arise if you did not correctly sign your request:
83
+
84
+ - Yp::Response::InvalidSignatureError (potentially malicious)
85
+ - Yp::Response::MissingSignatureError
86
+ - Yp::Response::MissingResponseCodeError
87
+
88
+ #### Gateway Errors
89
+
90
+ These occur if, during validation, we found a Gateway error code. These come in
91
+ three different flavours:
92
+
93
+ - Yp::Response::MissingFieldError: _a field was missing in your request_
94
+ - Yp::Response::InvalidFieldError: _a field value was invalid in your request_
95
+ - Yp::Response::GatewayError: _the error could not be classified. The gateway's
96
+ error message will be supplied_
97
+
98
+ #### Transaction Declined Error
99
+
100
+ This occurs if the transaction has entered the **Declined** state:
101
+
102
+ - Yp::Response::DeclinedError
24
103
 
25
104
  ## Development
26
105
 
@@ -30,7 +109,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
30
109
 
31
110
  ## Contributing
32
111
 
33
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/yp-ruby. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
112
+ Bug reports and pull requests are welcome on GitHub at https://github.com/andytango/yp-ruby. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
34
113
 
35
114
 
36
115
  ## License
@@ -0,0 +1,69 @@
1
+ missing_field:
2
+ 66048: 'request'
3
+ 66049: 'merchantID'
4
+ 66055: 'action'
5
+ 66056: 'amount'
6
+ 66057: 'currencyCode'
7
+ 66058: 'cardNumber'
8
+ 66059: 'cardExpiryMonth'
9
+ 66060: 'cardExpiryYear'
10
+ 66064: 'cardCVV'
11
+ 66065: 'customerName'
12
+ 66066: 'customerAddress'
13
+ 66067: 'customerPostCode'
14
+ 66068: 'customerEmail'
15
+ 66070: 'countyCode'
16
+ 66087: 'signature'
17
+ 66092: 'threeDSPaRes'
18
+ 66093: 'threeDSECI'
19
+ 66094: 'threeDSCAVV'
20
+ 66095: 'threeDSXID'
21
+ 66096: 'threeDSEnrolled'
22
+ 66097: 'threeDSAuthenticated'
23
+ 66098: 'threeDSCheckPref'
24
+ 66099: 'cv2CheckPref'
25
+ 66100: 'addressCheckPref'
26
+ 66101: 'postcodeCheckPref'
27
+ 66102: 'captureDelay'
28
+ 66103: 'orderDate'
29
+ 66104: 'grossAmount'
30
+ 66105: 'netAmount'
31
+ 66016: 'taxRate'
32
+ 66016: 'taxReason'
33
+ 66160: 'cardExpiryDate'
34
+ 66161: 'cardStartDate'
35
+
36
+ invalid_field:
37
+ 66304: 'request'
38
+ 66305: 'merchantID'
39
+ 66311: 'action'
40
+ 66312: 'amount'
41
+ 66313: 'currencyCode'
42
+ 66314: 'cardNumber'
43
+ 66315: 'cardExpiryMonth'
44
+ 66316: 'cardExpiryYear'
45
+ 66317: 'cardCVV'
46
+ 66332: 'customerName'
47
+ 66322: 'customerAddress'
48
+ 66323: 'customerPostCode'
49
+ 66324: 'customerEmail'
50
+ 66326: 'countyCode'
51
+ 66343: 'signature'
52
+ 66348: 'threeDSPaRes'
53
+ 66349: 'threeDSECI'
54
+ 66350: 'threeDSCAVV'
55
+ 66351: 'threeDSXID'
56
+ 66352: 'threeDSEnrolled'
57
+ 66353: 'threeDSAuthenticated'
58
+ 66354: 'threeDSCheckPref'
59
+ 66355: 'cv2CheckPref'
60
+ 66356: 'addressCheckPref'
61
+ 66357: 'postcodeCheckPref'
62
+ 66358: 'captureDelay'
63
+ 66359: 'orderDate'
64
+ 66360: 'grossAmount'
65
+ 66361: 'netAmount'
66
+ 66362: 'taxRate'
67
+ 66363: 'taxReason'
68
+ 66416: 'cardExpiryDate'
69
+ 66417: 'cardStartDate'
data/lib/base.rb CHANGED
@@ -1,4 +1,6 @@
1
1
  require 'transaction_logger'
2
+ require 'signing_hash_creator'
3
+ require 'response'
2
4
 
3
5
  module Yp
4
6
  class Base
@@ -15,24 +17,13 @@ module Yp
15
17
  @signature_key = signature_key
16
18
  end
17
19
 
18
- def body
19
- @params.clone.tap do |params|
20
- params[:signature] = create_signing_hash
21
- TransactionLogger.log_request(params)
22
- end
23
- end
24
-
25
- def create_signing_hash
26
- self.class.digest(self.class.serialize_params(@params) + @signature_key)
27
- end
28
-
29
- def send
20
+ def send!
30
21
  if block_given?
31
22
  RestClient.post(URL, body) do |response|
32
- yield(self.class.parse response)
23
+ yield(parse_and_validate response)
33
24
  end
34
25
  else
35
- self.class.parse(RestClient.post(URL, body))
26
+ parse_and_validate(RestClient.post(URL, body))
36
27
  end
37
28
  end
38
29
 
@@ -59,42 +50,22 @@ module Yp
59
50
  { type: TYPE[type] }
60
51
  end
61
52
 
62
- class << self
63
-
64
- def digest(str_params)
65
- Digest::SHA512.hexdigest str_params
66
- end
67
-
68
- def serialize_params(params)
69
- uri_string_from_hash(params)
70
- end
71
-
72
- def parse(response)
73
- ruby_hash_from_response(CGI::parse(response)).tap do |parsed|
74
- TransactionLogger.log_response(parsed)
75
- end
76
- end
77
-
78
- private
79
-
80
- def uri_string_from_hash(hash)
81
- hash.map { |key, value| uri_query_param(key, value) }.sort.join '&'
82
- end
83
-
84
- def uri_query_param(key, value)
85
- encode_url_component(key) + '=' + encode_url_component(value)
86
- end
87
-
88
- def encode_url_component(key)
89
- ERB::Util::url_encode(key).gsub(/%20/, '+')
53
+ def body
54
+ @params.clone.tap do |params|
55
+ params[:signature] = create_signing_hash
56
+ TransactionLogger.log_request(params)
90
57
  end
58
+ end
91
59
 
92
- def ruby_hash_from_response(hash)
93
- hash.reduce({}) do |memo, (key, value)|
94
- memo.merge({key.to_sym => value.first})
95
- end
96
- end
60
+ def create_signing_hash
61
+ SigningHashCreator.new(@params, @signature_key).create
62
+ end
97
63
 
64
+ def parse_and_validate(response)
65
+ Response
66
+ .new(@signature_key, response, TransactionLogger)
67
+ .parse_and_validate
98
68
  end
69
+
99
70
  end
100
71
  end
data/lib/response.rb ADDED
@@ -0,0 +1,36 @@
1
+ require 'response/parser'
2
+ require 'response/error'
3
+ require 'response/validator'
4
+ require 'response/error_logger'
5
+
6
+ module Yp
7
+ class Response
8
+ def initialize(signature, params, logger)
9
+ @signature = signature
10
+ @params = params
11
+ @logger = logger
12
+ end
13
+
14
+ def parse_and_validate
15
+ validate!
16
+ parsed
17
+ end
18
+
19
+ private
20
+
21
+ def validate!
22
+ ErrorLogger.new(@logger).log do
23
+ Validator.new(parsed, @signature).validate!
24
+ end
25
+ end
26
+
27
+ def parsed
28
+ @parsed ||= parse_params
29
+ end
30
+
31
+ def parse_params
32
+ Parser.new(@params).parse.tap { |parsed| @logger.log_response(parsed) }
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,16 @@
1
+ module Yp
2
+ class Response
3
+ class ValidationError < StandardError
4
+ def initialize
5
+ super('Gateway response could not be validated.')
6
+ end
7
+ end
8
+
9
+ class InvalidSignatureError < ValidationError; end
10
+ class MissingSignatureError < ValidationError; end
11
+ class MissingResponseCodeError < ValidationError; end
12
+ class MissingResponseMessageError < ValidationError; end
13
+
14
+ class DeclinedError < StandardError; end
15
+ end
16
+ end
@@ -0,0 +1,53 @@
1
+ module Yp
2
+ class Response
3
+ class ErrorLogger
4
+ extend Forwardable
5
+
6
+ def_delegators :@logger, :fatal, :error
7
+
8
+ def initialize(logger)
9
+ @logger = logger
10
+ end
11
+
12
+ def log
13
+ begin
14
+ yield
15
+ rescue InvalidSignatureError => e
16
+ log_fatal 'An invalid signature was received', e
17
+ rescue MissingSignatureError => e
18
+ log_not_found 'Signature', e
19
+ rescue MissingResponseCodeError => e
20
+ log_not_found 'Response Code', e
21
+ rescue MissingResponseMessageError => e
22
+ log_not_found 'Response Message', e
23
+ rescue DeclinedError => e
24
+ log_error 'Transaction was declined by the acquirer', e
25
+ rescue MissingFieldError => e
26
+ log_error 'Gateway responded with missing field', e
27
+ rescue InvalidFieldError => e
28
+ log_error 'Gateway responded with invalid field', e
29
+ rescue GatewayError => e
30
+ log_error 'Gateway responded with error', e
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def log_not_found(thing, e)
37
+ log_fatal("#{thing} was not found in the response", e)
38
+ end
39
+
40
+ def log_fatal(str, e)
41
+ fatal(str)
42
+ raise e
43
+ end
44
+
45
+ def log_error(str, e)
46
+ error("#{str} (#{e})")
47
+ raise e
48
+ end
49
+
50
+
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,68 @@
1
+ require 'yaml'
2
+
3
+ module Yp
4
+ class Response
5
+ class GatewayError < StandardError
6
+
7
+ def self.from_response_code(code, message)
8
+ Factory.new(code, message).error
9
+ end
10
+
11
+ class Factory
12
+
13
+ class << self
14
+ def error_map
15
+ @error_map ||= load_error_map
16
+ end
17
+
18
+ private
19
+
20
+ def load_error_map
21
+ YAML.load(File.read('data/gateway_responses.yml'))
22
+ end
23
+ end
24
+
25
+ def initialize(code, message)
26
+ @code = code
27
+ @message = message
28
+ end
29
+
30
+ def error
31
+ if is_missing_field?
32
+ MissingFieldError.new(missing_field)
33
+ elsif is_invalid_field?
34
+ InvalidFieldError.new(invalid_field)
35
+ else
36
+ GatewayError.new(@message)
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def is_missing_field?
43
+ !missing_field.nil?
44
+ end
45
+
46
+ def missing_field
47
+ @missing_field ||= mapped_error(:missing_field)
48
+ end
49
+
50
+ def is_invalid_field?
51
+ !invalid_field.nil?
52
+ end
53
+
54
+ def invalid_field
55
+ @invalid_field ||= mapped_error(:invalid_field)
56
+ end
57
+
58
+ def mapped_error(sym)
59
+ Factory.error_map[sym.to_s][@code.to_i]
60
+ end
61
+
62
+ end
63
+ end
64
+
65
+ class MissingFieldError < GatewayError; end
66
+ class InvalidFieldError < GatewayError; end
67
+ end
68
+ end
@@ -0,0 +1,27 @@
1
+ module Yp
2
+ class Response
3
+ class Parser
4
+
5
+ def initialize(params)
6
+ @params = params
7
+ end
8
+
9
+ def parse
10
+ ruby_hash_from_response(parse_string)
11
+ end
12
+
13
+ private
14
+
15
+ def parse_string
16
+ CGI::parse(@params)
17
+ end
18
+
19
+ def ruby_hash_from_response(hash)
20
+ hash.reduce({}) do |memo, (key, value)|
21
+ memo.merge({key.to_sym => value.first})
22
+ end
23
+ end
24
+
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,21 @@
1
+ require_relative 'validator/signing_key'
2
+ require_relative 'validator/gateway'
3
+ require_relative 'validator/acquirer'
4
+
5
+ module Yp
6
+ class Response
7
+ class Validator
8
+ def initialize(params, signature)
9
+ @params = params
10
+ @signature = signature
11
+ end
12
+
13
+ def validate!
14
+ SigningKey.new(@params, @signature).validate!
15
+ Gateway.new(@params).validate!
16
+ Acquirer.new(@params).validate!
17
+ end
18
+
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,27 @@
1
+ module Yp
2
+ class Response
3
+ class Validator
4
+ class Acquirer
5
+
6
+ def initialize(params)
7
+ @params = params
8
+ end
9
+
10
+ def validate!
11
+ !declined? || (raise DeclinedError)
12
+ end
13
+
14
+ private
15
+
16
+ def declined?
17
+ response_code == 4 || response_code == 5
18
+ end
19
+
20
+ def response_code
21
+ @response_code ||= @params[:responseCode].to_i
22
+ end
23
+
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,41 @@
1
+ require_relative '../gateway_error'
2
+
3
+ module Yp
4
+ class Response
5
+ class Validator
6
+ class Gateway
7
+
8
+ def initialize(params)
9
+ @params = params
10
+ end
11
+
12
+ def validate!
13
+ is_valid_response_code? || (raise error_from_response)
14
+ end
15
+
16
+ private
17
+
18
+ def is_valid_response_code?
19
+ response_code.to_i <= 5
20
+ end
21
+
22
+ def error_from_response
23
+ GatewayError.from_response_code(response_code, response_message)
24
+ end
25
+
26
+ def response_code
27
+ @response_code ||= find_response_code
28
+ end
29
+
30
+ def find_response_code
31
+ @params[:responseCode] || (raise MissingResponseCodeError)
32
+ end
33
+
34
+ def response_message
35
+ @params[:responseMessage] || (raise MissingResponseMessageError)
36
+ end
37
+
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,38 @@
1
+ require 'signing_hash_creator'
2
+
3
+ module Yp
4
+ class Response
5
+ class Validator
6
+ class SigningKey
7
+
8
+ def initialize(params, signature)
9
+ @params = params
10
+ @signature = signature
11
+ end
12
+
13
+ def validate!
14
+ has_valid_signing_key? || (raise InvalidSignatureError)
15
+ end
16
+
17
+ private
18
+
19
+ def has_valid_signing_key?
20
+ their_signing_key == our_signing_key
21
+ end
22
+
23
+ def their_signing_key
24
+ @params[:signature] || (raise MissingSignatureError)
25
+ end
26
+
27
+ def our_signing_key
28
+ SigningHashCreator.new(signing_key_params, @signature).create
29
+ end
30
+
31
+ def signing_key_params
32
+ @params.reject { |k, _| k == :signature }
33
+ end
34
+
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,32 @@
1
+ module Yp
2
+ class SigningHashCreator
3
+ def initialize(params, signature_key)
4
+ @params = params
5
+ @signature_key = signature_key
6
+ end
7
+
8
+ def create
9
+ self.class.digest(self.class.serialize_params(@params) + @signature_key)
10
+ end
11
+
12
+ class << self
13
+
14
+ def digest(str_params)
15
+ Digest::SHA512.hexdigest str_params
16
+ end
17
+
18
+ def serialize_params(params)
19
+ params.map { |key, value| uri_query_param(key, value) }.sort.join('&')
20
+ end
21
+
22
+ def uri_query_param(key, value)
23
+ encode_url_component(key) + '=' + encode_url_component(value)
24
+ end
25
+
26
+ def encode_url_component(key)
27
+ ERB::Util::url_encode(key).gsub(/%20/, '+')
28
+ end
29
+
30
+ end
31
+ end
32
+ end
@@ -12,10 +12,22 @@ module Yp
12
12
  info 'Response received', params
13
13
  end
14
14
 
15
+ def info(str, params=nil)
16
+ Yp.logger.info(format(str, params))
17
+ end
18
+
19
+ def error(str)
20
+ Yp.logger.error(format_message(str))
21
+ end
22
+
23
+ def fatal(str)
24
+ Yp.logger.fatal(format_message(str))
25
+ end
26
+
15
27
  private
16
28
 
17
- def info(str, params=nil)
18
- Yp.logger.info("#{format_message(str)} #{format_params(params)}")
29
+ def format(str, params)
30
+ "#{format_message(str)} #{format_params(params)}"
19
31
  end
20
32
 
21
33
  def format_message(str)
@@ -23,7 +35,7 @@ module Yp
23
35
  end
24
36
 
25
37
  def format_params(params)
26
- " with params #{filter_card_params(params).to_s}"
38
+ " with params #{filter_card_params(params).to_s}" unless params.nil?
27
39
  end
28
40
 
29
41
  def filter_card_params(hash)
data/lib/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Yp
2
- VERSION = "0.1.3"
2
+ VERSION = "0.1.4a"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yp-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.4a
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Hall
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-01-21 00:00:00.000000000 Z
11
+ date: 2017-03-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rest-client
@@ -133,12 +133,23 @@ files:
133
133
  - Rakefile
134
134
  - bin/console
135
135
  - bin/setup
136
+ - data/gateway_responses.yml
136
137
  - exe/yp-ruby
137
138
  - lib/base.rb
138
139
  - lib/factory.rb
139
140
  - lib/nil_logger.rb
140
141
  - lib/preauth.rb
142
+ - lib/response.rb
143
+ - lib/response/error.rb
144
+ - lib/response/error_logger.rb
145
+ - lib/response/gateway_error.rb
146
+ - lib/response/parser.rb
147
+ - lib/response/validator.rb
148
+ - lib/response/validator/acquirer.rb
149
+ - lib/response/validator/gateway.rb
150
+ - lib/response/validator/signing_key.rb
141
151
  - lib/sale.rb
152
+ - lib/signing_hash_creator.rb
142
153
  - lib/transaction_logger.rb
143
154
  - lib/verify.rb
144
155
  - lib/version.rb
@@ -160,9 +171,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
160
171
  version: '0'
161
172
  required_rubygems_version: !ruby/object:Gem::Requirement
162
173
  requirements:
163
- - - ">="
174
+ - - ">"
164
175
  - !ruby/object:Gem::Version
165
- version: '0'
176
+ version: 1.3.1
166
177
  requirements: []
167
178
  rubyforge_project:
168
179
  rubygems_version: 2.5.2