spreedly-core-ruby 0.1.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/LICENSE +15 -0
- data/README.md +213 -0
- data/Rakefile +34 -0
- data/lib/spreedly_core.rb +72 -0
- data/lib/spreedly_core/base.rb +95 -0
- data/lib/spreedly_core/gateway.rb +23 -0
- data/lib/spreedly_core/payment_method.rb +118 -0
- data/lib/spreedly_core/test_extensions.rb +62 -0
- data/lib/spreedly_core/test_gateway.rb +32 -0
- data/lib/spreedly_core/transactions.rb +142 -0
- data/lib/spreedly_core/version.rb +4 -0
- data/test/config/spreedly_core.yml.example +3 -0
- data/test/configuration_test.rb +27 -0
- data/test/factories.rb +103 -0
- data/test/spreedly_core_test.rb +204 -0
- data/test/test_factory.rb +99 -0
- data/test/test_helper.rb +33 -0
- data/test/transaction_test.rb +62 -0
- metadata +113 -0
data/LICENSE
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
Copyright (c) 2011 403 Labs, LLC <http://www.403labs.com>
|
2
|
+
|
3
|
+
SpreedlyCore library is free software: you can redistribute it and/or
|
4
|
+
modify it under the terms of the GNU Lesser General Public License as
|
5
|
+
published by the Free Software Foundation, either version 3 of the
|
6
|
+
License, or (at your option) any later version.
|
7
|
+
|
8
|
+
SpreedlyCore library is distributed in the hope that it will be useful,
|
9
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
|
11
|
+
General Public License for more details.
|
12
|
+
|
13
|
+
You should have received a copy of the GNU Lesser General Public License
|
14
|
+
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
15
|
+
|
data/README.md
ADDED
@@ -0,0 +1,213 @@
|
|
1
|
+
SpreedlyCore
|
2
|
+
======
|
3
|
+
|
4
|
+
SpreedlyCore is a Ruby library for accessing the [Spreedly Core API](https://spreedlycore.com/).
|
5
|
+
|
6
|
+
The beauty behind Spreedly Core is that you lower your
|
7
|
+
[PCI Compliance](https://www.pcisecuritystandards.org/) risk
|
8
|
+
by storing credit card information on their service while still having access
|
9
|
+
to make payments and credits. This is possible by having your customers POST their
|
10
|
+
credit card info to the spreedly core service while embedding a transparent
|
11
|
+
redirect URL back to your application. See "Submit payment form" on
|
12
|
+
[the quick start guide](https://spreedlycore.com/manual/quickstart)
|
13
|
+
how the magic happens.
|
14
|
+
|
15
|
+
|
16
|
+
Quickstart
|
17
|
+
----------
|
18
|
+
|
19
|
+
RubyGems:
|
20
|
+
|
21
|
+
gem install spreedly_core
|
22
|
+
irb
|
23
|
+
require 'rubygems'
|
24
|
+
require 'spreedly_core'
|
25
|
+
SpreedlyCore.configure("Your API Login", "Your API Secret", "Test Gateway Token")
|
26
|
+
See the [quickstart guide](https://spreedlycore.com/manual/quickstart) for
|
27
|
+
information regarding tokens.
|
28
|
+
|
29
|
+
We'll now look up the payment method stored on SpreedlyCore using token param
|
30
|
+
from the transparent redirect URL
|
31
|
+
|
32
|
+
payment_token = SpreedlyCore::PaymentMethod.find(payment_token)
|
33
|
+
transaction = payment_token.purchase(100)
|
34
|
+
|
35
|
+
Test Integration
|
36
|
+
----------
|
37
|
+
|
38
|
+
Since your web form handles the creation of payment methods on their service,
|
39
|
+
integration testing can be a bit of a headache. No worries though:
|
40
|
+
|
41
|
+
require 'spreedly_core'
|
42
|
+
require 'spreedly_core/test_extensions'
|
43
|
+
SpreedlyCore.configure("Your API Login", "Your API Secret", "Test Gateway Token")
|
44
|
+
master_card_data = SpreedlyCore::TestHelper.cc_data(:master) # Lookup test credit card data
|
45
|
+
token = SpreedlyCore::PaymentMethod.create_test_token(master_card_data)
|
46
|
+
|
47
|
+
You now have access to a payment method token, which can be used just like your
|
48
|
+
application would use it. Note, you should use a test gateway since you are
|
49
|
+
actually hitting the Spreedly Core service. Let's use the test credit card
|
50
|
+
payment method to make a purchase:
|
51
|
+
|
52
|
+
payment_method = SpreedlyCore::PaymentMethod.find(token)
|
53
|
+
purchase_transaction = payment_method.purchase(100)
|
54
|
+
purchase_transaction.succeeded? # true
|
55
|
+
|
56
|
+
Let's now use a credit card that is configured to fail upon purchase:
|
57
|
+
|
58
|
+
master_card_data = SpreedlyCore::TestHelper.cc_data(:master, :card_type => :failed)
|
59
|
+
token = SpreedlyCore::PaymentMethod.create_test_token(master_card_data)
|
60
|
+
payment_method = SpreedlyCore::PaymentMethod.find(token)
|
61
|
+
purchase_transaction = payment_method.purchase(100)
|
62
|
+
purchase_transaction.succeeded? # false
|
63
|
+
|
64
|
+
Other test cards available include :visa, :american_express, and :discover
|
65
|
+
|
66
|
+
Usage
|
67
|
+
----------
|
68
|
+
|
69
|
+
Using spreedly_core in irb:
|
70
|
+
|
71
|
+
require 'spreedly_core'
|
72
|
+
require 'spreedly_core/test_extensions' # allow creating payment methods from the command line
|
73
|
+
SpreedlyCore.configure("Your API Login", "Your API Secret", "Test Gateway Token")
|
74
|
+
# or if loading from YAML for example, configure takes a hash as well
|
75
|
+
SpreedlyCore.configure(:login => "Your API Login", :secret => "Your API Secret",
|
76
|
+
:token => "Test Gateway Token")
|
77
|
+
master_card_data = SpreedlyCore::TestHelper.cc_data(:master)
|
78
|
+
token = SpreedlyCore::PaymentMethod.create_test_token(master_card_data)
|
79
|
+
|
80
|
+
|
81
|
+
Look up a payment method:
|
82
|
+
|
83
|
+
payment_method = SpreedlyCore::PaymentMethod.find(token)
|
84
|
+
|
85
|
+
Retain a payment method for later use:
|
86
|
+
|
87
|
+
retain_transaction = payment_method.retain
|
88
|
+
retain_transaction.succeeded? # true
|
89
|
+
|
90
|
+
Redact a previously retained payment method:
|
91
|
+
|
92
|
+
redact_transaction = payment_method.redact
|
93
|
+
redact_transaction.succeeded?
|
94
|
+
|
95
|
+
Make a purchase against a payment method:
|
96
|
+
|
97
|
+
purchase_transaction = payment_method.purchase(100)
|
98
|
+
purchase_transaction.succeeded? # true
|
99
|
+
|
100
|
+
Make an authorize request against a payment method, then capture the payment
|
101
|
+
|
102
|
+
authorize = payment_method.authorize(100)
|
103
|
+
authorize.succeeded? # true
|
104
|
+
capture = authorize.capture(50) # Capture only half of the authorized amount
|
105
|
+
capture.succeeded? # true
|
106
|
+
|
107
|
+
authorize = payment_method.authorize(100)
|
108
|
+
authorize.succeeded? # true
|
109
|
+
authorized.capture # Capture the full amount
|
110
|
+
capture.succeeded? # true
|
111
|
+
|
112
|
+
Void a previous purchase:
|
113
|
+
|
114
|
+
purchase_transaction.void # void the purchase
|
115
|
+
|
116
|
+
Credit a previous purchase:
|
117
|
+
|
118
|
+
purchase_transaction = payment_method.purchase(100) # make a purchase
|
119
|
+
purchase_transaction.credit
|
120
|
+
purchase_transaction.succeeded? # true
|
121
|
+
|
122
|
+
Credit part of a previous purchase:
|
123
|
+
|
124
|
+
purchase_transaction = payment_method.purchase(100) # make a purchase
|
125
|
+
purchase_transaction.credit(50) # provide a partial credit
|
126
|
+
purchase_transaction.succeeded? # true
|
127
|
+
|
128
|
+
Handling Exceptions:
|
129
|
+
|
130
|
+
There are 2 types of exceptions which can be raised by the library:
|
131
|
+
|
132
|
+
1. SpreedlyCore::TimeOutError is raised if communication with SpreedlyCore
|
133
|
+
takes longer than 10 seconds
|
134
|
+
2. SpreedlyCore::InvalidResponse is raised when the response code is
|
135
|
+
unexpected (I.E. we expect a HTTP response code of 200 bunt instead got a
|
136
|
+
500) or if the response does not contain an expected attribute. For
|
137
|
+
example, the response from retaining a payment method should contain an XML
|
138
|
+
attribute of "transaction". If this is not found (for example a HTTP
|
139
|
+
response 404 or 500 is returned), then an InvalidResponse is raised.
|
140
|
+
|
141
|
+
|
142
|
+
Both TimeOutError and InvalidResponse subclass SpreedlyCore::Error.
|
143
|
+
|
144
|
+
Look up a payment method that does not exist:
|
145
|
+
|
146
|
+
begin
|
147
|
+
payment_method = SpreedlyCore::PaymentMethod.find("NOT-FOUND")
|
148
|
+
rescue SpreedlyCore::InvalidResponse => e
|
149
|
+
puts e.inspect
|
150
|
+
end
|
151
|
+
|
152
|
+
|
153
|
+
Additional Field Validation
|
154
|
+
----------
|
155
|
+
|
156
|
+
|
157
|
+
The Spreedyly Core API provides validation of the credit card number, CVE, and
|
158
|
+
first and last name. In most cases this is enough, however sometimes you want to
|
159
|
+
enforce the billing information as well. This can be accomplished via:
|
160
|
+
|
161
|
+
require 'spreedly_core'
|
162
|
+
require 'spreedly_core/test_extensions'
|
163
|
+
SpreedlyCore.configure("Your API Login", "Your API Secret", "Test Gateway Token")
|
164
|
+
SpreedlyCore::PaymentMethod.additional_required_cc_fields :address1, :city, :state, :zip
|
165
|
+
master_card_data = SpreedlyCore::TestHelper.cc_data(:master)
|
166
|
+
token = SpreedlyCore::PaymentMethod.create_test_token(master_card_data)
|
167
|
+
payment_method = SpreedlyCore::PaymentMethod.find(token)
|
168
|
+
payment_method.valid? # returns false
|
169
|
+
payment_method.errors # ["Address1 can't be blank", "City can't be blank", "State can't be blank", "Zip can't be blank"]
|
170
|
+
master_card_data = SpreedlyCore::TestHelper.cc_data(:master, :credit_card => {:address1 => "742 Evergreen Terrace", :city => "Springfield", :state => "IL", 62701})
|
171
|
+
payment_method = SpreedlyCore::PaymentMethod.find(token)
|
172
|
+
payment_method.valid? # returns true
|
173
|
+
payment_method.errors # []
|
174
|
+
|
175
|
+
|
176
|
+
Configuring SpreedlyCore with Rails
|
177
|
+
----------
|
178
|
+
|
179
|
+
Inside your Rails project create config/spreedly_core.yml formatted like config/database.yml. For example:
|
180
|
+
|
181
|
+
development:
|
182
|
+
login: <Login Key>
|
183
|
+
secret: <Secret Key>
|
184
|
+
gateway_token: 'JncEWj22g59t3CRB1VnPXmUUgKc' # this is the test gateway, replace with your real gateway in production
|
185
|
+
test:
|
186
|
+
login: <Login Key>
|
187
|
+
secret: <Secret Key>
|
188
|
+
gateway_token: 'JncEWj22g59t3CRB1VnPXmUUgKc' # this is the test gateway, replace with your real gateway in production
|
189
|
+
production:
|
190
|
+
login: <Login Key>
|
191
|
+
secret: <Secret Key>
|
192
|
+
gateway_token: 'JncEWj22g59t3CRB1VnPXmUUgKc' # this is the test gateway, replace with your real gateway in production
|
193
|
+
|
194
|
+
Then create config/initializers/spreedly_core.rb with the following:
|
195
|
+
|
196
|
+
config = YAML.load(File.read(RAILS_ROOT + '/config/spreedly_core.yml'))[RAILS_ENV]
|
197
|
+
SpreedlyCore.configure(config)
|
198
|
+
|
199
|
+
Optionally require additional credit card fields:
|
200
|
+
|
201
|
+
SpreedlyCore::PaymentMethod.additional_required_cc_fields :address1, :city, :state, :zip
|
202
|
+
|
203
|
+
Contributing
|
204
|
+
------------
|
205
|
+
|
206
|
+
Once you've made your commits:
|
207
|
+
|
208
|
+
1. [Fork](http://help.github.com/forking/) SpreedlyCore
|
209
|
+
2. Create a topic branch - `git checkout -b my_branch`
|
210
|
+
3. Push to your branch - `git push origin my_branch`
|
211
|
+
4. Create a [Pull Request](http://help.github.com/pull-requests/) from your branch
|
212
|
+
5. Profit!
|
213
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require 'rake/testtask'
|
3
|
+
|
4
|
+
Rake::TestTask.new(:test) do |test|
|
5
|
+
test.libs << 'lib' << 'test'
|
6
|
+
test.pattern = 'test/**/*_test.rb'
|
7
|
+
test.verbose = true
|
8
|
+
end
|
9
|
+
|
10
|
+
begin
|
11
|
+
require 'rcov/rcovtask'
|
12
|
+
Rcov::RcovTask.new do |test|
|
13
|
+
test.libs << 'test'
|
14
|
+
test.pattern = 'test/**/test_*.rb'
|
15
|
+
test.verbose = true
|
16
|
+
end
|
17
|
+
rescue LoadError
|
18
|
+
task :rcov do
|
19
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
task :default => :test
|
24
|
+
|
25
|
+
require 'rake/rdoctask'
|
26
|
+
Rake::RDocTask.new do |rdoc|
|
27
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
28
|
+
|
29
|
+
rdoc.rdoc_dir = 'rdoc'
|
30
|
+
rdoc.title = "someproject #{version}"
|
31
|
+
rdoc.rdoc_files.include('README*')
|
32
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
33
|
+
end
|
34
|
+
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
require 'httparty'
|
4
|
+
|
5
|
+
require 'spreedly_core/base'
|
6
|
+
require 'spreedly_core/payment_method'
|
7
|
+
require 'spreedly_core/gateway'
|
8
|
+
require 'spreedly_core/test_gateway'
|
9
|
+
require 'spreedly_core/transactions'
|
10
|
+
|
11
|
+
module SpreedlyCore
|
12
|
+
# Hash of user friendly credit card name to SpreedlyCore API name
|
13
|
+
CARD_TYPES = {
|
14
|
+
"Visa" => "visa",
|
15
|
+
"MasterCard" => "master",
|
16
|
+
"American Express" => "american_express",
|
17
|
+
"Discover" => "discover"
|
18
|
+
}
|
19
|
+
|
20
|
+
class Error < RuntimeError; end
|
21
|
+
# Custom exception which occurs when a request to SpreedlyCore times out
|
22
|
+
# See SpreedlyCore::Base.default_timeout
|
23
|
+
class TimeOutError < Error; end
|
24
|
+
class InvalidResponse < Error
|
25
|
+
def initialize(response, message)
|
26
|
+
super("#{message}\nResponse:\n#{response.inspect}")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Configure SpreedlyCore with a particular account.
|
31
|
+
# Strongly prefers environment variables for credentials
|
32
|
+
# and will issue a stern warning should they not be present.
|
33
|
+
# Reluctantly accepts :login and :secret as options
|
34
|
+
def self.configure(options = {})
|
35
|
+
login = ENV['SPREEDLYCORE_API_LOGIN']
|
36
|
+
secret = ENV['SPREEDLYCORE_API_SECRET']
|
37
|
+
|
38
|
+
if options[:api_login]
|
39
|
+
Kernel.warn("ENV and arg both present for api_login. Defaulting to arg value") if login
|
40
|
+
login = options[:api_login]
|
41
|
+
end
|
42
|
+
|
43
|
+
if options[:api_secret]
|
44
|
+
Kernel.warn("ENV and arg both present for api_secret. Defaulting to arg value") if login
|
45
|
+
secret = options[:api_secret]
|
46
|
+
end
|
47
|
+
|
48
|
+
if options[:api_login] || options[:api_secret]
|
49
|
+
Kernel.warn("It is STRONGLY preferred that you house your Spreedly Core credentials only in environment variables.")
|
50
|
+
Kernel.warn("This gem prefers only environment variables named SPREEDLYCORE_API_LOGIN and SPREEDLYCORE_API_SECRET.")
|
51
|
+
end
|
52
|
+
|
53
|
+
if login.nil? || secret.nil?
|
54
|
+
raise ArgumentError.new("You must provide a login and a secret. Gem will look for ENV['SPREEDLYCORE_API_LOGIN'] and ENV['SPREEDLYCORE_API_SECRET'], but you may also pass in a hash with :api_login and :api_secret keys.")
|
55
|
+
end
|
56
|
+
Base.configure(login, secret, options)
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.gateway_token=(gateway_token)
|
60
|
+
Base.gateway_token = gateway_token
|
61
|
+
end
|
62
|
+
|
63
|
+
# returns the configured SpreedlyCore login
|
64
|
+
def self.login; Base.login; end
|
65
|
+
|
66
|
+
# A container for a response from a payment gateway
|
67
|
+
class Response < Base
|
68
|
+
attr_reader(:success, :message, :avs_code, :avs_message, :cvv_code,
|
69
|
+
:cvv_message, :error_code, :error_detail, :created_at,
|
70
|
+
:updated_at)
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module SpreedlyCore
|
2
|
+
# Base class for all SpreedlyCore API requests
|
3
|
+
class Base
|
4
|
+
include HTTParty
|
5
|
+
|
6
|
+
# Net::HTTP::Options is configured to not have a body.
|
7
|
+
# Lets give it the body it's always dreamed of
|
8
|
+
Net::HTTP::Options::RESPONSE_HAS_BODY = true
|
9
|
+
|
10
|
+
format :xml
|
11
|
+
|
12
|
+
# timeout requests after 10 seconds
|
13
|
+
default_timeout 10
|
14
|
+
|
15
|
+
#base_uri "https://spreedlycore.com/#{API_VERSION}"
|
16
|
+
base_uri "http://core.spreedly.dev:11001/#{API_VERSION}"
|
17
|
+
|
18
|
+
def self.configure(login, secret, options = {})
|
19
|
+
@@login = login
|
20
|
+
self.basic_auth(@@login, secret)
|
21
|
+
@@gateway_token = options.delete(:gateway_token)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.login; @@login; end
|
25
|
+
def self.gateway_token; @@gateway_token; end
|
26
|
+
def self.gateway_token=(gateway_token); @@gateway_token = gateway_token; end
|
27
|
+
|
28
|
+
# make a post request to path
|
29
|
+
# If the request succeeds, provide the respones to the &block
|
30
|
+
def self.verify_post(path, options={}, &block)
|
31
|
+
verify_request(:post, path, options, 200, 201, 422, &block)
|
32
|
+
end
|
33
|
+
|
34
|
+
# make a put request to path
|
35
|
+
# If the request succeeds, provide the respones to the &block
|
36
|
+
def self.verify_put(path, options={}, &block)
|
37
|
+
verify_request(:put, path, options, &block)
|
38
|
+
end
|
39
|
+
|
40
|
+
# make a get request to path
|
41
|
+
# If the request succeeds, provide the respones to the &block
|
42
|
+
def self.verify_get(path, options={}, &block)
|
43
|
+
verify_request(:get, path, options, 200, &block)
|
44
|
+
end
|
45
|
+
|
46
|
+
# make an options request to path
|
47
|
+
# If the request succeeds, provide the respones to the &block
|
48
|
+
def self.verify_options(path, options={}, &block)
|
49
|
+
verify_request(:options, path, options, 200, &block)
|
50
|
+
end
|
51
|
+
|
52
|
+
# make a request to path using the HTTP method provided as request_type
|
53
|
+
# *allowed_codes are passed in, verify the response code (200, 404, etc)
|
54
|
+
# is one of the allowed codes.
|
55
|
+
# If *allowed_codes is empty, don't check the response code, but set an instance
|
56
|
+
# variable on the object created in the block containing the response code.
|
57
|
+
def self.verify_request(request_type, path, options, *allowed_codes, &block)
|
58
|
+
begin
|
59
|
+
response = self.send(request_type, path, options)
|
60
|
+
rescue Timeout::Error, Errno::ETIMEDOUT => e
|
61
|
+
raise TimeOutError.new("Request to #{path} timed out. Is Spreedly Core down?")
|
62
|
+
end
|
63
|
+
|
64
|
+
if allowed_codes.any? && !allowed_codes.include?(response.code)
|
65
|
+
raise InvalidResponse.new(response, "Error retrieving #{path}. Got status of #{response.code}. Expected status to be in #{allowed_codes.join(",")}")
|
66
|
+
end
|
67
|
+
|
68
|
+
if options.has_key?(:has_key) &&
|
69
|
+
(response.parsed_response.nil? || !response.parsed_response.has_key?(options[:has_key]))
|
70
|
+
raise InvalidResponse.new(response, "Expected parsed response to contain key '#{options[:has_key]}'")
|
71
|
+
end
|
72
|
+
|
73
|
+
block.call(response).tap do |obj|
|
74
|
+
obj.instance_variable_set("@http_code", response.code)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Given a hash of attrs, assign instance variables using the hash key as the
|
79
|
+
# attribute name and hash value as the attribute value
|
80
|
+
#
|
81
|
+
def initialize(attrs={})
|
82
|
+
attrs.each do |k, v|
|
83
|
+
instance_variable_set("@#{k}", v)
|
84
|
+
end
|
85
|
+
# errors may be nil, empty, a string, or an array of strings.
|
86
|
+
@errors = if @errors.nil? || @errors["error"].blank?
|
87
|
+
[]
|
88
|
+
elsif @errors["error"].is_a?(String)
|
89
|
+
[@errors["error"]]
|
90
|
+
else
|
91
|
+
@errors["error"]
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module SpreedlyCore
|
2
|
+
class Gateway < Base
|
3
|
+
attr_reader(:name, :token, :gateway_type, :auth_modes, :supports_capture,
|
4
|
+
:supports_authorize, :supports_purchase, :supports_void,
|
5
|
+
:supports_credit, :redacted)
|
6
|
+
|
7
|
+
# returns an array of Gateway which are supported
|
8
|
+
def self.supported_gateways
|
9
|
+
verify_options("/gateways.xml") do |response|
|
10
|
+
response.parsed_response["gateways"]["gateway"].map{|h| new(h) }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(attrs={})
|
15
|
+
attrs.merge!(attrs.delete("characteristics") || {})
|
16
|
+
super(attrs)
|
17
|
+
end
|
18
|
+
|
19
|
+
def use!
|
20
|
+
self.class.gateway_token = self.token
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|