yoti 1.5.0 → 1.6.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.
- 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
|