yoti 1.5.0 → 1.6.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/ISSUE_TEMPLATE.md +17 -0
- data/CONTRIBUTING.md +0 -29
- data/Gemfile +0 -2
- data/README.md +1 -1
- data/Rakefile +16 -3
- data/lib/yoti.rb +16 -0
- data/lib/yoti/activity_details.rb +21 -2
- data/lib/yoti/client.rb +2 -1
- data/lib/yoti/data_type/age_verification.rb +54 -0
- data/lib/yoti/data_type/attribute.rb +3 -0
- data/lib/yoti/data_type/base_profile.rb +13 -0
- data/lib/yoti/data_type/document_details.rb +88 -0
- data/lib/yoti/data_type/profile.rb +76 -0
- data/lib/yoti/dynamic_share_service/dynamic_scenario.rb +67 -0
- data/lib/yoti/dynamic_share_service/extension/extension.rb +45 -0
- data/lib/yoti/dynamic_share_service/extension/location_constraint_extension.rb +88 -0
- data/lib/yoti/dynamic_share_service/extension/thirdparty_attribute_extension.rb +119 -0
- data/lib/yoti/dynamic_share_service/extension/transactional_flow_extension.rb +47 -0
- data/lib/yoti/dynamic_share_service/policy/dynamic_policy.rb +184 -0
- data/lib/yoti/dynamic_share_service/policy/source_constraint.rb +88 -0
- data/lib/yoti/dynamic_share_service/policy/wanted_anchor.rb +53 -0
- data/lib/yoti/dynamic_share_service/policy/wanted_attribute.rb +85 -0
- data/lib/yoti/dynamic_share_service/share_url.rb +82 -0
- data/lib/yoti/http/profile_request.rb +1 -0
- data/lib/yoti/http/request.rb +15 -0
- data/lib/yoti/http/signed_request.rb +0 -3
- data/lib/yoti/protobuf/main.rb +25 -4
- data/lib/yoti/protobuf/sharepubapi/DataEntry_pb.rb +29 -0
- data/lib/yoti/protobuf/sharepubapi/ExtraData_pb.rb +19 -0
- data/lib/yoti/protobuf/sharepubapi/IssuingAttributes_pb.rb +23 -0
- data/lib/yoti/protobuf/sharepubapi/ThirdPartyAttribute_pb.rb +20 -0
- data/lib/yoti/share/attribute_issuance_details.rb +43 -0
- data/lib/yoti/share/extra_data.rb +25 -0
- data/lib/yoti/ssl.rb +8 -0
- data/lib/yoti/util/age_processor.rb +4 -0
- data/lib/yoti/version.rb +1 -1
- data/rubocop.yml +4 -0
- data/yoti.gemspec +3 -3
- metadata +31 -14
- data/.travis.yml +0 -17
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Yoti
|
4
|
+
module DynamicSharingService
|
5
|
+
# Describes a dynamic share
|
6
|
+
class DynamicScenario
|
7
|
+
attr_reader :policy
|
8
|
+
attr_reader :extensions
|
9
|
+
attr_reader :callback_endpoint
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@extensions = []
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_json(*_args)
|
16
|
+
as_json.to_json
|
17
|
+
end
|
18
|
+
|
19
|
+
def as_json(*_args)
|
20
|
+
{
|
21
|
+
policy: @policy,
|
22
|
+
extensions: @extensions,
|
23
|
+
callback_endpoint: @callback_endpoint
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.builder
|
28
|
+
DynamicScenarioBuilder.new
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Builder for DynamicScenario
|
33
|
+
class DynamicScenarioBuilder
|
34
|
+
def initialize
|
35
|
+
@scenario = DynamicScenario.new
|
36
|
+
end
|
37
|
+
|
38
|
+
def build
|
39
|
+
Marshal.load Marshal.dump @scenario
|
40
|
+
end
|
41
|
+
|
42
|
+
#
|
43
|
+
# @param [Yoti::DynamicSharingService::DynamicPolicy] policy
|
44
|
+
#
|
45
|
+
def with_policy(policy)
|
46
|
+
@scenario.instance_variable_set(:@policy, policy)
|
47
|
+
self
|
48
|
+
end
|
49
|
+
|
50
|
+
#
|
51
|
+
# @param [Yoti::DynamicSharingService::Extension] extension
|
52
|
+
#
|
53
|
+
def with_extension(extension)
|
54
|
+
@scenario.instance_variable_get(:@extensions) << extension
|
55
|
+
self
|
56
|
+
end
|
57
|
+
|
58
|
+
#
|
59
|
+
# @param [String] endpoint
|
60
|
+
#
|
61
|
+
def with_callback_endpoint(endpoint)
|
62
|
+
@scenario.instance_variable_set(:@callback_endpoint, endpoint)
|
63
|
+
self
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Yoti
|
4
|
+
module DynamicSharingService
|
5
|
+
class Extension
|
6
|
+
attr_reader :type
|
7
|
+
attr_reader :content
|
8
|
+
|
9
|
+
def to_json(*_args)
|
10
|
+
as_json.to_json
|
11
|
+
end
|
12
|
+
|
13
|
+
def as_json(*_args)
|
14
|
+
{
|
15
|
+
type: @type,
|
16
|
+
content: @content
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.builder
|
21
|
+
ExtensionBuilder.new
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class ExtensionBuilder
|
26
|
+
def initialize
|
27
|
+
@extension = Extension.new
|
28
|
+
end
|
29
|
+
|
30
|
+
def with_type(type)
|
31
|
+
@extension.instance_variable_set(:@type, type)
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
def with_content(content)
|
36
|
+
@extension.instance_variable_set(:@content, content)
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
def build
|
41
|
+
Marshal.load Marshal.dump @extension
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Yoti
|
4
|
+
module DynamicSharingService
|
5
|
+
# A Location Constraint
|
6
|
+
class LocationConstraintExtension
|
7
|
+
EXTENSION_TYPE = 'LOCATION_CONSTRAINT'
|
8
|
+
|
9
|
+
attr_reader :content
|
10
|
+
attr_reader :type
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@content = {}
|
14
|
+
@type = EXTENSION_TYPE
|
15
|
+
end
|
16
|
+
|
17
|
+
def as_json(*_args)
|
18
|
+
{
|
19
|
+
type: @type,
|
20
|
+
content: @content
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_json(*_args)
|
25
|
+
as_json.to_json
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.builder
|
29
|
+
LocationConstraintExtensionBuilder.new
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Builder for LocationConstraintExtension
|
34
|
+
class LocationConstraintExtensionBuilder
|
35
|
+
def with_latitude(latitude)
|
36
|
+
raise ArgumentError, 'Latitude must be Integer or Float'\
|
37
|
+
unless latitude.is_a?(Integer) || latitude.is_a?(Float)
|
38
|
+
raise ArgumentError, 'Latitude must be between -90 and 90'\
|
39
|
+
unless latitude >= -90 && latitude <= 90
|
40
|
+
|
41
|
+
@latitude = latitude
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
def with_longitude(longitude)
|
46
|
+
raise ArgumentError, 'Longitude must be Integer or Float'\
|
47
|
+
unless longitude.is_a?(Integer) || longitude.is_a?(Float)
|
48
|
+
raise ArgumentError, 'Longitude must be between -180 and 180'\
|
49
|
+
unless longitude >= -180 && longitude <= 180
|
50
|
+
|
51
|
+
@longitude = longitude
|
52
|
+
self
|
53
|
+
end
|
54
|
+
|
55
|
+
def with_radius(radius)
|
56
|
+
raise ArgumentError, 'Radius must be Integer or Float'\
|
57
|
+
unless radius.is_a?(Integer) || radius.is_a?(Float)
|
58
|
+
raise ArgumentError, 'Radius must be >= 0' unless radius >= 0
|
59
|
+
|
60
|
+
@radius = radius
|
61
|
+
self
|
62
|
+
end
|
63
|
+
|
64
|
+
def with_max_uncertainty(uncertainty)
|
65
|
+
raise ArgumentError, 'Uncertainty must be Integer or Float'\
|
66
|
+
unless uncertainty.is_a?(Integer) || uncertainty.is_a?(Float)
|
67
|
+
raise ArgumentError, 'Uncertainty must be >= 0' unless uncertainty >= 0
|
68
|
+
|
69
|
+
@uncertainty = uncertainty
|
70
|
+
self
|
71
|
+
end
|
72
|
+
|
73
|
+
def build
|
74
|
+
@radius ||= 150 unless @radius
|
75
|
+
@uncertainty ||= 150 unless @uncertainty
|
76
|
+
|
77
|
+
extension = LocationConstraintExtension.new
|
78
|
+
extension.instance_variable_get(:@content)[:expected_device_location] = {
|
79
|
+
latitude: @latitude,
|
80
|
+
longitude: @longitude,
|
81
|
+
radius: @radius,
|
82
|
+
max_uncertainty_radius: @uncertainty
|
83
|
+
}
|
84
|
+
extension
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'time'
|
4
|
+
|
5
|
+
module Yoti
|
6
|
+
module DynamicSharingService
|
7
|
+
class ThirdPartyAttributeDefinition
|
8
|
+
#
|
9
|
+
# @param [String] name
|
10
|
+
#
|
11
|
+
def initialize(name)
|
12
|
+
@name = name
|
13
|
+
end
|
14
|
+
|
15
|
+
def as_json(*_args)
|
16
|
+
{ name: @name }
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_json(*_args)
|
20
|
+
as_json.to_json
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class ThirdPartyAttributeExtensionBuilder
|
25
|
+
def initialize
|
26
|
+
@expiry_date = nil
|
27
|
+
@definitions = []
|
28
|
+
end
|
29
|
+
|
30
|
+
#
|
31
|
+
# @param [DateTime,Time] expiry_date
|
32
|
+
#
|
33
|
+
# @return [self]
|
34
|
+
#
|
35
|
+
def with_expiry_date(expiry_date)
|
36
|
+
@expiry_date = expiry_date
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
#
|
41
|
+
# @param [String] *names
|
42
|
+
#
|
43
|
+
# @return [self]
|
44
|
+
#
|
45
|
+
def with_definitions(*names)
|
46
|
+
@definitions += names.map do |name|
|
47
|
+
ThirdPartyAttributeDefinition.new(name)
|
48
|
+
end
|
49
|
+
self
|
50
|
+
end
|
51
|
+
|
52
|
+
#
|
53
|
+
# @return [ThirdPartyAttributeExtension]
|
54
|
+
#
|
55
|
+
def build
|
56
|
+
content = ThirdPartyAttributeExtensionContent.new(@expiry_date, @definitions)
|
57
|
+
ThirdPartyAttributeExtension.new(content)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class ThirdPartyAttributeExtension
|
62
|
+
EXTENSION_TYPE = 'THIRD_PARTY_ATTRIBUTE'
|
63
|
+
|
64
|
+
# @return [ThirdPartyAttributeExtensionContent]
|
65
|
+
attr_reader :content
|
66
|
+
|
67
|
+
# @return [String]
|
68
|
+
attr_reader :type
|
69
|
+
|
70
|
+
#
|
71
|
+
# @param [ThirdPartyAttributeExtensionContent] content
|
72
|
+
#
|
73
|
+
def initialize(content = nil)
|
74
|
+
@content = content
|
75
|
+
@type = EXTENSION_TYPE
|
76
|
+
end
|
77
|
+
|
78
|
+
def as_json(*_args)
|
79
|
+
json = {}
|
80
|
+
json[:type] = @type
|
81
|
+
json[:content] = @content.as_json unless @content.nil?
|
82
|
+
json
|
83
|
+
end
|
84
|
+
|
85
|
+
def to_json(*_args)
|
86
|
+
as_json.to_json
|
87
|
+
end
|
88
|
+
|
89
|
+
#
|
90
|
+
# @return [ThirdPartyAttributeExtensionBuilder]
|
91
|
+
#
|
92
|
+
def self.builder
|
93
|
+
ThirdPartyAttributeExtensionBuilder.new
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
class ThirdPartyAttributeExtensionContent
|
98
|
+
#
|
99
|
+
# @param [DateTime,Time] expiry_date
|
100
|
+
# @param [Array<ThirdPartyAttributeDefinition>] definitions
|
101
|
+
#
|
102
|
+
def initialize(expiry_date, definitions)
|
103
|
+
@expiry_date = expiry_date
|
104
|
+
@definitions = definitions
|
105
|
+
end
|
106
|
+
|
107
|
+
def as_json(*_args)
|
108
|
+
json = {}
|
109
|
+
json[:expiry_date] = @expiry_date.to_time.utc.strftime('%FT%T.%3NZ') unless @expiry_date.nil?
|
110
|
+
json[:definitions] = @definitions.map(&:as_json)
|
111
|
+
json
|
112
|
+
end
|
113
|
+
|
114
|
+
def to_json(*_args)
|
115
|
+
as_json.to_json
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Yoti
|
4
|
+
module DynamicSharingService
|
5
|
+
# Extension for transactional flows
|
6
|
+
class TransactionalFlowExtension
|
7
|
+
EXTENSION_TYPE = 'TRANSACTIONAL_FLOW'
|
8
|
+
attr_reader :content
|
9
|
+
attr_reader :type
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@type = EXTENSION_TYPE
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_json(*_args)
|
16
|
+
as_json.to_json
|
17
|
+
end
|
18
|
+
|
19
|
+
def as_json(*_args)
|
20
|
+
{
|
21
|
+
content: @content,
|
22
|
+
type: @type
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.builder
|
27
|
+
TransactionalFlowExtensionBuilder.new
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Builder for TransactionalFlowExtension
|
32
|
+
class TransactionalFlowExtensionBuilder
|
33
|
+
def initialize
|
34
|
+
@extension = TransactionalFlowExtension.new
|
35
|
+
end
|
36
|
+
|
37
|
+
def with_content(content)
|
38
|
+
@extension.instance_variable_set(:@content, content)
|
39
|
+
self
|
40
|
+
end
|
41
|
+
|
42
|
+
def build
|
43
|
+
Marshal.load Marshal.dump @extension
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,184 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Yoti
|
4
|
+
module DynamicSharingService
|
5
|
+
# Describes a policy for a dynamic share
|
6
|
+
class DynamicPolicy
|
7
|
+
SELFIE_AUTH_TYPE = 1
|
8
|
+
PIN_AUTH_TYPE = 2
|
9
|
+
|
10
|
+
attr_reader :wanted_auth_types
|
11
|
+
attr_reader :wanted
|
12
|
+
|
13
|
+
def wanted_remember_me
|
14
|
+
return true if @wanted_remember_me
|
15
|
+
|
16
|
+
false
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_json(*args)
|
20
|
+
as_json.to_json(*args)
|
21
|
+
end
|
22
|
+
|
23
|
+
def as_json(*_args)
|
24
|
+
{
|
25
|
+
wanted_auth_types: @wanted_auth_types,
|
26
|
+
wanted: @wanted
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.builder
|
31
|
+
DynamicPolicyBuilder.new
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Builder for DynamicPolicy
|
36
|
+
class DynamicPolicyBuilder
|
37
|
+
def initialize
|
38
|
+
@policy = DynamicPolicy.new
|
39
|
+
@wanted_auth_types = {}
|
40
|
+
@wanted_attributes = {}
|
41
|
+
end
|
42
|
+
|
43
|
+
def build
|
44
|
+
@policy.instance_variable_set(
|
45
|
+
:@wanted_auth_types,
|
46
|
+
@wanted_auth_types
|
47
|
+
.select { |_, wanted| wanted }
|
48
|
+
.keys
|
49
|
+
)
|
50
|
+
@policy.instance_variable_set(:@wanted, @wanted_attributes.values)
|
51
|
+
Marshal.load Marshal.dump @policy
|
52
|
+
end
|
53
|
+
|
54
|
+
#
|
55
|
+
# @param [Bool] wanted
|
56
|
+
#
|
57
|
+
def with_wanted_remember_me(wanted = true)
|
58
|
+
@policy.instance_variable_set(:@wanted_remember_me, wanted)
|
59
|
+
self
|
60
|
+
end
|
61
|
+
|
62
|
+
#
|
63
|
+
# @param [Integer] auth
|
64
|
+
# @param [Bool] wanted
|
65
|
+
#
|
66
|
+
def with_wanted_auth_type(auth, wanted = true)
|
67
|
+
@wanted_auth_types[auth] = wanted
|
68
|
+
self
|
69
|
+
end
|
70
|
+
|
71
|
+
#
|
72
|
+
# @param [Bool] wanted
|
73
|
+
#
|
74
|
+
def with_selfie_auth(wanted = true)
|
75
|
+
with_wanted_auth_type(DynamicPolicy::SELFIE_AUTH_TYPE, wanted)
|
76
|
+
end
|
77
|
+
|
78
|
+
#
|
79
|
+
# @param [Bool] wanted
|
80
|
+
#
|
81
|
+
def with_pin_auth(wanted = true)
|
82
|
+
with_wanted_auth_type(DynamicPolicy::PIN_AUTH_TYPE, wanted)
|
83
|
+
end
|
84
|
+
|
85
|
+
#
|
86
|
+
# @param [Yoti::DynamicSharingService::WantedAttribute] attribute
|
87
|
+
#
|
88
|
+
def with_wanted_attribute(attribute)
|
89
|
+
key = attribute.derivation || attribute.name
|
90
|
+
@wanted_attributes[key] = attribute
|
91
|
+
self
|
92
|
+
end
|
93
|
+
|
94
|
+
#
|
95
|
+
# @param [String] name
|
96
|
+
# @param [Hash] constraints
|
97
|
+
#
|
98
|
+
def with_wanted_attribute_by_name(name, constraints: nil)
|
99
|
+
attribute_builder = WantedAttribute.builder.with_name(name)
|
100
|
+
constraints&.each do |constraint|
|
101
|
+
attribute_builder.with_constraint constraint
|
102
|
+
end
|
103
|
+
attribute = attribute_builder.build
|
104
|
+
with_wanted_attribute attribute
|
105
|
+
end
|
106
|
+
|
107
|
+
def with_family_name(options = {})
|
108
|
+
with_wanted_attribute_by_name Attribute::FAMILY_NAME, **options
|
109
|
+
end
|
110
|
+
|
111
|
+
def with_given_names(options = {})
|
112
|
+
with_wanted_attribute_by_name Attribute::GIVEN_NAMES, **options
|
113
|
+
end
|
114
|
+
|
115
|
+
def with_full_name(options = {})
|
116
|
+
with_wanted_attribute_by_name Attribute::FULL_NAME, **options
|
117
|
+
end
|
118
|
+
|
119
|
+
def with_date_of_birth(options = {})
|
120
|
+
with_wanted_attribute_by_name Attribute::DATE_OF_BIRTH, **options
|
121
|
+
end
|
122
|
+
|
123
|
+
#
|
124
|
+
# @param [String] derivation
|
125
|
+
# @param [Hash] constraints
|
126
|
+
#
|
127
|
+
def with_age_derived_attribute(derivation, constraints: nil)
|
128
|
+
attribute_builder = WantedAttribute.builder
|
129
|
+
attribute_builder.with_name(Attribute::DATE_OF_BIRTH)
|
130
|
+
attribute_builder.with_derivation(derivation)
|
131
|
+
constraints&.each do |constraint|
|
132
|
+
attribute_builder.with_constraint constraint
|
133
|
+
end
|
134
|
+
with_wanted_attribute(attribute_builder.build)
|
135
|
+
end
|
136
|
+
|
137
|
+
#
|
138
|
+
# @param [Integer] derivation
|
139
|
+
#
|
140
|
+
def with_age_over(age, options = {})
|
141
|
+
with_age_derived_attribute(Attribute::AGE_OVER + age.to_s, **options)
|
142
|
+
end
|
143
|
+
|
144
|
+
#
|
145
|
+
# @param [Integer] derivation
|
146
|
+
#
|
147
|
+
def with_age_under(age, options = {})
|
148
|
+
with_age_derived_attribute(Attribute::AGE_UNDER + age.to_s, **options)
|
149
|
+
end
|
150
|
+
|
151
|
+
def with_gender(options = {})
|
152
|
+
with_wanted_attribute_by_name Attribute::GENDER, **options
|
153
|
+
end
|
154
|
+
|
155
|
+
def with_postal_address(options = {})
|
156
|
+
with_wanted_attribute_by_name(Attribute::POSTAL_ADDRESS, **options)
|
157
|
+
end
|
158
|
+
|
159
|
+
def with_structured_postal_address(options = {})
|
160
|
+
with_wanted_attribute_by_name(Attribute::STRUCTURED_POSTAL_ADDRESS, **options)
|
161
|
+
end
|
162
|
+
|
163
|
+
def with_nationality(options = {})
|
164
|
+
with_wanted_attribute_by_name(Attribute::NATIONALITY, **options)
|
165
|
+
end
|
166
|
+
|
167
|
+
def with_phone_number(options = {})
|
168
|
+
with_wanted_attribute_by_name(Attribute::PHONE_NUMBER, **options)
|
169
|
+
end
|
170
|
+
|
171
|
+
def with_selfie(options = {})
|
172
|
+
with_wanted_attribute_by_name(Attribute::SELFIE, **options)
|
173
|
+
end
|
174
|
+
|
175
|
+
def with_email(options = {})
|
176
|
+
with_wanted_attribute_by_name(Attribute::EMAIL_ADDRESS, **options)
|
177
|
+
end
|
178
|
+
|
179
|
+
def with_document_details
|
180
|
+
with_wanted_attribute_by_name(Attribute::DOCUMENT_DETAILS)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|