Ogni destinatario mostra se la mail è partita e se è stata aperta, con conteggi in dashboard. Co-authored-by: Cursor <cursoragent@cursor.com>
156 lines
4.5 KiB
Ruby
156 lines
4.5 KiB
Ruby
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
|