rocketgate-ruby 0.0.1.pre
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.coveralls.yml +1 -0
- data/.gitignore +14 -0
- data/.travis.yml +20 -0
- data/Gemfile +3 -0
- data/Guardfile +11 -0
- data/LICENSE.txt +22 -0
- data/README.md +161 -0
- data/Rakefile +14 -0
- data/lib/rocketgate.rb +52 -0
- data/lib/rocketgate/authorization.rb +31 -0
- data/lib/rocketgate/configuration.rb +53 -0
- data/lib/rocketgate/connection.rb +30 -0
- data/lib/rocketgate/credit_card.rb +42 -0
- data/lib/rocketgate/customer.rb +33 -0
- data/lib/rocketgate/hashable.rb +63 -0
- data/lib/rocketgate/request.rb +48 -0
- data/lib/rocketgate/response.rb +64 -0
- data/lib/rocketgate/response_code.rb +148 -0
- data/lib/rocketgate/transaction.rb +75 -0
- data/lib/rocketgate/validatable.rb +52 -0
- data/lib/rocketgate/version.rb +3 -0
- data/rocketgate-ruby.gemspec +34 -0
- data/spec/authorization_spec.rb +87 -0
- data/spec/configuration_spec.rb +33 -0
- data/spec/connection_spec.rb +65 -0
- data/spec/credit_card_spec.rb +73 -0
- data/spec/customer_spec.rb +57 -0
- data/spec/hashable_spec.rb +85 -0
- data/spec/integration_spec.rb +131 -0
- data/spec/request_spec.rb +92 -0
- data/spec/response_spec.rb +190 -0
- data/spec/rocketgate_spec.rb +108 -0
- data/spec/spec_helper.rb +9 -0
- data/spec/transaction_spec.rb +250 -0
- data/spec/validatable_spec.rb +79 -0
- metadata +261 -0
@@ -0,0 +1,33 @@
|
|
1
|
+
module RocketGate
|
2
|
+
class Customer
|
3
|
+
include Hashable, Validatable
|
4
|
+
|
5
|
+
attr_accessor :id, :first_name, :last_name, :street_address, :city,
|
6
|
+
:postal_code, :state, :country, :email, :username, :ip_address
|
7
|
+
|
8
|
+
validatable :id, :first_name, :last_name
|
9
|
+
|
10
|
+
hashable({
|
11
|
+
merchantCustomerID: :id,
|
12
|
+
customerFirstName: :first_name,
|
13
|
+
customerLastName: :last_name,
|
14
|
+
billingAddress: :street_address,
|
15
|
+
billingCity: :city,
|
16
|
+
billingState: :state,
|
17
|
+
billingZipCode: :postal_code,
|
18
|
+
billingCountry: :country,
|
19
|
+
email: :email,
|
20
|
+
username: :username,
|
21
|
+
ipAddress: :ip_address
|
22
|
+
})
|
23
|
+
|
24
|
+
def initialize(*args)
|
25
|
+
@first_name, @last_name, @street_address, @city, @state, @postal_code,
|
26
|
+
@country, @email, @username, @ip_address, @id = *args
|
27
|
+
end
|
28
|
+
|
29
|
+
def id
|
30
|
+
@id ||= SecureRandom.uuid
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module RocketGate::Hashable
|
2
|
+
module ClassMethods
|
3
|
+
def hashable(mapping_hash, *mergable_items)
|
4
|
+
@mappings, @mergables = {}, []
|
5
|
+
|
6
|
+
mapping_hash.each do |display_name, obj|
|
7
|
+
if self.new.send(:is_callable?, obj)
|
8
|
+
@mappings[display_name] = obj
|
9
|
+
else
|
10
|
+
raise RocketGate::ValidationError.new("Invalid attribute: #{obj}")
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
mergable_items.each do |obj|
|
15
|
+
if self.new.send(:is_callable?, obj)
|
16
|
+
@mergables << obj
|
17
|
+
else
|
18
|
+
raise RocketGate::ValidationError.new("Invalid attribute: #{obj}")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def mappings
|
24
|
+
@mappings ||= {}
|
25
|
+
end
|
26
|
+
|
27
|
+
def mergables
|
28
|
+
@mergables ||= []
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.included(base)
|
33
|
+
base.extend(ClassMethods)
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_hash
|
37
|
+
hash = Hash.new.tap do |hsh|
|
38
|
+
self.class.mappings.each do |display_name, obj|
|
39
|
+
hsh[display_name] = attribute_or_proc_value(obj)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
self.class.mergables.each do |obj|
|
44
|
+
hash.merge!(attribute_or_proc_value(obj))
|
45
|
+
end if self.class.mergables.any?
|
46
|
+
|
47
|
+
hash
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def attribute_or_proc_value(obj)
|
53
|
+
if obj.is_a?(Proc)
|
54
|
+
obj.call
|
55
|
+
elsif self.respond_to?(obj)
|
56
|
+
self.send(obj)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def is_callable?(obj)
|
61
|
+
obj.is_a?(Proc) || self.respond_to?(obj)
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'gyoku'
|
2
|
+
|
3
|
+
module RocketGate
|
4
|
+
class Request
|
5
|
+
attr_accessor :transaction
|
6
|
+
|
7
|
+
def initialize(transaction = nil)
|
8
|
+
@transaction = transaction
|
9
|
+
end
|
10
|
+
|
11
|
+
def build_gateway_request
|
12
|
+
transaction.validate!
|
13
|
+
|
14
|
+
parameters = base_parameters
|
15
|
+
parameters.merge!(transaction.to_hash)
|
16
|
+
|
17
|
+
xml_head = '<?xml version="1.0" encoding="UTF-8"?>'
|
18
|
+
xml_body = Gyoku.xml({
|
19
|
+
gatewayRequest: parameters.select{|_,v| !v.nil? }
|
20
|
+
})
|
21
|
+
|
22
|
+
[xml_head, xml_body].join
|
23
|
+
end
|
24
|
+
|
25
|
+
def base_parameters
|
26
|
+
config = RocketGate.configuration
|
27
|
+
config.validate!
|
28
|
+
|
29
|
+
params = {
|
30
|
+
version: config.request_version,
|
31
|
+
merchantID: config.merchant_id,
|
32
|
+
merchantPassword: config.merchant_password
|
33
|
+
}
|
34
|
+
|
35
|
+
params.merge!({
|
36
|
+
avsCheck: yes_or_ignore(config.require_avs),
|
37
|
+
cvv2Check: yes_or_ignore(config.require_cvv),
|
38
|
+
scrub: yes_or_ignore(config.require_scrub)
|
39
|
+
}) unless transaction && transaction.is_referenced?
|
40
|
+
|
41
|
+
params
|
42
|
+
end
|
43
|
+
|
44
|
+
def yes_or_ignore(boolean)
|
45
|
+
boolean ? 'YES' : 'IGNORE'
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
|
3
|
+
module RocketGate
|
4
|
+
class Response
|
5
|
+
attr_accessor :authorization, :response_code, :xml_document
|
6
|
+
|
7
|
+
def self.from_xml(xml)
|
8
|
+
response = self.new.tap do |r|
|
9
|
+
r.xml_document = Nokogiri.parse(xml)
|
10
|
+
r.build!
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def build!
|
15
|
+
@response_code = RocketGate::ResponseCode::RESPONSE[find_value('responseCode')]
|
16
|
+
|
17
|
+
@authorization = RocketGate::Authorization.new.tap do |auth|
|
18
|
+
auth.avs_response = RocketGate::ResponseCode::AVS[find_value('avsResponse')]
|
19
|
+
auth.cvv_response = RocketGate::ResponseCode::CVV[find_value('cvv2Code')]
|
20
|
+
auth.reason_code = RocketGate::ResponseCode::REASON[find_value('reasonCode')]
|
21
|
+
auth.auth_code = find_value('authNo')
|
22
|
+
auth.reference_id = find_value('guidNo')
|
23
|
+
auth.approved_amount = find_value('approvedAmount').to_f
|
24
|
+
auth.approved_currency = find_value('approvedCurrency')
|
25
|
+
auth.card_hash = find_value('cardHash')
|
26
|
+
auth.card_expiration = find_value('cardExpiration')
|
27
|
+
auth.card_last_four = find_value('cardLastFour')
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def confirm!
|
32
|
+
if success?
|
33
|
+
confirmation = RocketGate::Transaction.new.tap do |tx|
|
34
|
+
tx.reference_id = self.authorization.reference_id
|
35
|
+
tx.type = RocketGate::Transaction::TYPE[:confirmation]
|
36
|
+
end
|
37
|
+
|
38
|
+
RocketGate.send_request!(RocketGate::Request.new(confirmation))
|
39
|
+
else
|
40
|
+
raise RocketGate::AuthorizationError.new('Unable to confirm unauthorized transaction')
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def authorized?
|
45
|
+
!!(authorization && authorization.success?)
|
46
|
+
end
|
47
|
+
|
48
|
+
def success?
|
49
|
+
authorized? && response_code == :success
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def find_value(key)
|
55
|
+
nodes = xml_document.at('gatewayResponse')
|
56
|
+
if nodes && nodes.children
|
57
|
+
element = nodes % key
|
58
|
+
element.content if element
|
59
|
+
else
|
60
|
+
raise RocketGate::GatewayResponseError.new('Unexpected response format')
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
module RocketGate::ResponseCode
|
2
|
+
AVS = {
|
3
|
+
'D' => :match,
|
4
|
+
'F' => :match,
|
5
|
+
'M' => :match,
|
6
|
+
'X' => :match,
|
7
|
+
'Y' => :match,
|
8
|
+
'A' => :address_only_match,
|
9
|
+
'B' => :address_only_match,
|
10
|
+
'P' => :zip_only_match,
|
11
|
+
'T' => :zip_only_match,
|
12
|
+
'W' => :zip_only_match,
|
13
|
+
'Z' => :zip_only_match,
|
14
|
+
'C' => :not_verified,
|
15
|
+
'G' => :not_verified,
|
16
|
+
'I' => :not_verified,
|
17
|
+
'R' => :unavailable,
|
18
|
+
'U' => :unavailable,
|
19
|
+
'S' => :unsupported,
|
20
|
+
'N' => :no_match
|
21
|
+
}
|
22
|
+
AVS_SUCCESSES = [ :match, :address_only_match, :zip_only_match, nil ]
|
23
|
+
|
24
|
+
CVV = {
|
25
|
+
'M' => :match,
|
26
|
+
'N' => :no_match,
|
27
|
+
'P' => :not_processed,
|
28
|
+
'S' => :should_have_been_present,
|
29
|
+
'U' => :issuer_could_not_process
|
30
|
+
}
|
31
|
+
CVV_SUCCESSES = [ :match, :not_processed, :issuer_could_not_process, nil ]
|
32
|
+
|
33
|
+
CARD_BRAND = {
|
34
|
+
'AMEX' => :amex,
|
35
|
+
'CARTEBLANCHE' => :carte_blanche,
|
36
|
+
'DINERSCLUB' => :diners_club,
|
37
|
+
'DISCOVER' => :discover,
|
38
|
+
'JCB' => :jcb,
|
39
|
+
'MAESTRO' => :maestro,
|
40
|
+
'MC' => :master_card,
|
41
|
+
'SOLO' => :solo,
|
42
|
+
'VISA' => :visa
|
43
|
+
}
|
44
|
+
|
45
|
+
CARD_REGION = {
|
46
|
+
'1' => :usa,
|
47
|
+
'2' => :canada,
|
48
|
+
'3' => :europe,
|
49
|
+
'4' => :asia_pacific,
|
50
|
+
'5' => :latin_america,
|
51
|
+
'6' => :africa_middle_east
|
52
|
+
}
|
53
|
+
|
54
|
+
CARD_TYPE = {
|
55
|
+
'0' => :debit,
|
56
|
+
'1' => :credit,
|
57
|
+
'2' => :both
|
58
|
+
}
|
59
|
+
|
60
|
+
REASON = {
|
61
|
+
'0' => :success,
|
62
|
+
'100' => :no_matching_transaction,
|
63
|
+
'101' => :cannot_void,
|
64
|
+
'102' => :cannot_credit,
|
65
|
+
'103' => :cannot_ticket,
|
66
|
+
'104' => :declined_generic,
|
67
|
+
'105' => :declined_over_limit,
|
68
|
+
'106' => :declined_cvv_mismatch,
|
69
|
+
'107' => :declined_expired,
|
70
|
+
'108' => :declined_call_issuer,
|
71
|
+
'109' => :declined_pickup_card,
|
72
|
+
'110' => :declined_excessive_use,
|
73
|
+
'111' => :declined_invalid_card,
|
74
|
+
'112' => :declined_invalid_expiration,
|
75
|
+
'113' => :declined_bank_unavailable,
|
76
|
+
'117' => :declined_avs_mismatch,
|
77
|
+
'123' => :declined_by_user_request,
|
78
|
+
'150' => :declined_avs_mismatch,
|
79
|
+
'151' => :declined_cvv_mismatch,
|
80
|
+
'152' => :declined_invalid_amount,
|
81
|
+
'154' => :declined_integration_error,
|
82
|
+
'156' => :declined_unsupported_card,
|
83
|
+
'157' => :declined_processor_risk,
|
84
|
+
'200' => :declined_fraud_scrubbing,
|
85
|
+
'201' => :declined_customer_blocked,
|
86
|
+
'208' => :declined_duplicate_membership_id,
|
87
|
+
'209' => :declined_duplicate_membership_card,
|
88
|
+
'210' => :declined_duplicate_membership_email,
|
89
|
+
'211' => :declined_over_max_purchase,
|
90
|
+
'212' => :declined_duplicate_transaction,
|
91
|
+
'213' => :declined_customer_velocity,
|
92
|
+
'214' => :declined_card_velocity,
|
93
|
+
'215' => :declined_email_velocity,
|
94
|
+
'216' => :declined_iovation_generic,
|
95
|
+
'217' => :declined_iovation_email_velocity,
|
96
|
+
'218' => :declined_iovation_duplicate_membership,
|
97
|
+
'219' => :declined_different_source_ip,
|
98
|
+
'220' => :declined_too_many_cards,
|
99
|
+
'221' => :declined_affiliate_blocked,
|
100
|
+
'222' => :declined_trial_abuse,
|
101
|
+
'300' => :error_dns_failed,
|
102
|
+
'301' => :error_connection_failed,
|
103
|
+
'303' => :error_read_timeout,
|
104
|
+
'311' => :error_bank_communcations,
|
105
|
+
'312' => :error_bank_communcations,
|
106
|
+
'313' => :error_bank_communcations,
|
107
|
+
'314' => :error_bank_communcations,
|
108
|
+
'315' => :error_bank_communcations,
|
109
|
+
'316' => :error_bank_communcations,
|
110
|
+
'321' => :error_bank_communcations,
|
111
|
+
'323' => :error_bank_communcations,
|
112
|
+
'325' => :error_3d_secure,
|
113
|
+
'403' => :error_invalid_card_number,
|
114
|
+
'404' => :error_invalid_expiration,
|
115
|
+
'405' => :error_invalid_amount,
|
116
|
+
'406' => :error_invalid_merchant_id,
|
117
|
+
'407' => :error_invalid_merchant_account,
|
118
|
+
'408' => :error_incompatible_card_type,
|
119
|
+
'409' => :error_no_suitable_account,
|
120
|
+
'410' => :error_invalid_transaction_id,
|
121
|
+
'411' => :error_invalid_access_code,
|
122
|
+
'412' => :error_invalid_customer_data,
|
123
|
+
'413' => :error_invalid_external_data,
|
124
|
+
'414' => :error_invalid_customer_id,
|
125
|
+
'418' => :error_invalid_currency,
|
126
|
+
'419' => :error_incompatible_currency,
|
127
|
+
'420' => :error_invalid_rebill_arguments,
|
128
|
+
'436' => :error_incompatible_descriptor,
|
129
|
+
'438' => :error_invalid_site_id,
|
130
|
+
'441' => :error_invoice_not_found,
|
131
|
+
'443' => :error_missing_customer_id,
|
132
|
+
'444' => :error_missing_customer_name,
|
133
|
+
'445' => :error_missing_customer_address,
|
134
|
+
'446' => :error_missing_cvv,
|
135
|
+
'448' => :error_no_active_membership,
|
136
|
+
'449' => :error_invalid_cvv,
|
137
|
+
'451' => :error_invalid_clone_data,
|
138
|
+
'452' => :error_redundant_operation
|
139
|
+
}
|
140
|
+
|
141
|
+
RESPONSE = {
|
142
|
+
'0' => :success,
|
143
|
+
'1' => :bank_decline,
|
144
|
+
'2' => :rocketgate_scrubbing_decline,
|
145
|
+
'3' => :system_error,
|
146
|
+
'4' => :missing_or_invalid_fields
|
147
|
+
}
|
148
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module RocketGate
|
2
|
+
class Transaction
|
3
|
+
extend Forwardable
|
4
|
+
include Hashable, Validatable
|
5
|
+
|
6
|
+
attr_accessor :amount, :credit_card, :customer, :currency, :type, :reference_id, :id
|
7
|
+
|
8
|
+
validatable :amount, :credit_card, :customer, :currency, :id
|
9
|
+
|
10
|
+
def_delegator :credit_card, :to_hash, :credit_card_hash
|
11
|
+
def_delegator :customer, :to_hash, :customer_hash
|
12
|
+
|
13
|
+
hashable({
|
14
|
+
amount: :amount,
|
15
|
+
currency: :currency,
|
16
|
+
merchantInvoiceID: :id,
|
17
|
+
transactionType: :type,
|
18
|
+
referenceGUID: :reference_id
|
19
|
+
}, :credit_card_hash, :customer_hash)
|
20
|
+
|
21
|
+
TYPE = {
|
22
|
+
auth: 'CC_AUTH',
|
23
|
+
confirmation: 'CC_CONFIRM',
|
24
|
+
credit: 'CC_CREDIT',
|
25
|
+
purchase: 'CC_PURCHASE',
|
26
|
+
rebill_cancel: 'REBILL_CANCEL',
|
27
|
+
rebill_update: 'REBILL_UPDATE',
|
28
|
+
scrub: 'CARDSCRUB',
|
29
|
+
ticket: 'CC_TICKET',
|
30
|
+
void: 'CC_VOID'
|
31
|
+
}
|
32
|
+
|
33
|
+
def initialize(*args)
|
34
|
+
@amount, @customer, @credit_card, @currency, @id = *args
|
35
|
+
end
|
36
|
+
|
37
|
+
def id
|
38
|
+
@id ||= SecureRandom.uuid
|
39
|
+
end
|
40
|
+
|
41
|
+
def currency
|
42
|
+
@currency ||= RocketGate.configuration.currency
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_hash
|
46
|
+
if is_referenced?
|
47
|
+
{ referenceGUID: reference_id, transactionType: type }
|
48
|
+
else
|
49
|
+
super
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def type=(new_type)
|
54
|
+
new_type = TYPE[new_type] if TYPE.keys.include?(new_type)
|
55
|
+
|
56
|
+
if TYPE.values.include?(new_type)
|
57
|
+
@type = new_type
|
58
|
+
else
|
59
|
+
raise ValidationError.new("Unknown transaction type: #{new_type}")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def type
|
64
|
+
@type ||= TYPE[:auth]
|
65
|
+
end
|
66
|
+
|
67
|
+
def is_referenced?
|
68
|
+
[ TYPE[:confirmation], TYPE[:ticket], TYPE[:void] ].include?(type) && !reference_id.nil?
|
69
|
+
end
|
70
|
+
|
71
|
+
def valid?
|
72
|
+
is_referenced? || super
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module RocketGate::Validatable
|
2
|
+
module ClassMethods
|
3
|
+
def validatable(*args)
|
4
|
+
args.each do |attribute|
|
5
|
+
if self.new.respond_to?(attribute)
|
6
|
+
validatable_attributes << attribute
|
7
|
+
else
|
8
|
+
raise RocketGate::ValidationError.new("Invalid attribute: #{attribute}")
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def validatable_attributes
|
14
|
+
@validatable_attributes ||= []
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.included(base)
|
19
|
+
base.extend(ClassMethods)
|
20
|
+
end
|
21
|
+
|
22
|
+
def valid?
|
23
|
+
self.class.validatable_attributes.all? {|v| is_object_valid?(send(v)) }
|
24
|
+
end
|
25
|
+
|
26
|
+
def validate!
|
27
|
+
if valid?
|
28
|
+
self
|
29
|
+
else
|
30
|
+
message = "Invalid attributes: #{invalid_attributes.join(', ')}"
|
31
|
+
raise RocketGate::ValidationError.new(message) unless valid?
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def invalid_attributes
|
36
|
+
self.class.validatable_attributes.select {|v| !is_object_valid?(send(v)) }
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def is_class_validatable?(klazz)
|
42
|
+
klazz.included_modules.include?(RocketGate::Validatable)
|
43
|
+
end
|
44
|
+
|
45
|
+
def is_object_valid?(obj)
|
46
|
+
if is_class_validatable?(obj.class)
|
47
|
+
obj.valid?
|
48
|
+
else
|
49
|
+
!obj.nil?
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|