completed SMS gateway project

This commit is contained in:
Min Zeya Phyo
2025-10-22 17:22:17 +08:00
commit c883fa7128
190 changed files with 16294 additions and 0 deletions

View 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

View 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

View 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

View 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

View 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

View 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
View 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

View 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