kapso-client-ruby 1.0.0 → 1.0.1

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.
@@ -0,0 +1,388 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Background jobs for WhatsApp message sending
4
+ # These jobs handle asynchronous message sending to improve performance
5
+
6
+ class SendWelcomeMessageJob < ApplicationJob
7
+ queue_as :whatsapp_messages
8
+
9
+ retry_on KapsoClientRuby::RateLimitError, wait: :exponentially_longer, attempts: 5
10
+ retry_on KapsoClientRuby::TemporaryError, wait: 30.seconds, attempts: 3
11
+
12
+ discard_on KapsoClientRuby::AuthenticationError
13
+ discard_on KapsoClientRuby::ValidationError
14
+
15
+ def perform(user)
16
+ return unless user.phone_number.present?
17
+
18
+ service = KapsoMessageService.new
19
+
20
+ begin
21
+ result = service.send_welcome_message(user)
22
+
23
+ if result && result.dig('messages', 0, 'id')
24
+ # Track the message
25
+ WhatsappMessage.track_message(
26
+ user: user,
27
+ message_id: result.dig('messages', 0, 'id'),
28
+ message_type: 'welcome',
29
+ phone_number: user.phone_number,
30
+ messageable: user
31
+ )
32
+
33
+ Rails.logger.info "Welcome message sent to user #{user.id}: #{result.dig('messages', 0, 'id')}"
34
+ end
35
+
36
+ rescue KapsoClientRuby::Error => e
37
+ Rails.logger.error "Failed to send welcome message to user #{user.id}: #{e.message}"
38
+
39
+ # Track failed message
40
+ WhatsappMessage.create!(
41
+ user: user,
42
+ message_id: "failed_#{SecureRandom.hex(8)}",
43
+ message_type: 'welcome',
44
+ phone_number: user.phone_number,
45
+ messageable: user,
46
+ status: 'failed',
47
+ error_message: e.message,
48
+ sent_at: Time.current
49
+ )
50
+
51
+ raise # Re-raise to trigger retry logic
52
+ end
53
+ end
54
+ end
55
+
56
+ class SendOrderConfirmationJob < ApplicationJob
57
+ queue_as :whatsapp_messages
58
+
59
+ retry_on KapsoClientRuby::RateLimitError, wait: :exponentially_longer, attempts: 5
60
+ retry_on KapsoClientRuby::TemporaryError, wait: 30.seconds, attempts: 3
61
+
62
+ def perform(user, order)
63
+ return unless user.phone_number.present?
64
+
65
+ service = KapsoMessageService.new
66
+
67
+ begin
68
+ result = service.send_order_confirmation(order)
69
+
70
+ if result && result.dig('messages', 0, 'id')
71
+ WhatsappMessage.track_message(
72
+ user: user,
73
+ message_id: result.dig('messages', 0, 'id'),
74
+ message_type: 'order_confirmation',
75
+ phone_number: user.phone_number,
76
+ messageable: order
77
+ )
78
+
79
+ Rails.logger.info "Order confirmation sent for order #{order.id}: #{result.dig('messages', 0, 'id')}"
80
+ end
81
+
82
+ rescue KapsoClientRuby::Error => e
83
+ Rails.logger.error "Failed to send order confirmation for order #{order.id}: #{e.message}"
84
+ raise
85
+ end
86
+ end
87
+ end
88
+
89
+ class SendOrderStatusUpdateJob < ApplicationJob
90
+ queue_as :whatsapp_messages
91
+
92
+ retry_on KapsoClientRuby::RateLimitError, wait: :exponentially_longer, attempts: 5
93
+
94
+ def perform(user, order)
95
+ return unless user.phone_number.present?
96
+
97
+ # Don't spam users with too many updates
98
+ return if user.received_message_type_recently?('order_status_update', within: 1.hour)
99
+
100
+ service = KapsoClientRuby::Rails::Service.new
101
+
102
+ begin
103
+ # Use different templates based on order status
104
+ template_name = case order.status
105
+ when 'confirmed' then 'order_confirmed'
106
+ when 'processing' then 'order_processing'
107
+ when 'shipped' then 'order_shipped'
108
+ when 'delivered' then 'order_delivered'
109
+ when 'cancelled' then 'order_cancelled'
110
+ else 'order_status_update'
111
+ end
112
+
113
+ components = build_order_status_components(order, user)
114
+
115
+ result = service.send_template_message(
116
+ to: user.phone_number,
117
+ template_name: template_name,
118
+ language: user.preferred_language || 'en',
119
+ components: components
120
+ )
121
+
122
+ if result && result.dig('messages', 0, 'id')
123
+ WhatsappMessage.track_message(
124
+ user: user,
125
+ message_id: result.dig('messages', 0, 'id'),
126
+ message_type: 'order_status_update',
127
+ phone_number: user.phone_number,
128
+ messageable: order
129
+ )
130
+
131
+ Rails.logger.info "Order status update sent for order #{order.id}: #{order.status}"
132
+ end
133
+
134
+ rescue KapsoClientRuby::Error => e
135
+ Rails.logger.error "Failed to send order status update for order #{order.id}: #{e.message}"
136
+ raise
137
+ end
138
+ end
139
+
140
+ private
141
+
142
+ def build_order_status_components(order, user)
143
+ [
144
+ {
145
+ type: 'body',
146
+ parameters: [
147
+ { type: 'text', text: user.first_name || 'Customer' },
148
+ { type: 'text', text: order.id.to_s },
149
+ { type: 'text', text: order.status.humanize },
150
+ { type: 'text', text: order.formatted_total }
151
+ ]
152
+ }
153
+ ].tap do |components|
154
+ # Add tracking info for shipped orders
155
+ if order.shipped? && order.tracking_number.present?
156
+ components << {
157
+ type: 'body',
158
+ parameters: [
159
+ { type: 'text', text: order.tracking_number }
160
+ ]
161
+ }
162
+ end
163
+ end
164
+ end
165
+ end
166
+
167
+ class SendPhoneVerificationJob < ApplicationJob
168
+ queue_as :whatsapp_messages
169
+
170
+ retry_on KapsoClientRuby::Error, wait: 30.seconds, attempts: 3
171
+
172
+ def perform(user)
173
+ return unless user.phone_number.present?
174
+
175
+ # Generate verification code
176
+ verification_code = rand(100000..999999).to_s
177
+
178
+ # Store verification code (you might use Redis or database)
179
+ Rails.cache.write("phone_verification:#{user.id}", verification_code, expires_in: 10.minutes)
180
+
181
+ service = KapsoClientRuby::Rails::Service.new
182
+
183
+ begin
184
+ result = service.send_template_message(
185
+ to: user.phone_number,
186
+ template_name: 'phone_verification',
187
+ language: user.preferred_language || 'en',
188
+ components: [
189
+ {
190
+ type: 'body',
191
+ parameters: [
192
+ { type: 'text', text: verification_code }
193
+ ]
194
+ }
195
+ ]
196
+ )
197
+
198
+ if result && result.dig('messages', 0, 'id')
199
+ WhatsappMessage.track_message(
200
+ user: user,
201
+ message_id: result.dig('messages', 0, 'id'),
202
+ message_type: 'phone_verification',
203
+ phone_number: user.phone_number,
204
+ messageable: user
205
+ )
206
+
207
+ Rails.logger.info "Phone verification sent to user #{user.id}"
208
+ end
209
+
210
+ rescue KapsoClientRuby::Error => e
211
+ Rails.logger.error "Failed to send phone verification to user #{user.id}: #{e.message}"
212
+ raise
213
+ end
214
+ end
215
+ end
216
+
217
+ class SendNotificationJob < ApplicationJob
218
+ queue_as :whatsapp_messages
219
+
220
+ retry_on KapsoClientRuby::RateLimitError, wait: :exponentially_longer, attempts: 3
221
+
222
+ def perform(user, message, message_type = 'general_notification')
223
+ return unless user.phone_number.present? && user.notifications_enabled?
224
+
225
+ service = KapsoMessageService.new
226
+
227
+ begin
228
+ result = service.send_text(
229
+ phone_number: user.phone_number,
230
+ message: message
231
+ )
232
+
233
+ if result && result.dig('messages', 0, 'id')
234
+ WhatsappMessage.track_message(
235
+ user: user,
236
+ message_id: result.dig('messages', 0, 'id'),
237
+ message_type: message_type,
238
+ phone_number: user.phone_number
239
+ )
240
+
241
+ Rails.logger.info "Notification sent to user #{user.id}: #{message.truncate(50)}"
242
+ end
243
+
244
+ rescue KapsoClientRuby::Error => e
245
+ Rails.logger.error "Failed to send notification to user #{user.id}: #{e.message}"
246
+ raise
247
+ end
248
+ end
249
+ end
250
+
251
+ class SendBulkNotificationJob < ApplicationJob
252
+ queue_as :bulk_whatsapp
253
+
254
+ # Process users in batches to avoid overwhelming the API
255
+ def perform(user_ids, message, message_type = 'marketing')
256
+ User.where(id: user_ids).with_phone_number.opted_in_for_notifications.find_each(batch_size: 50) do |user|
257
+ # Add delay between messages to respect rate limits
258
+ SendNotificationJob.set(wait: rand(1..5).seconds).perform_later(user, message, message_type)
259
+ end
260
+ end
261
+ end
262
+
263
+ class HandleIncomingMessageJob < ApplicationJob
264
+ queue_as :whatsapp_webhooks
265
+
266
+ def perform(message_data)
267
+ phone_number = message_data['from']
268
+ message_text = message_data.dig('text', 'body')
269
+ message_id = message_data['id']
270
+
271
+ # Find user by phone number
272
+ user = User.find_by(phone_number: phone_number)
273
+
274
+ unless user
275
+ Rails.logger.warn "Received message from unknown number: #{phone_number}"
276
+ return
277
+ end
278
+
279
+ Rails.logger.info "Received message from user #{user.id}: #{message_text}"
280
+
281
+ # Process the incoming message based on content
282
+ case message_text&.downcase&.strip
283
+ when 'stop', 'unsubscribe'
284
+ handle_unsubscribe_request(user)
285
+ when 'start', 'subscribe'
286
+ handle_subscribe_request(user)
287
+ when 'help', 'menu'
288
+ send_help_message(user)
289
+ when 'status'
290
+ send_account_status(user)
291
+ else
292
+ # Forward to customer service or handle as general inquiry
293
+ handle_general_inquiry(user, message_text, message_id)
294
+ end
295
+ end
296
+
297
+ private
298
+
299
+ def handle_unsubscribe_request(user)
300
+ user.update!(notifications_enabled: false)
301
+
302
+ service = KapsoMessageService.new
303
+ service.send_text(
304
+ phone_number: user.phone_number,
305
+ message: "You have been unsubscribed from notifications. Reply 'START' to re-enable."
306
+ )
307
+
308
+ Rails.logger.info "User #{user.id} unsubscribed from notifications"
309
+ end
310
+
311
+ def handle_subscribe_request(user)
312
+ user.update!(notifications_enabled: true)
313
+
314
+ service = KapsoMessageService.new
315
+ service.send_text(
316
+ phone_number: user.phone_number,
317
+ message: "Welcome back! You'll now receive notifications. Reply 'STOP' to unsubscribe."
318
+ )
319
+
320
+ Rails.logger.info "User #{user.id} subscribed to notifications"
321
+ end
322
+
323
+ def send_help_message(user)
324
+ help_text = <<~TEXT
325
+ Available commands:
326
+ • STOP - Unsubscribe from messages
327
+ • START - Subscribe to messages
328
+ • STATUS - Check your account status
329
+ • HELP - Show this menu
330
+
331
+ For support, contact us at support@example.com
332
+ TEXT
333
+
334
+ service = KapsoMessageService.new
335
+ service.send_text(phone_number: user.phone_number, message: help_text)
336
+ end
337
+
338
+ def send_account_status(user)
339
+ recent_orders = user.orders.recent.limit(3)
340
+
341
+ status_text = "Account Status:\n"
342
+ status_text += "Name: #{user.name}\n"
343
+ status_text += "Email: #{user.email}\n"
344
+ status_text += "Recent orders: #{recent_orders.count}\n"
345
+
346
+ if recent_orders.any?
347
+ status_text += "\nLast order: ##{recent_orders.first.id} - #{recent_orders.first.status.humanize}"
348
+ end
349
+
350
+ service = KapsoMessageService.new
351
+ service.send_text(phone_number: user.phone_number, message: status_text)
352
+ end
353
+
354
+ def handle_general_inquiry(user, message_text, message_id)
355
+ # Create a support ticket or notification for customer service
356
+ SupportTicket.create!(
357
+ user: user,
358
+ subject: "WhatsApp Inquiry",
359
+ message: message_text,
360
+ source: 'whatsapp',
361
+ whatsapp_message_id: message_id
362
+ )
363
+
364
+ # Send auto-reply
365
+ service = KapsoMessageService.new
366
+ service.send_text(
367
+ phone_number: user.phone_number,
368
+ message: "Thanks for your message! Our team will get back to you soon. For urgent matters, call us at (555) 123-4567."
369
+ )
370
+
371
+ Rails.logger.info "Created support ticket for user #{user.id} from WhatsApp message"
372
+ end
373
+ end
374
+
375
+ class UpdateMessageStatusJob < ApplicationJob
376
+ queue_as :whatsapp_webhooks
377
+
378
+ def perform(status_data)
379
+ message_id = status_data['id']
380
+ status = status_data['status']
381
+ timestamp = status_data['timestamp']
382
+
383
+ # Update message status in database
384
+ WhatsappMessage.update_status_from_webhook(message_id, status, timestamp)
385
+
386
+ Rails.logger.debug "Updated message #{message_id} status to #{status}"
387
+ end
388
+ end
@@ -0,0 +1,240 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Example Rails model integration showing WhatsApp messaging hooks
4
+ class User < ApplicationRecord
5
+ has_many :orders, dependent: :destroy
6
+ has_many :whatsapp_messages, dependent: :destroy
7
+
8
+ # Validations
9
+ validates :email, presence: true, uniqueness: true
10
+ validates :phone_number, format: { with: /\A\+\d{10,15}\z/, message: "must be in E.164 format" }, allow_blank: true
11
+ validates :preferred_language, inclusion: { in: %w[en es fr], message: "must be a supported language" }
12
+
13
+ # Callbacks for WhatsApp integration
14
+ after_create :send_welcome_message, if: :phone_number?
15
+ after_update :send_phone_verification, if: :phone_number_changed?
16
+
17
+ # Scopes
18
+ scope :with_phone_number, -> { where.not(phone_number: nil) }
19
+ scope :opted_in_for_notifications, -> { where(notifications_enabled: true) }
20
+
21
+ # Instance methods
22
+
23
+ def send_welcome_message
24
+ SendWelcomeMessageJob.perform_later(self)
25
+ end
26
+
27
+ def send_phone_verification
28
+ return unless phone_number.present?
29
+
30
+ SendPhoneVerificationJob.perform_later(self)
31
+ end
32
+
33
+ def send_notification(message, type: 'general')
34
+ return unless phone_number.present? && notifications_enabled?
35
+
36
+ SendNotificationJob.perform_later(self, message, type)
37
+ end
38
+
39
+ def send_order_confirmation(order)
40
+ return unless phone_number.present?
41
+
42
+ SendOrderConfirmationJob.perform_later(self, order)
43
+ end
44
+
45
+ # Format phone number for WhatsApp (ensure E.164 format)
46
+ def formatted_phone_number
47
+ return nil unless phone_number.present?
48
+
49
+ # Remove any non-digit characters except the leading +
50
+ cleaned = phone_number.gsub(/[^\d+]/, '')
51
+
52
+ # Ensure it starts with +
53
+ cleaned.start_with?('+') ? cleaned : "+#{cleaned}"
54
+ end
55
+
56
+ # Check if user can receive WhatsApp messages
57
+ def can_receive_whatsapp?
58
+ phone_number.present? && notifications_enabled?
59
+ end
60
+
61
+ # Get recent WhatsApp messages
62
+ def recent_whatsapp_messages(limit = 10)
63
+ whatsapp_messages.order(created_at: :desc).limit(limit)
64
+ end
65
+
66
+ # Check if user has received a specific message type recently
67
+ def received_message_type_recently?(message_type, within: 24.hours)
68
+ whatsapp_messages
69
+ .where(message_type: message_type)
70
+ .where('created_at > ?', within.ago)
71
+ .exists?
72
+ end
73
+
74
+ private
75
+
76
+ def phone_number_changed?
77
+ saved_change_to_phone_number? && phone_number.present?
78
+ end
79
+ end
80
+
81
+ # Example Order model with WhatsApp integration
82
+ class Order < ApplicationRecord
83
+ belongs_to :user
84
+ has_many :order_items, dependent: :destroy
85
+ has_many :whatsapp_messages, through: :user
86
+
87
+ # Order statuses
88
+ enum status: {
89
+ pending: 0,
90
+ confirmed: 1,
91
+ processing: 2,
92
+ shipped: 3,
93
+ delivered: 4,
94
+ cancelled: 5
95
+ }
96
+
97
+ # Callbacks for WhatsApp notifications
98
+ after_create :send_order_confirmation
99
+ after_update :send_status_update, if: :saved_change_to_status?
100
+
101
+ # Instance methods
102
+
103
+ def send_order_confirmation
104
+ return unless user.can_receive_whatsapp?
105
+
106
+ SendOrderConfirmationJob.perform_later(user, self)
107
+ end
108
+
109
+ def send_status_update
110
+ return unless user.can_receive_whatsapp?
111
+
112
+ # Don't send updates for pending status (already sent confirmation)
113
+ return if status == 'pending'
114
+
115
+ SendOrderStatusUpdateJob.perform_later(user, self)
116
+ end
117
+
118
+ def total_amount_in_cents
119
+ (total_amount * 100).to_i
120
+ end
121
+
122
+ def formatted_total
123
+ "$#{'%.2f' % total_amount}"
124
+ end
125
+
126
+ def estimated_delivery_date
127
+ return nil unless shipped?
128
+
129
+ shipped_at + 3.days # Example: 3 days for delivery
130
+ end
131
+ end
132
+
133
+ # Example WhatsAppMessage model for tracking sent messages
134
+ class WhatsappMessage < ApplicationRecord
135
+ belongs_to :user
136
+ belongs_to :messageable, polymorphic: true, optional: true # Could be Order, User, etc.
137
+
138
+ # Message types
139
+ enum message_type: {
140
+ welcome: 0,
141
+ order_confirmation: 1,
142
+ order_status_update: 2,
143
+ phone_verification: 3,
144
+ general_notification: 4,
145
+ marketing: 5,
146
+ support: 6
147
+ }
148
+
149
+ # Message status from WhatsApp API
150
+ enum status: {
151
+ sent: 0,
152
+ delivered: 1,
153
+ read: 2,
154
+ failed: 3
155
+ }
156
+
157
+ # Validations
158
+ validates :message_id, presence: true, uniqueness: true
159
+ validates :phone_number, presence: true
160
+ validates :message_type, presence: true
161
+
162
+ # Scopes
163
+ scope :recent, -> { order(created_at: :desc) }
164
+ scope :successful, -> { where(status: [:sent, :delivered, :read]) }
165
+ scope :failed, -> { where(status: :failed) }
166
+
167
+ # Class methods
168
+
169
+ def self.track_message(user:, message_id:, message_type:, phone_number:, messageable: nil)
170
+ create!(
171
+ user: user,
172
+ message_id: message_id,
173
+ message_type: message_type,
174
+ phone_number: phone_number,
175
+ messageable: messageable,
176
+ status: :sent,
177
+ sent_at: Time.current
178
+ )
179
+ end
180
+
181
+ def self.update_status_from_webhook(message_id, new_status, timestamp = nil)
182
+ message = find_by(message_id: message_id)
183
+ return unless message
184
+
185
+ status_mapping = {
186
+ 'sent' => :sent,
187
+ 'delivered' => :delivered,
188
+ 'read' => :read,
189
+ 'failed' => :failed
190
+ }
191
+
192
+ mapped_status = status_mapping[new_status.to_s.downcase]
193
+ return unless mapped_status
194
+
195
+ message.update!(
196
+ status: mapped_status,
197
+ status_updated_at: timestamp ? Time.at(timestamp) : Time.current
198
+ )
199
+ end
200
+
201
+ # Instance methods
202
+
203
+ def delivered?
204
+ %w[delivered read].include?(status)
205
+ end
206
+
207
+ def failed?
208
+ status == 'failed'
209
+ end
210
+
211
+ def delivery_time
212
+ return nil unless delivered? && sent_at.present? && status_updated_at.present?
213
+
214
+ status_updated_at - sent_at
215
+ end
216
+ end
217
+
218
+ # Example migration for WhatsApp messages tracking
219
+ #
220
+ # class CreateWhatsappMessages < ActiveRecord::Migration[8.0]
221
+ # def change
222
+ # create_table :whatsapp_messages do |t|
223
+ # t.references :user, null: false, foreign_key: true
224
+ # t.references :messageable, polymorphic: true, null: true
225
+ # t.string :message_id, null: false, index: { unique: true }
226
+ # t.string :phone_number, null: false
227
+ # t.integer :message_type, null: false
228
+ # t.integer :status, default: 0
229
+ # t.datetime :sent_at
230
+ # t.datetime :status_updated_at
231
+ # t.text :error_message
232
+ # t.json :metadata # Store additional message data
233
+ #
234
+ # t.timestamps
235
+ # end
236
+ #
237
+ # add_index :whatsapp_messages, [:user_id, :message_type]
238
+ # add_index :whatsapp_messages, [:message_type, :status]
239
+ # end
240
+ # end