baby-braspag 0.1.3

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.
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ nbproject
2
+ pkg
3
+ doc
4
+ tags
5
+ *.sw*
6
+ .bundle
7
+ .rvmrc
8
+ Gemfile.lock
9
+
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --colour
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in rbraspag.gemspec
4
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,10 @@
1
+ guard 'bundler' do
2
+ watch('Gemfile')
3
+ end
4
+
5
+ guard 'rspec', :version => 2, :bundler => false do
6
+ watch(%r{^spec/(.*)_spec\.rb$})
7
+ watch(%r{^lib/rbraspag.rb$}) { "spec" }
8
+ watch(%r{^lib/rbraspag/(.*)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
9
+ watch('spec/spec_helper.rb') { "spec" }
10
+ end
data/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # Baby Braspag
2
+
3
+ baby-braspag gem to use Braspag gateway
4
+
5
+ * This gem need RACK_ENV environment variable to identify the environment
6
+
7
+ # How to install
8
+
9
+ ## for Rails 3 app
10
+
11
+ ### Add on your Gemfile
12
+
13
+ gem "baby-braspag"
14
+
15
+ ### Create a config/braspag.yml file
16
+
17
+ $ rails generate braspag:install
18
+
19
+ ### Set RACK_ENV (our suggest)
20
+
21
+ # add last line in config/environment.rb
22
+ # ...
23
+ # ENV["RACK_ENV"] ||= ENV["RAILS_ENV"]
24
+
25
+ ### Edit config/braspag.yml with your Braspag merchant_id
26
+
27
+ # Examples
28
+
29
+ ## to create a Bill (Boleto/Bloqueto for brazilian guys)
30
+ @bill = Braspag::Bill.generate({
31
+ :order_id => 1,
32
+ :amount => 3,
33
+ :payment_method => 10
34
+ })
35
+
36
+ # License
37
+
38
+ (The MIT License)
39
+
40
+ Copyright (c) 2010
41
+
42
+ Permission is hereby granted, free of charge, to any person obtaining
43
+ a copy of this software and associated documentation files (the
44
+ 'Software'), to deal in the Software without restriction, including
45
+ without limitation the rights to use, copy, modify, merge, publish,
46
+ distribute, sublicense, and/or sell copies of the Software, and to
47
+ permit persons to whom the Software is furnished to do so, subject to
48
+ the following conditions:
49
+
50
+ The above copyright notice and this permission notice shall be
51
+ included in all copies or substantial portions of the Software.
52
+
53
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
54
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
55
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
56
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
57
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
58
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
59
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
60
+
data/RELEASES.md ADDED
@@ -0,0 +1,20 @@
1
+ # RELEASES
2
+
3
+ ## For next release
4
+
5
+ d2b9a7a Voltando a usar o HTTPI
6
+
7
+ ## 0.1.1 - 09/01/2012
8
+
9
+ 9ffbcbd adicionando novos códigos de métodos de pagamento
10
+
11
+
12
+ ## 0.1.0 - 09/01/2012
13
+
14
+ 5cf697e adicionando Gemfile.lock ao .gitignore
15
+ a6b1f54 removendo Gemfile.lock; modificando verificação do argumento has_interest
16
+ a663e0a baixando a versão mínima requerida do nokogiri
17
+ 628d108 refatorando testes e implementação
18
+ 087549a moving Connection exceptions to errors.rb; tests for Utils class
19
+ 9a727eb bump up version
20
+ 6e5b1c3 refatorando para suportar o ambiente de produção da Braspag
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,33 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "baby-braspag/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "baby-braspag"
7
+ s.version = Braspag::VERSION
8
+ s.authors = ["baby dev"]
9
+ s.email = %w["dev-team@baby.com.br"]
10
+ s.homepage = "https://github.com/Baby-com-br/braspag"
11
+ s.summary = "baby braspag gem to use Braspag gateway"
12
+ s.description = "baby braspag gem to use Braspag gateway"
13
+
14
+ s.rubyforge_project = "baby-braspag"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_dependency 'httpi', '>= 0.9.6'
22
+ s.add_dependency 'json', '>= 1.6.1'
23
+ s.add_dependency 'nokogiri', '>= 1.4.7'
24
+ s.add_dependency 'savon', '>= 0.9.9'
25
+
26
+ s.add_development_dependency "rake"
27
+ s.add_development_dependency "rspec"
28
+ s.add_development_dependency "fakeweb"
29
+ s.add_development_dependency "shoulda-matchers"
30
+ s.add_development_dependency "guard-rspec"
31
+ s.add_development_dependency "guard-bundler"
32
+ s.add_development_dependency "debugger"
33
+ end
@@ -0,0 +1,17 @@
1
+ development:
2
+ environment: "homologation"
3
+ merchant_id: "{84BE7E7F-698A-6C74-F820-AE359C2A07C2}"
4
+ crypto_url: "http://localhost:9292"
5
+ crypto_key: "1234561246"
6
+
7
+ test:
8
+ environment: "homologation"
9
+ merchant_id: "{84BE7E7F-698A-6C74-F820-AE359C2A07C2}"
10
+ crypto_url: "http://localhost:9292"
11
+ crypto_key: "1234561246"
12
+
13
+ production:
14
+ environment: "production"
15
+ merchant_id: "{84BE7E7F-698A-6C74-F820-AE359C2A07C2}"
16
+ crypto_url: "http://localhost:9292"
17
+ crypto_key: "1234561246"
@@ -0,0 +1,45 @@
1
+ require 'singleton'
2
+ require 'httpi'
3
+ require 'nokogiri'
4
+ require 'json'
5
+ require 'savon'
6
+
7
+ require "baby-braspag/version"
8
+ require 'baby-braspag/connection'
9
+ require 'baby-braspag/payment_method'
10
+ require 'baby-braspag/crypto/jar_webservice'
11
+ require 'baby-braspag/crypto/webservice'
12
+ require 'baby-braspag/bill'
13
+ require 'baby-braspag/poster'
14
+ require 'baby-braspag/credit_card'
15
+ require 'baby-braspag/protected_credit_card'
16
+ require 'baby-braspag/eft'
17
+ require 'baby-braspag/errors'
18
+ require 'baby-braspag/utils'
19
+ require 'baby-braspag/order'
20
+
21
+ module Braspag
22
+ def self.logger=(value)
23
+ @logger = value
24
+ end
25
+
26
+ def self.logger
27
+ @logger
28
+ end
29
+
30
+ def self.config_file_path=(path)
31
+ @config_path = path
32
+ end
33
+
34
+ def self.config_file_path
35
+ @config_path || 'config/braspag.yml'
36
+ end
37
+
38
+ def self.proxy_address=(value)
39
+ @proxy_address = value
40
+ end
41
+
42
+ def self.proxy_address
43
+ @proxy_address
44
+ end
45
+ end
@@ -0,0 +1,160 @@
1
+ require "bigdecimal"
2
+
3
+ module Braspag
4
+ class Bill < PaymentMethod
5
+
6
+ PAYMENT_METHODS = {
7
+ :bradesco => "06",
8
+ :cef => "07",
9
+ :hsbc => "08",
10
+ :bb => "09",
11
+ :real => "10",
12
+ :citibank => "13",
13
+ :itau => "14",
14
+ :unibanco => "26"
15
+ }
16
+
17
+ MAPPING = {
18
+ :merchant_id => "merchantId",
19
+ :order_id => "orderId",
20
+ :customer_name => "customerName",
21
+ :customer_id => "customerIdNumber",
22
+ :amount => "amount",
23
+ :payment_method => "paymentMethod",
24
+ :number => "boletoNumber",
25
+ :instructions => "instructions",
26
+ :expiration_date => "expirationDate",
27
+ :emails => "emails"
28
+ }
29
+
30
+ PRODUCTION_INFO_URI = "/webservices/pagador/pedido.asmx/GetDadosBoleto"
31
+ HOMOLOGATION_INFO_URI = "/pagador/webservice/pedido.asmx/GetDadosBoleto"
32
+ CREATION_URI = "/webservices/pagador/Boleto.asmx/CreateBoleto"
33
+
34
+ def self.generate(params)
35
+ connection = Braspag::Connection.instance
36
+ params[:merchant_id] = connection.merchant_id
37
+
38
+ params = self.normalize_params(params)
39
+ self.check_params(params)
40
+
41
+ data = {}
42
+
43
+ MAPPING.each do |k, v|
44
+ case k
45
+ when :payment_method
46
+ data[v] = PAYMENT_METHODS[params[:payment_method]]
47
+ when :amount
48
+ data[v] = Utils.convert_decimal_to_string(params[:amount])
49
+ else
50
+ data[v] = params[k] || ""
51
+ end
52
+ end
53
+
54
+ request = ::HTTPI::Request.new(self.creation_url)
55
+ request.body = data
56
+
57
+ response = Utils::convert_to_map(::HTTPI.post(request).body,
58
+ {
59
+ :url => nil,
60
+ :amount => nil,
61
+ :number => "boletoNumber",
62
+ :expiration_date => Proc.new { |document|
63
+ begin
64
+ Date.parse(document.search("expirationDate").first.to_s)
65
+ rescue
66
+ nil
67
+ end
68
+ },
69
+ :return_code => "returnCode",
70
+ :status => nil,
71
+ :message => nil
72
+ })
73
+
74
+ raise InvalidMerchantId if response[:message] == "Invalid merchantId"
75
+ raise InvalidAmount if response[:message] == "Invalid purchase amount"
76
+ raise InvalidPaymentMethod if response[:message] == "Invalid payment method"
77
+ raise InvalidStringFormat if response[:message] == "Input string was not in a correct format."
78
+ raise UnknownError if response[:status].nil?
79
+
80
+ response[:amount] = BigDecimal.new(response[:amount])
81
+
82
+ response
83
+ end
84
+
85
+ def self.normalize_params(params)
86
+ params = super
87
+
88
+ if params[:expiration_date].respond_to?(:strftime)
89
+ params[:expiration_date] = params[:expiration_date].strftime("%d/%m/%y")
90
+ end
91
+
92
+ params
93
+ end
94
+
95
+ def self.check_params(params)
96
+ super
97
+
98
+ if params[:number]
99
+ raise InvalidNumber unless (1..255).include?(params[:number].to_s.size)
100
+ end
101
+
102
+ if params[:instructions]
103
+ raise InvalidInstructions unless (1..512).include?(params[:instructions].to_s.size)
104
+ end
105
+
106
+ if params[:expiration_date]
107
+ matches = params[:expiration_date].to_s.match /(\d{2})\/(\d{2})\/(\d{2})/
108
+ raise InvalidExpirationDate unless matches
109
+ begin
110
+ Date.new(matches[3].to_i, matches[2].to_i, matches[1].to_i)
111
+ rescue ArgumentError
112
+ raise InvalidExpirationDate
113
+ end
114
+ end
115
+ end
116
+
117
+ def self.info_url
118
+ connection = Braspag::Connection.instance
119
+ connection.braspag_url + (connection.production? ? PRODUCTION_INFO_URI : HOMOLOGATION_INFO_URI)
120
+ end
121
+
122
+ def self.creation_url
123
+ Braspag::Connection.instance.braspag_url + CREATION_URI
124
+ end
125
+
126
+ def self.info(order_id)
127
+ connection = Braspag::Connection.instance
128
+
129
+ raise InvalidOrderId unless self.valid_order_id?(order_id)
130
+
131
+ request = ::HTTPI::Request.new(self.info_url)
132
+ request.body = {
133
+ :loja => connection.merchant_id,
134
+ :numeroPedido => order_id.to_s
135
+ }
136
+
137
+ response = ::HTTPI.post(request)
138
+
139
+ response = Utils::convert_to_map(response.body, {
140
+ :document_number => "NumeroDocumento",
141
+ :payer => "Sacado",
142
+ :our_number => "NossoNumero",
143
+ :bill_line => "LinhaDigitavel",
144
+ :document_date => "DataDocumento",
145
+ :expiration_date => "DataVencimento",
146
+ :receiver => "Cedente",
147
+ :bank => "Banco",
148
+ :agency => "Agencia",
149
+ :account => "Conta",
150
+ :wallet => "Carteira",
151
+ :amount => "ValorDocumento",
152
+ :amount_invoice => "ValorPago",
153
+ :invoice_date => "DataCredito"
154
+ })
155
+
156
+ raise UnknownError if response[:document_number].nil?
157
+ response
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,37 @@
1
+ module Braspag
2
+ class Connection
3
+ include Singleton
4
+
5
+ PRODUCTION_URL = "https://transaction.pagador.com.br"
6
+ HOMOLOGATION_URL = "https://homologacao.pagador.com.br"
7
+
8
+ PROTECTED_CARD_PRODUCTION_URL = "https://cartaoprotegido.braspag.com.br/Services"
9
+ PROTECTED_CARD_HOMOLOGATION_URL = "https://homologacao.braspag.com.br/services/testenvironment"
10
+
11
+ attr_reader :braspag_url, :protected_card_url, :merchant_id, :crypto_url, :crypto_key, :options, :environment
12
+
13
+ def initialize
14
+ raise InvalidEnv if ENV["BRASPAG_ENV"].nil? || ENV["BRASPAG_ENV"].empty?
15
+
16
+ @options = YAML.load_file(Braspag.config_file_path)[ ENV['BRASPAG_ENV'] ]
17
+ @merchant_id = @options['merchant_id']
18
+
19
+ raise InvalidMerchantId unless @merchant_id =~ /\{[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}\}/i
20
+
21
+ @crypto_key = @options["crypto_key"]
22
+ @crypto_url = @options["crypto_url"]
23
+ @environment = @options["environment"] == 'production' ? 'production' : 'homologation'
24
+
25
+ @braspag_url = self.production? ? PRODUCTION_URL : HOMOLOGATION_URL
26
+ @protected_card_url = self.production? ? PROTECTED_CARD_PRODUCTION_URL : PROTECTED_CARD_HOMOLOGATION_URL
27
+ end
28
+
29
+ def production?
30
+ @environment == 'production'
31
+ end
32
+
33
+ def homologation?
34
+ @environment == 'homologation'
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,210 @@
1
+ module Braspag
2
+ class CreditCard < PaymentMethod
3
+
4
+ MAPPING = {
5
+ :merchant_id => "merchantId",
6
+ :order => 'order',
7
+ :order_id => "orderId",
8
+ :customer_name => "customerName",
9
+ :amount => "amount",
10
+ :payment_method => "paymentMethod",
11
+ :holder => "holder",
12
+ :card_number => "cardNumber",
13
+ :expiration => "expiration",
14
+ :security_code => "securityCode",
15
+ :number_payments => "numberPayments",
16
+ :type => "typePayment"
17
+ }
18
+
19
+ AUTHORIZE_URI = "/webservices/pagador/Pagador.asmx/Authorize"
20
+ CAPTURE_URI = "/webservices/pagador/Pagador.asmx/Capture"
21
+ PARTIAL_CAPTURE_URI = "/webservices/pagador/Pagador.asmx/CapturePartial"
22
+ CANCELLATION_URI = "/webservices/pagador/Pagador.asmx/VoidTransaction"
23
+ PRODUCTION_INFO_URI = "/webservices/pagador/pedido.asmx/GetDadosCartao"
24
+ HOMOLOGATION_INFO_URI = "/pagador/webservice/pedido.asmx/GetDadosCartao"
25
+
26
+ def self.authorize(params = {})
27
+ connection = Braspag::Connection.instance
28
+ params[:merchant_id] = connection.merchant_id
29
+
30
+ self.check_params(params)
31
+
32
+ data = {}
33
+ MAPPING.each do |k, v|
34
+ case k
35
+ when :payment_method
36
+ data[v] = Braspag::Connection.instance.homologation? ? PAYMENT_METHODS[:braspag] : PAYMENT_METHODS[params[:payment_method]]
37
+ when :amount
38
+ data[v] = Utils.convert_decimal_to_string(params[:amount])
39
+ else
40
+ data[v] = params[k] || ""
41
+ end
42
+ end
43
+
44
+ response = Braspag::Poster.new(self.authorize_url).do_post(:authorize, data)
45
+
46
+ Utils::convert_to_map(response.body, {
47
+ :amount => nil,
48
+ :number => "authorisationNumber",
49
+ :message => 'message',
50
+ :return_code => 'returnCode',
51
+ :status => 'status',
52
+ :transaction_id => "transactionId"
53
+ })
54
+ end
55
+
56
+ def self.capture(order_id)
57
+ connection = Braspag::Connection.instance
58
+ merchant_id = connection.merchant_id
59
+
60
+ raise InvalidOrderId unless self.valid_order_id?(order_id)
61
+
62
+ data = {
63
+ MAPPING[:order_id] => order_id,
64
+ MAPPING[:merchant_id] => merchant_id
65
+ }
66
+
67
+ response = Braspag::Poster.new(self.capture_url).do_post(:capture, data)
68
+
69
+ Utils::convert_to_map(response.body, {
70
+ :amount => nil,
71
+ :number => "authorisationNumber",
72
+ :message => 'message',
73
+ :return_code => 'returnCode',
74
+ :status => 'status',
75
+ :transaction_id => "transactionId"
76
+ })
77
+ end
78
+
79
+ def self.partial_capture(order_id, amount)
80
+ connection = Braspag::Connection.instance
81
+ merchant_id = connection.merchant_id
82
+
83
+ raise InvalidOrderId unless self.valid_order_id?(order_id)
84
+
85
+ data = {
86
+ MAPPING[:order_id] => order_id,
87
+ MAPPING[:merchant_id] => merchant_id,
88
+ "captureAmount" => Utils.convert_decimal_to_string(amount)
89
+ }
90
+
91
+ response = Braspag::Poster.new(self.partial_capture_url).do_post(:partial_capture, data)
92
+
93
+ Utils::convert_to_map(response.body, {
94
+ :amount => nil,
95
+ :number => "authorisationNumber",
96
+ :message => 'message',
97
+ :return_code => 'returnCode',
98
+ :status => 'status',
99
+ :transaction_id => "transactionId"
100
+ })
101
+ end
102
+
103
+ def self.void(order_id)
104
+ connection = Braspag::Connection.instance
105
+ merchant_id = connection.merchant_id
106
+
107
+ raise InvalidOrderId unless self.valid_order_id?(order_id)
108
+
109
+ data = {
110
+ MAPPING[:order] => order_id,
111
+ MAPPING[:merchant_id] => merchant_id
112
+ }
113
+
114
+ response = Braspag::Poster.new(self.cancellation_url).do_post(:void, data)
115
+
116
+ Utils::convert_to_map(response.body, {
117
+ :amount => nil,
118
+ :number => "authorisationNumber",
119
+ :message => 'message',
120
+ :return_code => 'returnCode',
121
+ :status => 'status',
122
+ :transaction_id => "transactionId"
123
+ })
124
+ end
125
+
126
+ def self.info(order_id)
127
+ connection = Braspag::Connection.instance
128
+
129
+ raise InvalidOrderId unless self.valid_order_id?(order_id)
130
+
131
+ data = {:loja => connection.merchant_id, :numeroPedido => order_id.to_s}
132
+ response = Braspag::Poster.new(self.info_url).do_post(:info_credit_card, data)
133
+
134
+ response = Utils::convert_to_map(response.body, {
135
+ :checking_number => "NumeroComprovante",
136
+ :certified => "Autenticada",
137
+ :autorization_number => "NumeroAutorizacao",
138
+ :card_number => "NumeroCartao",
139
+ :transaction_number => "NumeroTransacao"
140
+ })
141
+
142
+ raise UnknownError if response[:checking_number].nil?
143
+ response
144
+ end
145
+
146
+ def self.check_params(params)
147
+ super
148
+
149
+ [:customer_name, :holder, :card_number, :expiration, :security_code, :number_payments, :type].each do |param|
150
+ raise IncompleteParams if params[param].nil?
151
+ end
152
+
153
+ raise InvalidHolder if params[:holder].to_s.size < 1 || params[:holder].to_s.size > 100
154
+
155
+ matches = params[:expiration].to_s.match /^(\d{2})\/(\d{2,4})$/
156
+ raise InvalidExpirationDate unless matches
157
+ begin
158
+ year = matches[2].to_i
159
+ year = "20#{year}" if year.size == 2
160
+
161
+ Date.new(year.to_i, matches[1].to_i)
162
+ rescue ArgumentError
163
+ raise InvalidExpirationDate
164
+ end
165
+
166
+ raise InvalidSecurityCode if params[:security_code].to_s.size < 1 || params[:security_code].to_s.size > 4
167
+
168
+ raise InvalidNumberPayments if params[:number_payments].to_i < 1 || params[:number_payments].to_i > 99
169
+ end
170
+
171
+ # <b>DEPRECATED:</b> Please use <tt>ProtectedCreditCard.save</tt> instead.
172
+ def self.save(params)
173
+ warn "[DEPRECATION] `CreditCard.save` is deprecated. Please use `ProtectedCreditCard.save` instead."
174
+ ProtectedCreditCard.save(params)
175
+ end
176
+
177
+ # <b>DEPRECATED:</b> Please use <tt>ProtectedCreditCard.get</tt> instead.
178
+ def self.get(just_click_key)
179
+ warn "[DEPRECATION] `CreditCard.get` is deprecated. Please use `ProtectedCreditCard.get` instead."
180
+ ProtectedCreditCard.get(just_click_key)
181
+ end
182
+
183
+ # <b>DEPRECATED:</b> Please use <tt>ProtectedCreditCard.just_click_shop</tt> instead.
184
+ def self.just_click_shop(params = {})
185
+ warn "[DEPRECATION] `CreditCard.just_click_shop` is deprecated. Please use `ProtectedCreditCard.just_click_shop` instead."
186
+ ProtectedCreditCard.just_click_shop(params)
187
+ end
188
+
189
+ def self.info_url
190
+ connection = Braspag::Connection.instance
191
+ connection.braspag_url + (connection.production? ? PRODUCTION_INFO_URI : HOMOLOGATION_INFO_URI)
192
+ end
193
+
194
+ def self.authorize_url
195
+ Braspag::Connection.instance.braspag_url + AUTHORIZE_URI
196
+ end
197
+
198
+ def self.capture_url
199
+ Braspag::Connection.instance.braspag_url + CAPTURE_URI
200
+ end
201
+
202
+ def self.partial_capture_url
203
+ Braspag::Connection.instance.braspag_url + PARTIAL_CAPTURE_URI
204
+ end
205
+
206
+ def self.cancellation_url
207
+ Braspag::Connection.instance.braspag_url + CANCELLATION_URI
208
+ end
209
+ end
210
+ end