sipwizard 0.0.1 → 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/.gitignore +1 -0
- data/Gemfile +9 -3
- data/Guardfile +6 -5
- data/lib/sipwizard.rb +3 -0
- data/lib/sipwizard/account.rb +101 -0
- data/lib/sipwizard/binding.rb +40 -0
- data/lib/sipwizard/cdr.rb +59 -0
- data/lib/sipwizard/connection.rb +31 -7
- data/lib/sipwizard/customer.rb +81 -0
- data/lib/sipwizard/dial_plan.rb +91 -0
- data/lib/sipwizard/provider.rb +99 -0
- data/lib/sipwizard/provider_binding.rb +53 -0
- data/lib/sipwizard/rate.rb +81 -0
- data/lib/sipwizard/relation.rb +27 -0
- data/lib/sipwizard/version.rb +1 -1
- data/sipwizard.gemspec +6 -4
- data/spec/lib/sipwizard/account_spec.rb +80 -0
- data/spec/lib/sipwizard/binding_spec.rb +22 -0
- data/spec/lib/sipwizard/cdr_spec.rb +21 -0
- data/spec/lib/sipwizard/configuration_spec.rb +1 -1
- data/spec/lib/sipwizard/connection_spec.rb +19 -25
- data/spec/lib/sipwizard/customer_spec.rb +80 -0
- data/spec/lib/sipwizard/dial_plan_spec.rb +95 -0
- data/spec/lib/sipwizard/provider_binding_spec.rb +22 -0
- data/spec/lib/sipwizard/provider_spec.rb +107 -0
- data/spec/lib/sipwizard/rate_spec.rb +83 -0
- data/spec/lib/sipwizard_spec.rb +1 -1
- data/spec/spec.yml.sample +7 -0
- data/spec/spec_helper.rb +32 -9
- data/spec/vcr/sipsorcery/cdr/count.yml +70 -0
- data/spec/vcr/sipsorcery/cdr/get.yml +38 -0
- data/spec/vcr/sipsorcery/customeraccount/add.yml +73 -0
- data/spec/vcr/sipsorcery/customeraccount/count.yml +36 -0
- data/spec/vcr/sipsorcery/customeraccount/delete.yml +36 -0
- data/spec/vcr/sipsorcery/customeraccount/get.yml +36 -0
- data/spec/vcr/sipsorcery/customeraccount/update.yml +38 -0
- data/spec/vcr/sipsorcery/dialplan/add.yml +75 -0
- data/spec/vcr/sipsorcery/dialplan/copy.yml +36 -0
- data/spec/vcr/sipsorcery/dialplan/count.yml +36 -0
- data/spec/vcr/sipsorcery/dialplan/delete.yml +36 -0
- data/spec/vcr/sipsorcery/dialplan/get.yml +37 -0
- data/spec/vcr/sipsorcery/dialplan/update.yml +39 -0
- data/spec/vcr/sipsorcery/rate/add.yml +73 -0
- data/spec/vcr/sipsorcery/rate/count.yml +36 -0
- data/spec/vcr/sipsorcery/rate/delete.yml +36 -0
- data/spec/vcr/sipsorcery/rate/get.yml +36 -0
- data/spec/vcr/sipsorcery/rate/update.yml +38 -0
- data/spec/vcr/sipsorcery/sipaccount/add.yml +74 -0
- data/spec/vcr/sipsorcery/sipaccount/count.yml +36 -0
- data/spec/vcr/sipsorcery/sipaccount/delete.yml +69 -0
- data/spec/vcr/sipsorcery/sipaccount/get.yml +36 -0
- data/spec/vcr/sipsorcery/sipaccount/update.yml +37 -0
- data/spec/vcr/sipsorcery/sipaccountbinding/count.yml +36 -0
- data/spec/vcr/sipsorcery/sipaccountbinding/get.yml +36 -0
- data/spec/vcr/sipsorcery/sipprovider/add.yml +74 -0
- data/spec/vcr/sipsorcery/sipprovider/count.yml +36 -0
- data/spec/vcr/sipsorcery/sipprovider/delete.yml +36 -0
- data/spec/vcr/sipsorcery/sipprovider/get.yml +36 -0
- data/spec/vcr/sipsorcery/sipprovider/update.yml +38 -0
- data/spec/vcr/sipsorcery/sipproviderbinding/count.yml +36 -0
- data/spec/vcr/sipsorcery/sipproviderbinding/get.yml +69 -0
- metadata +129 -6
@@ -0,0 +1,99 @@
|
|
1
|
+
module Sipwizard
|
2
|
+
class Provider < Hashie::Trash
|
3
|
+
API_PATH_MAP = {
|
4
|
+
count: 'sipprovider/count',
|
5
|
+
find: 'sipprovider/get',
|
6
|
+
create: 'sipprovider/add',
|
7
|
+
update: 'sipprovider/update',
|
8
|
+
delete: 'sipprovider/delete'
|
9
|
+
}
|
10
|
+
|
11
|
+
string_to_bool = ->(string) { string == "true" }
|
12
|
+
|
13
|
+
property :id, from: :ID
|
14
|
+
property :provider_name, from: :ProviderName
|
15
|
+
property :provider_username, from: :ProviderUsername
|
16
|
+
property :provider_password, from: :ProviderPassword
|
17
|
+
property :provider_server, from: :ProviderServer
|
18
|
+
property :provider_auth_username, from: :ProviderAuthUsername
|
19
|
+
property :provider_outbound_proxy, from: :ProviderOutboundProxy
|
20
|
+
property :provider_type, from: :ProviderType
|
21
|
+
property :provider_from, from: :ProviderFrom
|
22
|
+
property :custom_headers, from: :CustomHeaders
|
23
|
+
property :register_contact, from: :RegisterContact
|
24
|
+
property :register_expiry, from: :RegisterExpiry
|
25
|
+
property :register_server, from: :RegisterServer
|
26
|
+
property :register_realm, from: :RegisterRealm
|
27
|
+
property :register_enabled, from: :RegisterEnabled, transform_with: ->(b) { string_to_bool.call(b) }
|
28
|
+
property :gv_callback_number, from: :GVCallbackNumber
|
29
|
+
property :gv_callback_pattern, from: :GVCallbackPattern
|
30
|
+
property :gv_callback_type, from: :GVCallbackType
|
31
|
+
|
32
|
+
alias :register_enabled? :register_enabled
|
33
|
+
|
34
|
+
def self.count(params={})
|
35
|
+
response = Connection.new.get(API_PATH_MAP[:count], params)
|
36
|
+
|
37
|
+
response['Success'] ? response['Result'] : -1
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.where(params)
|
41
|
+
Relation.new.where(params)
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.find(id)
|
45
|
+
relation = self.where({ ID: id }).count(1)
|
46
|
+
|
47
|
+
result = Connection.new.get(API_PATH_MAP[:find], relation.relation)
|
48
|
+
|
49
|
+
return nil unless result['Success']
|
50
|
+
|
51
|
+
self.new(result['Result'][0])
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.build_for_request(h)
|
55
|
+
provider = self.new(h)
|
56
|
+
provider = Hash[provider.map{ |k,v| ["#{k}".camelize, v] }]
|
57
|
+
provider['ID'] = provider.delete('Id')
|
58
|
+
provider['GVCallbackType'] = provider.delete('GvCallbackType')
|
59
|
+
provider['GVCallbackNumber'] = provider.delete('GvCallbackType')
|
60
|
+
provider['GVCallbackPattern'] = provider.delete('GvCallbackPattern')
|
61
|
+
|
62
|
+
provider.delete_if{ |_,v| v.nil? } #delete all the keys for which we dont have value
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.create(params)
|
66
|
+
payload = self.build_for_request(params)
|
67
|
+
result = Connection.new.post(API_PATH_MAP[:create], payload)
|
68
|
+
|
69
|
+
raise ArgumentError.new(result["Error"]) unless result['Success']
|
70
|
+
|
71
|
+
result['Result'] #ID
|
72
|
+
end
|
73
|
+
|
74
|
+
def save
|
75
|
+
payload = Provider.build_for_request(self.to_hash)
|
76
|
+
result = Connection.new.post(API_PATH_MAP[:update], payload)
|
77
|
+
raise ArgumentError.new(result["Error"]) unless result['Success']
|
78
|
+
|
79
|
+
result['Result'] #ID
|
80
|
+
end
|
81
|
+
|
82
|
+
def binding(cache=true)
|
83
|
+
return @binding if @binding && cache
|
84
|
+
@binding = ProviderBinding.find_by_provider_id(self.id)
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.delete(id)
|
88
|
+
result = Connection.new.get(API_PATH_MAP[:delete], {id: id})
|
89
|
+
|
90
|
+
raise ArgumentError.new(result["Error"]) unless result['Success']
|
91
|
+
|
92
|
+
result['Result'] #true | false
|
93
|
+
end
|
94
|
+
|
95
|
+
def delete
|
96
|
+
Provider.delete(self.id)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Sipwizard
|
2
|
+
class ProviderBinding < Hashie::Trash
|
3
|
+
API_PATH_MAP={
|
4
|
+
count: 'sipproviderbinding/count',
|
5
|
+
find: 'sipproviderbinding/get'
|
6
|
+
}
|
7
|
+
|
8
|
+
string_to_bool = ->(string) { string == "true" }
|
9
|
+
|
10
|
+
property :id, from: :ID
|
11
|
+
property :provider_id, from: :ProviderID
|
12
|
+
property :provider_name, from: :ProviderName
|
13
|
+
property :registration_failur_message, from: :RegistrationFailureMessage
|
14
|
+
property :last_register_time, from: :LastRegisterTime
|
15
|
+
property :next_registration_time, from: :NextRegistrationTime
|
16
|
+
property :last_register_attempt, from: :LastRegisterAttempt
|
17
|
+
property :is_registered, from: :IsRegistered, transform_with: ->(b) { string_to_bool.call(b) }
|
18
|
+
property :binding_expiry, from: :BindingExpiry
|
19
|
+
property :binding_uri, from: :BindingURI
|
20
|
+
property :registrar_sip_socket, from: :RegistrarSIPSocket
|
21
|
+
property :cseq, from: :CSeq
|
22
|
+
|
23
|
+
def self.count(params={})
|
24
|
+
response = Connection.new.get(API_PATH_MAP[:count], params)
|
25
|
+
|
26
|
+
response['Success'] ? response['Result'] : -1
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.where(params)
|
30
|
+
Relation.new.where(params)
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.find(id)
|
34
|
+
relation = self.where({ ID: id }).count(1)
|
35
|
+
|
36
|
+
result = Connection.new.get(API_PATH_MAP[:find], relation.relation)
|
37
|
+
|
38
|
+
return nil unless result['Success']
|
39
|
+
|
40
|
+
self.new(result['Result'][0])
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.find_by_provider_id(id)
|
44
|
+
relation = self.where({ ProviderID: id }).count(1)
|
45
|
+
|
46
|
+
result = Connection.new.get(API_PATH_MAP[:find], relation.relation)
|
47
|
+
|
48
|
+
return nil unless result['Success']
|
49
|
+
|
50
|
+
self.new(result['Result'][0])
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Sipwizard
|
2
|
+
class Rate < Hashie::Trash
|
3
|
+
API_PATH_MAP={
|
4
|
+
count: 'rate/count',
|
5
|
+
create: 'rate/add',
|
6
|
+
find: 'rate/get',
|
7
|
+
update: 'rate/update',
|
8
|
+
delete: 'rate/delete'
|
9
|
+
}
|
10
|
+
|
11
|
+
property :id, from: :ID
|
12
|
+
property :description, from: :Description
|
13
|
+
property :prefix, from: :Prefix
|
14
|
+
property :rate, from: :Rate
|
15
|
+
property :rate_code, from: :RateCode
|
16
|
+
property :setup_cost, from: :SetupCost
|
17
|
+
property :inserted, from: :Inserted
|
18
|
+
property :increment_seconds, from: :IncrementSeconds
|
19
|
+
|
20
|
+
def self.count(params={})
|
21
|
+
response = connection.get(API_PATH_MAP[:count], params)
|
22
|
+
|
23
|
+
response['Success'] ? response['Result'] : -1
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.build_for_request(h)
|
27
|
+
rate = self.new(h)
|
28
|
+
rate = Hash[rate.map{ |k,v| ["#{k}".camelize, v] }]
|
29
|
+
rate['ID'] = rate.delete('Id')
|
30
|
+
|
31
|
+
rate.delete_if{ |_,v| v.nil? } #delete all the keys for which we dont have value
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.create(params)
|
35
|
+
payload = self.build_for_request(params)
|
36
|
+
result = connection.post(API_PATH_MAP[:create], payload)
|
37
|
+
|
38
|
+
raise ArgumentError.new(result["Error"]) unless result['Success']
|
39
|
+
|
40
|
+
result['Result'] #ID
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.where(params)
|
44
|
+
Relation.new.where(params)
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.find(id)
|
48
|
+
relation = self.where({ ID: id }).count(1)
|
49
|
+
result = connection.get(API_PATH_MAP[:find], relation.relation)
|
50
|
+
|
51
|
+
return nil unless result['Success']
|
52
|
+
|
53
|
+
self.new(result['Result'][0])
|
54
|
+
end
|
55
|
+
|
56
|
+
def save
|
57
|
+
payload = Rate.build_for_request(self.to_hash)
|
58
|
+
result = Connection.new(api_type: :accounting).post(API_PATH_MAP[:update], payload)
|
59
|
+
raise ArgumentError.new(result["Error"]) unless result['Success']
|
60
|
+
|
61
|
+
result['Result'] #ID
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.delete(id)
|
65
|
+
result = connection.get(API_PATH_MAP[:delete], {id: id})
|
66
|
+
|
67
|
+
raise ArgumentError.new(result["Error"]) unless result['Success']
|
68
|
+
|
69
|
+
result['Result'] #true | false
|
70
|
+
end
|
71
|
+
|
72
|
+
def delete
|
73
|
+
Rate.delete(self.id)
|
74
|
+
end
|
75
|
+
private
|
76
|
+
|
77
|
+
def self.connection
|
78
|
+
Connection.new(api_type: :accounting)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Sipwizard
|
2
|
+
class Relation
|
3
|
+
attr_reader :relation
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@relation = Hashie::Clash.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def where(params)
|
10
|
+
@relation.where( hash_to_query(params) )
|
11
|
+
self
|
12
|
+
end
|
13
|
+
|
14
|
+
def count(nb)
|
15
|
+
@relation.merge!({count: nb})
|
16
|
+
self
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
#Hack to comply with the api spec ... which sucks
|
22
|
+
def hash_to_query(h)
|
23
|
+
h = Hash[h.map{|k,v| [k, "\"#{v}\""]}]
|
24
|
+
Rack::Utils.unescape Rack::Utils.build_query(h)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/sipwizard/version.rb
CHANGED
data/sipwizard.gemspec
CHANGED
@@ -19,8 +19,10 @@ Gem::Specification.new do |spec|
|
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
|
21
21
|
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
-
spec.
|
23
|
-
spec.
|
24
|
-
spec.
|
25
|
-
spec.
|
22
|
+
spec.add_dependency "rake"
|
23
|
+
spec.add_dependency 'rack', "~> 1.5.2"
|
24
|
+
spec.add_dependency "faraday", '~> 0.8.9'
|
25
|
+
spec.add_dependency "faraday_middleware", "~> 0.9.0"
|
26
|
+
spec.add_dependency "hashie", "~> 2.0.5"
|
27
|
+
spec.add_dependency "activesupport"
|
26
28
|
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Sipwizard::Account do
|
4
|
+
describe '.count(params={})' do
|
5
|
+
subject{ described_class.count }
|
6
|
+
it 'returns a result' do
|
7
|
+
expect(subject).to be_instance_of Fixnum
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
describe '.find(id)' do
|
12
|
+
let(:id){ settings['sensitive_data']['ID'] }
|
13
|
+
|
14
|
+
subject{ described_class.find(id) }
|
15
|
+
|
16
|
+
it 'returns an account' do
|
17
|
+
subject.should be_instance_of Sipwizard::Account
|
18
|
+
subject.id.should eq id
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe '.create(params)' do
|
23
|
+
let(:params) do
|
24
|
+
{
|
25
|
+
username: "foo",
|
26
|
+
password: "bar"
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
subject{ described_class.create(params) }
|
31
|
+
|
32
|
+
it 'creates a new account' do
|
33
|
+
response = subject
|
34
|
+
expect(response).not_to be_nil
|
35
|
+
expect(response).to be_instance_of String
|
36
|
+
expect(response).to match(/(?:\w|-)+/)
|
37
|
+
end
|
38
|
+
|
39
|
+
context 'if the username already exists' do
|
40
|
+
it 'raise an argument error' do
|
41
|
+
expect do
|
42
|
+
described_class.create(params)
|
43
|
+
described_class.create({username: 'foo', password: 'bra'})
|
44
|
+
end.to raise_exception(ArgumentError)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe '.save' do
|
50
|
+
let(:id){ settings['sensitive_data']['ID'] }
|
51
|
+
let(:account){ described_class.find(id) }
|
52
|
+
|
53
|
+
before{ account.should be_instance_of Sipwizard::Account }
|
54
|
+
|
55
|
+
subject{ account.save }
|
56
|
+
|
57
|
+
it 'updates the account' do
|
58
|
+
account.avatar_url = "foo"
|
59
|
+
response = subject
|
60
|
+
expect(response).not_to be_nil
|
61
|
+
expect(response).to be_instance_of String
|
62
|
+
expect(response).to match(/(?:\w|-)+/)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe 'delete' do
|
67
|
+
let(:id){ settings['sensitive_data']['ID'] }
|
68
|
+
|
69
|
+
let(:account){ described_class.find(id) }
|
70
|
+
|
71
|
+
before{ account.should be_instance_of Sipwizard::Account }
|
72
|
+
|
73
|
+
subject{ account.delete }
|
74
|
+
|
75
|
+
it 'delete the account' do
|
76
|
+
response = subject
|
77
|
+
expect(response).to be_true
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Sipwizard::Binding do
|
4
|
+
describe '.count' do
|
5
|
+
subject{ described_class.count }
|
6
|
+
|
7
|
+
it 'returns the nb of account bindings' do
|
8
|
+
expect(subject).to be_instance_of Fixnum
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
describe '.find(id)' do
|
13
|
+
let(:id){ settings['sensitive_data']['ACCOUNT_BINDING_ID'] }
|
14
|
+
|
15
|
+
subject{ described_class.find(id) }
|
16
|
+
|
17
|
+
it 'returns an account binding' do
|
18
|
+
subject.should be_instance_of Sipwizard::Binding
|
19
|
+
subject.id.should eq id
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Sipwizard::Cdr do
|
4
|
+
describe '.count' do
|
5
|
+
subject{ described_class.count }
|
6
|
+
it 'returns a result' do
|
7
|
+
expect(subject).to be_instance_of Fixnum
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
describe '.find(id)' do
|
12
|
+
let(:id){ settings['sensitive_data']['CDR_ID'] }
|
13
|
+
|
14
|
+
subject{ described_class.find(id) }
|
15
|
+
|
16
|
+
it 'returns an account' do
|
17
|
+
subject.should be_instance_of Sipwizard::Cdr
|
18
|
+
subject.id.should eq id
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -2,40 +2,34 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe Sipwizard::Connection do
|
4
4
|
let(:default_faraday_adapter) { Faraday::Adapter::NetHttp }
|
5
|
-
subject{
|
5
|
+
subject{ described_class.new }
|
6
6
|
|
7
7
|
it 'returns a Faraday::Connection with the nethttp adapter' do
|
8
|
-
subject.faraday_connection.
|
9
|
-
subject.faraday_connection.builder.handlers.
|
8
|
+
subject.faraday_connection.should be_instance_of Faraday::Connection
|
9
|
+
subject.faraday_connection.builder.handlers.should include(default_faraday_adapter)
|
10
10
|
end
|
11
11
|
|
12
|
-
describe '
|
13
|
-
|
14
|
-
subject{ Sipwizard::Connection.uri_for_path(path) }
|
12
|
+
describe '#get(params)' do
|
13
|
+
subject{ described_class.new.get('cdr/count') }
|
15
14
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
describe 'get(params)' do
|
22
|
-
let(:params){ { foo: 'bar' } }
|
23
|
-
let(:path){ '/path' }
|
24
|
-
let(:uri){ Sipwizard::Connection.uri_for_path(path) }
|
25
|
-
let(:faraday_connection) { Minitest::Mock.new }
|
26
|
-
let(:connection) do
|
27
|
-
Sipwizard::Connection.new.tap do |connection|
|
28
|
-
connection.faraday_connection = faraday_connection
|
15
|
+
context 'when the api key is valid' do
|
16
|
+
it 'doesnt complain' do
|
17
|
+
expect(subject["Error"]).to be_nil
|
18
|
+
expect(subject["Success"]).to be_true
|
29
19
|
end
|
30
20
|
end
|
31
21
|
|
32
|
-
|
22
|
+
context 'when the api key is invalid' do
|
23
|
+
let(:connection){ described_class.new }
|
24
|
+
subject{ connection.get('cdr/count') }
|
25
|
+
before do
|
26
|
+
connection.faraday_connection.headers['apikey'] = 'bar'
|
27
|
+
end
|
33
28
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
faraday_connection.verify
|
29
|
+
it 'returns an error' do
|
30
|
+
expect(subject["Error"]).not_to be_nil
|
31
|
+
expect(subject["Success"]).to be_false
|
32
|
+
end
|
39
33
|
end
|
40
34
|
end
|
41
35
|
end
|