nmi_direct_post 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ require "logger"
2
+
3
+ module NmiDirectPost
4
+ class << self
5
+ def logger
6
+ @logger ||= defined?(::Rails.logger) ? Rails.logger : ::Logger.new(STDOUT)
7
+ end
8
+ def logger=(_)
9
+ raise ArgumentError, "NmiDirectPost logger must respond to :info and :debug" unless logger_responds(_)
10
+ @logger = _
11
+ end
12
+ private
13
+ def logger_responds(logger)
14
+ logger.respond_to?(:info) && logger.respond_to?(:debug)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,168 @@
1
+ require_relative 'base'
2
+ require_relative 'customer_vault'
3
+
4
+ module NmiDirectPost
5
+ class TransactionNotFoundError < StandardError; end
6
+
7
+ class TransactionNotSavedError < StandardError; end
8
+
9
+ class Transaction < Base
10
+ SAFE_PARAMS = [:customer_vault_id, :type, :amount]
11
+
12
+ attr_reader *SAFE_PARAMS
13
+ attr_reader :auth_code, :avs_response, :cvv_response, :order_id, :type, :dup_seconds, :condition
14
+ attr_reader :transaction_id
15
+
16
+ validates_presence_of :customer_vault_id, :amount, :unless => :'finding_by_transaction_id?', :message => "%{attribute} cannot be blank"
17
+ validates_presence_of :customer_vault, :unless => 'customer_vault_id.blank?', :message => "%{attribute} with the given customer_vault could not be found"
18
+ validates_inclusion_of :type, :in => ["sale", "auth", "capture", "void", "refund", "credit", "validate", "update", ""]
19
+ validates_exclusion_of :type, :in => ["validate", "auth", "capture", "void"], :if => :'customer_vault_is_checking?', :message => "%{value} is not a valid action for a customer vault that uses a checking account"
20
+ validates_numericality_of :amount, :equal_to => 0, :if => :'is_validate?', :message => "%{attribute} must be 0 when validating a credit card"
21
+ validates_numericality_of :amount, :greater_than => 0, :if => :'is_sale?', :message => "%{attribute} cannot be 0 for a sale"
22
+ validates_numericality_of :amount, :greater_than => 0, :if => :'is_auth?', :message => "%{attribute} cannot be 0 for an authorization"
23
+ validate :voidable_transaction?, :if => :is_void?
24
+ validate :persisted?, :if => :is_void?
25
+ validate :save_successful?, :unless => 'response_text.blank?'
26
+
27
+ def initialize(attributes)
28
+ super()
29
+ @type, @amount = attributes[:type].to_s, attributes[:amount].to_f
30
+ @transaction_id = attributes[:transaction_id].to_i if attributes[:transaction_id]
31
+ @customer_vault_id = attributes[:customer_vault_id].to_i if attributes[:customer_vault_id]
32
+ reload if (finding_by_transaction_id? && self.valid?)
33
+ @type, @amount = attributes[:type].to_s, attributes[:amount].to_f if ['void', 'capture'].include?(attributes[:type].to_s)
34
+ end
35
+
36
+ def save
37
+ void! if ('void' == type && condition.blank?)
38
+ return false if self.invalid?
39
+ _safe_params = safe_params
40
+ logger.info { "Sending Direct Post Transaction to NMI: #{_safe_params}" }
41
+ post([_safe_params, transaction_params].join('&'))
42
+ valid?.tap { |_| reload if _ }
43
+ end
44
+
45
+ def save!
46
+ save || raise(TransactionNotSavedError)
47
+ end
48
+
49
+ def self.find_by_transaction_id(transaction_id)
50
+ raise StandardError, "TransactionID cannot be blank" if transaction_id.blank?
51
+ NmiDirectPost.logger.debug { "Looking up NMI transaction by transaction_id(#{transaction_id})" }
52
+ begin
53
+ new(:transaction_id => transaction_id)
54
+ rescue TransactionNotFoundError
55
+ return nil
56
+ end
57
+ end
58
+
59
+ def pending?
60
+ 'pendingsettlement' == condition
61
+ end
62
+
63
+ def cleared?
64
+ "complete" == condition
65
+ end
66
+
67
+ def failed?
68
+ "failed" == condition
69
+ end
70
+
71
+ def declined?
72
+ 2 == response
73
+ end
74
+
75
+ def void!
76
+ @type='void'
77
+ if condition.blank?
78
+ return false if invalid?
79
+ reload
80
+ @type = 'void'
81
+ end
82
+ save
83
+ end
84
+
85
+ def customer_vault
86
+ @customer_vault ||= CustomerVault.find_by_customer_vault_id(@customer_vault_id) unless @customer_vault_id.blank?
87
+ end
88
+
89
+ def reload
90
+ get(transaction_params) if finding_by_transaction_id?
91
+ self
92
+ end
93
+
94
+ private
95
+ def safe_params
96
+ generate_query_string(SAFE_PARAMS)
97
+ end
98
+
99
+ def transaction_params
100
+ generate_query_string([AUTH_PARAMS, :transaction_id].flatten)
101
+ end
102
+
103
+ def get(query)
104
+ hash = self.class.get(query)["transaction"]
105
+ hash = hash.keep_if { |v| v['transaction_id'].to_s == self.transaction_id.to_s }.first if hash.is_a?(Array)
106
+ raise TransactionNotFoundError, "No transaction found for TransactionID #{@transaction_id}" if hash.nil?
107
+ @auth_code = hash["authorization_code"]
108
+ @customer_vault_id = hash["customerid"].to_i
109
+ @avs_response = hash["avs_response"]
110
+ @condition = hash["condition"]
111
+ action = hash["action"]
112
+ action = action.last unless action.is_a?(Hash)
113
+ @amount = action["amount"].to_f
114
+ @type = action["action_type"]
115
+ @response = action["success"].to_i if action.key?("success")
116
+ @response_code = action["response_code"].to_i if action.key?("response_code")
117
+ @response_text = action["response_text"]
118
+ end
119
+
120
+ def post(query)
121
+ response = self.class.post(query)
122
+ @response = response["response"].to_i if response.key?("response")
123
+ @response_code = response["response_code"].to_i if response.key?("response_code")
124
+ @response_text, @avs_response, @cvv_response = response["responsetext"], response["avsresponse"], response["cvvresponse"]
125
+ @dup_seconds, @order_id, @auth_code = response["dup_seconds"], response["orderid"], response["authcode"]
126
+ @transaction_id = response["transactionid"]
127
+ end
128
+
129
+ def customer_vault_is_checking?
130
+ !customer_vault.blank? && customer_vault.checking?
131
+ end
132
+
133
+ def finding_by_transaction_id?
134
+ !transaction_id.blank?
135
+ end
136
+
137
+ def is_validate?
138
+ !finding_by_transaction_id? && ('validate' == type.to_s)
139
+ end
140
+
141
+ def is_sale?
142
+ !finding_by_transaction_id? && (['sale', ''].include?(type.to_s))
143
+ end
144
+
145
+ def is_auth?
146
+ !finding_by_transaction_id? && ('auth' == type.to_s)
147
+ end
148
+
149
+ def is_void?
150
+ !customer_vault_is_checking? && ('void' == type.to_s)
151
+ end
152
+
153
+ def save_successful?
154
+ return if (success? || declined?)
155
+ self.errors.add(:response, response.to_s)
156
+ self.errors.add(:response_code, response_code.to_s)
157
+ self.errors.add(:response_text, response_text)
158
+ end
159
+
160
+ def voidable_transaction?
161
+ self.errors.add(:type, "Void is only a valid action for a pending or unsettled authorization, or an unsettled sale") if (finding_by_transaction_id? && !['pending', 'pendingsettlement'].include?(condition)) unless condition.blank?
162
+ end
163
+
164
+ def persisted?
165
+ self.errors.add(:type, "Void is only a valid action for a transaction that has already been sent to NMI") unless finding_by_transaction_id?
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,3 @@
1
+ module NmiDirectPost
2
+ VERSION = "0.2.0"
3
+ end
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'nmi_direct_post/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "nmi_direct_post"
8
+ spec.version = NmiDirectPost::VERSION
9
+ spec.authors = ["Isaac Betesh"]
10
+ spec.email = ["iybetesh@gmail.com"]
11
+ spec.description = %q{Gem that encapsulates the NMI Direct Post API in an ActiveRecord-like syntax}
12
+ spec.summary = `cat README.md`
13
+ spec.homepage = "https://github.com/betesh/nmi_direct_post"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", ">= 1.3"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_development_dependency "rspec"
24
+ spec.add_development_dependency "rspec-rails"
25
+ spec.add_development_dependency "simplecov"
26
+
27
+ spec.add_dependency 'addressable'
28
+ spec.add_dependency 'activemodel'
29
+ spec.add_dependency 'activesupport', ' >= 3.0'
30
+ end
data/spec/base_spec.rb ADDED
@@ -0,0 +1,37 @@
1
+ require_relative 'spec_helper'
2
+
3
+ describe NmiDirectPost::Base do
4
+ def a_query
5
+ NmiDirectPost::CustomerVault.find_by_customer_vault_id(a_cc_customer_vault_id)
6
+ end
7
+ before(:each) do
8
+ NmiDirectPost::Base.establish_connection(nil, nil)
9
+ NmiDirectPost::CustomerVault.establish_connection(nil, nil)
10
+ end
11
+ let(:credentials) { TestCredentials::INSTANCE }
12
+ let(:a_cc_customer_vault_id) { credentials.cc_customer }
13
+ it "should raise exception when username is an empty string" do
14
+ NmiDirectPost::Base.establish_connection('', credentials.nmi_password)
15
+ expect{a_query}.to raise_error(StandardError, "Please set a username by calling NmiDirectPost::Base.establish_connection(ENV['NMI_USERNAME'], ENV['NMI_PASSWORD'])")
16
+ end
17
+ it "should raise exception when password is an empty string" do
18
+ NmiDirectPost::Base.establish_connection(credentials.nmi_username, nil)
19
+ expect{a_query}.to raise_error(StandardError, "Please set a username by calling NmiDirectPost::Base.establish_connection(ENV['NMI_USERNAME'], ENV['NMI_PASSWORD'])")
20
+ end
21
+ it "should raise exception when username is nil" do
22
+ NmiDirectPost::Base.establish_connection('', credentials.nmi_password)
23
+ expect{a_query}.to raise_error(StandardError, "Please set a username by calling NmiDirectPost::Base.establish_connection(ENV['NMI_USERNAME'], ENV['NMI_PASSWORD'])")
24
+ end
25
+ it "should raise exception when password is nil" do
26
+ NmiDirectPost::Base.establish_connection(credentials.nmi_username, nil)
27
+ expect{a_query}.to raise_error(StandardError, "Please set a username by calling NmiDirectPost::Base.establish_connection(ENV['NMI_USERNAME'], ENV['NMI_PASSWORD'])")
28
+ end
29
+ it "should find parent connection" do
30
+ NmiDirectPost::Base.establish_connection(credentials.nmi_username, credentials.nmi_password)
31
+ expect{a_query}.to_not raise_error
32
+ end
33
+ it "should find parent connection" do
34
+ NmiDirectPost::CustomerVault.establish_connection(credentials.nmi_username, credentials.nmi_password)
35
+ expect{a_query}.to_not raise_error
36
+ end
37
+ end
@@ -0,0 +1,243 @@
1
+ require_relative 'spec_helper'
2
+
3
+ describe NmiDirectPost::CustomerVault do
4
+ def get_new_email
5
+ "someone#{Random.rand(1..1000)}@example.com"
6
+ end
7
+
8
+ CV = NmiDirectPost::CustomerVault
9
+ let(:a_cc_customer_vault_id) { TestCredentials::INSTANCE.cc_customer }
10
+ let(:a_cc_customer) { CV.find_by_customer_vault_id(a_cc_customer_vault_id) }
11
+ let(:a_checking_account_customer_vault_id) { TestCredentials::INSTANCE.ach_customer }
12
+ let(:a_checking_account_customer) { CV.find_by_customer_vault_id(a_checking_account_customer_vault_id) }
13
+
14
+ before :all do
15
+ credentials = TestCredentials::INSTANCE
16
+ NmiDirectPost::Base.establish_connection(credentials.nmi_username, credentials.nmi_password)
17
+ end
18
+
19
+ describe "find_by_customer_vault_id" do
20
+ it "should find a customer vault" do
21
+ expect(a_cc_customer.customer_vault_id).to eq(a_cc_customer_vault_id)
22
+ expect(a_cc_customer.first_name).not_to be_nil
23
+ expect(a_cc_customer.last_name).not_to be_nil
24
+ expect(a_cc_customer.email).not_to be_nil
25
+ end
26
+
27
+ it "should raise exception when customer_vault_id is blank" do
28
+ expect{CV.find_by_customer_vault_id("")}.to raise_error(StandardError, "CustomerVaultID cannot be blank")
29
+ end
30
+
31
+ it "should return exception when no customer is found" do
32
+ expect(CV.find_by_customer_vault_id("123456")).to be_nil
33
+ end
34
+ end
35
+
36
+ describe "create" do
37
+ it "should not create a customer vault when no payment info is specified" do
38
+ new_email = get_new_email
39
+ @customer = CV.new(:first_name => "George", :last_name => "Washington")
40
+ expect(@customer.create).to eq(false)
41
+ expect(@customer.errors.full_messages).to eq(["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"])
42
+ end
43
+
44
+ it "should not create a customer vault when a customer valut id is already present" do
45
+ new_email = get_new_email
46
+ @customer = CV.new(:first_name => "George", :last_name => "Washington", :cc_number => "4111111111111111", :cc_exp => "06/16", :customer_vault_id => a_cc_customer_vault_id)
47
+ expect(@customer.create).to eq(false)
48
+ expect(@customer.errors.full_messages).to eq(["Customer vault You cannot specify a Customer vault ID when creating a new customer vault. NMI will assign one upon creating the record"])
49
+ end
50
+
51
+ it "should create a customer vault with an automatically assigned customer_vault_id when a credit card number and expiration date are specified" do
52
+ new_email = get_new_email
53
+ @customer = CV.new(:first_name => "George", :last_name => "Washington", :cc_number => "4111111111111111", :cc_exp => "06/16")
54
+ expect(@customer.create).to eq(true)
55
+ expect(@customer.destroy).to be_success
56
+ expect(@customer.cc_exp).to eq("06/16")
57
+ end
58
+
59
+ it "should create a customer vault with an automatically assigned customer_vault_id when a checking account number and routing number are specified" do
60
+ new_email = get_new_email
61
+ @customer = CV.new(:first_name => "George", :last_name => "Washington", :check_aba => "123123123", :check_account => "123123123", :check_name => "my checking account")
62
+ expect(@customer.create).to eq(true)
63
+ expect(@customer.destroy).to be_success
64
+ end
65
+
66
+ [[:cc_number, :check_name], [:cc_number, :check_account], [:cc_number, :check_aba], [:cc_exp, :check_name], [:cc_exp, :check_account], [:cc_exp, :check_aba]].each do |attrs|
67
+ attributes = {:first_name => "George", :last_name => "Washington", :cc_number => "4111111111111111", :cc_exp => "06/16", :check_aba => "123123123", :check_account => "123123123", :check_name => "my checking account"}
68
+ attributes.delete(attrs.first)
69
+ attributes.delete(attrs.last)
70
+ it "should not create a customer vault when missing #{attrs.first} and #{attrs.last}" do
71
+ new_email = get_new_email
72
+ @customer = CV.new(attributes)
73
+ expect(@customer.create).to eq(false)
74
+ expect(@customer.errors.full_messages).to eq(["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"])
75
+ end
76
+ end
77
+ end
78
+
79
+ describe "save" do
80
+ it "should update the customer vault with new shipping_email when shipping_email is set before calling save!" do
81
+ new_email = get_new_email
82
+ a_cc_customer.shipping_email = new_email
83
+ a_cc_customer.save!
84
+ expect(a_cc_customer.response_text).to eq("Customer Update Successful")
85
+ expect(a_cc_customer).to be_success
86
+ expect(a_cc_customer.shipping_email).to eq(new_email)
87
+ expect(a_cc_customer.reload.shipping_email).to eq(new_email)
88
+ end
89
+ end
90
+
91
+ describe "update" do
92
+ it "should update the customer vault with new merchant_defined_fields" do
93
+ new_field_1 = Random.rand(1..1000)
94
+ new_field_2 = Random.rand(1..1000)
95
+ a_cc_customer.update!(:merchant_defined_field_1 => new_field_1, :merchant_defined_field_2 => new_field_2)
96
+ expect(a_cc_customer.response_text).to eq("Customer Update Successful")
97
+ expect(a_cc_customer).to be_success
98
+ a_cc_customer.reload
99
+ expect(a_cc_customer.merchant_defined_field_1).to eq(new_field_1.to_s)
100
+ expect(a_cc_customer.merchant_defined_field_2).to eq(new_field_2.to_s)
101
+ end
102
+
103
+ it "should update the customer vault with new shipping_email when shipping_email is passed to update!" do
104
+ new_email = get_new_email
105
+ a_cc_customer.update!(:shipping_email => new_email)
106
+ expect(a_cc_customer.response_text).to eq("Customer Update Successful")
107
+ expect(a_cc_customer).to be_success
108
+ expect(a_cc_customer.shipping_email).to eq(new_email)
109
+ expect(a_cc_customer.reload.shipping_email).to eq(new_email)
110
+ end
111
+
112
+ it "should not update the customer vault with new shipping_email when shipping_email is set before calling update!" do
113
+ new_email = get_new_email
114
+ new_address = "#{Random.rand(1..1000)} Sesame Street"
115
+ old_email = a_cc_customer.shipping_email
116
+ a_cc_customer.shipping_email = new_email
117
+ a_cc_customer.update!(:shipping_address_1 => new_address)
118
+ expect(a_cc_customer.response_text).to eq("Customer Update Successful")
119
+ expect(a_cc_customer).to be_success
120
+ expect(a_cc_customer.shipping_email).to eq(new_email)
121
+ expect(a_cc_customer.reload.shipping_email).to eq(old_email)
122
+ end
123
+
124
+ it "should not allow updating the customer_vault_id" do
125
+ new_email = get_new_email
126
+ expect{a_cc_customer.update!(:customer_vault_id => '')}.to raise_error(NmiDirectPost::MassAssignmentSecurity::Error, "Cannot mass-assign the following attributes: customer_vault_id")
127
+ end
128
+
129
+ it "should not interfere with other set variables" do
130
+ new_email = get_new_email
131
+ new_address = "#{Random.rand(1..1000)} Sesame Street"
132
+ old_email = a_cc_customer.shipping_email
133
+ a_cc_customer.shipping_email = new_email
134
+ a_cc_customer.update!(:shipping_address_1 => new_address)
135
+ expect(a_cc_customer.response_text).to eq("Customer Update Successful")
136
+ expect(a_cc_customer).to be_success
137
+ expect(a_cc_customer.shipping_email).to eq(new_email)
138
+ a_cc_customer.save!
139
+ expect(a_cc_customer.reload.shipping_email).to eq(new_email)
140
+ end
141
+ end
142
+
143
+ describe "reload" do
144
+ it "should not reload if customer vault id is missing" do
145
+ @customer = CV.new({})
146
+ expect(@customer.customer_vault_id).to be_nil
147
+ @customer.reload
148
+ expect(@customer).not_to be_success
149
+ expect(@customer.response).to be_nil
150
+ expect(@customer.errors.full_messages).to eq(["Customer vault You must specify a Customer vault ID when looking up an individual customer vault"])
151
+ end
152
+ end
153
+
154
+ describe "first/last/all" do
155
+ before(:all) do
156
+ @all_ids = CV.all_ids
157
+ end
158
+ it "should get all ids" do
159
+ expect(@all_ids).to be_a(Array)
160
+ expect(@all_ids).to eq(@all_ids.uniq)
161
+ @all_ids.each do |id|
162
+ expect(id).to be_a(Fixnum)
163
+ end
164
+ end
165
+
166
+ it "should get the first customer vault" do
167
+ expected = CV.find_by_customer_vault_id(@all_ids.first)
168
+ first = CV.first
169
+ expect(first).to be_a(CV)
170
+ expect(first).to have_same_attributes_as(expected)
171
+ end
172
+
173
+ it "should get the last customer vault" do
174
+ expected = CV.find_by_customer_vault_id(@all_ids.last)
175
+ last = CV.last
176
+ expect(last).to be_a(CV)
177
+ expect(last).to have_same_attributes_as(expected)
178
+ end
179
+
180
+ it "should get all customer vaults" do
181
+ customers = CV.all
182
+ expect(customers.count).to eq(@all_ids.count)
183
+ customers.each do |customer|
184
+ expect(customer).to be_a(CV)
185
+ expect(customer.customer_vault_id).not_to be_nil
186
+ end
187
+ end
188
+ end
189
+
190
+ describe "cc_hash" do
191
+ it "should not be settable" do
192
+ expect(a_cc_customer.respond_to?('cc_hash=')).to eq(false)
193
+ end
194
+ it "should be a string on a CC customer" do
195
+ expect(a_cc_customer.cc_hash).to be_a(String)
196
+ end
197
+ it "should be nil on a checking customer" do
198
+ expect(a_checking_account_customer.cc_hash).to be_nil
199
+ end
200
+ it "should not be allowed in a mass assignment update" do
201
+ expect{a_cc_customer.update!(:cc_hash => 'abcdefg')}.to raise_error(NmiDirectPost::MassAssignmentSecurity::Error, 'Cannot mass-assign the following attributes: cc_hash')
202
+ end
203
+ it "should not be allowed when initialized" do
204
+ expect{CV.new(:first_name => "George", :last_name => "Washington", :cc_number => "4111111111111111", :cc_exp => "06/16", :cc_hash => 'abcdefg')}.to raise_error(NmiDirectPost::MassAssignmentSecurity::Error, 'Cannot mass-assign the following attributes: cc_hash')
205
+ end
206
+ end
207
+
208
+ describe "check_hash" do
209
+ it "should not be settable" do
210
+ expect(a_cc_customer.respond_to?('check_hash=')).to eq(false)
211
+ end
212
+ it "should be a string on a CC customer" do
213
+ expect(a_checking_account_customer.check_hash).to be_a(String)
214
+ end
215
+ it "should be nil on a checking customer" do
216
+ expect(a_cc_customer.check_hash).to be_nil
217
+ end
218
+ it "should not be allowed in a mass assignment update" do
219
+ expect{a_cc_customer.update!(:check_hash => 'abcdefg')}.to raise_error(NmiDirectPost::MassAssignmentSecurity::Error, 'Cannot mass-assign the following attributes: check_hash')
220
+ end
221
+ it "should not be allowed when initialized" do
222
+ expect{CV.new(:first_name => "George", :last_name => "Washington", :cc_number => "4111111111111111", :cc_exp => "06/16", :check_hash => 'abcdefg')}.to raise_error(NmiDirectPost::MassAssignmentSecurity::Error, 'Cannot mass-assign the following attributes: check_hash')
223
+ end
224
+ end
225
+
226
+ describe "checking?" do
227
+ it "should be true for a checking account customer" do
228
+ expect(a_checking_account_customer).to be_checking
229
+ end
230
+ it "should be false for a CC customer" do
231
+ expect(a_cc_customer).not_to be_checking
232
+ end
233
+ end
234
+
235
+ describe "credit_card?" do
236
+ it "should be true for a CC customer" do
237
+ expect(a_cc_customer).to be_credit_card
238
+ end
239
+ it "should be false for a checking account customer" do
240
+ expect(a_checking_account_customer).not_to be_credit_card
241
+ end
242
+ end
243
+ end