nmi_direct_post 0.2.0

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 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