Files
eminuxCRM/app/models/mailing.rb
T
eminuxandCursor d98c3e87a8
CI / scan_ruby (push) Failing after 14m2s
CI / scan_js (push) Successful in 12m13s
CI / lint (push) Failing after 11m23s
Aggiunge pulsante per fermare un invio email in corso.
Permette di annullare i destinatari ancora in coda mantenendo le email già inviate.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-18 00:49:57 +02:00

221 lines
5.9 KiB
Ruby
Raw 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
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 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 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