judopay 1.0.0.pre

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 (84) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +23 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +47 -0
  5. data/.yardopts +1 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +43 -0
  9. data/Rakefile +7 -0
  10. data/judopay.gemspec +37 -0
  11. data/lib/certs/rapidssl_ca.crt +64 -0
  12. data/lib/faraday/judo_mashify.rb +13 -0
  13. data/lib/faraday/raise_http_exception.rb +36 -0
  14. data/lib/judopay.rb +67 -0
  15. data/lib/judopay/api.rb +10 -0
  16. data/lib/judopay/connection.rb +67 -0
  17. data/lib/judopay/core_ext/hash.rb +29 -0
  18. data/lib/judopay/core_ext/string.rb +31 -0
  19. data/lib/judopay/error.rb +153 -0
  20. data/lib/judopay/mash.rb +7 -0
  21. data/lib/judopay/model.rb +109 -0
  22. data/lib/judopay/models/card_address.rb +11 -0
  23. data/lib/judopay/models/card_payment.rb +39 -0
  24. data/lib/judopay/models/card_preauth.rb +12 -0
  25. data/lib/judopay/models/collection.rb +16 -0
  26. data/lib/judopay/models/consumer_location.rb +8 -0
  27. data/lib/judopay/models/market/collection.rb +10 -0
  28. data/lib/judopay/models/market/payment.rb +10 -0
  29. data/lib/judopay/models/market/preauth.rb +10 -0
  30. data/lib/judopay/models/market/refund.rb +10 -0
  31. data/lib/judopay/models/market/transaction.rb +10 -0
  32. data/lib/judopay/models/payment.rb +8 -0
  33. data/lib/judopay/models/preauth.rb +8 -0
  34. data/lib/judopay/models/refund.rb +16 -0
  35. data/lib/judopay/models/token_payment.rb +30 -0
  36. data/lib/judopay/models/token_preauth.rb +10 -0
  37. data/lib/judopay/models/transaction.rb +8 -0
  38. data/lib/judopay/models/web_payments/payment.rb +24 -0
  39. data/lib/judopay/models/web_payments/preauth.rb +10 -0
  40. data/lib/judopay/models/web_payments/transaction.rb +19 -0
  41. data/lib/judopay/null_logger.rb +11 -0
  42. data/lib/judopay/request.rb +50 -0
  43. data/lib/judopay/response.rb +7 -0
  44. data/lib/judopay/serializer.rb +31 -0
  45. data/lib/judopay/version.rb +3 -0
  46. data/spec/factories.rb +103 -0
  47. data/spec/faraday/response_spec.rb +29 -0
  48. data/spec/fixtures/card_payments/create.json +22 -0
  49. data/spec/fixtures/card_payments/create_3dsecure.json +8 -0
  50. data/spec/fixtures/card_payments/create_bad_request.json +5 -0
  51. data/spec/fixtures/card_payments/create_declined.json +24 -0
  52. data/spec/fixtures/card_payments/validate.json +5 -0
  53. data/spec/fixtures/token_payments/create.json +22 -0
  54. data/spec/fixtures/transactions/all.json +238 -0
  55. data/spec/fixtures/transactions/find.json +23 -0
  56. data/spec/fixtures/transactions/find_not_found.json +5 -0
  57. data/spec/fixtures/web_payments/payments/create.json +4 -0
  58. data/spec/fixtures/web_payments/payments/find.json +39 -0
  59. data/spec/judopay/card_address_spec.rb +10 -0
  60. data/spec/judopay/card_payment_spec.rb +64 -0
  61. data/spec/judopay/card_preauth_spec.rb +35 -0
  62. data/spec/judopay/collection_spec.rb +26 -0
  63. data/spec/judopay/core_ext/hash_spec.rb +24 -0
  64. data/spec/judopay/core_ext/string_spec.rb +16 -0
  65. data/spec/judopay/error_spec.rb +49 -0
  66. data/spec/judopay/judopay_spec.rb +19 -0
  67. data/spec/judopay/market/collection_spec.rb +26 -0
  68. data/spec/judopay/market/payment_spec.rb +14 -0
  69. data/spec/judopay/market/preauth_spec.rb +14 -0
  70. data/spec/judopay/market/refund_spec.rb +26 -0
  71. data/spec/judopay/market/transaction_spec.rb +14 -0
  72. data/spec/judopay/payment_spec.rb +14 -0
  73. data/spec/judopay/preauth_spec.rb +14 -0
  74. data/spec/judopay/refund_spec.rb +26 -0
  75. data/spec/judopay/token_payment_spec.rb +22 -0
  76. data/spec/judopay/token_preauth_spec.rb +22 -0
  77. data/spec/judopay/transaction_spec.rb +51 -0
  78. data/spec/judopay/web_payments/payment_spec.rb +16 -0
  79. data/spec/judopay/web_payments/preauth_spec.rb +16 -0
  80. data/spec/judopay/web_payments/transaction_spec.rb +15 -0
  81. data/spec/spec_helper.rb +37 -0
  82. data/tutorials/judo_getting_started.md +74 -0
  83. data/tutorials/judo_making_a_payment.md +84 -0
  84. metadata +373 -0
@@ -0,0 +1,29 @@
1
+ require 'addressable/uri'
2
+ require_relative 'string'
3
+
4
+ class Hash
5
+ # Convert hash keys to camelcase
6
+ #
7
+ # {'this_key' => 1}.camel_case_keys! #=> { 'thisKey' => 1 }
8
+ def camel_case_keys!
9
+ output_hash = {}
10
+ each do |key, value|
11
+ camel_case_key = key.to_s.camel_case
12
+ if value.is_a?(Hash)
13
+ output_hash[camel_case_key] = value.camel_case_keys!
14
+ else
15
+ output_hash[key.to_s.camel_case] = value
16
+ end
17
+ end
18
+ replace(output_hash)
19
+ end
20
+
21
+ # Produce a URL query string from the hash
22
+ #
23
+ # {'this_key' => 1}.to_query_string #=> this_key=1
24
+ def to_query_string
25
+ uri = Addressable::URI.new
26
+ uri.query_values = self
27
+ uri.query
28
+ end
29
+ end
@@ -0,0 +1,31 @@
1
+ class String
2
+ # Convert to camel case
3
+ #
4
+ # "foo_bar".camel_case #=> "fooBar"
5
+ def camel_case
6
+ return self if self !~ /_/ && self =~ /[A-Z]+.*/
7
+ split('_').map { |e| e.capitalize }.join.uncapitalize
8
+ end
9
+
10
+ # Convert first letter to lower case
11
+ #
12
+ # "BananaMan".uncapitalize #=> "bananaMan"
13
+ def uncapitalize
14
+ self[0, 1].downcase + self[1..-1]
15
+ end
16
+
17
+ # Converts a camel_cased string to a underscored string
18
+ # Replaces spaces with underscores, and strips whitespace
19
+ #
20
+ # "BananaMan".underscore #=> "banana_man"
21
+ def underscore
22
+ to_s.strip
23
+ .gsub(' ', '_')
24
+ .gsub(/::/, '/')
25
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
26
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
27
+ .tr('-', '_')
28
+ .squeeze('_')
29
+ .downcase
30
+ end
31
+ end
@@ -0,0 +1,153 @@
1
+ require 'json'
2
+
3
+ module Judopay
4
+ # Base error class
5
+ class Error < StandardError
6
+ attr_writer :message
7
+
8
+ def initialize(message = nil)
9
+ @message = message
10
+ end
11
+
12
+ def message
13
+ @message || self.class.name
14
+ end
15
+
16
+ def to_s
17
+ @message
18
+ end
19
+ end
20
+
21
+ # Custom error class for rescuing from all API errors
22
+ class APIError < StandardError
23
+ attr_accessor :response, :error_type, :model_errors, :message, :parsed_body
24
+
25
+ def initialize(response = nil)
26
+ @response = response
27
+
28
+ # Log the error
29
+ Judopay.log(Logger::ERROR, self.class.name + ' ' + @message.to_s)
30
+
31
+ # If we got a JSON response body, set variables
32
+ return if parsed_body.nil?
33
+
34
+ @message = body_attribute('errorMessage')
35
+ @error_type = body_attribute('errorType').to_i
36
+ api_model_errors = body_attribute('modelErrors')
37
+ return if api_model_errors.nil?
38
+
39
+ process_api_model_errors(api_model_errors)
40
+ end
41
+
42
+ def http_status
43
+ @response.status.to_i if @response
44
+ end
45
+
46
+ def http_body
47
+ @response.body if @response
48
+ end
49
+
50
+ def to_s
51
+ return @message if model_errors.nil?
52
+
53
+ summary = []
54
+ model_errors.each do |_key, value|
55
+ summary.push(value.join('; '))
56
+ end
57
+
58
+ @message + ' (' + summary.join('; ') + ')'
59
+ end
60
+
61
+ def message
62
+ @message || self.class.name
63
+ end
64
+
65
+ def parsed_body
66
+ @parsed_body ||= parse_body
67
+ end
68
+
69
+ protected
70
+
71
+ def parse_body
72
+ return unless @response.respond_to?('response_headers')
73
+ return unless @response.response_headers.include?('Content-Type')
74
+ return unless @response.response_headers['Content-Type'].include?('application/json')
75
+
76
+ ::JSON.parse(@response.body)
77
+ end
78
+
79
+ def body_attribute(attribute)
80
+ return nil if parsed_body.nil? || !parsed_body.include?(attribute)
81
+ parsed_body[attribute]
82
+ end
83
+
84
+ # Turn API model errors into a more ActiveRecord-like format
85
+ def process_api_model_errors(api_model_errors)
86
+ @model_errors = {}
87
+ api_model_errors.each do |api_model_error|
88
+ next unless api_model_error.is_a?(Hash)
89
+ field_name = api_model_error['fieldName'].underscore.to_sym
90
+ @model_errors[field_name] = [] if @model_errors[field_name].nil?
91
+ @model_errors[field_name].push(api_model_error['errorMessage'])
92
+ end
93
+ end
94
+ end
95
+
96
+ # Raised when API returns the HTTP status code 400
97
+ class BadRequest < APIError; end
98
+
99
+ # Raised when API returns the HTTP status code 401/403
100
+ class NotAuthorized < APIError
101
+ def message
102
+ 'Authorization has been denied for this request'
103
+ end
104
+ end
105
+
106
+ # Raised when API returns the HTTP status code 404
107
+ class NotFound < APIError; end
108
+
109
+ # Raised when API returns the HTTP status code 409
110
+ class Conflict < APIError; end
111
+
112
+ # Raised when API returns the HTTP status code 500
113
+ class InternalServerError < APIError; end
114
+
115
+ # Raised when API returns the HTTP status code 502
116
+ class BadGateway < APIError; end
117
+
118
+ # Raised when API returns the HTTP status code 503
119
+ class ServiceUnavailable < APIError; end
120
+
121
+ # Raised when API returns the HTTP status code 504
122
+ class GatewayTimeout < APIError; end
123
+
124
+ # A validation error that hasn't reached the API
125
+ class ValidationError < StandardError
126
+ attr_accessor :errors, :message
127
+
128
+ def initialize(errors)
129
+ @errors = errors
130
+ @message = 'Missing required fields' + model_errors_summary
131
+ end
132
+
133
+ def to_s
134
+ @message
135
+ end
136
+
137
+ def model_errors
138
+ return if @errors.nil?
139
+ @errors.messages
140
+ end
141
+
142
+ protected
143
+
144
+ def model_errors_summary
145
+ summary = []
146
+ model_errors.each do |key, value|
147
+ summary.push(key.to_s + ' ' + value.join('; '))
148
+ end
149
+
150
+ ' (' + summary.join('; ') + ')'
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,7 @@
1
+ require 'hashie'
2
+ require 'rash'
3
+
4
+ module Judopay
5
+ class Mash < ::Hashie::Rash
6
+ end
7
+ end
@@ -0,0 +1,109 @@
1
+ require 'addressable/uri'
2
+ require 'virtus'
3
+ require 'active_model'
4
+ require_relative 'core_ext/hash'
5
+ require_relative 'error'
6
+
7
+ module Judopay
8
+ # Base model for Judopay API model objects
9
+ class Model
10
+ send :include, Virtus.model
11
+ include ActiveModel::Validations
12
+ VALID_PAGING_OPTIONS = [:sort, :offset, :page_size]
13
+
14
+ class << self
15
+ @resource_path = nil
16
+ @valid_api_methods = []
17
+ attr_accessor :resource_path, :valid_api_methods
18
+ end
19
+
20
+ # List all records
21
+ #
22
+ # @param options [Hash] Paging options (sort, offset and page_size)
23
+ # @return [Judopay::Mash] Mash of the API response
24
+ def self.all(options = {})
25
+ check_api_method_is_supported(__method__)
26
+ api = Judopay::API.new
27
+ valid_options = self.valid_options(options).camel_case_keys!
28
+ uri = resource_path + '?' + valid_options.to_query_string
29
+ api.get(uri)
30
+ end
31
+
32
+ # Retrieve a specific record
33
+ #
34
+ # @param receipt_id [Integer] Paging options (sort, offset and page_size)
35
+ # @return [Judopay::Mash] Mash of the API response
36
+ def self.find(receipt_id)
37
+ check_api_method_is_supported(__method__)
38
+ api = Judopay::API.new
39
+ api.get(resource_path + receipt_id.to_i.to_s)
40
+ end
41
+
42
+ # Create a new record
43
+ #
44
+ # @return [Judopay::Mash] Mash of the API response
45
+ def create
46
+ check_api_method_is_supported(__method__)
47
+ check_judo_id
48
+ check_validation
49
+ api = Judopay::API.new
50
+ api.post(resource_path, self)
51
+ end
52
+
53
+ # Retrieve the current API resource path (e.g. /transactions/payments)
54
+ #
55
+ # @return [String] Resource path
56
+ def resource_path
57
+ self.class.resource_path
58
+ end
59
+
60
+ # Retrieve an array of valid API methods for the current model
61
+ # e.g [:find, :create]
62
+ #
63
+ # @return [Array<Symbol>] Valid API methods
64
+ def valid_api_methods
65
+ self.class.valid_api_methods
66
+ end
67
+
68
+ # Verify if an API method is supported for the current model
69
+ def check_api_method_is_supported(method)
70
+ self.class.check_api_method_is_supported(method)
71
+ end
72
+
73
+ # Use judo_id from configuration if it hasn't been explicitly set
74
+ def check_judo_id
75
+ return unless self.respond_to?('judo_id') && judo_id.nil?
76
+ self.judo_id = Judopay.configuration.judo_id
77
+ end
78
+
79
+ protected
80
+
81
+ # Has the pre-validation found any problems?
82
+ # We check the basics have been completed to avoid round trip to API
83
+ #
84
+ # @return nil
85
+ # @raise [Judopay::ValidationError] if there are validation errors on the model
86
+ def check_validation
87
+ fail Judopay::ValidationError, errors unless valid?
88
+ end
89
+
90
+ # Check if the specified API method is supported by the current model
91
+ #
92
+ # @raise [Judopay::Error] if the API method is not supported
93
+ def self.check_api_method_is_supported(method)
94
+ if valid_api_methods.nil? || !valid_api_methods.include?(method.to_sym)
95
+ fail Judopay::Error, 'API method not supported'
96
+ end
97
+ end
98
+
99
+ # Take an paging options hash and filter all but the valid keys
100
+ def self.valid_options(options)
101
+ valid_options = {}
102
+ options.each do |key, value|
103
+ next unless VALID_PAGING_OPTIONS.include?(key)
104
+ valid_options[key] = value
105
+ end
106
+ valid_options
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,11 @@
1
+ require_relative '../model'
2
+
3
+ module Judopay
4
+ class CardAddress < Model
5
+ attribute :line1, String
6
+ attribute :line2, String
7
+ attribute :line3, String
8
+ attribute :town, String
9
+ attribute :postcode, String
10
+ end
11
+ end
@@ -0,0 +1,39 @@
1
+ require_relative '../model'
2
+ require_relative 'payment'
3
+ require_relative 'card_address'
4
+ require_relative 'consumer_location'
5
+
6
+ module Judopay
7
+ class CardPayment < Model
8
+ @resource_path = 'transactions/payments'
9
+ @valid_api_methods = [:create]
10
+
11
+ attribute :your_consumer_reference, String # required
12
+ attribute :your_payment_reference, String # required
13
+ attribute :your_payment_meta_data, Hash
14
+ attribute :judo_id, String # required
15
+ attribute :amount, Float # required
16
+ attribute :card_number, String # required for card transactions
17
+ attribute :expiry_date, String # required for card transactions
18
+ attribute :cv2, String # required for card transactions
19
+ attribute :card_address, Judopay::CardAddress
20
+ attribute :consumer_location, Judopay::ConsumerLocation
21
+ attribute :mobile_number, String
22
+ attribute :email_address, String
23
+
24
+ validates_presence_of :your_consumer_reference,
25
+ :your_payment_reference,
26
+ :judo_id,
27
+ :amount,
28
+ :card_number,
29
+ :expiry_date,
30
+ :cv2
31
+
32
+ def validate
33
+ check_judo_id
34
+ check_validation
35
+ api = Judopay::API.new
36
+ api.post(resource_path + '/validate', self)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,12 @@
1
+ require_relative '../model'
2
+ require_relative 'payment'
3
+ require_relative 'card_address'
4
+ require_relative 'consumer_location'
5
+
6
+ module Judopay
7
+ # Inherit from CardPayment - attributes are identical
8
+ class CardPreauth < CardPayment
9
+ @resource_path = 'transactions/preauths'
10
+ @valid_api_methods = [:create]
11
+ end
12
+ end
@@ -0,0 +1,16 @@
1
+ require_relative '../model'
2
+
3
+ module Judopay
4
+ class Collection < Model
5
+ @resource_path = 'transactions/collections'
6
+ @valid_api_methods = [:all, :create]
7
+
8
+ attribute :receipt_id, Integer
9
+ attribute :amount, Float
10
+ attribute :your_payment_reference, String
11
+
12
+ validates_presence_of :receipt_id,
13
+ :amount,
14
+ :your_payment_reference
15
+ end
16
+ end
@@ -0,0 +1,8 @@
1
+ require_relative '../model'
2
+
3
+ module Judopay
4
+ class ConsumerLocation < Model
5
+ attribute :latitude, Float
6
+ attribute :longitude, Float
7
+ end
8
+ end
@@ -0,0 +1,10 @@
1
+ require_relative '../../model'
2
+
3
+ module Judopay
4
+ module Market
5
+ class Collection < Judopay::Collection
6
+ @resource_path = 'market/transactions/collections'
7
+ @valid_api_methods = [:all, :create]
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ require_relative '../../model'
2
+
3
+ module Judopay
4
+ module Market
5
+ class Payment < Model
6
+ @resource_path = 'market/transactions/payments'
7
+ @valid_api_methods = [:all]
8
+ end
9
+ end
10
+ end