Ogni destinatario mostra se la mail è partita e se è stata aperta, con conteggi in dashboard. Co-authored-by: Cursor <cursoragent@cursor.com>
236 lines
6.2 KiB
Ruby
236 lines
6.2 KiB
Ruby
class Mailing < ApplicationRecord
|
||
include Auditable
|
||
include HtmlBlankable
|
||
|
||
belongs_to :project
|
||
belongs_to :mail_identity
|
||
belongs_to :mail_template, optional: true
|
||
belongs_to :mail_template_b, class_name: "MailTemplate", optional: true
|
||
has_many :mailing_recipients, dependent: :destroy
|
||
has_many :organizations, through: :mailing_recipients
|
||
has_many_attached :files
|
||
|
||
validates :name, :subject, :body_html, presence: true
|
||
validates :status, inclusion: { in: Catalog::MAILING_STATUSES.keys }
|
||
validates :audience, inclusion: { in: Catalog::MAILING_AUDIENCES.keys }
|
||
validates :ab_assignment, inclusion: { in: Catalog::MAILING_AB_ASSIGNMENTS.keys }
|
||
validates :interval_seconds, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 86_400 }
|
||
validates :subject_b, :body_html_b, presence: true, if: :ab_test?
|
||
validates :send_window_start_minutes, :send_window_end_minutes,
|
||
numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 24 * 60 },
|
||
allow_nil: true
|
||
validates :send_window_start_minutes, :send_window_end_minutes, presence: true, if: :send_window_enabled?
|
||
validate :send_window_order
|
||
clears_blank_html :body_html, :body_html_b
|
||
|
||
scope :recent, -> { order(created_at: :desc) }
|
||
scope :for_project, ->(project) { where(project_id: project.id) }
|
||
scope :sending, -> { where(status: "sending") }
|
||
scope :due_to_send, -> { where("next_send_at IS NOT NULL AND next_send_at <= ?", Time.current) }
|
||
|
||
def status_label
|
||
Catalog.label_for(Catalog::MAILING_STATUSES, status)
|
||
end
|
||
|
||
def audience_label
|
||
Catalog.label_for(Catalog::MAILING_AUDIENCES, audience)
|
||
end
|
||
|
||
def ab_assignment_label
|
||
Catalog.label_for(Catalog::MAILING_AB_ASSIGNMENTS, ab_assignment)
|
||
end
|
||
|
||
def ab_from_record?
|
||
ab_assignment == "from_record"
|
||
end
|
||
|
||
def subject_for(variant)
|
||
ab_test? && variant.to_s.upcase == "B" ? subject_b : subject
|
||
end
|
||
|
||
def body_for(variant)
|
||
ab_test? && variant.to_s.upcase == "B" ? body_html_b : body_html
|
||
end
|
||
|
||
def variant_pending_count(variant)
|
||
mailing_recipients.pending.where(ab_variant: variant).count
|
||
end
|
||
|
||
def variant_sent_count(variant)
|
||
mailing_recipients.sent.where(ab_variant: variant).count
|
||
end
|
||
|
||
def draft?
|
||
status == "draft"
|
||
end
|
||
|
||
def sending?
|
||
status == "sending"
|
||
end
|
||
|
||
def sent?
|
||
status == "sent"
|
||
end
|
||
|
||
def cancelled?
|
||
status == "cancelled"
|
||
end
|
||
|
||
def editable?
|
||
draft?
|
||
end
|
||
|
||
def pending_count
|
||
mailing_recipients.pending.count
|
||
end
|
||
|
||
def skipped_count
|
||
mailing_recipients.skipped.count
|
||
end
|
||
|
||
def sent_count
|
||
mailing_recipients.sent.count
|
||
end
|
||
|
||
def failed_count
|
||
mailing_recipients.failed.count
|
||
end
|
||
|
||
def opened_count
|
||
mailing_recipients.opened.count
|
||
end
|
||
|
||
def variant_opened_count(variant)
|
||
mailing_recipients.opened.where(ab_variant: variant).count
|
||
end
|
||
|
||
def open_rate
|
||
total = sent_count
|
||
return 0 if total.zero?
|
||
|
||
((opened_count * 100.0) / total).round
|
||
end
|
||
|
||
def queued_count
|
||
mailing_recipients.queued.count
|
||
end
|
||
|
||
def tracked_count
|
||
pending_count + queued_count + sent_count + failed_count
|
||
end
|
||
|
||
def progress_percentage
|
||
total = tracked_count
|
||
return 0 if total.zero?
|
||
|
||
(((sent_count + failed_count) * 100.0) / total).round
|
||
end
|
||
|
||
def tested?
|
||
test_sent_at.present?
|
||
end
|
||
|
||
def used_merge_tokens
|
||
blobs = [ subject, body_html ]
|
||
blobs += [ subject_b, body_html_b ] if ab_test?
|
||
blobs.join.scan(MailMerge::TOKEN).flatten.map { |token| token.to_s.downcase }.uniq
|
||
end
|
||
|
||
def rebuild_recipients!
|
||
mailing_recipients.delete_all
|
||
Mailings::RecipientBuilder.new(self).call
|
||
end
|
||
|
||
def queue_send!
|
||
raise "Invia prima una email di prova con dati di test" if test_sent_at.blank?
|
||
raise "Nessun destinatario da inviare" if pending_count.zero?
|
||
|
||
update!(status: "sending", queued_at: Time.current, authorized_at: authorized_at || Time.current)
|
||
enqueue_next_recipient!(mailing_recipients.pending.order(:id).first, from: Time.current)
|
||
end
|
||
|
||
def enqueue_next_recipient!(recipient = nil, from: Time.current)
|
||
recipient ||= mailing_recipients.pending.order(:id).first
|
||
if recipient.nil?
|
||
update!(next_send_at: nil) if next_send_at.present?
|
||
mark_finished_if_done!
|
||
return
|
||
end
|
||
|
||
at = Mailings::SendClock.new(self, time: from).next_send_at(earliest: from)
|
||
update!(next_send_at: at)
|
||
wait = at - Time.current
|
||
return if wait > 2.minutes
|
||
|
||
SendMailingRecipientJob.set(wait: [ wait, 0 ].max).perform_later(recipient.id)
|
||
end
|
||
|
||
def send_clock
|
||
Mailings::SendClock.new(self)
|
||
end
|
||
|
||
def send_window_label
|
||
return unless send_window_enabled?
|
||
|
||
"#{send_clock.window_label}, lun–ven"
|
||
end
|
||
|
||
def send_window_start_tod
|
||
minutes_to_tod(send_window_start_minutes)
|
||
end
|
||
|
||
def send_window_start_tod=(value)
|
||
self.send_window_start_minutes = self.class.parse_tod_minutes(value)
|
||
end
|
||
|
||
def send_window_end_tod
|
||
minutes_to_tod(send_window_end_minutes)
|
||
end
|
||
|
||
def send_window_end_tod=(value)
|
||
self.send_window_end_minutes = self.class.parse_tod_minutes(value)
|
||
end
|
||
|
||
def self.parse_tod_minutes(value)
|
||
return if value.blank?
|
||
return (value.hour * 60) + value.min if value.respond_to?(:hour)
|
||
|
||
hours, minutes = value.to_s.split(":").map(&:to_i)
|
||
(hours * 60) + minutes.to_i
|
||
end
|
||
|
||
def mark_finished_if_done!
|
||
return unless sending?
|
||
return if mailing_recipients.pending.exists? || mailing_recipients.queued.exists?
|
||
|
||
update!(status: "sent", completed_at: Time.current, next_send_at: nil)
|
||
end
|
||
|
||
def cancel_send!
|
||
raise "Solo un invio in corso può essere fermato" unless sending?
|
||
|
||
transaction do
|
||
mailing_recipients.where(status: %w[pending queued]).find_each do |recipient|
|
||
recipient.update!(status: "skipped", skip_reason: "invio annullato")
|
||
end
|
||
update!(status: "cancelled", next_send_at: nil, completed_at: Time.current)
|
||
end
|
||
end
|
||
|
||
private
|
||
|
||
def send_window_order
|
||
return unless send_window_enabled?
|
||
return if send_window_start_minutes.blank? || send_window_end_minutes.blank?
|
||
return if send_window_start_minutes < send_window_end_minutes
|
||
|
||
errors.add(:send_window_end_tod, "deve essere successiva all'inizio della fascia")
|
||
end
|
||
|
||
def minutes_to_tod(minutes)
|
||
return if minutes.nil?
|
||
|
||
Time.zone.local(2000, 1, 1, minutes / 60, minutes % 60, 0)
|
||
end
|
||
end
|