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 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](http://www.authorize.net/solutions/merchantsolutions/merchantservices/cim/)
16
- * [Network Merchant Inc. Customer Vault](https://www.nmi.com/newsmedia/index.php?ann_id=14)
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](http://gembundler.com/), then add the following to your Gemfile:
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
- require 'vaulted_billing'
37
-
38
- bogus = VaultedBilling::Gateways::Bogus.new(:username => 'Foo', :password => 'Bar')
39
- customer = VaultedBilling::Customer.new(:email => "foo@example.com")
40
- credit_card = VaultedBilling::CreditCard.new({
41
- :card_number => '4111111111111111',
42
- :cvv_number => '123',
43
- :expires_on => Date.today + 1.year
44
- })
45
-
46
- bogus.add_customer(customer).tap do |customer_response|
47
- if customer_response.success?
48
- # normally, you'd store the vault_id on your local customer object,
49
- # because you use this when referencing that customer in the future.
50
- # But, for now, we'll just:
51
- customer.vault_id = customer_response.vault_id
52
-
53
- bogus.add_customer_credit_card(customer, credit_card).tap do |credit_response|
54
- if response.success?
55
- # Again, same as above, but for the credit card information:
56
- credit_card.vault_id = credit_response.vault_id
57
-
58
- puts "Wow! We stored a the payment credentials successfully!"
59
-
60
- if bogus.purchase(customer, credit_card, 10.00).success?
61
- puts "OMG WE'RE RICH!"
62
- end
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](https://github.com/myronmarston/vcr) with [WebMock](https://github.com/bblimke/webmock).
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
@@ -0,0 +1,7 @@
1
+ module VaultedBilling
2
+ class ChainableHash < ::Hash
3
+ def initialize
4
+ super { |hash, key| ChainableHash.new }
5
+ end
6
+ end
7
+ end
@@ -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
  #
@@ -0,0 +1,4 @@
1
+ module VaultedBilling
2
+ Error = Class.new(RuntimeError)
3
+ CredentialError = Class.new(Error)
4
+ end
@@ -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
- include VaultedBilling::HttpsInterface
12
+ attr_accessor :use_test_uri
12
13
 
13
14
  def initialize(options = {})
14
- self.test_uri = 'https://apitest.authorize.net/xml/v1/request.api'
15
- self.live_uri = 'https://api.authorize.net/xml/v1/request.api'
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
- self.use_test_uri = options.has_key?(:test) ? options[:test] : (VaultedBilling.config.authorize_net_cim.test_mode || VaultedBilling.config.test_mode)
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 = post_data(data)
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
- result = post_data(build_request('updateCustomerProfileRequest') { |xml|
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
- result = post_data(build_request('deleteCustomerProfileRequest') { |xml|
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
- result = post_data(build_request('createCustomerPaymentProfileRequest') { |xml|
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
- result = post_data(build_request('updateCustomerPaymentProfileRequest') { |xml|
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
- result = post_data(build_request('deleteCustomerPaymentProfileRequest') { |xml|
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
- result = post_data(build_request('createCustomerProfileTransactionRequest') { |xml|
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
- result = post_data(build_request('createCustomerProfileTransactionRequest') { |xml|
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
- result = post_data(build_request('createCustomerProfileTransactionRequest') { |xml|
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
- result = post_data(build_request('createCustomerProfileTransactionRequest') { |xml|
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
- result = post_data(build_request('createCustomerProfileTransactionRequest') { |xml|
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 = Hash.from_xml(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