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 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 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 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