yoti_sandbox 1.0.0 → 1.4.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 +8 -1
- data/lib/yoti_sandbox.rb +1 -0
- data/lib/yoti_sandbox/doc_scan.rb +25 -0
- data/lib/yoti_sandbox/doc_scan/client.rb +59 -0
- data/lib/yoti_sandbox/doc_scan/errors.rb +83 -0
- data/lib/yoti_sandbox/doc_scan/request/check/check.rb +115 -0
- data/lib/yoti_sandbox/doc_scan/request/check/document_authenticity_check.rb +29 -0
- data/lib/yoti_sandbox/doc_scan/request/check/document_check.rb +48 -0
- data/lib/yoti_sandbox/doc_scan/request/check/document_face_match_check.rb +29 -0
- data/lib/yoti_sandbox/doc_scan/request/check/document_text_data_check.rb +78 -0
- data/lib/yoti_sandbox/doc_scan/request/check/id_document_comparison_check.rb +56 -0
- data/lib/yoti_sandbox/doc_scan/request/check/liveness_check.rb +34 -0
- data/lib/yoti_sandbox/doc_scan/request/check/report/breakdown.rb +97 -0
- data/lib/yoti_sandbox/doc_scan/request/check/report/detail.rb +34 -0
- data/lib/yoti_sandbox/doc_scan/request/check/report/recommendation.rb +88 -0
- data/lib/yoti_sandbox/doc_scan/request/check/supplementary_document_text_data_check.rb +78 -0
- data/lib/yoti_sandbox/doc_scan/request/check/zoom_liveness_check.rb +36 -0
- data/lib/yoti_sandbox/doc_scan/request/check_reports.rb +182 -0
- data/lib/yoti_sandbox/doc_scan/request/document_filter.rb +77 -0
- data/lib/yoti_sandbox/doc_scan/request/response_config.rb +75 -0
- data/lib/yoti_sandbox/doc_scan/request/task/document_id_photo.rb +35 -0
- data/lib/yoti_sandbox/doc_scan/request/task/document_text_data_extraction_task.rb +170 -0
- data/lib/yoti_sandbox/doc_scan/request/task/supplementary_document_text_data_extraction_task.rb +152 -0
- data/lib/yoti_sandbox/doc_scan/request/task/text_data_extraction_reason.rb +78 -0
- data/lib/yoti_sandbox/doc_scan/request/task/text_data_extraction_recommendation.rb +86 -0
- data/lib/yoti_sandbox/doc_scan/request/task_results.rb +96 -0
- data/lib/yoti_sandbox/profile.rb +5 -0
- data/lib/yoti_sandbox/profile/age_verification.rb +7 -0
- data/lib/yoti_sandbox/profile/client.rb +8 -26
- data/lib/yoti_sandbox/profile/document_images.rb +69 -0
- data/lib/yoti_sandbox/profile/extra_data.rb +92 -0
- data/lib/yoti_sandbox/profile/third_party.rb +158 -0
- data/lib/yoti_sandbox/profile/token_request.rb +47 -6
- data/yoti_sandbox.gemspec +18 -5
- metadata +38 -18
- data/.github/ISSUE_TEMPLATE.md +0 -17
- data/.gitignore +0 -56
- data/CONTRIBUTING.md +0 -30
- data/Guardfile +0 -11
- data/Rakefile +0 -41
- data/rubocop.yml +0 -44
- data/yardstick.yml +0 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 50664707d70da0ad9d28f183b68074c795f74ef59c05dbf4fbd1a0cef280d819
|
4
|
+
data.tar.gz: 6bae10a1f1675592f178d9ae1f47fdf3992196f1480a03e6a736b177a7638ea9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fe42559fa4b25d2a3f5d239471bfe86a0f1b59e00cb6c4bcb52cd895b52f2e2a4bf8087ce710d50be5514cf745e804204e245dac7f8bdf1dccf6157c9092efa1
|
7
|
+
data.tar.gz: 8de1edf888b5ede3604532ed62dd4210f7a55aa3de899c79062fefa980c37bd7d1ebf4045a40e9d879b5c87e83b3f2331d79985d7ecc447c1224a28f836afef7
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,5 +1,11 @@
|
|
1
1
|
# Yoti Ruby Sandbox SDK
|
2
2
|
|
3
|
+
[](https://travis-ci.com/getyoti/yoti-ruby-sdk-sandbox)
|
4
|
+
[](https://sonarcloud.io/dashboard?id=getyoti%3Aruby-sandbox)
|
5
|
+
[](https://sonarcloud.io/dashboard?id=getyoti%3Aruby-sandbox)
|
6
|
+
[](https://sonarcloud.io/dashboard?id=getyoti%3Aruby-sandbox)
|
7
|
+
[](https://sonarcloud.io/dashboard?id=getyoti%3Aruby-sandbox)
|
8
|
+
|
3
9
|
This repository contains the tools you need to test your Yoti integration.
|
4
10
|
|
5
11
|
## Installing the Sandbox
|
@@ -42,4 +48,5 @@ end
|
|
42
48
|
|
43
49
|
## Examples
|
44
50
|
|
45
|
-
-
|
51
|
+
- [Profile Sandbox](examples/profile)
|
52
|
+
- [Doc Scan Sandbox](examples/doc_scan)
|
data/lib/yoti_sandbox.rb
CHANGED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'yoti'
|
2
|
+
|
3
|
+
require_relative 'doc_scan/client'
|
4
|
+
require_relative 'doc_scan/errors'
|
5
|
+
require_relative 'doc_scan/request/task_results'
|
6
|
+
require_relative 'doc_scan/request/check_reports'
|
7
|
+
require_relative 'doc_scan/request/response_config'
|
8
|
+
require_relative 'doc_scan/request/document_filter'
|
9
|
+
require_relative 'doc_scan/request/check/check'
|
10
|
+
require_relative 'doc_scan/request/check/document_check'
|
11
|
+
require_relative 'doc_scan/request/check/document_authenticity_check'
|
12
|
+
require_relative 'doc_scan/request/check/id_document_comparison_check'
|
13
|
+
require_relative 'doc_scan/request/check/document_face_match_check'
|
14
|
+
require_relative 'doc_scan/request/check/document_text_data_check'
|
15
|
+
require_relative 'doc_scan/request/check/supplementary_document_text_data_check'
|
16
|
+
require_relative 'doc_scan/request/check/liveness_check'
|
17
|
+
require_relative 'doc_scan/request/check/zoom_liveness_check'
|
18
|
+
require_relative 'doc_scan/request/check/report/breakdown'
|
19
|
+
require_relative 'doc_scan/request/check/report/recommendation'
|
20
|
+
require_relative 'doc_scan/request/check/report/detail'
|
21
|
+
require_relative 'doc_scan/request/task/document_text_data_extraction_task'
|
22
|
+
require_relative 'doc_scan/request/task/supplementary_document_text_data_extraction_task'
|
23
|
+
require_relative 'doc_scan/request/task/document_id_photo'
|
24
|
+
require_relative 'doc_scan/request/task/text_data_extraction_recommendation'
|
25
|
+
require_relative 'doc_scan/request/task/text_data_extraction_reason'
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Yoti
|
4
|
+
module Sandbox
|
5
|
+
module DocScan
|
6
|
+
#
|
7
|
+
# Client for the Doc Scan sandbox service
|
8
|
+
#
|
9
|
+
class Client
|
10
|
+
#
|
11
|
+
# @param [String] base_url
|
12
|
+
#
|
13
|
+
def initialize(base_url: nil)
|
14
|
+
@base_url = base_url || "#{Yoti.configuration.api_url}/sandbox/idverify/v1"
|
15
|
+
end
|
16
|
+
|
17
|
+
#
|
18
|
+
# @param [String] session_id
|
19
|
+
# @param [Yoti::SandboxDocScan::Request::ResponseConfig] response_config
|
20
|
+
#
|
21
|
+
def configure_session_response(session_id, response_config)
|
22
|
+
request = Yoti::Request
|
23
|
+
.builder
|
24
|
+
.with_http_method('PUT')
|
25
|
+
.with_base_url(@base_url)
|
26
|
+
.with_endpoint("sessions/#{session_id}/response-config")
|
27
|
+
.with_query_param('sdkId', Yoti.configuration.client_sdk_id)
|
28
|
+
.with_payload(response_config)
|
29
|
+
.build
|
30
|
+
|
31
|
+
begin
|
32
|
+
request.execute
|
33
|
+
rescue Yoti::RequestError => e
|
34
|
+
raise Yoti::Sandbox::DocScan::Error.wrap(e)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
#
|
39
|
+
# @param [Yoti::SandboxDocScan::Request::ResponseConfig] response_config
|
40
|
+
#
|
41
|
+
def configure_application_response(response_config)
|
42
|
+
request = Yoti::Request
|
43
|
+
.builder
|
44
|
+
.with_http_method('PUT')
|
45
|
+
.with_base_url(@base_url)
|
46
|
+
.with_endpoint("apps/#{Yoti.configuration.client_sdk_id}/response-config")
|
47
|
+
.with_payload(response_config)
|
48
|
+
.build
|
49
|
+
|
50
|
+
begin
|
51
|
+
request.execute
|
52
|
+
rescue Yoti::RequestError => e
|
53
|
+
raise Yoti::Sandbox::DocScan::Error.wrap(e)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Yoti
|
4
|
+
module Sandbox
|
5
|
+
module DocScan
|
6
|
+
#
|
7
|
+
# Raises exceptions related to Doc Scan Sandbox API requests
|
8
|
+
#
|
9
|
+
class Error < RequestError
|
10
|
+
def initialize(msg = nil, response = nil)
|
11
|
+
super(msg, response)
|
12
|
+
|
13
|
+
@default_message = msg
|
14
|
+
end
|
15
|
+
|
16
|
+
def message
|
17
|
+
@message ||= format_message
|
18
|
+
end
|
19
|
+
|
20
|
+
#
|
21
|
+
# Wraps an existing error
|
22
|
+
#
|
23
|
+
# @param [Error] error
|
24
|
+
#
|
25
|
+
# @return [self]
|
26
|
+
#
|
27
|
+
def self.wrap(error)
|
28
|
+
new(error.message, error.response)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
#
|
34
|
+
# Formats error message from response.
|
35
|
+
#
|
36
|
+
# @return [String]
|
37
|
+
#
|
38
|
+
def format_message
|
39
|
+
return @default_message if @response.nil? || @response['Content-Type'] != 'application/json'
|
40
|
+
|
41
|
+
json = JSON.parse(@response.body)
|
42
|
+
format_response(json) || @default_message
|
43
|
+
end
|
44
|
+
|
45
|
+
#
|
46
|
+
# Format JSON error response.
|
47
|
+
#
|
48
|
+
# @param [Hash] json
|
49
|
+
#
|
50
|
+
# @return [String, nil]
|
51
|
+
#
|
52
|
+
def format_response(json)
|
53
|
+
return nil if json['code'].nil? || json['message'].nil?
|
54
|
+
|
55
|
+
code_message = "#{json['code']} - #{json['message']}"
|
56
|
+
|
57
|
+
unless json['errors'].nil?
|
58
|
+
property_errors = format_property_errors(json['errors'])
|
59
|
+
|
60
|
+
return "#{code_message}: #{property_errors.compact.join(', ')}" if property_errors.count.positive?
|
61
|
+
end
|
62
|
+
|
63
|
+
code_message
|
64
|
+
end
|
65
|
+
|
66
|
+
#
|
67
|
+
# Format property errors.
|
68
|
+
#
|
69
|
+
# @param [Array<Hash>] errors
|
70
|
+
#
|
71
|
+
# @return [Array<String>]
|
72
|
+
#
|
73
|
+
def format_property_errors(errors)
|
74
|
+
errors
|
75
|
+
.map do |e|
|
76
|
+
"#{e['property']} \"#{e['message']}\"" if e['property'] && e['message']
|
77
|
+
end
|
78
|
+
.compact
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Yoti
|
4
|
+
module Sandbox
|
5
|
+
module DocScan
|
6
|
+
module Request
|
7
|
+
class Check
|
8
|
+
#
|
9
|
+
# @param [CheckResult] result
|
10
|
+
#
|
11
|
+
def initialize(result)
|
12
|
+
raise(TypeError, "#{self.class} cannot be instantiated") if instance_of?(Check)
|
13
|
+
|
14
|
+
Validation.assert_is_a(CheckResult, result, 'result')
|
15
|
+
@result = result
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_json(*_args)
|
19
|
+
as_json.to_json
|
20
|
+
end
|
21
|
+
|
22
|
+
def as_json(*_args)
|
23
|
+
{
|
24
|
+
result: @result.as_json
|
25
|
+
}
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class CheckBuilder
|
30
|
+
def initialize
|
31
|
+
@breakdowns = []
|
32
|
+
end
|
33
|
+
|
34
|
+
#
|
35
|
+
# @param [Recommendation] recommendation
|
36
|
+
#
|
37
|
+
# @return [self]
|
38
|
+
#
|
39
|
+
def with_recommendation(recommendation)
|
40
|
+
Validation.assert_is_a(Recommendation, recommendation, 'recommendation')
|
41
|
+
@recommendation = recommendation
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
#
|
46
|
+
# @param [Breakdown] breakdown
|
47
|
+
#
|
48
|
+
# @return [self]
|
49
|
+
#
|
50
|
+
def with_breakdown(breakdown)
|
51
|
+
Validation.assert_is_a(Breakdown, breakdown, 'breakdown')
|
52
|
+
@breakdowns.push(breakdown)
|
53
|
+
self
|
54
|
+
end
|
55
|
+
|
56
|
+
#
|
57
|
+
# @param [Array<Breakdown>] breakdowns
|
58
|
+
#
|
59
|
+
# @return [self]
|
60
|
+
#
|
61
|
+
def with_breakdowns(breakdowns)
|
62
|
+
Validation.assert_is_a(Array, breakdowns, 'breakdown')
|
63
|
+
@breakdowns = breakdowns
|
64
|
+
self
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class CheckResult
|
69
|
+
#
|
70
|
+
# @param [CheckReport] report
|
71
|
+
#
|
72
|
+
def initialize(report)
|
73
|
+
Validation.assert_is_a(CheckReport, report, 'report')
|
74
|
+
@report = report
|
75
|
+
end
|
76
|
+
|
77
|
+
def to_json(*_args)
|
78
|
+
as_json.to_json
|
79
|
+
end
|
80
|
+
|
81
|
+
def as_json(*_args)
|
82
|
+
{
|
83
|
+
report: @report.as_json
|
84
|
+
}
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
class CheckReport
|
89
|
+
#
|
90
|
+
# @param [Recommendation] recommendation
|
91
|
+
# @param [Breakdown] breakdowns
|
92
|
+
#
|
93
|
+
def initialize(recommendation, breakdowns)
|
94
|
+
Validation.assert_is_a(Recommendation, recommendation, 'recommendation')
|
95
|
+
@recommendation = recommendation
|
96
|
+
|
97
|
+
Validation.assert_is_a(Array, breakdowns, 'breakdowns')
|
98
|
+
@breakdowns = breakdowns
|
99
|
+
end
|
100
|
+
|
101
|
+
def to_json(*_args)
|
102
|
+
as_json.to_json
|
103
|
+
end
|
104
|
+
|
105
|
+
def as_json(*_args)
|
106
|
+
{
|
107
|
+
recommendation: @recommendation.as_json,
|
108
|
+
breakdown: @breakdowns.map(&:as_json)
|
109
|
+
}
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Yoti
|
4
|
+
module Sandbox
|
5
|
+
module DocScan
|
6
|
+
module Request
|
7
|
+
class DocumentAuthenticityCheck < DocumentCheck
|
8
|
+
#
|
9
|
+
# @return [DocumentAuthenticityCheckBuilder]
|
10
|
+
#
|
11
|
+
def self.builder
|
12
|
+
DocumentAuthenticityCheckBuilder.new
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class DocumentAuthenticityCheckBuilder < DocumentCheckBuilder
|
17
|
+
#
|
18
|
+
# @return [DocumentAuthenticityCheck]
|
19
|
+
#
|
20
|
+
def build
|
21
|
+
report = CheckReport.new(@recommendation, @breakdowns)
|
22
|
+
result = CheckResult.new(report)
|
23
|
+
DocumentAuthenticityCheck.new(result, @document_filter)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Yoti
|
4
|
+
module Sandbox
|
5
|
+
module DocScan
|
6
|
+
module Request
|
7
|
+
class DocumentCheck < Check
|
8
|
+
#
|
9
|
+
# @param [CheckResult] result
|
10
|
+
# @param [DocumentFilter] document_filter
|
11
|
+
#
|
12
|
+
def initialize(result, document_filter)
|
13
|
+
raise(TypeError, "#{self.class} cannot be instantiated") if instance_of?(DocumentCheck)
|
14
|
+
|
15
|
+
super(result)
|
16
|
+
|
17
|
+
Validation.assert_is_a(DocumentFilter, document_filter, 'document_filter', true)
|
18
|
+
@document_filter = document_filter
|
19
|
+
end
|
20
|
+
|
21
|
+
def as_json(*_args)
|
22
|
+
json = super
|
23
|
+
json[:document_filter] = @document_filter.as_json unless @document_filter.nil?
|
24
|
+
json
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class DocumentCheckBuilder < CheckBuilder
|
29
|
+
def initialize
|
30
|
+
raise(TypeError, "#{self.class} cannot be instantiated") if instance_of?(DocumentCheckBuilder)
|
31
|
+
|
32
|
+
super
|
33
|
+
end
|
34
|
+
|
35
|
+
#
|
36
|
+
# @param [DocumentFilter] document_filter
|
37
|
+
#
|
38
|
+
# @return [self]
|
39
|
+
#
|
40
|
+
def with_document_filter(document_filter)
|
41
|
+
@document_filter = document_filter
|
42
|
+
self
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Yoti
|
4
|
+
module Sandbox
|
5
|
+
module DocScan
|
6
|
+
module Request
|
7
|
+
class DocumentFaceMatchCheck < DocumentCheck
|
8
|
+
#
|
9
|
+
# @return [DocumentFaceMatchCheckBuilder]
|
10
|
+
#
|
11
|
+
def self.builder
|
12
|
+
DocumentFaceMatchCheckBuilder.new
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class DocumentFaceMatchCheckBuilder < DocumentCheckBuilder
|
17
|
+
#
|
18
|
+
# @return [DocumentFaceMatchCheck]
|
19
|
+
#
|
20
|
+
def build
|
21
|
+
report = CheckReport.new(@recommendation, @breakdowns)
|
22
|
+
result = CheckResult.new(report)
|
23
|
+
DocumentFaceMatchCheck.new(result, @document_filter)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module Yoti
|
6
|
+
module Sandbox
|
7
|
+
module DocScan
|
8
|
+
module Request
|
9
|
+
class DocumentTextDataCheck < DocumentCheck
|
10
|
+
#
|
11
|
+
# @return [DocumentTextDataCheckBuilder]
|
12
|
+
#
|
13
|
+
def self.builder
|
14
|
+
DocumentTextDataCheckBuilder.new
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class DocumentTextDataCheckResult < CheckResult
|
19
|
+
#
|
20
|
+
# @param [CheckReport] report
|
21
|
+
# @param [Hash,nil] document_fields
|
22
|
+
#
|
23
|
+
def initialize(report, document_fields)
|
24
|
+
super(report)
|
25
|
+
|
26
|
+
unless document_fields.nil?
|
27
|
+
Validation.assert_is_a(Hash, document_fields, 'document_fields')
|
28
|
+
document_fields.each { |_k, v| Validation.assert_respond_to(:to_json, v, 'document_fields value') }
|
29
|
+
end
|
30
|
+
@document_fields = document_fields
|
31
|
+
end
|
32
|
+
|
33
|
+
def as_json(*_args)
|
34
|
+
super.merge(
|
35
|
+
document_fields: @document_fields
|
36
|
+
).compact
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class DocumentTextDataCheckBuilder < DocumentCheckBuilder
|
41
|
+
#
|
42
|
+
# @param [String] key
|
43
|
+
# @param [#to_json] value
|
44
|
+
#
|
45
|
+
# @return [self]
|
46
|
+
#
|
47
|
+
def with_document_field(key, value)
|
48
|
+
Validation.assert_is_a(String, key, 'key')
|
49
|
+
Validation.assert_respond_to(:to_json, value, 'value')
|
50
|
+
@document_fields ||= {}
|
51
|
+
@document_fields[key] = value
|
52
|
+
self
|
53
|
+
end
|
54
|
+
|
55
|
+
#
|
56
|
+
# @param [Hash] document_fields
|
57
|
+
#
|
58
|
+
# @return [self]
|
59
|
+
#
|
60
|
+
def with_document_fields(document_fields)
|
61
|
+
Validation.assert_is_a(Hash, document_fields, 'document_fields')
|
62
|
+
@document_fields = document_fields
|
63
|
+
self
|
64
|
+
end
|
65
|
+
|
66
|
+
#
|
67
|
+
# @return [DocumentTextDataCheck]
|
68
|
+
#
|
69
|
+
def build
|
70
|
+
report = CheckReport.new(@recommendation, @breakdowns)
|
71
|
+
result = DocumentTextDataCheckResult.new(report, @document_fields)
|
72
|
+
DocumentTextDataCheck.new(result, @document_filter)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|