Aggiunge il rilevamento di invio e apertura delle email di campagna.
CI / scan_ruby (push) Failing after 12m56s
CI / scan_js (push) Successful in 12m23s
CI / lint (push) Failing after 12m15s

Ogni destinatario mostra se la mail è partita e se è stata aperta, con conteggi in dashboard.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-20 12:26:14 +02:00
co-authored by Cursor
parent ab57309f5c
commit 2db7504629
18 changed files with 332 additions and 5 deletions
+50
View File
@@ -5,12 +5,16 @@ class MailingRecipient < ApplicationRecord
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
@@ -21,10 +25,33 @@ class MailingRecipient < ApplicationRecord
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)
{
@@ -90,6 +117,29 @@ class MailingRecipient < ApplicationRecord
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!(