nmi_direct_post 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d376bc5d9401a3274444f713f3ef40f04a4a41e3
4
+ data.tar.gz: 383d6fb624a33d30f1121904b76c5b77eb0af761
5
+ SHA512:
6
+ metadata.gz: d40ddf52eceb8174336a6b9675d8a3a0e7161135cbdaed6effd7c1a361c54ebcb84432e08391a9b8829ffed9dd366401123cd750ecbe488e5b9cccf13b77e8a4
7
+ data.tar.gz: 9ac20fafa098b00e4069d05b994f0d15175033cd6b7408c617c10cd32e0318f1c6984039db20d95606fdb7375803c5413bef446914629ef29d74120cd3a53229
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ spec/support/credentials.rb
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/.simplecov ADDED
@@ -0,0 +1,4 @@
1
+ if ENV['COVERAGE']
2
+ SimpleCov.start 'test_frameworks' do
3
+ end
4
+ end
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in nmi_direct_post.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Isaac Betesh
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # NmiDirectPost
2
+
3
+ NmiDirectPost is a gem that encapsulates the NMI Direct Post API in an ActiveRecord-like syntax.
4
+ For more information on the NMI Direct Post API, see:
5
+ https://secure.nmi.com/merchants/resources/integration/integration_portal.php
6
+
7
+ To mimic ActivRecord syntax, it is necessary to blur, from the client's standpoint, the boundary between NMI's Direct Post API and its Query API. This fuzziness is part of the encapsulation.
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ gem 'nmi_direct_post'
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install nmi_direct_post
22
+
23
+ ## Usage
24
+
25
+ 1) Before you can query or post, establish the connection:
26
+
27
+ NmiDirectPost::Base.establish_connection("MY_NMI_USERNAME", "MY_NMI_PASSWORD")
28
+
29
+ Theoretically, you can use a different connection for NmiDirectPost::Transaction or NmiDirectPost::CustomerVault by calling establish_connection on either of those derived classes, instead of on Base.
30
+ However, it's hard to imagine a case where this would be useful; the option is only present to mimic the syntax of ActiveRecord.
31
+
32
+ 2) Query the API:
33
+
34
+ NmiDirectPost::Transaction.find_by_transaction_id(123456789)
35
+ NmiDirectPost::CustomerVault.find_by_customer_vault_id(123123123)
36
+
37
+ 3) Create a CustomerVault:
38
+
39
+ george = NmiDirectPostCustomerVault.new(:first_name => 'George', :last_name => 'Washington', :cc_number => '4111111111111111', :cc_exp => '03/17')
40
+ george.create
41
+
42
+ 4) Update a CustomerVault:
43
+
44
+ george.update!(:email => 'el_primero_presidente@whitehouse.gov', :address_1 => '1600 Pennsylvania Ave NW', :city => 'Washington', :state => 'DC', :postal_code => '20500')
45
+
46
+ ALTERNATIVELY:
47
+
48
+ george.email = 'el_primero_presidente@whitehouse.gov'
49
+ george.address_1 = '1600 Pennsylvania Ave NW'
50
+ george.city = 'Washington'
51
+ george.state = 'DC'
52
+ george.postal_code = '20500'
53
+ george.save! # Returns true
54
+
55
+ 5) Delete a CustomerVault:
56
+
57
+ george.destroy # Returns the CustomerVault
58
+
59
+ 6) Reload a CustomerVault:
60
+
61
+ george.email = 'el_primero_presidente@whitehouse.gov'
62
+ george.reload # Returns the Customer Vault
63
+ george.email # Returns the previously set email
64
+
65
+ 7) CustomerVault class methods:
66
+
67
+ NmiDirectPost::CustomerVault.all_ids # Returns array of `customer_vault_id`s
68
+ NmiDirectPost::CustomerVault.first
69
+ NmiDirectPost::CustomerVault.last
70
+ NmiDirectPost::CustomerVault.all # Returns very, very big array. This method had very poor performance and could be optimized significantly in a future version of this gem.
71
+
72
+ 8) Create a Transaction:
73
+
74
+ parking_ticket = NmiDirectPost::Transaction(:type => :sale, :amount => 150.01, :customer_vault_id => george.customer_vault_id)
75
+ parking_ticket.save! # Returns true
76
+
77
+ ## Contributing
78
+
79
+ 1. Fork it
80
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
81
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
82
+ 4. Push to the branch (`git push origin my-new-feature`)
83
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,7 @@
1
+ require "nmi_direct_post/version"
2
+ require "nmi_direct_post/transaction"
3
+ require "nmi_direct_post/customer_vault"
4
+
5
+ module NmiDirectPost
6
+ # Your code goes here...
7
+ end
@@ -0,0 +1,102 @@
1
+ require 'net/https'
2
+ require 'openssl'
3
+
4
+ require 'active_support/concern'
5
+ require 'active_support/callbacks'
6
+ require 'active_model/conversion'
7
+ require 'active_model/validator'
8
+ require 'active_model/callbacks'
9
+ require 'active_support/core_ext/module/delegation'
10
+ require 'active_model/naming'
11
+ require 'active_model/translation'
12
+ require 'active_model/validations'
13
+ require 'active_model/errors'
14
+ require 'active_support/core_ext/object/blank'
15
+ require 'addressable/uri'
16
+ require_relative 'logger'
17
+
18
+ module NmiDirectPost
19
+ class Base
20
+ POST_URI = "https://secure.nmi.com/api/transact.php"
21
+ GET_URI = "https://secure.nmi.com/api/query.php"
22
+
23
+ AUTH_PARAMS = [:username, :password]
24
+ attr_reader *AUTH_PARAMS
25
+ attr_reader :response, :response_text, :response_code
26
+
27
+ include ActiveModel::Validations
28
+ include ActiveModel::Conversion
29
+ validates_presence_of :username, :password
30
+
31
+ def initialize
32
+ @username, @password = self.class.username, self.class.password
33
+ end
34
+
35
+ def persisted?
36
+ false
37
+ end
38
+
39
+ def success?
40
+ 1 == self.response
41
+ end
42
+
43
+ def logger
44
+ NmiDirectPost.logger
45
+ end
46
+
47
+ class << self
48
+ NO_CONNECTION = "Please set a username by calling NmiDirectPost::Base.establish_connection(ENV['NMI_USERNAME'], ENV['NMI_PASSWORD'])"
49
+ def establish_connection(username, password)
50
+ @username, @password = username, password
51
+ end
52
+
53
+ def username
54
+ @username || (in_base? ? raise_no_connection_error : superclass.username).tap { |_| raise_no_connection_error if _.blank? }
55
+ end
56
+
57
+ def password
58
+ @password || (in_base? ? raise_no_connection_error : superclass.password).tap { |_| raise_no_connection_error if _.blank? }
59
+ end
60
+
61
+ def generate_query_string(attributes, target = self)
62
+ ((attributes.reject { |attr| target.__send__(attr).blank? }).collect { |attr| "#{attr}=#{Addressable::URI.escape(target.__send__(attr).to_s)}"}).join('&')
63
+ end
64
+
65
+ def get(query)
66
+ uri = [GET_URI, query].join('?')
67
+ data = get_http_response(uri).body
68
+ Hash.from_xml(data)["nm_response"]
69
+ end
70
+
71
+ def post(query)
72
+ uri = [POST_URI, query].join('?')
73
+ data = get_http_response(uri)
74
+ Addressable::URI.parse([POST_URI, data.body].join('?')).query_values
75
+ end
76
+
77
+ protected
78
+ def get_http_response(uri)
79
+ request = Net::HTTP::Get.new(uri)
80
+ url = URI.parse(uri)
81
+ http = Net::HTTP.new(url.host, url.port)
82
+ http.use_ssl = true
83
+ http.ssl_version = :TLSv1
84
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
85
+ http.request(request)
86
+ end
87
+
88
+ def in_base?
89
+ 'Object' == superclass.name
90
+ end
91
+
92
+ def raise_no_connection_error
93
+ raise(StandardError, NO_CONNECTION)
94
+ end
95
+ end
96
+
97
+ protected
98
+ def generate_query_string(attributes)
99
+ self.class.generate_query_string(attributes, self)
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,245 @@
1
+ require 'active_model/serialization'
2
+ require 'active_model/serializers/xml'
3
+ require 'active_support/core_ext/hash/indifferent_access'
4
+ require_relative 'base'
5
+
6
+ module NmiDirectPost
7
+ class CustomerVaultNotFoundError < StandardError; end
8
+
9
+ class CustomerVaultInvalidPostActionError < StandardError; end
10
+
11
+ module MassAssignmentSecurity
12
+ class Error < StandardError; end
13
+ end
14
+
15
+ class CustomerVault < Base
16
+ private
17
+ def self.attr_accessor_with_tracking_of_changes(*list)
18
+ list.each do |attr|
19
+ attr_reader attr
20
+ define_method("#{attr}=") do |val|
21
+ (@attributes_to_save ||=[]) << attr
22
+ instance_variable_set("@#{attr}", val)
23
+ end
24
+ end
25
+ end
26
+ public
27
+ READ_ONLY_ATTRIBUTES ||= [:check_hash, :cc_hash]
28
+ attr_reader *READ_ONLY_ATTRIBUTES
29
+ attr_reader :customer_vault_id, :customer_vault, :report_type
30
+
31
+ MERCHANT_DEFINED_FIELDS ||= 20.times.collect { |i| :"merchant_defined_field_#{i+1}" }
32
+ WHITELIST_ATTRIBUTES ||= [:id, :first_name, :last_name, :address_1, :address_2, :company, :city, :state, :postal_code, :country, :email, :phone, :fax, :cell_phone, :customertaxid, :website, :shipping_first_name, :shipping_last_name, :shipping_address_1, :shipping_address_2, :shipping_company, :shipping_city, :shipping_state, :shipping_postal_code, :shipping_country, :shipping_email, :shipping_carrier, :tracking_number, :shipping_date, :shipping, :cc_number, :cc_exp, :cc_issue_number, :check_account, :check_aba, :check_name, :account_holder_type, :account_type, :sec_code, :processor_id, :cc_bin, :cc_start_date] + MERCHANT_DEFINED_FIELDS
33
+ attr_accessor_with_tracking_of_changes *WHITELIST_ATTRIBUTES
34
+
35
+ validate :billing_information_present?, :if => Proc.new { |record| :add_customer == record.customer_vault }
36
+ validates_presence_of :customer_vault_id, :message => "You must specify a %{attribute} ID when looking up an individual customer vault", :if => Proc.new { |record| :customer_vault == record.report_type }
37
+ validates_presence_of :customer_vault_id, :message => "You must specify a %{attribute} ID when updating a customer vault", :if => Proc.new { |record| :update_customer == record.customer_vault }
38
+ validates_inclusion_of :customer_vault_id, :in => [nil], :message => "You cannot specify a %{attribute} ID when creating a new customer vault. NMI will assign one upon creating the record",
39
+ :if => Proc.new { |record| :add_customer == record.customer_vault }
40
+
41
+ def initialize(attributes)
42
+ super()
43
+ if attributes[:customer_vault_id].blank?
44
+ set_attributes(attributes.dup) unless attributes.empty?
45
+ else
46
+ @customer_vault_id = attributes[:customer_vault_id].to_i
47
+ reload
48
+ end
49
+ end
50
+
51
+ def create
52
+ post_action(:add)
53
+ self.success?
54
+ end
55
+
56
+ def update!(attributes)
57
+ begin
58
+ set_attributes(attributes)
59
+ post_action(:update)
60
+ ensure
61
+ @attributes_to_save.delete_if {|v| @attributes_to_update.include?(v) } if @attributes_to_save
62
+ @attributes_to_update = nil
63
+ end
64
+ self
65
+ end
66
+
67
+ def save!
68
+ post_action(:update)
69
+ reload
70
+ self.success?
71
+ end
72
+
73
+ def destroy
74
+ post_action(:delete)
75
+ self
76
+ end
77
+
78
+ def reload
79
+ @report_type = :customer_vault
80
+ if invalid?
81
+ @report_type = nil
82
+ return self
83
+ end
84
+ begin
85
+ safe_params = customer_vault_instance_params
86
+ logger.debug { "Loading NMI customer vault from customer_vault_id(#{customer_vault_id}) using query: #{safe_params}" }
87
+ response = self.class.get(self.class.all_params(safe_params))["customer_vault"]
88
+ raise CustomerVaultNotFoundError, "No record found for customer vault ID #{self.customer_vault_id}" if response.nil?
89
+ attributes = response["customer"].with_indifferent_access
90
+ READ_ONLY_ATTRIBUTES.each do |a|
91
+ if attributes.key?(a)
92
+ val = attributes.delete(a)
93
+ instance_variable_set("@#{a}",val)
94
+ end
95
+ end
96
+ set_attributes(attributes.tap { |_| _.delete(:customer_vault_id) })
97
+ ensure
98
+ @report_type = nil
99
+ @attributes_to_update = nil
100
+ @attributes_to_save = nil
101
+ end
102
+ self
103
+ end
104
+
105
+ def credit_card?
106
+ !@cc_hash.blank?
107
+ end
108
+
109
+ def checking?
110
+ !@check_hash.blank?
111
+ end
112
+
113
+ def find!
114
+ begin
115
+ @report_type = :customer_vault
116
+ safe_params = generate_query_string(MERCHANT_DEFINED_FIELDS + [:last_name, :email, :report_type]) # These are the only fields you can use when looking up without a customer_vault_id
117
+ logger.info { "Querying NMI customer vault: #{safe_params}" }
118
+ @customer_vault_id = self.class.get(self.class.all_params(safe_params))['customer_vault'][0]['customer_vault_id'] # This assumes there is only 1 result.
119
+ # TODO: When there are multiple results, we don't know which one you want. Maybe raise an error in that case?
120
+ reload
121
+ ensure
122
+ @report_type = nil
123
+ end
124
+ end
125
+
126
+ class << self
127
+ attr_reader :report_type
128
+
129
+ def find_by_customer_vault_id(customer_vault_id)
130
+ raise StandardError, "CustomerVaultID cannot be blank" if customer_vault_id.blank?
131
+ begin
132
+ new(:customer_vault_id => customer_vault_id)
133
+ rescue CustomerVaultNotFoundError
134
+ return nil
135
+ end
136
+ end
137
+
138
+ def first(limit = 1)
139
+ limit(0, limit-1).first
140
+ end
141
+
142
+ def last(limit = 1)
143
+ limit(-limit, -1).first
144
+ end
145
+
146
+ def all_ids
147
+ @report_type = :customer_vault
148
+ safe_params = generate_query_string([:report_type])
149
+ NmiDirectPost.logger.debug { "Loading all NMI customer vaults using query: #{safe_params}" }
150
+ begin
151
+ customers = get(all_params(safe_params))["customer_vault"]
152
+ ensure
153
+ @report_type = nil
154
+ end
155
+ return [] if customers.nil?
156
+ customers = customers["customer"]
157
+ customers.collect { |customer| customer["customer_vault_id"].to_i }
158
+ end
159
+
160
+ def all
161
+ limit
162
+ end
163
+
164
+ def all_params(safe_params)
165
+ [safe_params, generate_query_string(Base::AUTH_PARAMS)].join('&')
166
+ end
167
+
168
+ private
169
+ def limit(first = 0, last = -1)
170
+ all_ids[first..last].collect { |id| new(:customer_vault_id => id) }
171
+ end
172
+ end
173
+
174
+ private
175
+ def customer_vault_instance_params
176
+ generate_query_string([:customer_vault, :customer_vault_id, :report_type])
177
+ end
178
+
179
+ def post(safe_params)
180
+ logger.info { "Sending Direct Post to NMI: #{safe_params}" }
181
+ response = self.class.post(self.class.all_params(safe_params))
182
+ @response, @response_text, @response_code = response["response"].to_i, response["responsetext"], response["response_code"].to_i
183
+ @customer_vault_id = response["customer_vault_id"].to_i if :add_customer == self.customer_vault
184
+ end
185
+
186
+ def set_attributes(attributes)
187
+ attributes = attributes.with_indifferent_access
188
+ @attributes_to_update = []
189
+ merchant_defined_fields = []
190
+ if attributes.key?(:merchant_defined_field) && attributes[:merchant_defined_field].is_a?(String)
191
+ self.merchant_defined_field_1 = attributes.delete(:merchant_defined_field)
192
+ end
193
+ WHITELIST_ATTRIBUTES.each do |a|
194
+ if attributes.key?(a)
195
+ val = attributes.delete(a)
196
+ @attributes_to_update << a
197
+ end
198
+ merchant_defined_field_index = a.to_s.split('merchant_defined_field_')[1]
199
+ if (!merchant_defined_field_index.nil? && val.nil? && attributes.key?(:merchant_defined_field) && attributes[:merchant_defined_field].is_a?(Array))
200
+ index = merchant_defined_field_index.to_i - 1
201
+ if attributes[:merchant_defined_field].size > index
202
+ val = attributes[:merchant_defined_field][index]
203
+ attributes[:merchant_defined_field][index] = nil
204
+ @attributes_to_update << a
205
+ end
206
+ end
207
+ self.__send__("#{a}=", val) if @attributes_to_update.include?(a)
208
+ end
209
+ attributes.delete(:merchant_defined_field) unless attributes.key?(:merchant_defined_field) && attributes[:merchant_defined_field].any?
210
+ @id = @id.to_i if @id
211
+ raise MassAssignmentSecurity::Error, "Cannot mass-assign the following attributes: #{attributes.keys.join(", ")}" unless attributes.empty?
212
+ end
213
+
214
+ def billing_information_present?
215
+ self.errors.add(:billing_information, "Either :cc_number (a credit card number) and :cc_exp (the credit card expiration date), or :check_account, :check_aba (the routing number of the checking account) and :check_name (a nickname for the account), must be present") if (missing_cc_information? && missing_checking_information?)
216
+ end
217
+
218
+ def missing_checking_information?
219
+ self.check_account.blank? || self.check_aba.blank? || self.check_name.blank?
220
+ end
221
+
222
+ def missing_cc_information?
223
+ self.cc_exp.blank? || self.cc_number.blank?
224
+ end
225
+
226
+ def post_action(action)
227
+ @customer_vault = :"#{action}_customer"
228
+ safe_params = case action.to_sym
229
+ when :delete
230
+ customer_vault_instance_params
231
+ when :add
232
+ [customer_vault_instance_params, generate_query_string(WHITELIST_ATTRIBUTES)].join("&")
233
+ when :update
234
+ [customer_vault_instance_params, generate_query_string(@attributes_to_update || @attributes_to_save)].join("&")
235
+ else
236
+ raise CustomerVaultInvalidPostActionError, "#{action} is not a valid post action. NmiDirectPost allows the following post actions: :add, :update, :delete"
237
+ end
238
+ begin
239
+ post(safe_params) if valid?
240
+ ensure
241
+ @customer_vault = nil
242
+ end
243
+ end
244
+ end
245
+ end