yoti 1.6.4 → 1.10.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.
- checksums.yaml +4 -4
- data/Gemfile +1 -1
- data/README.md +22 -224
- data/lib/yoti.rb +63 -1
- data/lib/yoti/activity_details.rb +3 -5
- data/lib/yoti/client.rb +13 -4
- data/lib/yoti/configuration.rb +8 -3
- data/lib/yoti/data_type/age_verification.rb +1 -1
- data/lib/yoti/data_type/base_profile.rb +1 -1
- data/lib/yoti/data_type/document_details.rb +1 -1
- data/lib/yoti/data_type/image.rb +4 -12
- data/lib/yoti/data_type/image_jpeg.rb +2 -0
- data/lib/yoti/data_type/image_png.rb +2 -0
- data/lib/yoti/data_type/media.rb +22 -0
- data/lib/yoti/data_type/signed_time_stamp.rb +1 -1
- data/lib/yoti/doc_scan/client.rb +191 -0
- data/lib/yoti/doc_scan/constants.rb +35 -0
- data/lib/yoti/doc_scan/errors.rb +81 -0
- data/lib/yoti/doc_scan/session/create/create_session_result.rb +50 -0
- data/lib/yoti/doc_scan/session/create/document_filter.rb +31 -0
- data/lib/yoti/doc_scan/session/create/document_restrictions_filter.rb +140 -0
- data/lib/yoti/doc_scan/session/create/notification_config.rb +142 -0
- data/lib/yoti/doc_scan/session/create/objective/objective.rb +31 -0
- data/lib/yoti/doc_scan/session/create/objective/proof_of_address_objective.rb +31 -0
- data/lib/yoti/doc_scan/session/create/orthogonal_restrictions_filter.rb +150 -0
- data/lib/yoti/doc_scan/session/create/requested_check.rb +39 -0
- data/lib/yoti/doc_scan/session/create/requested_document_authenticity_check.rb +95 -0
- data/lib/yoti/doc_scan/session/create/requested_face_match_check.rb +95 -0
- data/lib/yoti/doc_scan/session/create/requested_id_document_comparison_check.rb +53 -0
- data/lib/yoti/doc_scan/session/create/requested_liveness_check.rb +108 -0
- data/lib/yoti/doc_scan/session/create/requested_supplementary_doc_text_extraction_task.rb +94 -0
- data/lib/yoti/doc_scan/session/create/requested_task.rb +39 -0
- data/lib/yoti/doc_scan/session/create/requested_text_extraction_task.rb +116 -0
- data/lib/yoti/doc_scan/session/create/required_document.rb +31 -0
- data/lib/yoti/doc_scan/session/create/required_id_document.rb +53 -0
- data/lib/yoti/doc_scan/session/create/required_supplementary_document.rb +90 -0
- data/lib/yoti/doc_scan/session/create/sdk_config.rb +221 -0
- data/lib/yoti/doc_scan/session/create/session_specification.rb +221 -0
- data/lib/yoti/doc_scan/session/retrieve/authenticity_check_response.rb +12 -0
- data/lib/yoti/doc_scan/session/retrieve/breakdown_response.rb +38 -0
- data/lib/yoti/doc_scan/session/retrieve/check_response.rb +63 -0
- data/lib/yoti/doc_scan/session/retrieve/details_response.rb +28 -0
- data/lib/yoti/doc_scan/session/retrieve/document_fields_response.rb +21 -0
- data/lib/yoti/doc_scan/session/retrieve/document_id_photo_response.rb +21 -0
- data/lib/yoti/doc_scan/session/retrieve/face_map_response.rb +21 -0
- data/lib/yoti/doc_scan/session/retrieve/face_match_check_response.rb +12 -0
- data/lib/yoti/doc_scan/session/retrieve/file_response.rb +21 -0
- data/lib/yoti/doc_scan/session/retrieve/frame_response.rb +21 -0
- data/lib/yoti/doc_scan/session/retrieve/generated_check_response.rb +28 -0
- data/lib/yoti/doc_scan/session/retrieve/generated_media.rb +28 -0
- data/lib/yoti/doc_scan/session/retrieve/generated_supplementary_document_text_data_check_response.rb +12 -0
- data/lib/yoti/doc_scan/session/retrieve/generated_text_data_check_response.rb +12 -0
- data/lib/yoti/doc_scan/session/retrieve/get_session_result.rb +145 -0
- data/lib/yoti/doc_scan/session/retrieve/id_document_comparison_check_response.rb +12 -0
- data/lib/yoti/doc_scan/session/retrieve/id_document_resource_response.rb +57 -0
- data/lib/yoti/doc_scan/session/retrieve/liveness_check_response.rb +12 -0
- data/lib/yoti/doc_scan/session/retrieve/liveness_resource_response.rb +24 -0
- data/lib/yoti/doc_scan/session/retrieve/media_response.rb +38 -0
- data/lib/yoti/doc_scan/session/retrieve/page_response.rb +37 -0
- data/lib/yoti/doc_scan/session/retrieve/recommendation_response.rb +34 -0
- data/lib/yoti/doc_scan/session/retrieve/report_response.rb +31 -0
- data/lib/yoti/doc_scan/session/retrieve/resource_container.rb +69 -0
- data/lib/yoti/doc_scan/session/retrieve/resource_response.rb +41 -0
- data/lib/yoti/doc_scan/session/retrieve/supplementary_document_resource_response.rb +57 -0
- data/lib/yoti/doc_scan/session/retrieve/supplementary_document_text_data_check_response.rb +12 -0
- data/lib/yoti/doc_scan/session/retrieve/supplementary_document_text_extraction_task_response.rb +18 -0
- data/lib/yoti/doc_scan/session/retrieve/task_response.rb +89 -0
- data/lib/yoti/doc_scan/session/retrieve/text_data_check_response.rb +12 -0
- data/lib/yoti/doc_scan/session/retrieve/text_extraction_task_response.rb +18 -0
- data/lib/yoti/doc_scan/session/retrieve/zoom_liveness_resource_response.rb +33 -0
- data/lib/yoti/doc_scan/support/supported_documents.rb +60 -0
- data/lib/yoti/dynamic_share_service/dynamic_scenario.rb +5 -0
- data/lib/yoti/dynamic_share_service/extension/extension.rb +3 -0
- data/lib/yoti/dynamic_share_service/extension/location_constraint_extension.rb +3 -0
- data/lib/yoti/dynamic_share_service/extension/thirdparty_attribute_extension.rb +1 -1
- data/lib/yoti/dynamic_share_service/extension/transactional_flow_extension.rb +4 -0
- data/lib/yoti/dynamic_share_service/policy/dynamic_policy.rb +74 -9
- data/lib/yoti/dynamic_share_service/policy/wanted_anchor.rb +3 -0
- data/lib/yoti/dynamic_share_service/policy/wanted_attribute.rb +5 -0
- data/lib/yoti/dynamic_share_service/share_url.rb +26 -33
- data/lib/yoti/errors.rb +15 -2
- data/lib/yoti/http/aml_check_request.rb +12 -6
- data/lib/yoti/http/payloads/aml_address.rb +4 -0
- data/lib/yoti/http/payloads/aml_profile.rb +7 -1
- data/lib/yoti/http/profile_request.rb +11 -6
- data/lib/yoti/http/request.rb +219 -18
- data/lib/yoti/http/signed_request.rb +13 -4
- data/lib/yoti/protobuf/main.rb +8 -8
- data/lib/yoti/share/attribute_issuance_details.rb +6 -0
- data/lib/yoti/ssl.rb +4 -4
- data/lib/yoti/util/age_processor.rb +1 -1
- data/lib/yoti/util/anchor_processor.rb +2 -2
- data/lib/yoti/util/log.rb +1 -1
- data/lib/yoti/util/validation.rb +41 -0
- data/lib/yoti/version.rb +1 -1
- data/yoti.gemspec +18 -6
- metadata +69 -33
- data/.github/ISSUE_TEMPLATE.md +0 -17
- data/.gitignore +0 -39
- data/CONTRIBUTING.md +0 -98
- data/Guardfile +0 -11
- data/Rakefile +0 -54
- data/login_flow.png +0 -0
- data/rubocop.yml +0 -57
- data/yardstick.yml +0 -9
@@ -0,0 +1,221 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Yoti
|
4
|
+
module DocScan
|
5
|
+
module Session
|
6
|
+
module Create
|
7
|
+
class SessionSpecification
|
8
|
+
#
|
9
|
+
# @param [Integer] client_session_token_ttl
|
10
|
+
# @param [Integer] resources_ttl
|
11
|
+
# @param [String] user_tracking_id
|
12
|
+
# @param [NotificationConfig] notifications
|
13
|
+
# @param [Array<RequestedCheck>] requested_checks
|
14
|
+
# @param [Array<RequestedTask>] requested_tasks
|
15
|
+
# @param [SdkConfig] sdk_config
|
16
|
+
# @param [Array<RequiredDocument>] required_documents
|
17
|
+
# @param [Boolean] block_biometric_consent
|
18
|
+
#
|
19
|
+
def initialize(
|
20
|
+
client_session_token_ttl,
|
21
|
+
resources_ttl,
|
22
|
+
user_tracking_id,
|
23
|
+
notifications,
|
24
|
+
requested_checks,
|
25
|
+
requested_tasks,
|
26
|
+
sdk_config,
|
27
|
+
required_documents,
|
28
|
+
block_biometric_consent = nil
|
29
|
+
)
|
30
|
+
Validation.assert_is_a(Integer, client_session_token_ttl, 'client_session_token_ttl', true)
|
31
|
+
@client_session_token_ttl = client_session_token_ttl
|
32
|
+
|
33
|
+
Validation.assert_is_a(Integer, resources_ttl, 'resources_ttl', true)
|
34
|
+
@resources_ttl = resources_ttl
|
35
|
+
|
36
|
+
Validation.assert_is_a(String, user_tracking_id, 'user_tracking_id', true)
|
37
|
+
@user_tracking_id = user_tracking_id
|
38
|
+
|
39
|
+
Validation.assert_is_a(NotificationConfig, notifications, 'notifications', true)
|
40
|
+
@notifications = notifications
|
41
|
+
|
42
|
+
Validation.assert_is_a(Array, requested_checks, 'requested_checks', true)
|
43
|
+
@requested_checks = requested_checks
|
44
|
+
|
45
|
+
Validation.assert_is_a(Array, requested_tasks, 'requested_tasks', true)
|
46
|
+
@requested_tasks = requested_tasks
|
47
|
+
|
48
|
+
Validation.assert_is_a(SdkConfig, sdk_config, 'sdk_config', true)
|
49
|
+
@sdk_config = sdk_config
|
50
|
+
|
51
|
+
Validation.assert_is_a(Array, required_documents, 'required_documents', true)
|
52
|
+
@required_documents = required_documents
|
53
|
+
|
54
|
+
@block_biometric_consent = block_biometric_consent
|
55
|
+
end
|
56
|
+
|
57
|
+
def to_json(*_args)
|
58
|
+
as_json.to_json
|
59
|
+
end
|
60
|
+
|
61
|
+
def as_json(*_args)
|
62
|
+
{
|
63
|
+
client_session_token_ttl: @client_session_token_ttl,
|
64
|
+
resources_ttl: @resources_ttl,
|
65
|
+
user_tracking_id: @user_tracking_id,
|
66
|
+
notifications: @notifications,
|
67
|
+
requested_checks: @requested_checks.map(&:as_json),
|
68
|
+
requested_tasks: @requested_tasks.map(&:as_json),
|
69
|
+
sdk_config: @sdk_config,
|
70
|
+
required_documents: @required_documents.map(&:as_json),
|
71
|
+
block_biometric_consent: @block_biometric_consent
|
72
|
+
}.compact
|
73
|
+
end
|
74
|
+
|
75
|
+
#
|
76
|
+
# @return [SessionSpecificationBuilder]
|
77
|
+
#
|
78
|
+
def self.builder
|
79
|
+
SessionSpecificationBuilder.new
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
class SessionSpecificationBuilder
|
84
|
+
def initialize
|
85
|
+
@requested_checks = []
|
86
|
+
@requested_tasks = []
|
87
|
+
@required_documents = []
|
88
|
+
end
|
89
|
+
|
90
|
+
#
|
91
|
+
# Client-session-token time-to-live to apply to the created session
|
92
|
+
#
|
93
|
+
# @param [Integer] client_session_token_ttl
|
94
|
+
#
|
95
|
+
# @return [self]
|
96
|
+
#
|
97
|
+
def with_client_session_token_ttl(client_session_token_ttl)
|
98
|
+
@client_session_token_ttl = client_session_token_ttl
|
99
|
+
self
|
100
|
+
end
|
101
|
+
|
102
|
+
#
|
103
|
+
# Time-to-live used for all Resources created in the course of the session
|
104
|
+
#
|
105
|
+
# @param [Integer] resources_ttl
|
106
|
+
#
|
107
|
+
# @return [self]
|
108
|
+
#
|
109
|
+
def with_resources_ttl(resources_ttl)
|
110
|
+
@resources_ttl = resources_ttl
|
111
|
+
self
|
112
|
+
end
|
113
|
+
|
114
|
+
#
|
115
|
+
# User tracking id, for the Relying Business to track returning users
|
116
|
+
#
|
117
|
+
# @param [String] user_tracking_id
|
118
|
+
#
|
119
|
+
# @return [self]
|
120
|
+
#
|
121
|
+
def with_user_tracking_id(user_tracking_id)
|
122
|
+
@user_tracking_id = user_tracking_id
|
123
|
+
self
|
124
|
+
end
|
125
|
+
|
126
|
+
#
|
127
|
+
# For configuring call-back messages
|
128
|
+
#
|
129
|
+
# @param [NotificationConfig] notifications
|
130
|
+
#
|
131
|
+
# @return [self]
|
132
|
+
#
|
133
|
+
def with_notifications(notifications)
|
134
|
+
@notifications = notifications
|
135
|
+
self
|
136
|
+
end
|
137
|
+
|
138
|
+
#
|
139
|
+
# The check to be performed on each Document
|
140
|
+
#
|
141
|
+
# @param [RequestedCheck] requested_check
|
142
|
+
#
|
143
|
+
# @return [self]
|
144
|
+
#
|
145
|
+
def with_requested_check(requested_check)
|
146
|
+
Validation.assert_is_a(RequestedCheck, requested_check, 'requested_check')
|
147
|
+
@requested_checks.push(requested_check)
|
148
|
+
self
|
149
|
+
end
|
150
|
+
|
151
|
+
#
|
152
|
+
# The task to be performed on each Document
|
153
|
+
#
|
154
|
+
# @param [RequestedTask] requested_task
|
155
|
+
#
|
156
|
+
# @return [self]
|
157
|
+
#
|
158
|
+
def with_requested_task(requested_task)
|
159
|
+
Validation.assert_is_a(RequestedTask, requested_task, 'requested_task')
|
160
|
+
@requested_tasks.push(requested_task)
|
161
|
+
self
|
162
|
+
end
|
163
|
+
|
164
|
+
#
|
165
|
+
# The SDK configuration set on the session specification
|
166
|
+
#
|
167
|
+
# @param [SdkConfig] sdk_config
|
168
|
+
#
|
169
|
+
# @return [self]
|
170
|
+
#
|
171
|
+
def with_sdk_config(sdk_config)
|
172
|
+
@sdk_config = sdk_config
|
173
|
+
self
|
174
|
+
end
|
175
|
+
|
176
|
+
#
|
177
|
+
# Adds a RequiredDocument to the list documents required from the client
|
178
|
+
#
|
179
|
+
# @param [RequiredDocument] required_document
|
180
|
+
#
|
181
|
+
# @return [self]
|
182
|
+
#
|
183
|
+
def with_required_document(required_document)
|
184
|
+
Validation.assert_is_a(RequiredDocument, required_document, 'required_document')
|
185
|
+
@required_documents.push(required_document)
|
186
|
+
self
|
187
|
+
end
|
188
|
+
|
189
|
+
#
|
190
|
+
# Whether or not to block the collection of biometric consent
|
191
|
+
#
|
192
|
+
# @param [Boolean] block_biometric_consent
|
193
|
+
#
|
194
|
+
# @return [self]
|
195
|
+
#
|
196
|
+
def with_block_biometric_consent(block_biometric_consent)
|
197
|
+
@block_biometric_consent = block_biometric_consent
|
198
|
+
self
|
199
|
+
end
|
200
|
+
|
201
|
+
#
|
202
|
+
# @return [SessionSpecification]
|
203
|
+
#
|
204
|
+
def build
|
205
|
+
SessionSpecification.new(
|
206
|
+
@client_session_token_ttl,
|
207
|
+
@resources_ttl,
|
208
|
+
@user_tracking_id,
|
209
|
+
@notifications,
|
210
|
+
@requested_checks,
|
211
|
+
@requested_tasks,
|
212
|
+
@sdk_config,
|
213
|
+
@required_documents,
|
214
|
+
@block_biometric_consent
|
215
|
+
)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Yoti
|
4
|
+
module DocScan
|
5
|
+
module Session
|
6
|
+
module Retrieve
|
7
|
+
class BreakdownResponse
|
8
|
+
# @return [String]
|
9
|
+
attr_reader :sub_check
|
10
|
+
|
11
|
+
# @return [String]
|
12
|
+
attr_reader :result
|
13
|
+
|
14
|
+
# @return [Array<DetailsResponse>]
|
15
|
+
attr_reader :details
|
16
|
+
|
17
|
+
#
|
18
|
+
# @param [Hash] breakdown
|
19
|
+
#
|
20
|
+
def initialize(breakdown)
|
21
|
+
Validation.assert_is_a(String, breakdown['sub_check'], 'sub_check', true)
|
22
|
+
@sub_check = breakdown['sub_check']
|
23
|
+
|
24
|
+
Validation.assert_is_a(String, breakdown['result'], 'result', true)
|
25
|
+
@result = breakdown['result']
|
26
|
+
|
27
|
+
if breakdown['details'].nil?
|
28
|
+
@details = []
|
29
|
+
else
|
30
|
+
Validation.assert_is_a(Array, breakdown['details'], 'details')
|
31
|
+
@details = breakdown['details'].map { |details| DetailsResponse.new(details) }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Yoti
|
4
|
+
module DocScan
|
5
|
+
module Session
|
6
|
+
module Retrieve
|
7
|
+
class CheckResponse
|
8
|
+
# @return [String]
|
9
|
+
attr_reader :type
|
10
|
+
|
11
|
+
# @return [String]
|
12
|
+
attr_reader :id
|
13
|
+
|
14
|
+
# @return [String]
|
15
|
+
attr_reader :state
|
16
|
+
|
17
|
+
# @return [Array<String>]
|
18
|
+
attr_reader :resources_used
|
19
|
+
|
20
|
+
# @return [Array<GeneratedMedia>]
|
21
|
+
attr_reader :generated_media
|
22
|
+
|
23
|
+
# @return [<ReportResponse>]
|
24
|
+
attr_reader :report
|
25
|
+
|
26
|
+
# @return [<DateTime>]
|
27
|
+
attr_reader :created
|
28
|
+
|
29
|
+
# @return [<DateTime>]
|
30
|
+
attr_reader :last_updated
|
31
|
+
|
32
|
+
#
|
33
|
+
# @param [Hash] check
|
34
|
+
#
|
35
|
+
def initialize(check)
|
36
|
+
Validation.assert_is_a(String, check['type'], 'type', true)
|
37
|
+
@type = check['type']
|
38
|
+
|
39
|
+
Validation.assert_is_a(String, check['id'], 'id', true)
|
40
|
+
@id = check['id']
|
41
|
+
|
42
|
+
Validation.assert_is_a(String, check['state'], 'state', true)
|
43
|
+
@state = check['state']
|
44
|
+
|
45
|
+
Validation.assert_is_a(Array, check['resources_used'], 'resources_used', true)
|
46
|
+
@resources_used = check['resources_used']
|
47
|
+
|
48
|
+
if check['generated_media'].nil?
|
49
|
+
@generated_media = []
|
50
|
+
else
|
51
|
+
Validation.assert_is_a(Array, check['generated_media'], 'generated_media')
|
52
|
+
@generated_media = check['generated_media'].map { |media| GeneratedMedia.new(media) }
|
53
|
+
end
|
54
|
+
|
55
|
+
@report = ReportResponse.new(check['report']) unless check['report'].nil?
|
56
|
+
@created = DateTime.parse(check['created']) unless check['created'].nil?
|
57
|
+
@last_updated = DateTime.parse(check['last_updated']) unless check['last_updated'].nil?
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Yoti
|
4
|
+
module DocScan
|
5
|
+
module Session
|
6
|
+
module Retrieve
|
7
|
+
class DetailsResponse
|
8
|
+
# @return [String]
|
9
|
+
attr_reader :name
|
10
|
+
|
11
|
+
# @return [String]
|
12
|
+
attr_reader :value
|
13
|
+
|
14
|
+
#
|
15
|
+
# @param [Hash] details
|
16
|
+
#
|
17
|
+
def initialize(details)
|
18
|
+
Validation.assert_is_a(String, details['name'], 'name', true)
|
19
|
+
@name = details['name']
|
20
|
+
|
21
|
+
Validation.assert_is_a(String, details['value'], 'value', true)
|
22
|
+
@value = details['value']
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Yoti
|
4
|
+
module DocScan
|
5
|
+
module Session
|
6
|
+
module Retrieve
|
7
|
+
class DocumentFieldsResponse
|
8
|
+
# @return [MediaResponse]
|
9
|
+
attr_reader :media
|
10
|
+
|
11
|
+
#
|
12
|
+
# @param [Hash] document_fields
|
13
|
+
#
|
14
|
+
def initialize(document_fields)
|
15
|
+
@media = MediaResponse.new(document_fields['media']) unless document_fields['media'].nil?
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Yoti
|
4
|
+
module DocScan
|
5
|
+
module Session
|
6
|
+
module Retrieve
|
7
|
+
class DocumentIdPhotoResponse
|
8
|
+
# @return [MediaResponse]
|
9
|
+
attr_reader :media
|
10
|
+
|
11
|
+
#
|
12
|
+
# @param [Hash] document_id_photo
|
13
|
+
#
|
14
|
+
def initialize(document_id_photo)
|
15
|
+
@media = MediaResponse.new(document_id_photo['media']) unless document_id_photo['media'].nil?
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Yoti
|
4
|
+
module DocScan
|
5
|
+
module Session
|
6
|
+
module Retrieve
|
7
|
+
class FaceMapResponse
|
8
|
+
# @return [MediaResponse]
|
9
|
+
attr_reader :media
|
10
|
+
|
11
|
+
#
|
12
|
+
# @param [Hash] facemap
|
13
|
+
#
|
14
|
+
def initialize(facemap)
|
15
|
+
@media = MediaResponse.new(facemap['media']) unless facemap['media'].nil?
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|