vaulted_billing 1.0.2 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +44 -34
- data/lib/vaulted_billing/chainable_hash.rb +7 -0
- data/lib/vaulted_billing/configuration.rb +13 -3
- data/lib/vaulted_billing/errors.rb +4 -0
- data/lib/vaulted_billing/gateway.rb +4 -4
- data/lib/vaulted_billing/gateways/authorize_net_cim.rb +60 -36
- data/lib/vaulted_billing/gateways/ipcommerce.rb +372 -0
- data/lib/vaulted_billing/gateways/nmi_customer_vault.rb +43 -24
- data/lib/vaulted_billing/http.rb +157 -0
- data/lib/vaulted_billing/version.rb +1 -1
- data/lib/vaulted_billing.rb +5 -2
- metadata +139 -138
- data/lib/vaulted_billing/https_interface.rb +0 -127
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# VaultedBilling
|
1
|
+
# VaultedBilling [![Build status][ci-image]][ci]
|
2
2
|
|
3
3
|
VaultedBilling is an abstraction library for use when working with "vaulted" payment processors. These processors store your customer's data - being their credit card number, verification number, name, address, and more - on their systems to alleviate your need for expensive software auditing, hardware security, and more. In nearly all cases, these processors provide you a unique customer and/or payment token in exchange for your actual customer payment information. Then, all current and future interactions with the payment processor on behalf of the customer are made using their identifiers, rather than credit card details.
|
4
4
|
|
@@ -12,8 +12,8 @@ Since you only store identifiers on your end, you are only responsible for: 1) t
|
|
12
12
|
|
13
13
|
VaultedBilling supports the following payment providers:
|
14
14
|
|
15
|
-
* [Authorize.net Customer Information Manager]
|
16
|
-
* [Network Merchant Inc. Customer Vault]
|
15
|
+
* [Authorize.net Customer Information Manager][authorize-net-cim]
|
16
|
+
* [Network Merchant Inc. Customer Vault][nmi-vault]
|
17
17
|
|
18
18
|
VaultedBilling also supports the following fictitious payment provider for testing purposes:
|
19
19
|
|
@@ -25,7 +25,7 @@ VaultedBilling should be installed as a RubyGem dependency:
|
|
25
25
|
|
26
26
|
gem install vaulted_billing
|
27
27
|
|
28
|
-
If your application uses [Bundler]
|
28
|
+
If your application uses [Bundler][bundler], then add the following to your Gemfile:
|
29
29
|
|
30
30
|
gem 'vaulted_billing'
|
31
31
|
|
@@ -33,37 +33,39 @@ If your application uses [Bundler](http://gembundler.com/), then add the followi
|
|
33
33
|
|
34
34
|
Simple (not particularly clean or recommended) example:
|
35
35
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
end
|
36
|
+
```ruby
|
37
|
+
require 'vaulted_billing'
|
38
|
+
|
39
|
+
bogus = VaultedBilling::Gateways::Bogus.new(:username => 'Foo', :password => 'Bar')
|
40
|
+
customer = VaultedBilling::Customer.new(:email => "foo@example.com")
|
41
|
+
credit_card = VaultedBilling::CreditCard.new({
|
42
|
+
:card_number => '4111111111111111',
|
43
|
+
:cvv_number => '123',
|
44
|
+
:expires_on => Date.today + 1.year
|
45
|
+
})
|
46
|
+
|
47
|
+
bogus.add_customer(customer).tap do |customer_response|
|
48
|
+
if customer_response.success?
|
49
|
+
# normally, you'd store the vault_id on your local customer object,
|
50
|
+
# because you use this when referencing that customer in the future.
|
51
|
+
# But, for now, we'll just:
|
52
|
+
customer.vault_id = customer_response.vault_id
|
53
|
+
|
54
|
+
bogus.add_customer_credit_card(customer, credit_card).tap do |credit_response|
|
55
|
+
if response.success?
|
56
|
+
# Again, same as above, but for the credit card information:
|
57
|
+
credit_card.vault_id = credit_response.vault_id
|
58
|
+
|
59
|
+
puts "Wow! We stored a the payment credentials successfully!"
|
60
|
+
|
61
|
+
if bogus.purchase(customer, credit_card, 10.00).success?
|
62
|
+
puts "OMG WE'RE RICH!"
|
64
63
|
end
|
65
64
|
end
|
66
65
|
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
```
|
67
69
|
|
68
70
|
### Real world example
|
69
71
|
|
@@ -71,6 +73,14 @@ TODO: Real world example coming soon.
|
|
71
73
|
|
72
74
|
## Testing
|
73
75
|
|
74
|
-
When you're manually testing your application - meaning Development mode - it is often best to actually have a "sandbox" or "test" account with your payment processor. In this mode, you should use those credentials with VaultedBilling and indicate to VaultedBilling that the processor is in test mode, either by setting it in the VaultedBilling::Configuration (see Configuration) or when you instantiate your Gateway. You should note that all gateways, except for the Bogus gateway, attempt to open network connections when in use. So, if you are testing with them (which is suggested), you should look into an HTTP mocking library like [VCR]
|
76
|
+
When you're manually testing your application - meaning Development mode - it is often best to actually have a "sandbox" or "test" account with your payment processor. In this mode, you should use those credentials with VaultedBilling and indicate to VaultedBilling that the processor is in test mode, either by setting it in the VaultedBilling::Configuration (see Configuration) or when you instantiate your Gateway. You should note that all gateways, except for the Bogus gateway, attempt to open network connections when in use. So, if you are testing with them (which is suggested), you should look into an HTTP mocking library like [VCR][vcr] with [WebMock][webmock].
|
75
77
|
|
76
|
-
Strictly for testing interaction with the VaultedBilling library, there is a "Bogus" gateway provided. This processor will always successfully store customer and credit card information and return their identifiers. It will also always respond successfully to transaction (authorize, capture, refund, void, etc.) requests. This processor does not attempt to make network requests to any 3rd parties. It is not recommended that you solely test against this gateway, as you will find that your actual payment processor may have quirks which are unique and cannot be easily replicated.
|
78
|
+
Strictly for testing interaction with the VaultedBilling library, there is a "Bogus" gateway provided. This processor will always successfully store customer and credit card information and return their identifiers. It will also always respond successfully to transaction (authorize, capture, refund, void, etc.) requests. This processor does not attempt to make network requests to any 3rd parties. It is not recommended that you solely test against this gateway, as you will find that your actual payment processor may have quirks which are unique and cannot be easily replicated.
|
79
|
+
|
80
|
+
[ci]: http://travis-ci.org/envylabs/vaulted_billing
|
81
|
+
[ci-image]: https://secure.travis-ci.org/envylabs/vaulted_billing.png
|
82
|
+
[authorize-net-cim]: http://www.authorize.net/solutions/merchantsolutions/merchantservices/cim/
|
83
|
+
[nmi-vault]: https://www.nmi.com/newsmedia/index.php?ann_id=14
|
84
|
+
[bundler]: http://gembundler.com/
|
85
|
+
[vcr]: https://github.com/myronmarston/vcr
|
86
|
+
[webmock]: https://github.com/bblimke/webmock
|
@@ -53,13 +53,14 @@ module VaultedBilling
|
|
53
53
|
options = options.with_indifferent_access
|
54
54
|
self.test_mode = options.has_key?(:test_mode) ? options[:test_mode] : true
|
55
55
|
@_authorize_net_cim = GatewayConfiguration.new(options[:authorize_net_cim]) if options[:authorize_net_cim]
|
56
|
+
@_ipcommerce = GatewayConfiguration.new(options[:ipcommerce]) if options[:ipcommerce]
|
56
57
|
@_nmi_customer_vault = GatewayConfiguration.new(options[:nmi_customer_vault]) if options[:nmi_customer_vault]
|
57
58
|
@_bogus = GatewayConfiguration.new(options[:bogus]) if options[:bogus]
|
58
59
|
@ca_file = File.expand_path('../../ext/cacert.pem', __FILE__)
|
59
60
|
end
|
60
61
|
|
61
62
|
##
|
62
|
-
# Returns a VaultedBilling::Configuration::GatewayConfiguration
|
63
|
+
# Returns a VaultedBilling::Configuration::GatewayConfiguration
|
63
64
|
# instance to be used for defining default settings for the
|
64
65
|
# Authorize.net CIM gateway.
|
65
66
|
#
|
@@ -68,7 +69,16 @@ module VaultedBilling
|
|
68
69
|
end
|
69
70
|
|
70
71
|
##
|
71
|
-
# Returns a VaultedBilling::Configuration::GatewayConfiguration
|
72
|
+
# Returns a VaultedBilling::Configuration::GatewayConfiguration
|
73
|
+
# instance to be used for defining default settings for the
|
74
|
+
# IP Commerce gateway.
|
75
|
+
#
|
76
|
+
def ipcommerce
|
77
|
+
@_ipcommerce ||= GatewayConfiguration.new
|
78
|
+
end
|
79
|
+
|
80
|
+
##
|
81
|
+
# Returns a VaultedBilling::Configuration::GatewayConfiguration
|
72
82
|
# instance to be used for defining default settings for the
|
73
83
|
# NMI Customer Vault gateway.
|
74
84
|
#
|
@@ -77,7 +87,7 @@ module VaultedBilling
|
|
77
87
|
end
|
78
88
|
|
79
89
|
##
|
80
|
-
# Returns a VaultedBilling::Configuration::GatewayConfiguration
|
90
|
+
# Returns a VaultedBilling::Configuration::GatewayConfiguration
|
81
91
|
# instance to be used for defining default settings for the
|
82
92
|
# Bogus gateway.
|
83
93
|
#
|
@@ -34,15 +34,15 @@ module VaultedBilling
|
|
34
34
|
raise NotImplementedError
|
35
35
|
end
|
36
36
|
|
37
|
-
def authorize(customer, credit_card, amount)
|
37
|
+
def authorize(customer, credit_card, amount, options = {})
|
38
38
|
raise NotImplementedError
|
39
39
|
end
|
40
40
|
|
41
|
-
def capture(transaction_id, amount)
|
41
|
+
def capture(transaction_id, amount, options = {})
|
42
42
|
raise NotImplementedError
|
43
43
|
end
|
44
44
|
|
45
|
-
def purchase(customer, credit_card, amount)
|
45
|
+
def purchase(customer, credit_card, amount, options = {})
|
46
46
|
raise NotImplementedError
|
47
47
|
end
|
48
48
|
|
@@ -50,7 +50,7 @@ module VaultedBilling
|
|
50
50
|
raise NotImplementedError
|
51
51
|
end
|
52
52
|
|
53
|
-
def void(transaction_id)
|
53
|
+
def void(transaction_id, options = {})
|
54
54
|
raise NotImplementedError
|
55
55
|
end
|
56
56
|
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'builder'
|
2
|
+
require 'multi_xml'
|
2
3
|
|
3
4
|
module VaultedBilling
|
4
5
|
module Gateways
|
@@ -8,17 +9,17 @@ module VaultedBilling
|
|
8
9
|
#
|
9
10
|
class AuthorizeNetCim
|
10
11
|
include VaultedBilling::Gateway
|
11
|
-
|
12
|
+
attr_accessor :use_test_uri
|
12
13
|
|
13
14
|
def initialize(options = {})
|
14
|
-
|
15
|
-
|
15
|
+
@test_uri = 'https://apitest.authorize.net/xml/v1/request.api'
|
16
|
+
@live_uri = 'https://api.authorize.net/xml/v1/request.api'
|
16
17
|
|
17
18
|
options = options.symbolize_keys!
|
18
19
|
@login = options[:username] || VaultedBilling.config.authorize_net_cim.username
|
19
20
|
@password = options[:password] || VaultedBilling.config.authorize_net_cim.password
|
20
21
|
@raw_options = options[:raw_options] || VaultedBilling.config.authorize_net_cim.raw_options
|
21
|
-
|
22
|
+
@use_test_uri = options.has_key?(:test) ? options[:test] : (VaultedBilling.config.authorize_net_cim.test_mode || VaultedBilling.config.test_mode)
|
22
23
|
end
|
23
24
|
|
24
25
|
def add_customer(customer)
|
@@ -29,7 +30,7 @@ module VaultedBilling
|
|
29
30
|
xml.email customer.email if customer.email
|
30
31
|
end
|
31
32
|
end
|
32
|
-
result =
|
33
|
+
result = http.post(data)
|
33
34
|
respond_with(customer, result, :success => result.success?) do |c|
|
34
35
|
c.vault_id = result.body['createCustomerProfileResponse']['customerProfileId'] if c.success?
|
35
36
|
end
|
@@ -37,34 +38,38 @@ module VaultedBilling
|
|
37
38
|
|
38
39
|
def update_customer(customer)
|
39
40
|
customer = customer.to_vaulted_billing
|
40
|
-
|
41
|
+
data = build_request('updateCustomerProfileRequest') { |xml|
|
41
42
|
xml.tag!('profile') {
|
42
43
|
xml.email customer.email
|
43
44
|
xml.customerProfileId customer.vault_id
|
44
45
|
}
|
45
|
-
}
|
46
|
+
}
|
47
|
+
result = http.post(data)
|
46
48
|
respond_with(customer, result, :success => result.success?)
|
47
49
|
end
|
48
50
|
|
49
51
|
def remove_customer(customer)
|
50
52
|
customer = customer.to_vaulted_billing
|
51
|
-
|
53
|
+
data = build_request('deleteCustomerProfileRequest') { |xml|
|
52
54
|
xml.customerProfileId customer.vault_id
|
53
|
-
}
|
55
|
+
}
|
54
56
|
|
57
|
+
result = http.post(data)
|
55
58
|
respond_with(customer, result, :success => result.success?)
|
56
59
|
end
|
57
60
|
|
58
61
|
def add_customer_credit_card(customer, credit_card)
|
59
62
|
customer = customer.to_vaulted_billing
|
60
63
|
credit_card = credit_card.to_vaulted_billing
|
61
|
-
|
64
|
+
data = build_request('createCustomerPaymentProfileRequest') { |xml|
|
62
65
|
xml.customerProfileId customer.vault_id
|
63
66
|
xml.paymentProfile do
|
64
67
|
billing_info!(xml, customer, credit_card)
|
65
68
|
credit_card_info!(xml, customer, credit_card)
|
66
69
|
end
|
67
|
-
}
|
70
|
+
}
|
71
|
+
|
72
|
+
result = http.post(data)
|
68
73
|
respond_with(credit_card, result, :success => result.success?) do |c|
|
69
74
|
c.vault_id = result.body['createCustomerPaymentProfileResponse']['customerPaymentProfileId'] if c.success?
|
70
75
|
end
|
@@ -73,31 +78,35 @@ module VaultedBilling
|
|
73
78
|
def update_customer_credit_card(customer, credit_card)
|
74
79
|
customer = customer.to_vaulted_billing
|
75
80
|
credit_card = credit_card.to_vaulted_billing
|
76
|
-
|
81
|
+
data = build_request('updateCustomerPaymentProfileRequest') { |xml|
|
77
82
|
xml.customerProfileId customer.vault_id
|
78
83
|
xml.paymentProfile do
|
79
84
|
billing_info!(xml, customer, credit_card)
|
80
85
|
credit_card_info!(xml, customer, credit_card)
|
81
86
|
xml.customerPaymentProfileId credit_card.vault_id
|
82
87
|
end
|
83
|
-
}
|
88
|
+
}
|
89
|
+
|
90
|
+
result = http.post(data)
|
84
91
|
respond_with(credit_card, result, :success => result.success?)
|
85
92
|
end
|
86
93
|
|
87
94
|
def remove_customer_credit_card(customer, credit_card)
|
88
95
|
customer = customer.to_vaulted_billing
|
89
96
|
credit_card = credit_card.to_vaulted_billing
|
90
|
-
|
97
|
+
data = build_request('deleteCustomerPaymentProfileRequest') { |xml|
|
91
98
|
xml.customerProfileId customer.vault_id
|
92
99
|
xml.customerPaymentProfileId credit_card.vault_id
|
93
|
-
}
|
100
|
+
}
|
101
|
+
|
102
|
+
result = http.post(data)
|
94
103
|
respond_with(credit_card, result, :success => result.success?)
|
95
104
|
end
|
96
105
|
|
97
|
-
def purchase(customer, credit_card, amount)
|
106
|
+
def purchase(customer, credit_card, amount, options = {})
|
98
107
|
customer = customer.to_vaulted_billing
|
99
108
|
credit_card = credit_card.to_vaulted_billing
|
100
|
-
|
109
|
+
data = build_request('createCustomerProfileTransactionRequest') { |xml|
|
101
110
|
xml.transaction do
|
102
111
|
xml.profileTransAuthCapture do
|
103
112
|
xml.amount amount
|
@@ -106,14 +115,16 @@ module VaultedBilling
|
|
106
115
|
end
|
107
116
|
end
|
108
117
|
xml.extraOptions @raw_options.presence
|
109
|
-
}
|
118
|
+
}
|
119
|
+
|
120
|
+
result = http.post(data)
|
110
121
|
respond_with(new_transaction_from_response(result.body), result, :success => result.success?)
|
111
122
|
end
|
112
123
|
|
113
|
-
def authorize(customer, credit_card, amount)
|
124
|
+
def authorize(customer, credit_card, amount, options = {})
|
114
125
|
customer = customer.to_vaulted_billing
|
115
126
|
credit_card = credit_card.to_vaulted_billing
|
116
|
-
|
127
|
+
data = build_request('createCustomerProfileTransactionRequest') { |xml|
|
117
128
|
xml.transaction do
|
118
129
|
xml.profileTransAuthOnly do
|
119
130
|
xml.amount amount
|
@@ -122,12 +133,14 @@ module VaultedBilling
|
|
122
133
|
end
|
123
134
|
end
|
124
135
|
xml.extraOptions @raw_options.presence
|
125
|
-
}
|
136
|
+
}
|
137
|
+
|
138
|
+
result = http.post(data)
|
126
139
|
respond_with(new_transaction_from_response(result.body), result, :success => result.success?)
|
127
140
|
end
|
128
141
|
|
129
|
-
def capture(transaction_id, amount)
|
130
|
-
|
142
|
+
def capture(transaction_id, amount, options = {})
|
143
|
+
data = build_request('createCustomerProfileTransactionRequest') { |xml|
|
131
144
|
xml.transaction do
|
132
145
|
xml.profileTransPriorAuthCapture do
|
133
146
|
xml.amount amount
|
@@ -135,12 +148,14 @@ module VaultedBilling
|
|
135
148
|
end
|
136
149
|
end
|
137
150
|
xml.extraOptions @raw_options.presence
|
138
|
-
}
|
151
|
+
}
|
152
|
+
|
153
|
+
result = http.post(data)
|
139
154
|
respond_with(new_transaction_from_response(result.body), result, :success => result.success?)
|
140
155
|
end
|
141
156
|
|
142
157
|
def refund(transaction_id, amount, options = {})
|
143
|
-
|
158
|
+
data = build_request('createCustomerProfileTransactionRequest') { |xml|
|
144
159
|
xml.transaction do
|
145
160
|
xml.profileTransRefund do
|
146
161
|
xml.amount amount
|
@@ -151,30 +166,33 @@ module VaultedBilling
|
|
151
166
|
end
|
152
167
|
end
|
153
168
|
xml.extraOptions @raw_options.presence
|
154
|
-
}
|
169
|
+
}
|
170
|
+
|
171
|
+
result = http.post(data)
|
155
172
|
respond_with(new_transaction_from_response(result.body), result, :success => result.success?)
|
156
173
|
end
|
157
174
|
|
158
|
-
def void(transaction_id)
|
159
|
-
|
175
|
+
def void(transaction_id, options = {})
|
176
|
+
data = build_request('createCustomerProfileTransactionRequest') { |xml|
|
160
177
|
xml.transaction do
|
161
178
|
xml.profileTransVoid do
|
162
179
|
xml.transId transaction_id
|
163
180
|
end
|
164
181
|
end
|
165
182
|
xml.extraOptions @raw_options.presence
|
166
|
-
}
|
183
|
+
}
|
184
|
+
|
185
|
+
result = http.post(data)
|
167
186
|
respond_with(new_transaction_from_response(result.body), result, :success => result.success?)
|
168
187
|
end
|
169
188
|
|
189
|
+
def uri
|
190
|
+
@use_test_uri ? @test_uri : @live_uri
|
191
|
+
end
|
170
192
|
|
171
193
|
protected
|
172
194
|
|
173
195
|
|
174
|
-
def post_data(data, headers = {})
|
175
|
-
super(data, {'Content-Type' => 'text/xml'}.merge(headers))
|
176
|
-
end
|
177
|
-
|
178
196
|
def after_post_on_exception(response, exception)
|
179
197
|
response.body = {
|
180
198
|
'ErrorResponse' => {
|
@@ -192,7 +210,7 @@ module VaultedBilling
|
|
192
210
|
end
|
193
211
|
|
194
212
|
def after_post_on_success(response)
|
195
|
-
response.body =
|
213
|
+
response.body = MultiXml.parse(response.body)
|
196
214
|
response.success = response.body[response.body.keys.first]['messages']['resultCode'] == 'Ok'
|
197
215
|
end
|
198
216
|
|
@@ -208,7 +226,7 @@ module VaultedBilling
|
|
208
226
|
xml.target!
|
209
227
|
end
|
210
228
|
|
211
|
-
|
229
|
+
|
212
230
|
private
|
213
231
|
|
214
232
|
|
@@ -278,7 +296,13 @@ module VaultedBilling
|
|
278
296
|
"XXXX%04d" % [input.to_s[-4..-1].to_i]
|
279
297
|
end
|
280
298
|
|
299
|
+
def http
|
300
|
+
VaultedBilling::HTTP.new(self, uri, {
|
301
|
+
:headers => {'Content-Type' => 'text/xml'},
|
302
|
+
:on_success => :after_post_on_success,
|
303
|
+
:on_error => :after_post_on_exception
|
304
|
+
})
|
305
|
+
end
|
281
306
|
end
|
282
|
-
|
283
307
|
end
|
284
308
|
end
|