Files
eminuxandCursor 3c8bfe053f
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / scan_ruby (push) Has been cancelled
Permette di mettere in pausa un invio e riprenderlo senza perdere la coda.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-07 12:35:42 +02:00

283 lines
7.7 KiB
Ruby
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
before_validation :sync_audience_filters
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
parsed_audience_filters.summary_parts.join(" · ")
end
def parsed_audience_filters
Mailings::AudienceFilters.new(audience_filters.presence || { "preset" => audience.presence || "to_send" })
end
def audience_query
Mailings::AudienceQuery.new(project, parsed_audience_filters, mailing: self)
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 paused?
status == "paused"
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}, lunven"
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 pause_send!
raise "Solo un invio in corso può essere messo in pausa" unless sending?
update!(status: "paused", next_send_at: nil)
end
def resume_send!
raise "Solo un invio in pausa può essere ripreso" unless paused?
raise "Nessun destinatario da inviare" if pending_count.zero? && queued_count.zero?
update!(status: "sending")
enqueue_next_recipient!(mailing_recipients.pending.order(:id).first, from: Time.current) if pending_count.positive?
DrainOutboundJob.perform_later if mailing_recipients.queued.exists?
end
def cancel_send!
raise "Solo un invio in corso o in pausa può essere annullato" unless sending? || paused?
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 sync_audience_filters
source = stringify_audience_filters(audience_filters)
if will_save_change_to_audience? && !will_save_change_to_audience_filters?
source["preset"] = audience
elsif source["preset"].blank?
source["preset"] = audience.presence || "to_send"
end
filters = Mailings::AudienceFilters.new(source)
self.audience_filters = filters.to_h
self.audience = filters.preset
end
def stringify_audience_filters(value)
return {} if value.blank?
value.respond_to?(:to_unsafe_h) ? value.to_unsafe_h.deep_stringify_keys : value.to_h.deep_stringify_keys
end
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