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.
- checksums.yaml +7 -0
- data/.gitignore +23 -0
- data/.rspec +2 -0
- data/.rubocop.yml +47 -0
- data/.yardopts +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +43 -0
- data/Rakefile +7 -0
- data/judopay.gemspec +37 -0
- data/lib/certs/rapidssl_ca.crt +64 -0
- data/lib/faraday/judo_mashify.rb +13 -0
- data/lib/faraday/raise_http_exception.rb +36 -0
- data/lib/judopay.rb +67 -0
- data/lib/judopay/api.rb +10 -0
- data/lib/judopay/connection.rb +67 -0
- data/lib/judopay/core_ext/hash.rb +29 -0
- data/lib/judopay/core_ext/string.rb +31 -0
- data/lib/judopay/error.rb +153 -0
- data/lib/judopay/mash.rb +7 -0
- data/lib/judopay/model.rb +109 -0
- data/lib/judopay/models/card_address.rb +11 -0
- data/lib/judopay/models/card_payment.rb +39 -0
- data/lib/judopay/models/card_preauth.rb +12 -0
- data/lib/judopay/models/collection.rb +16 -0
- data/lib/judopay/models/consumer_location.rb +8 -0
- data/lib/judopay/models/market/collection.rb +10 -0
- data/lib/judopay/models/market/payment.rb +10 -0
- data/lib/judopay/models/market/preauth.rb +10 -0
- data/lib/judopay/models/market/refund.rb +10 -0
- data/lib/judopay/models/market/transaction.rb +10 -0
- data/lib/judopay/models/payment.rb +8 -0
- data/lib/judopay/models/preauth.rb +8 -0
- data/lib/judopay/models/refund.rb +16 -0
- data/lib/judopay/models/token_payment.rb +30 -0
- data/lib/judopay/models/token_preauth.rb +10 -0
- data/lib/judopay/models/transaction.rb +8 -0
- data/lib/judopay/models/web_payments/payment.rb +24 -0
- data/lib/judopay/models/web_payments/preauth.rb +10 -0
- data/lib/judopay/models/web_payments/transaction.rb +19 -0
- data/lib/judopay/null_logger.rb +11 -0
- data/lib/judopay/request.rb +50 -0
- data/lib/judopay/response.rb +7 -0
- data/lib/judopay/serializer.rb +31 -0
- data/lib/judopay/version.rb +3 -0
- data/spec/factories.rb +103 -0
- data/spec/faraday/response_spec.rb +29 -0
- data/spec/fixtures/card_payments/create.json +22 -0
- data/spec/fixtures/card_payments/create_3dsecure.json +8 -0
- data/spec/fixtures/card_payments/create_bad_request.json +5 -0
- data/spec/fixtures/card_payments/create_declined.json +24 -0
- data/spec/fixtures/card_payments/validate.json +5 -0
- data/spec/fixtures/token_payments/create.json +22 -0
- data/spec/fixtures/transactions/all.json +238 -0
- data/spec/fixtures/transactions/find.json +23 -0
- data/spec/fixtures/transactions/find_not_found.json +5 -0
- data/spec/fixtures/web_payments/payments/create.json +4 -0
- data/spec/fixtures/web_payments/payments/find.json +39 -0
- data/spec/judopay/card_address_spec.rb +10 -0
- data/spec/judopay/card_payment_spec.rb +64 -0
- data/spec/judopay/card_preauth_spec.rb +35 -0
- data/spec/judopay/collection_spec.rb +26 -0
- data/spec/judopay/core_ext/hash_spec.rb +24 -0
- data/spec/judopay/core_ext/string_spec.rb +16 -0
- data/spec/judopay/error_spec.rb +49 -0
- data/spec/judopay/judopay_spec.rb +19 -0
- data/spec/judopay/market/collection_spec.rb +26 -0
- data/spec/judopay/market/payment_spec.rb +14 -0
- data/spec/judopay/market/preauth_spec.rb +14 -0
- data/spec/judopay/market/refund_spec.rb +26 -0
- data/spec/judopay/market/transaction_spec.rb +14 -0
- data/spec/judopay/payment_spec.rb +14 -0
- data/spec/judopay/preauth_spec.rb +14 -0
- data/spec/judopay/refund_spec.rb +26 -0
- data/spec/judopay/token_payment_spec.rb +22 -0
- data/spec/judopay/token_preauth_spec.rb +22 -0
- data/spec/judopay/transaction_spec.rb +51 -0
- data/spec/judopay/web_payments/payment_spec.rb +16 -0
- data/spec/judopay/web_payments/preauth_spec.rb +16 -0
- data/spec/judopay/web_payments/transaction_spec.rb +15 -0
- data/spec/spec_helper.rb +37 -0
- data/tutorials/judo_getting_started.md +74 -0
- data/tutorials/judo_making_a_payment.md +84 -0
- 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
|
data/lib/judopay/mash.rb
ADDED
@@ -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,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
|