epay 0.0.4
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 +15 -0
- data/.travis.yml +5 -0
- data/CHANGELOG.md +13 -0
- data/Gemfile +2 -0
- data/Guardfile +10 -0
- data/MIT-LICENSE +20 -0
- data/README.markdown +21 -0
- data/Rakefile +8 -0
- data/bin/epay-console +8 -0
- data/epay.gemspec +55 -0
- data/lib/epay.rb +104 -0
- data/lib/epay/api.rb +92 -0
- data/lib/epay/api/response.rb +29 -0
- data/lib/epay/card.rb +23 -0
- data/lib/epay/model.rb +23 -0
- data/lib/epay/subscription.rb +132 -0
- data/lib/epay/transaction.rb +171 -0
- data/lib/epay/version.rb +3 -0
- data/lib/extensions/hash.rb +4 -0
- data/spec/api/response_spec.rb +71 -0
- data/spec/api_spec.rb +58 -0
- data/spec/card_spec.rb +21 -0
- data/spec/fixtures/vcr_cassettes/existing_subscription.yml +75 -0
- data/spec/fixtures/vcr_cassettes/existing_transaction.yml +47 -0
- data/spec/fixtures/vcr_cassettes/non_existing_transaction.yml +48 -0
- data/spec/fixtures/vcr_cassettes/subscription_authorization.yml +95 -0
- data/spec/fixtures/vcr_cassettes/subscription_creation.yml +101 -0
- data/spec/fixtures/vcr_cassettes/subscription_invalid_creation.yml +47 -0
- data/spec/fixtures/vcr_cassettes/subscriptions.yml +87 -0
- data/spec/fixtures/vcr_cassettes/transaction_creation.yml +90 -0
- data/spec/fixtures/vcr_cassettes/transaction_invalid_creation.yml +47 -0
- data/spec/fixtures/vcr_cassettes/transactions.yml +50 -0
- data/spec/helpers/http_responses.rb +13 -0
- data/spec/model_spec.rb +36 -0
- data/spec/spec_helper.rb +20 -0
- data/spec/subscription_spec.rb +139 -0
- data/spec/transaction_spec.rb +245 -0
- metadata +280 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# Changelog: epay
|
2
|
+
|
3
|
+
## 0.0.4
|
4
|
+
- Ruby 1.9.7 and REE compatibility
|
5
|
+
|
6
|
+
## 0.0.3
|
7
|
+
- Add methods for permanent and temporary error
|
8
|
+
|
9
|
+
## 0.0.2
|
10
|
+
- Bugfix: Subscriptions now properly returns failed transaction, if unable to authorize.
|
11
|
+
|
12
|
+
## 0.0.1
|
13
|
+
- Initial release
|
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
guard 'rspec', :version => 2 do
|
5
|
+
watch(%r{^spec/.+_spec\.rb$})
|
6
|
+
watch(%r{^lib/.*([^\/]+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
7
|
+
|
8
|
+
watch('spec/spec_helper.rb') { "spec" }
|
9
|
+
end
|
10
|
+
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2012 Mattias Pfeiffer, Netdate.dk ApS
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
ePay
|
2
|
+
=========
|
3
|
+
This gem is extracted from Netdate.dk, and gives you a comfortable and easy way of communicating with the ePay API.
|
4
|
+
|
5
|
+
Installation
|
6
|
+
---------
|
7
|
+
Installation is pretty straight-forward. Just install the gem:
|
8
|
+
|
9
|
+
$ gem install epay
|
10
|
+
|
11
|
+
Usage
|
12
|
+
---------
|
13
|
+
See http://pfeiffer.github.com/epay/
|
14
|
+
|
15
|
+
License
|
16
|
+
---------
|
17
|
+
Released under MIT-license.
|
18
|
+
|
19
|
+
-----
|
20
|
+
|
21
|
+
[Mattias Pfeiffer](http://pfeiffer.dk/) - [Netdate.dk](http://netdate.dk/)
|
data/Rakefile
ADDED
data/bin/epay-console
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb'
|
3
|
+
|
4
|
+
libs = " -r irb/completion"
|
5
|
+
libs << " -r #{File.dirname(__FILE__) + '/../lib/epay'}"
|
6
|
+
load_paths = " -I #{File.dirname(__FILE__) + '/../lib'}"
|
7
|
+
puts "Loading epay gem"
|
8
|
+
exec "#{irb} #{libs} #{load_paths} --simple-prompt"
|
data/epay.gemspec
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "epay/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "epay"
|
7
|
+
s.version = Epay::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.summary = "Ruby client for ePay API"
|
10
|
+
s.homepage = "http://github.com/netdate/epay"
|
11
|
+
s.authors = [ 'Mattias Pfeiffer' ]
|
12
|
+
s.email = 'mattias@netdate.dk'
|
13
|
+
|
14
|
+
s.files = `git ls-files`.split("\n")
|
15
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
16
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
17
|
+
|
18
|
+
s.require_paths = ["lib"]
|
19
|
+
|
20
|
+
s.extra_rdoc_files = [ "README.markdown" ]
|
21
|
+
s.rdoc_options = [ "--charset=UTF-8" ]
|
22
|
+
|
23
|
+
s.required_rubygems_version = ">= 1.3.6"
|
24
|
+
|
25
|
+
# = Library dependencies
|
26
|
+
#
|
27
|
+
s.add_dependency "rest-client", "~> 1.6.0"
|
28
|
+
s.add_dependency "activesupport", "~> 3"
|
29
|
+
s.add_dependency "builder", "~> 2.1.2"
|
30
|
+
|
31
|
+
# = Development dependencies
|
32
|
+
#
|
33
|
+
s.add_development_dependency "bundler", "~> 1.0"
|
34
|
+
s.add_development_dependency "yajl-ruby", "~> 0.8.0"
|
35
|
+
s.add_development_dependency "shoulda"
|
36
|
+
s.add_development_dependency "rspec"
|
37
|
+
s.add_development_dependency "webmock", "~>1.7"
|
38
|
+
s.add_development_dependency "nokogiri"
|
39
|
+
s.add_development_dependency "vcr", "~> 2.0.0.rc1"
|
40
|
+
s.add_development_dependency "rake"
|
41
|
+
|
42
|
+
# These gems are not needed for CI
|
43
|
+
#
|
44
|
+
unless ENV["CI"]
|
45
|
+
s.add_development_dependency "guard"
|
46
|
+
s.add_development_dependency "guard-rspec"
|
47
|
+
s.add_development_dependency "rb-fsevent"
|
48
|
+
s.add_development_dependency "rdoc"
|
49
|
+
s.add_development_dependency "turn", "~> 0.9"
|
50
|
+
end
|
51
|
+
|
52
|
+
s.description = <<-DESC
|
53
|
+
Coming soon..
|
54
|
+
DESC
|
55
|
+
end
|
data/lib/epay.rb
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'rest_client'
|
2
|
+
require 'active_support/core_ext'
|
3
|
+
require 'builder'
|
4
|
+
|
5
|
+
require 'extensions/hash'
|
6
|
+
|
7
|
+
require 'epay/version'
|
8
|
+
|
9
|
+
require 'epay/api'
|
10
|
+
require 'epay/api/response'
|
11
|
+
|
12
|
+
require 'epay/model'
|
13
|
+
require 'epay/card'
|
14
|
+
require 'epay/subscription'
|
15
|
+
require 'epay/transaction'
|
16
|
+
|
17
|
+
module Epay
|
18
|
+
API_HOST = 'ssl.ditonlinebetalingssystem.dk'
|
19
|
+
PAYMENT_SOAP_URL = 'https://' + API_HOST + '/remote/payment'
|
20
|
+
SUBSCRIPTION_SOAP_URL = 'https://' + API_HOST + '/remote/subscription'
|
21
|
+
AUTHORIZE_URL = 'https://' + API_HOST + '/auth/default.aspx'
|
22
|
+
|
23
|
+
CURRENCY_CODES = {
|
24
|
+
:ADP => '020', :AED => '784', :AFA => '004', :ALL => '008', :AMD => '051',
|
25
|
+
:ANG => '532', :AOA => '973', :ARS => '032', :AUD => '036', :AWG => '533',
|
26
|
+
:AZM => '031', :BAM => '977', :BBD => '052', :BDT => '050', :BGL => '100',
|
27
|
+
:BGN => '975', :BHD => '048', :BIF => '108', :BMD => '060', :BND => '096',
|
28
|
+
:BOB => '068', :BOV => '984', :BRL => '986', :BSD => '044', :BTN => '064',
|
29
|
+
:BWP => '072', :BYR => '974', :BZD => '084', :CAD => '124', :CDF => '976',
|
30
|
+
:CHF => '756', :CLF => '990', :CLP => '152', :CNY => '156', :COP => '170',
|
31
|
+
:CRC => '188', :CUP => '192', :CVE => '132', :CYP => '196', :CZK => '203',
|
32
|
+
:DJF => '262', :DKK => '208', :DOP => '214', :DZD => '012', :ECS => '218',
|
33
|
+
:ECV => '983', :EEK => '233', :EGP => '818', :ERN => '232', :ETB => '230',
|
34
|
+
:EUR => '978', :FJD => '242', :FKP => '238', :GBP => '826', :GEL => '981',
|
35
|
+
:GHC => '288', :GIP => '292', :GMD => '270', :GNF => '324', :GTQ => '320',
|
36
|
+
:GWP => '624', :GYD => '328', :HKD => '344', :HNL => '340', :HRK => '191',
|
37
|
+
:HTG => '332', :HUF => '348', :IDR => '360', :ILS => '376', :INR => '356',
|
38
|
+
:IQD => '368', :IRR => '364', :ISK => '352', :JMD => '388', :JOD => '400',
|
39
|
+
:JPY => '392', :KES => '404', :KGS => '417', :KHR => '116', :KMF => '174',
|
40
|
+
:KPW => '408', :KRW => '410', :KWD => '414', :KYD => '136', :KZT => '398',
|
41
|
+
:LAK => '418', :LBP => '422', :LKR => '144', :LRD => '430', :LSL => '426',
|
42
|
+
:LTL => '440', :LVL => '428', :LYD => '434', :MAD => '504', :MDL => '498',
|
43
|
+
:MGF => '450', :MKD => '807', :MMK => '104', :MNT => '496', :MOP => '446',
|
44
|
+
:MRO => '478', :MTL => '470', :MUR => '480', :MVR => '462', :MWK => '454',
|
45
|
+
:MXN => '484', :MXV => '979', :MYR => '458', :MZM => '508', :NAD => '516',
|
46
|
+
:NGN => '566', :NIO => '558', :NOK => '578', :NPR => '524', :NZD => '554',
|
47
|
+
:OMR => '512', :PAB => '590', :PEN => '604', :PGK => '598', :PHP => '608',
|
48
|
+
:PKR => '586', :PLN => '985', :PYG => '600', :QAR => '634', :ROL => '642',
|
49
|
+
:RUB => '643', :RUR => '810', :RWF => '646', :SAR => '682', :SBD => '090',
|
50
|
+
:SCR => '690', :SDD => '736', :SEK => '752', :SGD => '702', :SHP => '654',
|
51
|
+
:SIT => '705', :SKK => '703', :SLL => '694', :SOS => '706', :SRG => '740',
|
52
|
+
:STD => '678', :SVC => '222', :SYP => '760', :SZL => '748', :THB => '764',
|
53
|
+
:TJS => '972', :TMM => '795', :TND => '788', :TOP => '776', :TPE => '626',
|
54
|
+
:TRL => '792', :TRY => '949', :TTD => '780', :TWD => '901', :TZS => '834',
|
55
|
+
:UAH => '980', :UGX => '800', :USD => '840', :UYU => '858', :UZS => '860',
|
56
|
+
:VEB => '862', :VND => '704', :VUV => '548', :XAF => '950', :XCD => '951',
|
57
|
+
:XOF => '952', :XPF => '953', :YER => '886', :YUM => '891', :ZAR => '710',
|
58
|
+
:ZMK => '894', :ZWD => '716'
|
59
|
+
}
|
60
|
+
|
61
|
+
CARD_KINDS = {
|
62
|
+
1 => :dankort,
|
63
|
+
2 => :visa_dankort,
|
64
|
+
3 => :visa_electron_foreign,
|
65
|
+
4 => :mastercard,
|
66
|
+
5 => :mastercard_foreign,
|
67
|
+
6 => :visa_electron,
|
68
|
+
7 => :jcb,
|
69
|
+
8 => :diners,
|
70
|
+
9 => :maestro,
|
71
|
+
10 => :american_express,
|
72
|
+
11 => :unknown,
|
73
|
+
12 => :edk,
|
74
|
+
13 => :diners_foreign,
|
75
|
+
14 => :american_express_foreign,
|
76
|
+
15 => :maestro_foreign,
|
77
|
+
16 => :forbrugsforeningen,
|
78
|
+
17 => :ewire,
|
79
|
+
18 => :visa,
|
80
|
+
19 => :ikano,
|
81
|
+
21 => :nordea_solo,
|
82
|
+
22 => :danske_bank,
|
83
|
+
23 => :bg_bank,
|
84
|
+
24 => :lic_mastercard,
|
85
|
+
25 => :lic_mastercard_foreign,
|
86
|
+
26 => :paypal,
|
87
|
+
27 => :mobilpenge
|
88
|
+
}
|
89
|
+
|
90
|
+
TEMPORARY_ERROR_CODES = ['-5511', '100', '102', '116', '121', '255', '256', '906',
|
91
|
+
'907', '910', '911', '912', '915', '920', '921', '923',
|
92
|
+
'945', '946', '-1000', '-1005', '-23', '-3', '-4']
|
93
|
+
|
94
|
+
mattr_accessor :merchant_number, :default_currency, :password
|
95
|
+
|
96
|
+
class ApiError < StandardError; end
|
97
|
+
class TransactionAlreadyCaptured < StandardError; end
|
98
|
+
class TransactionNotFound < StandardError; end
|
99
|
+
class AuthorizationNotFound < StandardError; end
|
100
|
+
class TransactionInGracePeriod < StandardError; end
|
101
|
+
class SubscriptionNotFound < StandardError; end
|
102
|
+
class InvalidMerchantNumber < StandardError; end
|
103
|
+
end
|
104
|
+
|
data/lib/epay/api.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
module Epay
|
2
|
+
module Api
|
3
|
+
class << self
|
4
|
+
def authorize(post)
|
5
|
+
# Authorize transaction:
|
6
|
+
RestClient.post AUTHORIZE_URL, post do |response, request, result|
|
7
|
+
# The authorization request redirects to either accept or decline url:
|
8
|
+
if location = response.headers[:location]
|
9
|
+
query = CGI::parse(URI.parse(location.gsub(' ', '%20')).query)
|
10
|
+
|
11
|
+
Hash[query.map do |k, v|
|
12
|
+
[k, v.is_a?(Array) && v.size == 1 ? v[0] : v] # make values like ['v'] into 'v'
|
13
|
+
end]
|
14
|
+
else
|
15
|
+
# No location header found
|
16
|
+
raise ApiError, response
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def default_post_for_params(params)
|
22
|
+
{
|
23
|
+
:merchantnumber => Epay.merchant_number,
|
24
|
+
|
25
|
+
:cardno => params[:card_no],
|
26
|
+
:cvc => params[:cvc],
|
27
|
+
:expmonth => params[:exp_month],
|
28
|
+
:expyear => params[:exp_year],
|
29
|
+
|
30
|
+
:amount => (params[:amount] * 100).to_i,
|
31
|
+
:currency => Epay::CURRENCY_CODES[(params[:currency] || Epay.default_currency).to_sym],
|
32
|
+
:orderid => params[:order_no],
|
33
|
+
|
34
|
+
:accepturl => AUTHORIZE_URL + "?accept=1",
|
35
|
+
:declineurl => AUTHORIZE_URL + "?decline=1",
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
def handle_failed_response(response)
|
40
|
+
case response.data['epayresponse']
|
41
|
+
when "-1002" then raise InvalidMerchantNumber
|
42
|
+
when "-1008" then raise TransactionNotFound
|
43
|
+
else raise ApiError, response.data['epayresponse']
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def request(url, action, params = {}, &block)
|
48
|
+
service_url = "#{url}.asmx"
|
49
|
+
soap_action = url + '/' + action
|
50
|
+
|
51
|
+
params[:merchantnumber] ||= Epay.merchant_number
|
52
|
+
params[:pwd] = Epay.password if Epay.password.present?
|
53
|
+
|
54
|
+
headers = {
|
55
|
+
'Content-Type' => 'text/xml; charset=utf-8',
|
56
|
+
'SOAPAction' => soap_action,
|
57
|
+
'User-Agent' => "Ruby / epay (#{VERSION})"
|
58
|
+
}
|
59
|
+
|
60
|
+
# Setup the SOAP body:
|
61
|
+
xml = Builder::XmlMarkup.new(:indent => 2)
|
62
|
+
xml.instruct!
|
63
|
+
xml.tag! 'soap:Envelope', { 'xmlns:xsi' => 'http://schemas.xmlsoap.org/soap/envelope/',
|
64
|
+
'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema',
|
65
|
+
'xmlns:soap' => 'http://schemas.xmlsoap.org/soap/envelope/' } do
|
66
|
+
|
67
|
+
xml.tag! 'soap:Body' do
|
68
|
+
xml.tag! action, { 'xmlns' => url } do
|
69
|
+
params.each do |attribute, value|
|
70
|
+
xml.tag! attribute, value if value.present?
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
RestClient.post service_url, xml.target!, headers do |raw_response, request, result|
|
77
|
+
response = Response.new(raw_response, action)
|
78
|
+
|
79
|
+
if block_given?
|
80
|
+
yield response
|
81
|
+
else
|
82
|
+
if response.success?
|
83
|
+
return response
|
84
|
+
else
|
85
|
+
handle_failed_response(response)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Epay
|
2
|
+
module Api
|
3
|
+
class Response
|
4
|
+
attr_accessor :raw_response, :action
|
5
|
+
|
6
|
+
def initialize(raw_response, action)
|
7
|
+
@raw_response = raw_response
|
8
|
+
@action = action
|
9
|
+
end
|
10
|
+
|
11
|
+
def success?
|
12
|
+
code == 200 && data["#{action}Result"] == "true"
|
13
|
+
end
|
14
|
+
|
15
|
+
def data
|
16
|
+
if headers[:content_type] =~ %r(text/xml) && code == 200
|
17
|
+
# Remove envelope and XML namespace objects
|
18
|
+
Hash.from_xml(raw_response.to_s).first.last["Body"]["#{action}Response"].reject { |k,v| k.match(/xmlns/) }
|
19
|
+
else
|
20
|
+
raw_response.to_s
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def method_missing(method, *args)
|
25
|
+
raw_response.send(method, *args)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/epay/card.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
module Epay
|
2
|
+
class Card
|
3
|
+
attr_accessor :exp_month, :exp_year, :kind, :number
|
4
|
+
|
5
|
+
def initialize(attributes = {})
|
6
|
+
attributes.each do |name, value|
|
7
|
+
self.send("#{name}=", value) if respond_to?("#{name}=")
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def expires_at
|
12
|
+
Date.new(2000 + exp_year, exp_month, 1).end_of_month
|
13
|
+
end
|
14
|
+
|
15
|
+
def hash
|
16
|
+
[number, exp_year, exp_month].join("") if number.present?
|
17
|
+
end
|
18
|
+
|
19
|
+
def last_digits
|
20
|
+
number[-4, 4] if number.present?
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/epay/model.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
module Epay
|
2
|
+
module Model
|
3
|
+
attr_accessor :id, :data
|
4
|
+
|
5
|
+
def initialize(id, data = {})
|
6
|
+
@id = id
|
7
|
+
@data = data
|
8
|
+
end
|
9
|
+
|
10
|
+
def inspect
|
11
|
+
inspection = self.class.inspectable_attributes.collect do |name|
|
12
|
+
#"#{name}: #{selfsend(name)}"
|
13
|
+
"#{name}: #{send(name).inspect}" if respond_to?(name)
|
14
|
+
end.join(", ")
|
15
|
+
|
16
|
+
"#<#{self.class} #{inspection}>"
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.inspect
|
20
|
+
"inspect via class"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|