class SmsMessage < ApplicationRecord # Normalize metadata to always be a Hash attribute :metadata, :jsonb, default: {} belongs_to :gateway, optional: true validates :phone_number, presence: true validates :message_body, presence: true, length: { maximum: 1600 } validates :direction, presence: true, inclusion: { in: %w[inbound outbound] } validates :message_id, presence: true, uniqueness: true validates :status, inclusion: { in: %w[pending queued sent delivered failed] } validate :phone_number_format before_validation :ensure_metadata_is_hash before_validation :generate_message_id, on: :create before_validation :normalize_phone_number after_create_commit :enqueue_sending, if: :outbound? scope :pending, -> { where(status: "pending") } scope :queued, -> { where(status: "queued") } scope :sent, -> { where(status: "sent") } scope :delivered, -> { where(status: "delivered") } scope :failed, -> { where(status: "failed") } scope :inbound, -> { where(direction: "inbound") } scope :outbound, -> { where(direction: "outbound") } scope :recent, -> { order(created_at: :desc) } # Check message direction def outbound? direction == "outbound" end def inbound? direction == "inbound" end # Check if message can be retried def can_retry? failed? && retry_count < 3 end # Mark message as sent def mark_sent!(gateway) update!( status: "sent", gateway: gateway, sent_at: Time.current ) gateway.increment_sent_count! end # Mark message as delivered def mark_delivered! update!( status: "delivered", delivered_at: Time.current ) end # Mark message as failed def mark_failed!(error_msg = nil) update!( status: "failed", failed_at: Time.current, error_message: error_msg ) end # Increment retry counter def increment_retry! increment!(:retry_count) end # Check if message status is failed def failed? status == "failed" end private def generate_message_id self.message_id ||= "msg_#{SecureRandom.hex(16)}" end def normalize_phone_number return unless phone_number.present? # Remove spaces and special characters self.phone_number = phone_number.gsub(/[^\d+]/, "") end def phone_number_format return unless phone_number.present? phone = Phonelib.parse(phone_number) unless phone.valid? errors.add(:phone_number, "is not a valid phone number") end end def enqueue_sending SendSmsJob.perform_later(id) end def ensure_metadata_is_hash self.metadata = {} if metadata.nil? self.metadata = {} unless metadata.is_a?(Hash) end end