completed SMS gateway project
This commit is contained in:
7
app/jobs/application_job.rb
Normal file
7
app/jobs/application_job.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
class ApplicationJob < ActiveJob::Base
|
||||
# Automatically retry jobs that encountered a deadlock
|
||||
# retry_on ActiveRecord::Deadlocked
|
||||
|
||||
# Most jobs are safe to ignore if the underlying records are no longer available
|
||||
# discard_on ActiveJob::DeserializationError
|
||||
end
|
||||
14
app/jobs/check_gateway_health_job.rb
Normal file
14
app/jobs/check_gateway_health_job.rb
Normal file
@@ -0,0 +1,14 @@
|
||||
class CheckGatewayHealthJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform
|
||||
# Mark gateways as offline if no heartbeat in last 2 minutes
|
||||
offline_count = Gateway.where("last_heartbeat_at < ?", 2.minutes.ago)
|
||||
.where.not(status: "offline")
|
||||
.update_all(status: "offline")
|
||||
|
||||
if offline_count > 0
|
||||
Rails.logger.warn("Marked #{offline_count} gateways as offline due to missing heartbeat")
|
||||
end
|
||||
end
|
||||
end
|
||||
8
app/jobs/cleanup_expired_otps_job.rb
Normal file
8
app/jobs/cleanup_expired_otps_job.rb
Normal file
@@ -0,0 +1,8 @@
|
||||
class CleanupExpiredOtpsJob < ApplicationJob
|
||||
queue_as :low_priority
|
||||
|
||||
def perform
|
||||
deleted_count = OtpCode.cleanup_expired!
|
||||
Rails.logger.info("Cleaned up #{deleted_count} expired OTP codes")
|
||||
end
|
||||
end
|
||||
57
app/jobs/process_inbound_sms_job.rb
Normal file
57
app/jobs/process_inbound_sms_job.rb
Normal file
@@ -0,0 +1,57 @@
|
||||
class ProcessInboundSmsJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform(sms_message_id)
|
||||
sms = SmsMessage.find(sms_message_id)
|
||||
|
||||
# Skip if not inbound
|
||||
return unless sms.inbound?
|
||||
|
||||
# Trigger webhooks for sms_received event
|
||||
trigger_webhooks(sms)
|
||||
|
||||
# Check if this is an OTP reply
|
||||
check_otp_reply(sms)
|
||||
|
||||
Rails.logger.info("Processed inbound SMS #{sms.message_id} from #{sms.phone_number}")
|
||||
rescue ActiveRecord::RecordNotFound => e
|
||||
Rails.logger.error("SMS message not found: #{e.message}")
|
||||
rescue StandardError => e
|
||||
Rails.logger.error("Failed to process inbound SMS #{sms_message_id}: #{e.message}")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def trigger_webhooks(sms)
|
||||
webhooks = WebhookConfig.for_event_type("sms_received")
|
||||
|
||||
webhooks.each do |webhook|
|
||||
payload = {
|
||||
event: "sms_received",
|
||||
message_id: sms.message_id,
|
||||
from: sms.phone_number,
|
||||
message: sms.message_body,
|
||||
received_at: sms.created_at,
|
||||
gateway_id: sms.gateway&.device_id
|
||||
}
|
||||
|
||||
webhook.trigger(payload)
|
||||
end
|
||||
end
|
||||
|
||||
def check_otp_reply(sms)
|
||||
# Extract potential OTP code from message (6 digits)
|
||||
code_match = sms.message_body.match(/\b\d{6}\b/)
|
||||
return unless code_match
|
||||
|
||||
code = code_match[0]
|
||||
|
||||
# Try to verify if there's a pending OTP for this phone number
|
||||
otp = OtpCode.valid_codes.find_by(phone_number: sms.phone_number, code: code)
|
||||
|
||||
if otp
|
||||
otp.update!(verified: true, verified_at: Time.current)
|
||||
Rails.logger.info("Auto-verified OTP for #{sms.phone_number}")
|
||||
end
|
||||
end
|
||||
end
|
||||
8
app/jobs/reset_daily_counters_job.rb
Normal file
8
app/jobs/reset_daily_counters_job.rb
Normal file
@@ -0,0 +1,8 @@
|
||||
class ResetDailyCountersJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform
|
||||
Gateway.reset_daily_counters!
|
||||
Rails.logger.info("Reset daily counters for all gateways")
|
||||
end
|
||||
end
|
||||
22
app/jobs/retry_failed_sms_job.rb
Normal file
22
app/jobs/retry_failed_sms_job.rb
Normal file
@@ -0,0 +1,22 @@
|
||||
class RetryFailedSmsJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform(sms_message_id)
|
||||
sms = SmsMessage.find(sms_message_id)
|
||||
|
||||
# Only retry if message can be retried
|
||||
return unless sms.can_retry?
|
||||
|
||||
# Reset status to queued and increment retry count
|
||||
sms.increment_retry!
|
||||
sms.update!(status: "queued", error_message: nil)
|
||||
|
||||
# Re-queue for sending with exponential backoff
|
||||
wait_time = (2 ** sms.retry_count).minutes
|
||||
SendSmsJob.set(wait: wait_time).perform_later(sms.id)
|
||||
|
||||
Rails.logger.info("Retrying failed SMS #{sms.message_id} (attempt #{sms.retry_count})")
|
||||
rescue ActiveRecord::RecordNotFound => e
|
||||
Rails.logger.error("SMS message not found: #{e.message}")
|
||||
end
|
||||
end
|
||||
41
app/jobs/send_sms_job.rb
Normal file
41
app/jobs/send_sms_job.rb
Normal file
@@ -0,0 +1,41 @@
|
||||
class SendSmsJob < ApplicationJob
|
||||
queue_as :default
|
||||
retry_on StandardError, wait: :exponentially_longer, attempts: 3
|
||||
|
||||
def perform(sms_message_id)
|
||||
sms = SmsMessage.find(sms_message_id)
|
||||
|
||||
# Skip if already sent or not outbound
|
||||
return unless sms.outbound? && sms.status == "queued"
|
||||
|
||||
# Find available gateway
|
||||
gateway = Gateway.available_for_sending
|
||||
|
||||
unless gateway
|
||||
Rails.logger.warn("No available gateway for message #{sms.message_id}")
|
||||
sms.update!(status: "pending")
|
||||
# Retry later
|
||||
SendSmsJob.set(wait: 1.minute).perform_later(sms_message_id)
|
||||
return
|
||||
end
|
||||
|
||||
# Update message status
|
||||
sms.mark_sent!(gateway)
|
||||
|
||||
# Broadcast message to gateway via WebSocket
|
||||
GatewayChannel.broadcast_to(gateway, {
|
||||
action: "send_sms",
|
||||
message_id: sms.message_id,
|
||||
recipient: sms.phone_number,
|
||||
message: sms.message_body
|
||||
})
|
||||
|
||||
Rails.logger.info("Sent SMS #{sms.message_id} to gateway #{gateway.device_id}")
|
||||
rescue ActiveRecord::RecordNotFound => e
|
||||
Rails.logger.error("SMS message not found: #{e.message}")
|
||||
rescue StandardError => e
|
||||
Rails.logger.error("Failed to send SMS #{sms_message_id}: #{e.message}")
|
||||
sms&.increment_retry!
|
||||
raise
|
||||
end
|
||||
end
|
||||
31
app/jobs/trigger_webhook_job.rb
Normal file
31
app/jobs/trigger_webhook_job.rb
Normal file
@@ -0,0 +1,31 @@
|
||||
class TriggerWebhookJob < ApplicationJob
|
||||
queue_as :default
|
||||
retry_on StandardError, wait: :exponentially_longer, attempts: 3
|
||||
|
||||
def perform(webhook_config_id, payload)
|
||||
webhook = WebhookConfig.find(webhook_config_id)
|
||||
|
||||
# Skip if webhook is not active
|
||||
return unless webhook.active?
|
||||
|
||||
success = webhook.execute(payload)
|
||||
|
||||
if success
|
||||
Rails.logger.info("Webhook #{webhook.name} triggered successfully")
|
||||
else
|
||||
Rails.logger.warn("Webhook #{webhook.name} failed")
|
||||
raise StandardError, "Webhook execution failed" if attempts_count < webhook.retry_count
|
||||
end
|
||||
rescue ActiveRecord::RecordNotFound => e
|
||||
Rails.logger.error("Webhook config not found: #{e.message}")
|
||||
rescue StandardError => e
|
||||
Rails.logger.error("Webhook trigger failed: #{e.message}")
|
||||
raise if attempts_count < (webhook&.retry_count || 3)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def attempts_count
|
||||
executions
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user