class MailingRecipient < ApplicationRecord belongs_to :mailing belongs_to :organization belongs_to :contact, optional: true validates :status, inclusion: { in: Catalog::MAILING_RECIPIENT_STATUSES.keys } validates :ab_variant, inclusion: { in: Catalog::AB_VARIANTS.keys }, allow_blank: true validates :tracking_token, presence: true, uniqueness: true before_validation :assign_tracking_token, on: :create scope :pending, -> { where(status: "pending") } scope :skipped, -> { where(status: "skipped") } scope :sent, -> { where(status: "sent") } scope :failed, -> { where(status: "failed") } scope :queued, -> { where(status: "queued") } scope :opened, -> { where.not(opened_at: nil) } scope :ordered, -> { joins(:organization).order("organizations.list_position ASC NULLS LAST", "organizations.name ASC") } def status_label Catalog.label_for(Catalog::MAILING_RECIPIENT_STATUSES, status) end def pending? status == "pending" end def sent? status == "sent" end def email_ok? email.present? && email.match?(URI::MailTo::EMAIL_REGEXP) end def opened? opened_at.present? end def record_open!(at: Time.current) return unless sent? first_open = false with_lock do return unless sent? first_open = opened_at.blank? attrs = { last_opened_at: at, open_count: open_count + 1 } attrs[:opened_at] = at if first_open update!(attrs) end log_open_activity! if first_open end def merge_context opportunity = organization.campaign_opportunity(mailing.project) { organization: organization, contact: contact, project: mailing.project, opportunity: opportunity, ab_variant: ab_variant.presence || opportunity&.ab_variant } end def rendered_html MailMerge.render(mailing.body_for(ab_variant), **merge_context) end def rendered_subject_line MailMerge.render(mailing.subject_for(ab_variant), **merge_context) end def deliver! with_lock do return if status.in?(%w[sent skipped]) return abort_delivery!("invio annullato") unless mailing.sending? update!(status: "queued") end return unless mailing.sending? html = rendered_html subject_line = rendered_subject_line CampaignMailer.outreach(self, html: html, subject: subject_line).deliver_now record_success!(subject_line) rescue StandardError => e update!(status: "failed", error_message: e.message.to_s.truncate(500)) ensure mailing.mark_finished_if_done! end private def abort_delivery!(reason) update!(status: "skipped", skip_reason: reason) end def record_success!(subject_line) transaction do update!(status: "sent", sent_at: Time.current, rendered_subject: subject_line, error_message: nil) sync_opportunity! log_activity!(subject_line) end end def sync_opportunity! opportunity = organization.campaign_opportunity(mailing.project) return unless opportunity attrs = {} attrs[:send_status] = "sent" if opportunity.send_status == "to_send" attrs[:sent_on] ||= Date.current if opportunity.sent_on.blank? attrs[:pipeline_stage] = "contacted" if opportunity.pipeline_stage == "to_contact" attrs[:ab_variant] = ab_variant if mailing.ab_test? && ab_variant.present? && opportunity.ab_variant.blank? opportunity.update!(attrs) if attrs.any? end def assign_tracking_token self.tracking_token ||= self.class.generate_tracking_token end def self.generate_tracking_token loop do token = SecureRandom.urlsafe_base64(24) break token unless exists?(tracking_token: token) end end def log_open_activity! organization.activities.create!( activity_type: "email_opened", subject: rendered_subject.presence || mailing.subject_for(ab_variant), description: "Apertura rilevata · campagna #{mailing.name}", happened_at: opened_at || Time.current, user: mailing.created_by, contact: contact, opportunity: organization.campaign_opportunity(mailing.project) ) end def log_activity!(subject_line) variant_note = "variante #{ab_variant}" if mailing.ab_test? && ab_variant.present? organization.activities.create!( activity_type: "email_sent", subject: subject_line.presence || mailing.name, description: ["Campagna email: #{mailing.name}", variant_note].compact.join(" · "), happened_at: Time.current, user: mailing.created_by, contact: contact, opportunity: organization.campaign_opportunity(mailing.project) ) end end