paypal_api 0.3.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.
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ .DS_Store
2
+ *.gem
3
+ .bundle
4
+ Gemfile.lock
5
+ pkg/*
6
+ *~
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in paypal_api.gemspec
4
+ gemspec
data/README.markdown ADDED
@@ -0,0 +1,161 @@
1
+ # Paypal Api Gem
2
+
3
+ a ruby library to handle the entire paypal api.
4
+
5
+ paypals documentation sucks, and there do not appear to be any officially supported paypal gems.
6
+ the gems that do exist do not cover the entire api.
7
+
8
+ # Usage
9
+
10
+ ## Interfacing with the gem:
11
+ ```ruby
12
+ require "paypal_api"
13
+
14
+ request = Paypal::PaymentsPro.do_direct_payment # returns instance of Paypal::DoDirectPaymentRequest
15
+
16
+ # Set required fields
17
+ request.first_name = "mark"
18
+ request.last_name = "winton"
19
+ request.amt = 10.00
20
+
21
+ # Add a list type field
22
+ request.item.push {
23
+ :l_email => "bro@dudeman.com",
24
+ :l_amt => 23.0
25
+ }
26
+
27
+ response = request.make
28
+
29
+ response.success? # true if successful
30
+
31
+ response[:correlation_id] # correlation id string returned by paypal
32
+ response[:transaction_id] # transaction id string, not return on all calls
33
+ ```
34
+
35
+ ## Configure
36
+
37
+ ```ruby
38
+ Paypal::Request.version = "84.0"
39
+ Paypal::Request.environment = "development" # or "production"
40
+ Paypal::Request.user = "user_api1.something.com"
41
+ Paypal::Request.pwd = "some_password_they_gave_you"
42
+ Paypal::Request.signature = "some_signature"
43
+ ```
44
+
45
+ paypal api credentials for production can be found here: [https://www.paypal.com/us/cgi-bin/webscr?cmd=_profile-api-signature](https://www.paypal.com/us/cgi-bin/webscr?cmd=_profile-api-signature)
46
+
47
+ sandbox credentials can be found here: [https://developer.paypal.com/cgi-bin/devscr?cmd=_certs-session&login_access=0](https://developer.paypal.com/cgi-bin/devscr?cmd=_certs-session&login_access=0)
48
+
49
+ ## Rails
50
+
51
+ if you'd like to have multi environment configuration in rails, place a file at `config/paypal.yml` and the gem will read from it accordingly
52
+
53
+ ```yml
54
+ test:
55
+ environment: "sandbox"
56
+ username: "user_api1.something.com"
57
+ password: "some_password_they_gave_you"
58
+ signature: "some_signature"
59
+
60
+ production:
61
+ environment: "production"
62
+ username: <%= ENV["PAYPAL_USERNAME"] %>
63
+ password: <%= ENV["PAYPAL_PASSWORD"] %>
64
+ signature: <%= ENV["PAYPAL_SIGNATURE"] %>
65
+ ```
66
+
67
+ # Current Status
68
+
69
+ alpha
70
+
71
+ the work i've done so far is in order to get up and running on a project. once the project is settled, i'll be spending
72
+ some more time making the gem complete. once i feel it is in the beta stage i will push it to the official ruby gems
73
+ repository. in order to get to that stage, i will need to add support for adaptive payments, express checkout, etc...
74
+
75
+ # How To Contribute
76
+
77
+ right now the most help i could use is in writing the specs for the various api calls from the Payments Pro api. i will be working on
78
+ separating out the different access methods shortly (there's a huge difference between how to call the Payments Pro api vs the Adaptive Payments api).
79
+
80
+ as this is my first gem, i could also use help with some of the niceties with rails. ideally there will be a generator for migrating your db
81
+ to store ipn messages, and a generated class with callbacks to handle the various cases. this will probably take a lot of effort since there
82
+ are many intricacies in the meanings of the different ipn's.
83
+
84
+ for contributing to the api method request specs, look at `lib/paypal_api/apis/payments_pro.rb`
85
+
86
+ this is my first gem, so i'll be excited for any contributions :'(
87
+
88
+ # Paypal API Checklist
89
+
90
+ here's a list of api methods, and whether or not they are implemented (please take a look at `lib/paypal_api/apis/payments_pro.rb` if you'd
91
+ like to contribute, i've made it pretty easy to add compatibility for a new api call)
92
+
93
+ ## Payments Pro
94
+
95
+ * do_direct_payment - &#10003;
96
+
97
+ * do_reference_transaction - &#10003;
98
+
99
+ * do_capture - &#10003;
100
+
101
+ * do_void - &#10003;
102
+
103
+ * get_recurring_payments_profile_details - started
104
+
105
+ * address_verify
106
+
107
+ * bill_outstanding_amount
108
+
109
+ * callback
110
+
111
+ * create_recurring_payments_profile
112
+
113
+ * do_authorization
114
+
115
+ * do_express_checkout_payment
116
+
117
+ * do_nonreferenced_credit
118
+
119
+ * do_reauthorization
120
+
121
+ * get_balance
122
+
123
+ * get_billing_agreement_customer_details
124
+
125
+ * get_express_checkout_details
126
+
127
+ * get_transaction_details
128
+
129
+ * manage_pending_transaction_status
130
+
131
+ * manage_recurring_payments_profile_status
132
+
133
+ * refund_transaction
134
+
135
+ * set_customer_billing_agreement
136
+
137
+ * set_express_checkout
138
+
139
+ * transaction_search
140
+
141
+ * update_recurring_payments_profile
142
+
143
+ ## Mass Pay
144
+
145
+ note that you need to request that paypal enable mass pay for your account before it will work
146
+
147
+ * mass_pay - &#10003;
148
+
149
+ ## Instant Pay Notifications
150
+
151
+ ## Express Checkout
152
+
153
+ ## Adaptive Payments
154
+
155
+ ## Adaptive Accounts
156
+
157
+ ## Invoicing
158
+
159
+ ## Button Manager
160
+
161
+ ## Permissions
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require "paypal_api"
@@ -0,0 +1,149 @@
1
+ module Paypal
2
+ module Formatters
3
+ def escape_uri_component(string)
4
+ string = string.to_s
5
+ return URI.escape(string, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
6
+ end
7
+
8
+ def to_key(symbol)
9
+ return symbol.to_s.gsub(/[^a-z0-9]/i, "").upcase
10
+ end
11
+ end
12
+
13
+ class Api
14
+
15
+ attr_accessor :request
16
+
17
+ protected
18
+
19
+ def self.set_accessor(klass, name, type = nil)
20
+ set_reader(klass, name, type)
21
+ set_writer(klass, name, type)
22
+ end
23
+
24
+ # writers should validate before assigning value
25
+ def self.set_writer(klass, field, type)
26
+ klass.class_eval do
27
+ define_method("#{field}=") do |val|
28
+ # arbitrary value
29
+ if type.nil?
30
+ instance_variable_set("@#{field}", val)
31
+ # special types
32
+ elsif type.class == Regexp
33
+ if match = type.match(val)
34
+ instance_variable_set("@#{field}", match[0])
35
+ else
36
+ raise Paypal::InvalidParameter, "#{field} expects a string that matches #{type}"
37
+ end
38
+ elsif type.class == Optional
39
+ instance_variable_set("@#{field}", type.parse(val))
40
+ elsif type.class == Enum
41
+ instance_variable_set("@#{field}", type.parse(val))
42
+ elsif type.class == Coerce
43
+ instance_variable_set("@#{field}", type.parse(val))
44
+ elsif type.class == Default
45
+ instance_variable_set("@#{field}", type.parse(val))
46
+ # custom type
47
+ elsif type.class == Proc && type.call(val)
48
+ instance_variable_set("@#{field}", val)
49
+ # is_a type
50
+ elsif type == val.class
51
+ instance_variable_set("@#{field}",val)
52
+ else
53
+ raise Paypal::InvalidParameter, "#{field}'s spec was set incorrectly"
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ def self.set_reader(klass, m, type, constant = false)
60
+ variable = "@#{m}"
61
+
62
+ klass.class_eval do
63
+ define_method(m) do
64
+ if constant
65
+ return type
66
+ elsif type.class == Default
67
+ instance_variable_set(variable, type.value) unless instance_variable_defined?(variable)
68
+ instance_variable_get(variable)
69
+ else
70
+ instance_variable_get(variable)
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ def self.set_sequential_reader(klass, k, v)
77
+ variable = "@#{k}"
78
+
79
+ klass.class_eval do
80
+ define_method(k) do
81
+ instance_variable_set(variable, v.clone) unless instance_variable_defined?(variable)
82
+ instance_variable_get(variable)
83
+ end
84
+ end
85
+ end
86
+
87
+ # collected from set request signature
88
+ def self.set_required(klass, keys)
89
+ klass.class_eval do
90
+ @required = keys
91
+ end
92
+ end
93
+
94
+ # collected from set request signature
95
+ def self.set_sequential(klass, keys)
96
+ klass.class_eval do
97
+ @sequential = keys
98
+ end
99
+ end
100
+
101
+ def self.symbol_to_camel(symbol)
102
+ return symbol.to_s.downcase.split("_").map(&:capitalize).join
103
+ end
104
+
105
+ def self.set_request_signature(name, hash)
106
+ # create request object
107
+ class_name = "#{self.symbol_to_camel name}Request"
108
+ self.class.class_eval <<-EOS
109
+ class Paypal::#{class_name} < Request; end
110
+ EOS
111
+ klass = Kernel.const_get("Paypal").const_get(class_name)
112
+
113
+ # add setters/getters
114
+ required = []
115
+ sequential = []
116
+ hash.each do |k,v|
117
+ if v.class == String || v.class == Fixnum || v.class == Float
118
+ set_reader klass, k, v, true
119
+ elsif v.class == Sequential
120
+ set_sequential_reader klass, k, v
121
+ else
122
+ set_accessor klass, k, v
123
+ end
124
+
125
+ required.push(k) unless v.class == Optional || v.class == Sequential
126
+ sequential.push(k) if v.class == Sequential
127
+ end
128
+
129
+ # set which keys are required for the request
130
+ set_required(klass, required)
131
+ set_sequential(klass, sequential)
132
+
133
+ # create api method
134
+ self.class_eval <<-EOS
135
+ def self.#{name}(hash = {})
136
+ return #{klass}.new(hash)
137
+ end
138
+ EOS
139
+ end
140
+
141
+ # TODO: make this useful :'(
142
+ # def set_response_signature(hash)
143
+ # hash.each do |k,v|
144
+ # set_reader k, v
145
+ # end
146
+ # end
147
+ end
148
+
149
+ end
@@ -0,0 +1,25 @@
1
+ module Paypal
2
+ class MassPay < Paypal::Api
3
+ set_request_signature :mass_pay, {
4
+ :method => "MassPay",
5
+ :email_subject => Optional.new(String), # max 255 char
6
+ :currency_code => Default.new("USD", String),
7
+ :receiver_type => Default.new("EmailAddress", Enum.new(:email_address => "EmailAddress", :user_id => "UserID")),
8
+ :payee => Sequential.new({
9
+ :email => String,
10
+ :amt => Float,
11
+ :unique_id => Optional.new(String)
12
+ }, 250, lambda {|key, i| "L_#{key.to_s.gsub("_","").upcase}#{i}"})
13
+ }
14
+ end
15
+
16
+ class MassPayRequest
17
+
18
+ protected
19
+ def validate!
20
+ if @payee.length == 0
21
+ raise Paypal::InvalidRequest, "you pust provide at least one payee"
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,155 @@
1
+ module Paypal
2
+ class PaymentsPro < Paypal::Api
3
+
4
+ set_request_signature :do_direct_payment, {
5
+
6
+ # DoDirectPayment Request Fields
7
+ :method => "DoDirectPayment",
8
+ :payment_action => Optional.new(Enum.new("Authorization", "Sale")),
9
+ :ip_address => /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/,
10
+ :return_mf_details => Optional.new(
11
+ Coerce.new( lambda do |val|
12
+ return [1, "1", true].include?(val) ? 1 : 0
13
+ end)
14
+ ),
15
+
16
+
17
+ # Credit Card Details Fields
18
+ :credit_card_type => Optional.new(Enum.new("Visa", "MasterCard", "Discover", "Amex", "Maestro")),
19
+ :acct => /\d+/, # this should be better
20
+ :exp_date => /^\d{6}$/, # "MMYYYY"
21
+ :cvv2 => /^\d{3,4}$/,
22
+ :start_date => Optional.new(/^\d{6}$/), # maestro only
23
+ :issue_number => Optional.new(/^\d{,2}$/), #maestro only
24
+
25
+
26
+ # Payer Information Fields
27
+ :email => Optional.new(/email/), # max 127 char
28
+ :first_name => String, # max 25 char
29
+ :last_name => String, # max 25 char
30
+
31
+
32
+ # Address Fields
33
+ :street => String, # max 100 char
34
+ :street_2 => Optional.new(String), # max 100 char
35
+ :city => String, # max 40 char
36
+ :state => String, # max 40 char
37
+ :country_code => Default.new("US", /^[a-z]{2}$/i),
38
+ :zip => String, # max 20 char
39
+ :ship_to_phone_num => Optional.new(String), # max 20 char
40
+
41
+
42
+ # Payment Details Fields
43
+ :amt => Float, # complicated: https://www.x.com/developers/paypal/documentation-tools/api/dodirectpayment-api-operation-nvp
44
+ :currency_code => Default.new("USD", /^[a-z]{3}$/i),
45
+
46
+ # TODO:
47
+ :item_amt => Optional.new,
48
+ :shipping_amt => Optional.new,
49
+ :insurance_amt => Optional.new,
50
+ :shipdisc_amt => Optional.new,
51
+ :handling_amt => Optional.new,
52
+ :tax_amt => Optional.new,
53
+
54
+ :desc => Optional.new(String), # max 127 char
55
+ :custom => Optional.new(String), # max 256 char
56
+ :inv_num => Optional.new(String), # max 127 char
57
+ :button_source => Optional.new(String), # max 32 char
58
+
59
+ :notify_url => Optional.new, # hard to tell if this is part of this api or not from the wording in the docs
60
+ :recurring => Default.new("N", lambda {|anything| "Y" }),
61
+
62
+
63
+ # TODO: Payment Details Item Fields
64
+ # TODO: Ebay Item Payment Details Item Fields
65
+ # TODO: Ship To Address Fields
66
+ # TODO: 3D Secure Request Fields (U.K. Merchants Only)
67
+
68
+ }
69
+
70
+ set_request_signature :do_reference_transaction, {
71
+ :method => "DoReferenceTransaction",
72
+ :reference_id => String,
73
+ :payment_action => Default.new("Sale", Enum.new("Authorization", "Sale")),
74
+ :return_mf_details => Optional.new(
75
+ Coerce.new( lambda do |val|
76
+ return [1, "1", true].include?(val) ? 1 : 0
77
+ end)
78
+ ),
79
+ :soft_descriptor =>Optional.new(lambda {|val|
80
+ if val.match(/^([a-z0-9]|\.|-|\*| )*$/i) && val.length <= 22
81
+ return true
82
+ else
83
+ return false
84
+ end
85
+ }),
86
+
87
+ # ship to address fields
88
+ :ship_to_name => Optional.new(String), # max 32
89
+ :ship_to_street => Optional.new(String), # max 100
90
+ :ship_to_street_2 => Optional.new(String), # max 100
91
+ :ship_to_city => Optional.new(String), # max 40
92
+ :ship_to_state => Optional.new(String), # max 40
93
+ :ship_to_zip => Optional.new(String), # max 20
94
+ :ship_to_country => Optional.new(String), # max 2
95
+ :ship_to_phone_num => Optional.new(/[0-9+-]+/), # max 20
96
+
97
+ # payment details fields
98
+ :amt => Float,
99
+ :currency_code => Default.new("USD", /^[a-z]{3}$/i),
100
+
101
+ # TODO:
102
+ :item_amt => Optional.new,
103
+ :shipping_amt => Optional.new,
104
+ :insurance_amt => Optional.new,
105
+ :shipdisc_amt => Optional.new,
106
+ :handling_amt => Optional.new,
107
+ :tax_amt => Optional.new,
108
+
109
+ :desc => Optional.new(String), # max 127 char
110
+ :custom => Optional.new(String), # max 256 char
111
+ :inv_num => Optional.new(String), # max 127 char
112
+ :button_source => Optional.new(String), # max 32 char
113
+
114
+ :notify_url => Optional.new, # hard to tell if this is part of this api or not from the wording in the docs
115
+ :recurring => Default.new("N", lambda {|anything| "Y" }),
116
+
117
+ :item => Sequential.new({
118
+ :l_item_category => Enum.new("Digital", "Physical")
119
+ })
120
+
121
+ }
122
+
123
+ set_request_signature :do_capture, {
124
+ :method => "DoCapture",
125
+ :authorization_id => String, # max 19 char
126
+ :amt => Float,
127
+ :currency_code => Default.new("USD", /^[a-z]{3}$/i),
128
+ :complete_type => Default.new("Complete", Enum.new("Complete", "NotComplete")),
129
+ :inv_num => Optional.new(String), # max 127 char
130
+ :note => Optional.new(String), # max 255 char
131
+ :soft_descriptor => Optional.new(lambda {|val|
132
+ if val.match(/^([a-z0-9]|\.|-|\*| )*$/i) && val.length <= 22
133
+ return true
134
+ else
135
+ return false
136
+ end
137
+ }),
138
+
139
+ :store_id => Optional.new(String), # max 50 char
140
+ :terminal_id => Optional.new(String) # max 50 char
141
+ }
142
+
143
+ set_request_signature :do_void, {
144
+ :method => "DoVoid",
145
+ :authorization_id => String, # Note: If you are voiding a transaction that has been reauthorized, use the ID from the original authorization, and not the reauthorization.
146
+ :note => Optional.new(String) # max 255 char
147
+ }
148
+
149
+ set_request_signature :get_recurring_payments_profile_details, {
150
+ :method => "GetRecurringPaymentsProfileDetails",
151
+ :profile_id => String # max 19 char
152
+ }
153
+
154
+ end
155
+ end
@@ -0,0 +1,195 @@
1
+
2
+ module Paypal
3
+
4
+ class Api
5
+
6
+ class Parameter
7
+ attr_accessor :value
8
+
9
+ def initialize(value)
10
+ @value = value
11
+ end
12
+
13
+ def parse(anything)
14
+ @value = anything
15
+ return @value
16
+ end
17
+
18
+ def parameter_parse(val)
19
+ if @parameter.class == Class
20
+ if val.class == @parameter
21
+ return val
22
+ else
23
+ raise Paypal::InvalidParameter, "'#{val}'' is not of type #{@parameter.class}"
24
+ end
25
+ elsif @parameter.class == Regexp
26
+ match = @parameter.match(val)
27
+ if match
28
+ return match[0]
29
+ else
30
+ raise Paypal::InvalidParameter, "'#{val}' does not match #{@parameter}"
31
+ end
32
+ elsif @parameter.class < Parameter
33
+ return @parameter.parse(val)
34
+ else
35
+ raise Paypal::InvalidParameter, "#{@parameter.class} is an invalid parameter specification"
36
+ end
37
+ end
38
+ end
39
+
40
+ class Sequential < Parameter
41
+ include Paypal::Formatters
42
+
43
+ attr_accessor :list
44
+
45
+ # allows you to specify an optional key -> paypal_key proc due to nonstandard key formatting
46
+ def initialize(hash = {}, limit = nil, key_proc = nil)
47
+ @list = []
48
+ @schema = hash
49
+ @required = hash.map{|(k,v)| k if v.class != Optional }.compact
50
+ @key_proc = key_proc if key_proc
51
+ @limit = limit
52
+ end
53
+
54
+ # necessary because sequential stores request state, need a new list created for each
55
+ # request instance
56
+ def clone
57
+ return self.class.new(@schema, @limit, @key_proc)
58
+ end
59
+
60
+ def length
61
+ return @list.length
62
+ end
63
+
64
+ def push(hash)
65
+ raise Paypal::InvalidParameter, "missing required parameter for sequential field" unless (@required - hash.keys).empty?
66
+ raise Paypal::InvalidParameter, "field cannot have more than #{@limit} items, #{@list.length} provided" if !@limit.nil? && @list.length == @limit
67
+
68
+ hash.each do |k,val|
69
+ type = @schema[k]
70
+
71
+ if type.nil?
72
+ hash[k] = val
73
+ elsif type.class == Regexp
74
+ if match = type.match(val)
75
+ hash[k] = match[0]
76
+ else
77
+ raise Paypal::InvalidParameter, "'#{val}' did not match #{type}"
78
+ end
79
+ elsif [Optional, Enum, Coerce, Default].include?(type.class)
80
+ hash[k] = type.parse(val)
81
+ elsif type.class == Proc && type.call(val)
82
+ hash[k] = val
83
+ elsif type == val.class
84
+ hash[k] = val
85
+ else
86
+ raise Paypal::InvalidParameter, "#{type.class} is an invalid parameter specification"
87
+ end
88
+ end
89
+
90
+ @list.push(hash)
91
+ end
92
+
93
+ def to_key(symbol, i)
94
+ if @key_proc
95
+ return @key_proc.call(symbol, i)
96
+ else
97
+ return symbol.to_s.split("_", 2).map{|s| s.gsub("_", "") }.join("_").gsub(/[^a-z0-9_]/i, "").upcase + "#{i}"
98
+ end
99
+ end
100
+
101
+ def to_query_string
102
+ output = ""
103
+ @list.each_index do |i|
104
+ @list[i].each do |(k,v)|
105
+ output = "#{output}&#{to_key(k, i)}=#{escape_uri_component(@list[i][k])}"
106
+ end
107
+ end
108
+ return output
109
+ end
110
+
111
+ end
112
+
113
+ class Coerce < Parameter
114
+
115
+ attr_reader :method
116
+
117
+ def initialize(method)
118
+ @method = method
119
+ end
120
+
121
+ def parse(val)
122
+ return @method.call(val)
123
+ end
124
+ end
125
+
126
+ class Enum < Parameter
127
+
128
+ # needs to return the exact string if given instead of symbol
129
+ attr_reader :allowed_values
130
+
131
+ def initialize(*values)
132
+ if values.length == 1 && values[0].is_a?(::Hash)
133
+ @hash_enum = true
134
+ @allowed_values = values[0]
135
+ else
136
+ @allowed_values = values
137
+ end
138
+ end
139
+
140
+ def parse(val)
141
+ if @hash_enum
142
+ if @allowed_values.include?(val)
143
+ return @allowed_values[val]
144
+ else
145
+ raise Paypal::InvalidParameter, "'#{val}' must be a key in #{@allowed_values.keys}"
146
+ end
147
+ else
148
+ if @allowed_values.include?(normalize(val))
149
+ return normalize(val)
150
+ else
151
+ raise Paypal::InvalidParameter, "'#{val}' must be one of #{@allowed_values}"
152
+ end
153
+ end
154
+ end
155
+
156
+ def normalize(val)
157
+ return val if val.class == String
158
+ return Paypal::Api.symbol_to_camel(val) if val.class == Symbol
159
+ return nil
160
+ end
161
+ end
162
+
163
+ class Hash < Parameter
164
+
165
+ end
166
+
167
+ # Optional and Default can take other parameters as input
168
+
169
+ class Optional < Parameter
170
+ def initialize(parameter = nil)
171
+ @parameter = parameter.is_a?(Sequential) ? parameter.clone : parameter
172
+ end
173
+
174
+ def parse(val)
175
+ return parameter_parse(val)
176
+ end
177
+ end
178
+
179
+
180
+ class Default < Parameter
181
+
182
+ def initialize(value, parameter)
183
+ @value = value
184
+ @parameter = parameter.is_a?(Sequential) ? parameter.clone : parameter
185
+ end
186
+
187
+ def parse(val)
188
+ return parameter_parse(val)
189
+ end
190
+
191
+ end
192
+
193
+ end
194
+
195
+ end