Aggiunge fascia oraria, prova manuale obbligatoria e cruscotto invii email.
Le campagne rispettano orario e giorni lavorativi, richiedono una mail di test con dati inseriti a mano prima dell'invio massivo e offrono una dashboard per monitorare l'avanzamento. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
module Italy
|
||||
class Holidays
|
||||
FIXED = [
|
||||
[1, 1],
|
||||
[1, 6],
|
||||
[4, 25],
|
||||
[5, 1],
|
||||
[6, 2],
|
||||
[8, 15],
|
||||
[11, 1],
|
||||
[12, 8],
|
||||
[12, 25],
|
||||
[12, 26]
|
||||
].freeze
|
||||
|
||||
def self.holiday?(date)
|
||||
date = date.to_date
|
||||
return true if FIXED.include?([date.month, date.day])
|
||||
|
||||
easter = easter_date(date.year)
|
||||
date == easter || date == (easter + 1)
|
||||
end
|
||||
|
||||
# Algoritmo gregoriano anonimo (Pasqua occidentale).
|
||||
def self.easter_date(year)
|
||||
a = year % 19
|
||||
b, c = year.divmod(100)
|
||||
d, e = b.divmod(4)
|
||||
f = (b + 8) / 25
|
||||
g = (b - f + 1) / 3
|
||||
h = (19 * a + b - d - g + 15) % 30
|
||||
i, k = c.divmod(4)
|
||||
l = (32 + 2 * e + 2 * i - h - k) % 7
|
||||
m = (a + 11 * h + 22 * l) / 451
|
||||
month, day = (h + l - 7 * m + 114).divmod(31)
|
||||
Date.new(year, month, day + 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
+102
-4
@@ -16,10 +16,17 @@ class Mailing < ApplicationRecord
|
||||
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)
|
||||
@@ -85,23 +92,114 @@ class Mailing < ApplicationRecord
|
||||
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)
|
||||
first = mailing_recipients.pending.order(:id).first
|
||||
SendMailingRecipientJob.perform_later(first.id)
|
||||
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)
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user