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,284 +1,284 @@
1
- # frozen_string_literal: true
2
-
3
- module KapsoClientRuby
4
- module Resources
5
- class Templates
6
- def initialize(client)
7
- @client = client
8
- end
9
-
10
- # List templates for a business account
11
- def list(business_account_id:, limit: nil, after: nil, before: nil,
12
- name: nil, status: nil, category: nil, language: nil,
13
- name_or_content: nil, quality_score: nil)
14
- query_params = {
15
- limit: limit,
16
- after: after,
17
- before: before,
18
- name: name,
19
- status: status,
20
- category: category,
21
- language: language,
22
- name_or_content: name_or_content,
23
- quality_score: quality_score
24
- }.compact
25
-
26
- response = @client.request(:get, "#{business_account_id}/message_templates",
27
- query: query_params, response_type: :json)
28
- Types::PagedResponse.new(response, Types::MessageTemplate)
29
- end
30
-
31
- # Get a specific template
32
- def get(business_account_id:, template_id:, fields: nil)
33
- query_params = {}
34
- query_params[:fields] = fields if fields
35
-
36
- response = @client.request(:get, "#{business_account_id}/message_templates/#{template_id}",
37
- query: query_params, response_type: :json)
38
- Types::MessageTemplate.new(response)
39
- end
40
-
41
- # Create a new template
42
- def create(business_account_id:, name:, language:, category:, components:,
43
- allow_category_change: nil, message_send_ttl_seconds: nil)
44
- validate_template_data(name: name, language: language, category: category, components: components)
45
-
46
- payload = {
47
- name: name,
48
- language: language,
49
- category: category,
50
- components: normalize_components(components)
51
- }
52
-
53
- payload[:allow_category_change] = allow_category_change unless allow_category_change.nil?
54
- payload[:message_send_ttl_seconds] = message_send_ttl_seconds if message_send_ttl_seconds
55
-
56
- response = @client.request(:post, "#{business_account_id}/message_templates",
57
- body: payload.to_json, response_type: :json)
58
- Types::TemplateCreateResponse.new(response)
59
- end
60
-
61
- # Update a template
62
- def update(business_account_id:, template_id:, category: nil, components: nil)
63
- payload = {}
64
- payload[:category] = category if category
65
- payload[:components] = normalize_components(components) if components
66
-
67
- return if payload.empty?
68
-
69
- response = @client.request(:post, "#{business_account_id}/message_templates/#{template_id}",
70
- body: payload.to_json, response_type: :json)
71
- Types::GraphSuccessResponse.new(response)
72
- end
73
-
74
- # Delete a template
75
- def delete(business_account_id:, name: nil, template_id: nil, hsm_id: nil, language: nil)
76
- if template_id
77
- # Delete by template ID
78
- response = @client.request(:delete, "#{business_account_id}/message_templates/#{template_id}",
79
- response_type: :json)
80
- elsif name
81
- # Delete by name and language
82
- query_params = { name: name }
83
- query_params[:language] = language if language
84
- query_params[:hsm_id] = hsm_id if hsm_id
85
-
86
- response = @client.request(:delete, "#{business_account_id}/message_templates",
87
- query: query_params, response_type: :json)
88
- else
89
- raise ArgumentError, 'Must provide either template_id or name'
90
- end
91
-
92
- Types::GraphSuccessResponse.new(response)
93
- end
94
-
95
- # Template builder helpers
96
- def build_text_component(text:, example: nil)
97
- component = { type: 'BODY', text: text }
98
- component[:example] = example if example
99
- component
100
- end
101
-
102
- def build_header_component(type:, text: nil, image: nil, video: nil,
103
- document: nil, example: nil)
104
- component = { type: 'HEADER', format: type.upcase }
105
-
106
- case type.upcase
107
- when 'TEXT'
108
- component[:text] = text if text
109
- when 'IMAGE'
110
- component[:example] = { header_handle: [image] } if image
111
- when 'VIDEO'
112
- component[:example] = { header_handle: [video] } if video
113
- when 'DOCUMENT'
114
- component[:example] = { header_handle: [document] } if document
115
- end
116
-
117
- component[:example] = example if example
118
- component
119
- end
120
-
121
- def build_footer_component(text: nil, code_expiration_minutes: nil)
122
- component = { type: 'FOOTER' }
123
- component[:text] = text if text
124
- component[:code_expiration_minutes] = code_expiration_minutes if code_expiration_minutes
125
- component
126
- end
127
-
128
- def build_buttons_component(buttons:)
129
- {
130
- type: 'BUTTONS',
131
- buttons: buttons.map { |btn| normalize_button(btn) }
132
- }
133
- end
134
-
135
- def build_button(type:, text: nil, url: nil, phone_number: nil,
136
- otp_type: nil, autofill_text: nil, package_name: nil,
137
- signature_hash: nil)
138
- button = { type: type.upcase }
139
-
140
- case type.upcase
141
- when 'QUICK_REPLY'
142
- button[:text] = text if text
143
- when 'URL'
144
- button[:text] = text if text
145
- button[:url] = url if url
146
- when 'PHONE_NUMBER'
147
- button[:text] = text if text
148
- button[:phone_number] = phone_number if phone_number
149
- when 'OTP'
150
- button[:otp_type] = otp_type if otp_type
151
- button[:text] = text if text
152
- button[:autofill_text] = autofill_text if autofill_text
153
- button[:package_name] = package_name if package_name
154
- button[:signature_hash] = signature_hash if signature_hash
155
- end
156
-
157
- button
158
- end
159
-
160
- # Authentication template builder
161
- def build_authentication_template(name:, language:, ttl_seconds: 60,
162
- add_security_recommendation: true,
163
- code_expiration_minutes: 10,
164
- otp_type: 'COPY_CODE')
165
- components = []
166
-
167
- # Body component with security recommendation
168
- body_component = { type: 'BODY' }
169
- body_component[:add_security_recommendation] = add_security_recommendation
170
- components << body_component
171
-
172
- # Footer component with expiration
173
- if code_expiration_minutes
174
- components << build_footer_component(code_expiration_minutes: code_expiration_minutes)
175
- end
176
-
177
- # OTP button
178
- components << build_buttons_component(
179
- buttons: [build_button(type: 'OTP', otp_type: otp_type)]
180
- )
181
-
182
- {
183
- name: name,
184
- language: language,
185
- category: 'AUTHENTICATION',
186
- message_send_ttl_seconds: ttl_seconds,
187
- components: components
188
- }
189
- end
190
-
191
- # Marketing template builder
192
- def build_marketing_template(name:, language:, header: nil, body:, footer: nil,
193
- buttons: nil, body_example: nil)
194
- components = []
195
-
196
- # Header component
197
- components << header if header
198
-
199
- # Body component
200
- body_component = build_text_component(text: body, example: body_example)
201
- components << body_component
202
-
203
- # Footer component
204
- components << build_footer_component(text: footer) if footer
205
-
206
- # Buttons component
207
- components << build_buttons_component(buttons: buttons) if buttons
208
-
209
- {
210
- name: name,
211
- language: language,
212
- category: 'MARKETING',
213
- components: components
214
- }
215
- end
216
-
217
- # Utility template builder
218
- def build_utility_template(name:, language:, body:, header: nil, footer: nil,
219
- buttons: nil, body_example: nil)
220
- components = []
221
-
222
- # Header component
223
- components << header if header
224
-
225
- # Body component
226
- body_component = build_text_component(text: body, example: body_example)
227
- components << body_component
228
-
229
- # Footer component
230
- components << build_footer_component(text: footer) if footer
231
-
232
- # Buttons component
233
- components << build_buttons_component(buttons: buttons) if buttons
234
-
235
- {
236
- name: name,
237
- language: language,
238
- category: 'UTILITY',
239
- components: components
240
- }
241
- end
242
-
243
- private
244
-
245
- def validate_template_data(name:, language:, category:, components:)
246
- raise ArgumentError, 'Template name cannot be empty' if name.nil? || name.strip.empty?
247
- raise ArgumentError, 'Language cannot be empty' if language.nil? || language.strip.empty?
248
- raise ArgumentError, 'Category cannot be empty' if category.nil? || category.strip.empty?
249
- raise ArgumentError, 'Components cannot be empty' if components.nil? || components.empty?
250
-
251
- # Validate category
252
- valid_categories = Types::TEMPLATE_CATEGORIES
253
- unless valid_categories.include?(category.upcase)
254
- raise ArgumentError, "Invalid category '#{category}'. Must be one of: #{valid_categories.join(', ')}"
255
- end
256
-
257
- # Validate components structure
258
- components.each_with_index do |component, index|
259
- unless component.is_a?(Hash) && component[:type]
260
- raise ArgumentError, "Component at index #{index} must be a Hash with :type key"
261
- end
262
- end
263
- end
264
-
265
- def normalize_components(components)
266
- components.map { |component| normalize_component(component) }
267
- end
268
-
269
- def normalize_component(component)
270
- # Ensure component keys are strings for API compatibility
271
- normalized = {}
272
- component.each { |key, value| normalized[key.to_s] = value }
273
- normalized
274
- end
275
-
276
- def normalize_button(button)
277
- # Ensure button keys are strings for API compatibility
278
- normalized = {}
279
- button.each { |key, value| normalized[key.to_s] = value }
280
- normalized
281
- end
282
- end
283
- end
1
+ # frozen_string_literal: true
2
+
3
+ module KapsoClientRuby
4
+ module Resources
5
+ class Templates
6
+ def initialize(client)
7
+ @client = client
8
+ end
9
+
10
+ # List templates for a business account
11
+ def list(business_account_id:, limit: nil, after: nil, before: nil,
12
+ name: nil, status: nil, category: nil, language: nil,
13
+ name_or_content: nil, quality_score: nil)
14
+ query_params = {
15
+ limit: limit,
16
+ after: after,
17
+ before: before,
18
+ name: name,
19
+ status: status,
20
+ category: category,
21
+ language: language,
22
+ name_or_content: name_or_content,
23
+ quality_score: quality_score
24
+ }.compact
25
+
26
+ response = @client.request(:get, "#{business_account_id}/message_templates",
27
+ query: query_params, response_type: :json)
28
+ Types::PagedResponse.new(response, Types::MessageTemplate)
29
+ end
30
+
31
+ # Get a specific template
32
+ def get(business_account_id:, template_id:, fields: nil)
33
+ query_params = {}
34
+ query_params[:fields] = fields if fields
35
+
36
+ response = @client.request(:get, "#{business_account_id}/message_templates/#{template_id}",
37
+ query: query_params, response_type: :json)
38
+ Types::MessageTemplate.new(response)
39
+ end
40
+
41
+ # Create a new template
42
+ def create(business_account_id:, name:, language:, category:, components:,
43
+ allow_category_change: nil, message_send_ttl_seconds: nil)
44
+ validate_template_data(name: name, language: language, category: category, components: components)
45
+
46
+ payload = {
47
+ name: name,
48
+ language: language,
49
+ category: category,
50
+ components: normalize_components(components)
51
+ }
52
+
53
+ payload[:allow_category_change] = allow_category_change unless allow_category_change.nil?
54
+ payload[:message_send_ttl_seconds] = message_send_ttl_seconds if message_send_ttl_seconds
55
+
56
+ response = @client.request(:post, "#{business_account_id}/message_templates",
57
+ body: payload.to_json, response_type: :json)
58
+ Types::TemplateCreateResponse.new(response)
59
+ end
60
+
61
+ # Update a template
62
+ def update(business_account_id:, template_id:, category: nil, components: nil)
63
+ payload = {}
64
+ payload[:category] = category if category
65
+ payload[:components] = normalize_components(components) if components
66
+
67
+ return if payload.empty?
68
+
69
+ response = @client.request(:post, "#{business_account_id}/message_templates/#{template_id}",
70
+ body: payload.to_json, response_type: :json)
71
+ Types::GraphSuccessResponse.new(response)
72
+ end
73
+
74
+ # Delete a template
75
+ def delete(business_account_id:, name: nil, template_id: nil, hsm_id: nil, language: nil)
76
+ if template_id
77
+ # Delete by template ID
78
+ response = @client.request(:delete, "#{business_account_id}/message_templates/#{template_id}",
79
+ response_type: :json)
80
+ elsif name
81
+ # Delete by name and language
82
+ query_params = { name: name }
83
+ query_params[:language] = language if language
84
+ query_params[:hsm_id] = hsm_id if hsm_id
85
+
86
+ response = @client.request(:delete, "#{business_account_id}/message_templates",
87
+ query: query_params, response_type: :json)
88
+ else
89
+ raise ArgumentError, 'Must provide either template_id or name'
90
+ end
91
+
92
+ Types::GraphSuccessResponse.new(response)
93
+ end
94
+
95
+ # Template builder helpers
96
+ def build_text_component(text:, example: nil)
97
+ component = { type: 'BODY', text: text }
98
+ component[:example] = example if example
99
+ component
100
+ end
101
+
102
+ def build_header_component(type:, text: nil, image: nil, video: nil,
103
+ document: nil, example: nil)
104
+ component = { type: 'HEADER', format: type.upcase }
105
+
106
+ case type.upcase
107
+ when 'TEXT'
108
+ component[:text] = text if text
109
+ when 'IMAGE'
110
+ component[:example] = { header_handle: [image] } if image
111
+ when 'VIDEO'
112
+ component[:example] = { header_handle: [video] } if video
113
+ when 'DOCUMENT'
114
+ component[:example] = { header_handle: [document] } if document
115
+ end
116
+
117
+ component[:example] = example if example
118
+ component
119
+ end
120
+
121
+ def build_footer_component(text: nil, code_expiration_minutes: nil)
122
+ component = { type: 'FOOTER' }
123
+ component[:text] = text if text
124
+ component[:code_expiration_minutes] = code_expiration_minutes if code_expiration_minutes
125
+ component
126
+ end
127
+
128
+ def build_buttons_component(buttons:)
129
+ {
130
+ type: 'BUTTONS',
131
+ buttons: buttons.map { |btn| normalize_button(btn) }
132
+ }
133
+ end
134
+
135
+ def build_button(type:, text: nil, url: nil, phone_number: nil,
136
+ otp_type: nil, autofill_text: nil, package_name: nil,
137
+ signature_hash: nil)
138
+ button = { type: type.upcase }
139
+
140
+ case type.upcase
141
+ when 'QUICK_REPLY'
142
+ button[:text] = text if text
143
+ when 'URL'
144
+ button[:text] = text if text
145
+ button[:url] = url if url
146
+ when 'PHONE_NUMBER'
147
+ button[:text] = text if text
148
+ button[:phone_number] = phone_number if phone_number
149
+ when 'OTP'
150
+ button[:otp_type] = otp_type if otp_type
151
+ button[:text] = text if text
152
+ button[:autofill_text] = autofill_text if autofill_text
153
+ button[:package_name] = package_name if package_name
154
+ button[:signature_hash] = signature_hash if signature_hash
155
+ end
156
+
157
+ button
158
+ end
159
+
160
+ # Authentication template builder
161
+ def build_authentication_template(name:, language:, ttl_seconds: 60,
162
+ add_security_recommendation: true,
163
+ code_expiration_minutes: 10,
164
+ otp_type: 'COPY_CODE')
165
+ components = []
166
+
167
+ # Body component with security recommendation
168
+ body_component = { type: 'BODY' }
169
+ body_component[:add_security_recommendation] = add_security_recommendation
170
+ components << body_component
171
+
172
+ # Footer component with expiration
173
+ if code_expiration_minutes
174
+ components << build_footer_component(code_expiration_minutes: code_expiration_minutes)
175
+ end
176
+
177
+ # OTP button
178
+ components << build_buttons_component(
179
+ buttons: [build_button(type: 'OTP', otp_type: otp_type)]
180
+ )
181
+
182
+ {
183
+ name: name,
184
+ language: language,
185
+ category: 'AUTHENTICATION',
186
+ message_send_ttl_seconds: ttl_seconds,
187
+ components: components
188
+ }
189
+ end
190
+
191
+ # Marketing template builder
192
+ def build_marketing_template(name:, language:, header: nil, body:, footer: nil,
193
+ buttons: nil, body_example: nil)
194
+ components = []
195
+
196
+ # Header component
197
+ components << header if header
198
+
199
+ # Body component
200
+ body_component = build_text_component(text: body, example: body_example)
201
+ components << body_component
202
+
203
+ # Footer component
204
+ components << build_footer_component(text: footer) if footer
205
+
206
+ # Buttons component
207
+ components << build_buttons_component(buttons: buttons) if buttons
208
+
209
+ {
210
+ name: name,
211
+ language: language,
212
+ category: 'MARKETING',
213
+ components: components
214
+ }
215
+ end
216
+
217
+ # Utility template builder
218
+ def build_utility_template(name:, language:, body:, header: nil, footer: nil,
219
+ buttons: nil, body_example: nil)
220
+ components = []
221
+
222
+ # Header component
223
+ components << header if header
224
+
225
+ # Body component
226
+ body_component = build_text_component(text: body, example: body_example)
227
+ components << body_component
228
+
229
+ # Footer component
230
+ components << build_footer_component(text: footer) if footer
231
+
232
+ # Buttons component
233
+ components << build_buttons_component(buttons: buttons) if buttons
234
+
235
+ {
236
+ name: name,
237
+ language: language,
238
+ category: 'UTILITY',
239
+ components: components
240
+ }
241
+ end
242
+
243
+ private
244
+
245
+ def validate_template_data(name:, language:, category:, components:)
246
+ raise ArgumentError, 'Template name cannot be empty' if name.nil? || name.strip.empty?
247
+ raise ArgumentError, 'Language cannot be empty' if language.nil? || language.strip.empty?
248
+ raise ArgumentError, 'Category cannot be empty' if category.nil? || category.strip.empty?
249
+ raise ArgumentError, 'Components cannot be empty' if components.nil? || components.empty?
250
+
251
+ # Validate category
252
+ valid_categories = Types::TEMPLATE_CATEGORIES
253
+ unless valid_categories.include?(category.upcase)
254
+ raise ArgumentError, "Invalid category '#{category}'. Must be one of: #{valid_categories.join(', ')}"
255
+ end
256
+
257
+ # Validate components structure
258
+ components.each_with_index do |component, index|
259
+ unless component.is_a?(Hash) && component[:type]
260
+ raise ArgumentError, "Component at index #{index} must be a Hash with :type key"
261
+ end
262
+ end
263
+ end
264
+
265
+ def normalize_components(components)
266
+ components.map { |component| normalize_component(component) }
267
+ end
268
+
269
+ def normalize_component(component)
270
+ # Ensure component keys are strings for API compatibility
271
+ normalized = {}
272
+ component.each { |key, value| normalized[key.to_s] = value }
273
+ normalized
274
+ end
275
+
276
+ def normalize_button(button)
277
+ # Ensure button keys are strings for API compatibility
278
+ normalized = {}
279
+ button.each { |key, value| normalized[key.to_s] = value }
280
+ normalized
281
+ end
282
+ end
283
+ end
284
284
  end