kapso-client-ruby 1.0.0 → 1.0.2

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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +81 -81
  3. data/CHANGELOG.md +262 -91
  4. data/Gemfile +20 -20
  5. data/RAILS_INTEGRATION.md +478 -0
  6. data/README.md +1053 -734
  7. data/Rakefile +40 -40
  8. data/TEMPLATE_TOOLS_GUIDE.md +120 -120
  9. data/WHATSAPP_24_HOUR_GUIDE.md +133 -133
  10. data/examples/advanced_features.rb +352 -349
  11. data/examples/advanced_messaging.rb +241 -0
  12. data/examples/basic_messaging.rb +139 -136
  13. data/examples/enhanced_interactive.rb +400 -0
  14. data/examples/flows_usage.rb +307 -0
  15. data/examples/interactive_messages.rb +343 -0
  16. data/examples/media_management.rb +256 -253
  17. data/examples/rails/jobs.rb +388 -0
  18. data/examples/rails/models.rb +240 -0
  19. data/examples/rails/notifications_controller.rb +227 -0
  20. data/examples/template_management.rb +393 -390
  21. data/kapso-ruby-logo.jpg +0 -0
  22. data/lib/kapso_client_ruby/client.rb +321 -316
  23. data/lib/kapso_client_ruby/errors.rb +348 -329
  24. data/lib/kapso_client_ruby/rails/generators/install_generator.rb +76 -0
  25. data/lib/kapso_client_ruby/rails/generators/templates/env.erb +21 -0
  26. data/lib/kapso_client_ruby/rails/generators/templates/initializer.rb.erb +33 -0
  27. data/lib/kapso_client_ruby/rails/generators/templates/message_service.rb.erb +138 -0
  28. data/lib/kapso_client_ruby/rails/generators/templates/webhook_controller.rb.erb +62 -0
  29. data/lib/kapso_client_ruby/rails/railtie.rb +55 -0
  30. data/lib/kapso_client_ruby/rails/service.rb +189 -0
  31. data/lib/kapso_client_ruby/rails/tasks.rake +167 -0
  32. data/lib/kapso_client_ruby/resources/calls.rb +172 -172
  33. data/lib/kapso_client_ruby/resources/contacts.rb +190 -190
  34. data/lib/kapso_client_ruby/resources/conversations.rb +103 -103
  35. data/lib/kapso_client_ruby/resources/flows.rb +382 -0
  36. data/lib/kapso_client_ruby/resources/media.rb +205 -205
  37. data/lib/kapso_client_ruby/resources/messages.rb +760 -380
  38. data/lib/kapso_client_ruby/resources/phone_numbers.rb +85 -85
  39. data/lib/kapso_client_ruby/resources/templates.rb +283 -283
  40. data/lib/kapso_client_ruby/types.rb +348 -262
  41. data/lib/kapso_client_ruby/version.rb +5 -5
  42. data/lib/kapso_client_ruby.rb +75 -68
  43. data/scripts/.env.example +17 -17
  44. data/scripts/kapso_template_finder.rb +91 -91
  45. data/scripts/sdk_setup.rb +404 -404
  46. data/scripts/test.rb +60 -60
  47. metadata +24 -3
@@ -1,191 +1,191 @@
1
- # frozen_string_literal: true
2
-
3
- module KapsoClientRuby
4
- module Resources
5
- class Contacts
6
- def initialize(client)
7
- @client = client
8
- end
9
-
10
- # List contacts (Kapso Proxy only)
11
- def list(phone_number_id:, customer_id: nil, phone_number: nil,
12
- profile_name: nil, limit: nil, after: nil, before: nil, fields: nil)
13
- assert_kapso_proxy('Contacts API')
14
-
15
- query_params = {
16
- customer_id: customer_id,
17
- phone_number: phone_number,
18
- profile_name: profile_name,
19
- limit: limit,
20
- after: after,
21
- before: before,
22
- fields: fields
23
- }.compact
24
-
25
- response = @client.request(:get, "#{phone_number_id}/contacts",
26
- query: query_params, response_type: :json)
27
- Types::PagedResponse.new(response, Types::ContactRecord)
28
- end
29
-
30
- # Get contact details (Kapso Proxy only)
31
- def get(phone_number_id:, wa_id:, fields: nil)
32
- assert_kapso_proxy('Contacts API')
33
-
34
- raise ArgumentError, 'wa_id cannot be empty' if wa_id.nil? || wa_id.strip.empty?
35
-
36
- query_params = {}
37
- query_params[:fields] = fields if fields
38
-
39
- response = @client.request(:get, "#{phone_number_id}/contacts/#{wa_id}",
40
- query: query_params, response_type: :json)
41
-
42
- # Handle both single object and data envelope responses
43
- if response.is_a?(Hash) && response.key?('data')
44
- Types::ContactRecord.new(response['data'])
45
- else
46
- Types::ContactRecord.new(response)
47
- end
48
- end
49
-
50
- # Update contact metadata (Kapso Proxy only)
51
- def update(phone_number_id:, wa_id:, metadata: nil, tags: nil,
52
- customer_id: nil, notes: nil)
53
- assert_kapso_proxy('Contacts API')
54
-
55
- raise ArgumentError, 'wa_id cannot be empty' if wa_id.nil? || wa_id.strip.empty?
56
-
57
- payload = {}
58
- payload[:metadata] = metadata if metadata
59
- payload[:tags] = tags if tags
60
- payload[:customer_id] = customer_id if customer_id
61
- payload[:notes] = notes if notes
62
-
63
- return if payload.empty?
64
-
65
- response = @client.request(:patch, "#{phone_number_id}/contacts/#{wa_id}",
66
- body: payload.to_json, response_type: :json)
67
- Types::GraphSuccessResponse.new(response)
68
- end
69
-
70
- # Add tags to contact (Kapso Proxy only)
71
- def add_tags(phone_number_id:, wa_id:, tags:)
72
- raise ArgumentError, 'tags cannot be empty' if tags.nil? || tags.empty?
73
-
74
- current_contact = get(phone_number_id: phone_number_id, wa_id: wa_id)
75
- existing_tags = (current_contact.metadata&.[]('tags') || [])
76
- new_tags = (existing_tags + Array(tags)).uniq
77
-
78
- update(phone_number_id: phone_number_id, wa_id: wa_id,
79
- metadata: { tags: new_tags })
80
- end
81
-
82
- # Remove tags from contact (Kapso Proxy only)
83
- def remove_tags(phone_number_id:, wa_id:, tags:)
84
- raise ArgumentError, 'tags cannot be empty' if tags.nil? || tags.empty?
85
-
86
- current_contact = get(phone_number_id: phone_number_id, wa_id: wa_id)
87
- existing_tags = (current_contact.metadata&.[]('tags') || [])
88
- remaining_tags = existing_tags - Array(tags)
89
-
90
- update(phone_number_id: phone_number_id, wa_id: wa_id,
91
- metadata: { tags: remaining_tags })
92
- end
93
-
94
- # Search contacts by various criteria (Kapso Proxy only)
95
- def search(phone_number_id:, query:, search_in: ['profile_name', 'phone_number'],
96
- limit: nil, after: nil, before: nil)
97
- assert_kapso_proxy('Contacts Search API')
98
-
99
- raise ArgumentError, 'query cannot be empty' if query.nil? || query.strip.empty?
100
-
101
- query_params = {
102
- q: query,
103
- search_in: Array(search_in).join(','),
104
- limit: limit,
105
- after: after,
106
- before: before
107
- }.compact
108
-
109
- response = @client.request(:get, "#{phone_number_id}/contacts/search",
110
- query: query_params, response_type: :json)
111
- Types::PagedResponse.new(response, Types::ContactRecord)
112
- end
113
-
114
- # Get contact analytics (Kapso Proxy only)
115
- def analytics(phone_number_id:, wa_id: nil, since: nil, until_time: nil,
116
- granularity: 'day', metrics: nil)
117
- assert_kapso_proxy('Contact Analytics API')
118
-
119
- query_params = {
120
- wa_id: wa_id,
121
- since: since,
122
- until: until_time,
123
- granularity: granularity
124
- }
125
- query_params[:metrics] = Array(metrics).join(',') if metrics
126
- query_params = query_params.compact
127
-
128
- response = @client.request(:get, "#{phone_number_id}/contacts/analytics",
129
- query: query_params, response_type: :json)
130
- response
131
- end
132
-
133
- # Export contacts (Kapso Proxy only)
134
- def export(phone_number_id:, format: 'csv', filters: nil)
135
- assert_kapso_proxy('Contacts Export API')
136
-
137
- payload = {
138
- format: format,
139
- filters: filters
140
- }.compact
141
-
142
- response = @client.request(:post, "#{phone_number_id}/contacts/export",
143
- body: payload.to_json, response_type: :json)
144
- response
145
- end
146
-
147
- # Import contacts (Kapso Proxy only)
148
- def import(phone_number_id:, file:, format: 'csv', mapping: nil,
149
- duplicate_handling: 'skip')
150
- assert_kapso_proxy('Contacts Import API')
151
-
152
- # Build multipart form data
153
- form_data = {
154
- 'format' => format,
155
- 'duplicate_handling' => duplicate_handling
156
- }
157
-
158
- # Handle file parameter
159
- file_obj = case file
160
- when String
161
- File.open(file, 'rb')
162
- when File, IO, StringIO
163
- file
164
- else
165
- raise ArgumentError, 'file must be a File, IO object, or file path string'
166
- end
167
-
168
- form_data['file'] = Faraday::UploadIO.new(file_obj, 'text/csv', 'contacts.csv')
169
- form_data['mapping'] = mapping.to_json if mapping
170
-
171
- headers = { 'Content-Type' => 'multipart/form-data' }
172
-
173
- response = @client.request(:post, "#{phone_number_id}/contacts/import",
174
- body: form_data, headers: headers, response_type: :json)
175
-
176
- # Close file if we opened it
177
- file_obj.close if file.is_a?(String) && file_obj.respond_to?(:close)
178
-
179
- response
180
- end
181
-
182
- private
183
-
184
- def assert_kapso_proxy(feature)
185
- unless @client.kapso_proxy?
186
- raise Errors::KapsoProxyRequiredError.new(feature)
187
- end
188
- end
189
- end
190
- end
1
+ # frozen_string_literal: true
2
+
3
+ module KapsoClientRuby
4
+ module Resources
5
+ class Contacts
6
+ def initialize(client)
7
+ @client = client
8
+ end
9
+
10
+ # List contacts (Kapso Proxy only)
11
+ def list(phone_number_id:, customer_id: nil, phone_number: nil,
12
+ profile_name: nil, limit: nil, after: nil, before: nil, fields: nil)
13
+ assert_kapso_proxy('Contacts API')
14
+
15
+ query_params = {
16
+ customer_id: customer_id,
17
+ phone_number: phone_number,
18
+ profile_name: profile_name,
19
+ limit: limit,
20
+ after: after,
21
+ before: before,
22
+ fields: fields
23
+ }.compact
24
+
25
+ response = @client.request(:get, "#{phone_number_id}/contacts",
26
+ query: query_params, response_type: :json)
27
+ Types::PagedResponse.new(response, Types::ContactRecord)
28
+ end
29
+
30
+ # Get contact details (Kapso Proxy only)
31
+ def get(phone_number_id:, wa_id:, fields: nil)
32
+ assert_kapso_proxy('Contacts API')
33
+
34
+ raise ArgumentError, 'wa_id cannot be empty' if wa_id.nil? || wa_id.strip.empty?
35
+
36
+ query_params = {}
37
+ query_params[:fields] = fields if fields
38
+
39
+ response = @client.request(:get, "#{phone_number_id}/contacts/#{wa_id}",
40
+ query: query_params, response_type: :json)
41
+
42
+ # Handle both single object and data envelope responses
43
+ if response.is_a?(Hash) && response.key?('data')
44
+ Types::ContactRecord.new(response['data'])
45
+ else
46
+ Types::ContactRecord.new(response)
47
+ end
48
+ end
49
+
50
+ # Update contact metadata (Kapso Proxy only)
51
+ def update(phone_number_id:, wa_id:, metadata: nil, tags: nil,
52
+ customer_id: nil, notes: nil)
53
+ assert_kapso_proxy('Contacts API')
54
+
55
+ raise ArgumentError, 'wa_id cannot be empty' if wa_id.nil? || wa_id.strip.empty?
56
+
57
+ payload = {}
58
+ payload[:metadata] = metadata if metadata
59
+ payload[:tags] = tags if tags
60
+ payload[:customer_id] = customer_id if customer_id
61
+ payload[:notes] = notes if notes
62
+
63
+ return if payload.empty?
64
+
65
+ response = @client.request(:patch, "#{phone_number_id}/contacts/#{wa_id}",
66
+ body: payload.to_json, response_type: :json)
67
+ Types::GraphSuccessResponse.new(response)
68
+ end
69
+
70
+ # Add tags to contact (Kapso Proxy only)
71
+ def add_tags(phone_number_id:, wa_id:, tags:)
72
+ raise ArgumentError, 'tags cannot be empty' if tags.nil? || tags.empty?
73
+
74
+ current_contact = get(phone_number_id: phone_number_id, wa_id: wa_id)
75
+ existing_tags = (current_contact.metadata&.[]('tags') || [])
76
+ new_tags = (existing_tags + Array(tags)).uniq
77
+
78
+ update(phone_number_id: phone_number_id, wa_id: wa_id,
79
+ metadata: { tags: new_tags })
80
+ end
81
+
82
+ # Remove tags from contact (Kapso Proxy only)
83
+ def remove_tags(phone_number_id:, wa_id:, tags:)
84
+ raise ArgumentError, 'tags cannot be empty' if tags.nil? || tags.empty?
85
+
86
+ current_contact = get(phone_number_id: phone_number_id, wa_id: wa_id)
87
+ existing_tags = (current_contact.metadata&.[]('tags') || [])
88
+ remaining_tags = existing_tags - Array(tags)
89
+
90
+ update(phone_number_id: phone_number_id, wa_id: wa_id,
91
+ metadata: { tags: remaining_tags })
92
+ end
93
+
94
+ # Search contacts by various criteria (Kapso Proxy only)
95
+ def search(phone_number_id:, query:, search_in: ['profile_name', 'phone_number'],
96
+ limit: nil, after: nil, before: nil)
97
+ assert_kapso_proxy('Contacts Search API')
98
+
99
+ raise ArgumentError, 'query cannot be empty' if query.nil? || query.strip.empty?
100
+
101
+ query_params = {
102
+ q: query,
103
+ search_in: Array(search_in).join(','),
104
+ limit: limit,
105
+ after: after,
106
+ before: before
107
+ }.compact
108
+
109
+ response = @client.request(:get, "#{phone_number_id}/contacts/search",
110
+ query: query_params, response_type: :json)
111
+ Types::PagedResponse.new(response, Types::ContactRecord)
112
+ end
113
+
114
+ # Get contact analytics (Kapso Proxy only)
115
+ def analytics(phone_number_id:, wa_id: nil, since: nil, until_time: nil,
116
+ granularity: 'day', metrics: nil)
117
+ assert_kapso_proxy('Contact Analytics API')
118
+
119
+ query_params = {
120
+ wa_id: wa_id,
121
+ since: since,
122
+ until: until_time,
123
+ granularity: granularity
124
+ }
125
+ query_params[:metrics] = Array(metrics).join(',') if metrics
126
+ query_params = query_params.compact
127
+
128
+ response = @client.request(:get, "#{phone_number_id}/contacts/analytics",
129
+ query: query_params, response_type: :json)
130
+ response
131
+ end
132
+
133
+ # Export contacts (Kapso Proxy only)
134
+ def export(phone_number_id:, format: 'csv', filters: nil)
135
+ assert_kapso_proxy('Contacts Export API')
136
+
137
+ payload = {
138
+ format: format,
139
+ filters: filters
140
+ }.compact
141
+
142
+ response = @client.request(:post, "#{phone_number_id}/contacts/export",
143
+ body: payload.to_json, response_type: :json)
144
+ response
145
+ end
146
+
147
+ # Import contacts (Kapso Proxy only)
148
+ def import(phone_number_id:, file:, format: 'csv', mapping: nil,
149
+ duplicate_handling: 'skip')
150
+ assert_kapso_proxy('Contacts Import API')
151
+
152
+ # Build multipart form data
153
+ form_data = {
154
+ 'format' => format,
155
+ 'duplicate_handling' => duplicate_handling
156
+ }
157
+
158
+ # Handle file parameter
159
+ file_obj = case file
160
+ when String
161
+ File.open(file, 'rb')
162
+ when File, IO, StringIO
163
+ file
164
+ else
165
+ raise ArgumentError, 'file must be a File, IO object, or file path string'
166
+ end
167
+
168
+ form_data['file'] = Faraday::UploadIO.new(file_obj, 'text/csv', 'contacts.csv')
169
+ form_data['mapping'] = mapping.to_json if mapping
170
+
171
+ headers = { 'Content-Type' => 'multipart/form-data' }
172
+
173
+ response = @client.request(:post, "#{phone_number_id}/contacts/import",
174
+ body: form_data, headers: headers, response_type: :json)
175
+
176
+ # Close file if we opened it
177
+ file_obj.close if file.is_a?(String) && file_obj.respond_to?(:close)
178
+
179
+ response
180
+ end
181
+
182
+ private
183
+
184
+ def assert_kapso_proxy(feature)
185
+ unless @client.kapso_proxy?
186
+ raise Errors::KapsoProxyRequiredError.new(feature)
187
+ end
188
+ end
189
+ end
190
+ end
191
191
  end
@@ -1,104 +1,104 @@
1
- # frozen_string_literal: true
2
-
3
- module KapsoClientRuby
4
- module Resources
5
- class Conversations
6
- def initialize(client)
7
- @client = client
8
- end
9
-
10
- # List conversations (Kapso Proxy only)
11
- def list(phone_number_id:, status: nil, last_active_since: nil,
12
- last_active_until: nil, phone_number: nil, limit: nil,
13
- after: nil, before: nil, fields: nil)
14
- assert_kapso_proxy('Conversations API')
15
-
16
- query_params = {
17
- status: status,
18
- last_active_since: last_active_since,
19
- last_active_until: last_active_until,
20
- phone_number: phone_number,
21
- limit: limit,
22
- after: after,
23
- before: before,
24
- fields: fields
25
- }.compact
26
-
27
- response = @client.request(:get, "#{phone_number_id}/conversations",
28
- query: query_params, response_type: :json)
29
- Types::PagedResponse.new(response, Types::ConversationRecord)
30
- end
31
-
32
- # Get conversation details (Kapso Proxy only)
33
- def get(conversation_id:)
34
- assert_kapso_proxy('Conversations API')
35
-
36
- raise ArgumentError, 'conversation_id cannot be empty' if conversation_id.nil? || conversation_id.strip.empty?
37
-
38
- response = @client.request(:get, "conversations/#{conversation_id}",
39
- response_type: :json)
40
-
41
- # Handle both single object and data envelope responses
42
- if response.is_a?(Hash) && response.key?('data')
43
- Types::ConversationRecord.new(response['data'])
44
- else
45
- Types::ConversationRecord.new(response)
46
- end
47
- end
48
-
49
- # Update conversation status (Kapso Proxy only)
50
- def update_status(conversation_id:, status:)
51
- assert_kapso_proxy('Conversations API')
52
-
53
- raise ArgumentError, 'conversation_id cannot be empty' if conversation_id.nil? || conversation_id.strip.empty?
54
- raise ArgumentError, 'status cannot be empty' if status.nil? || status.strip.empty?
55
-
56
- payload = { status: status }
57
-
58
- response = @client.request(:patch, "conversations/#{conversation_id}",
59
- body: payload.to_json, response_type: :json)
60
- Types::GraphSuccessResponse.new(response)
61
- end
62
-
63
- # Archive conversation (Kapso Proxy only)
64
- def archive(conversation_id:)
65
- update_status(conversation_id: conversation_id, status: 'archived')
66
- end
67
-
68
- # Unarchive conversation (Kapso Proxy only)
69
- def unarchive(conversation_id:)
70
- update_status(conversation_id: conversation_id, status: 'active')
71
- end
72
-
73
- # End conversation (Kapso Proxy only)
74
- def end_conversation(conversation_id:)
75
- update_status(conversation_id: conversation_id, status: 'ended')
76
- end
77
-
78
- # Get conversation analytics (Kapso Proxy only)
79
- def analytics(phone_number_id:, conversation_id: nil, since: nil,
80
- until_time: nil, granularity: 'day')
81
- assert_kapso_proxy('Conversation Analytics API')
82
-
83
- query_params = {
84
- conversation_id: conversation_id,
85
- since: since,
86
- until: until_time,
87
- granularity: granularity
88
- }.compact
89
-
90
- response = @client.request(:get, "#{phone_number_id}/conversations/analytics",
91
- query: query_params, response_type: :json)
92
- response
93
- end
94
-
95
- private
96
-
97
- def assert_kapso_proxy(feature)
98
- unless @client.kapso_proxy?
99
- raise Errors::KapsoProxyRequiredError.new(feature)
100
- end
101
- end
102
- end
103
- end
1
+ # frozen_string_literal: true
2
+
3
+ module KapsoClientRuby
4
+ module Resources
5
+ class Conversations
6
+ def initialize(client)
7
+ @client = client
8
+ end
9
+
10
+ # List conversations (Kapso Proxy only)
11
+ def list(phone_number_id:, status: nil, last_active_since: nil,
12
+ last_active_until: nil, phone_number: nil, limit: nil,
13
+ after: nil, before: nil, fields: nil)
14
+ assert_kapso_proxy('Conversations API')
15
+
16
+ query_params = {
17
+ status: status,
18
+ last_active_since: last_active_since,
19
+ last_active_until: last_active_until,
20
+ phone_number: phone_number,
21
+ limit: limit,
22
+ after: after,
23
+ before: before,
24
+ fields: fields
25
+ }.compact
26
+
27
+ response = @client.request(:get, "#{phone_number_id}/conversations",
28
+ query: query_params, response_type: :json)
29
+ Types::PagedResponse.new(response, Types::ConversationRecord)
30
+ end
31
+
32
+ # Get conversation details (Kapso Proxy only)
33
+ def get(conversation_id:)
34
+ assert_kapso_proxy('Conversations API')
35
+
36
+ raise ArgumentError, 'conversation_id cannot be empty' if conversation_id.nil? || conversation_id.strip.empty?
37
+
38
+ response = @client.request(:get, "conversations/#{conversation_id}",
39
+ response_type: :json)
40
+
41
+ # Handle both single object and data envelope responses
42
+ if response.is_a?(Hash) && response.key?('data')
43
+ Types::ConversationRecord.new(response['data'])
44
+ else
45
+ Types::ConversationRecord.new(response)
46
+ end
47
+ end
48
+
49
+ # Update conversation status (Kapso Proxy only)
50
+ def update_status(conversation_id:, status:)
51
+ assert_kapso_proxy('Conversations API')
52
+
53
+ raise ArgumentError, 'conversation_id cannot be empty' if conversation_id.nil? || conversation_id.strip.empty?
54
+ raise ArgumentError, 'status cannot be empty' if status.nil? || status.strip.empty?
55
+
56
+ payload = { status: status }
57
+
58
+ response = @client.request(:patch, "conversations/#{conversation_id}",
59
+ body: payload.to_json, response_type: :json)
60
+ Types::GraphSuccessResponse.new(response)
61
+ end
62
+
63
+ # Archive conversation (Kapso Proxy only)
64
+ def archive(conversation_id:)
65
+ update_status(conversation_id: conversation_id, status: 'archived')
66
+ end
67
+
68
+ # Unarchive conversation (Kapso Proxy only)
69
+ def unarchive(conversation_id:)
70
+ update_status(conversation_id: conversation_id, status: 'active')
71
+ end
72
+
73
+ # End conversation (Kapso Proxy only)
74
+ def end_conversation(conversation_id:)
75
+ update_status(conversation_id: conversation_id, status: 'ended')
76
+ end
77
+
78
+ # Get conversation analytics (Kapso Proxy only)
79
+ def analytics(phone_number_id:, conversation_id: nil, since: nil,
80
+ until_time: nil, granularity: 'day')
81
+ assert_kapso_proxy('Conversation Analytics API')
82
+
83
+ query_params = {
84
+ conversation_id: conversation_id,
85
+ since: since,
86
+ until: until_time,
87
+ granularity: granularity
88
+ }.compact
89
+
90
+ response = @client.request(:get, "#{phone_number_id}/conversations/analytics",
91
+ query: query_params, response_type: :json)
92
+ response
93
+ end
94
+
95
+ private
96
+
97
+ def assert_kapso_proxy(feature)
98
+ unless @client.kapso_proxy?
99
+ raise Errors::KapsoProxyRequiredError.new(feature)
100
+ end
101
+ end
102
+ end
103
+ end
104
104
  end