Files
eminuxCRM/app/controllers/mailings_controller.rb
T
eminuxandCursor 353ace4d99
CI / scan_ruby (push) Failing after 12m32s
CI / scan_js (push) Successful in 13m28s
CI / lint (push) Failing after 13m58s
Permette di scegliere 12 ore, 24 ore o la settimana nello storico invii.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-07 13:00:09 +02:00

385 lines
14 KiB
Ruby

class MailingsController < ApplicationController
before_action :require_current_project!
before_action :set_mailing, only: %i[show edit update destroy queue pause resume cancel test_send test_preview refresh_recipients update_recipients preview audience update_audience]
before_action :load_form_collections, only: %i[new create edit update]
def index
redirect_to dashboard_mailings_path
end
def dashboard
@page_title = "Invii email"
@mailings = Mailing.for_project(current_project).includes(:mail_identity).recent
@mail_identities_count = MailIdentity.active.count
@active_mailings = @mailings.select { |mailing| mailing.sending? || mailing.paused? }
@window_paused_mailings = @mailings.select(&:sending?).select { |mailing| mailing.next_send_at.present? && mailing.next_send_at > Time.current }
@paused_mailings = @window_paused_mailings
@completed_today = @mailings.count { |mailing| mailing.sent? && mailing.completed_at&.to_date == Time.zone.today }
@failed_open = @mailings.sum(&:failed_count)
@hourly_throughput = Mailings::HourlyThroughput.new(current_project, range: params[:history])
end
def new
@page_title = "Nuovo invio"
template = MailTemplate.for_project(current_project).order(:name).first
identity = MailIdentity.active.ordered.first
@mailing = Mailing.new(
project: current_project,
mail_template: template,
mail_identity: identity,
audience: "to_send",
audience_filters: Mailings::AudienceFilters.default("to_send").to_h,
interval_seconds: 0,
send_window_start_minutes: 10 * 60,
send_window_end_minutes: 18 * 60,
ab_assignment: "from_record",
name: "Invio #{l(Time.zone.today)}",
subject: template&.subject,
body_html: template&.body_html
)
end
def create
@mailing = Mailing.new(mailing_params)
@mailing.project = current_project
apply_template_if_needed
if @mailing.save
redirect_to audience_mailing_path(@mailing), notice: "Bozza creata. Ora raffina i destinatari, poi genera la lista."
else
@page_title = "Nuovo invio"
render :new, status: :unprocessable_entity
end
end
def show
@page_title = @mailing.name
@recipients = @mailing.mailing_recipients.includes(:organization, :contact).ordered
@preview_recipient = preview_recipient
@preview_variant = preview_variant
context = @preview_recipient&.merge_context || { project: current_project, ab_variant: @preview_variant }
@preview_html = MailMerge.render(@mailing.body_for(@preview_variant), **context)
@preview_subject = MailMerge.render(@mailing.subject_for(@preview_variant), **context)
assign_test_preview
end
def edit
unless @mailing.editable?
redirect_to @mailing, alert: "Questo invio non è più modificabile."
return
end
@page_title = "Modifica invio"
end
def update
unless @mailing.editable?
redirect_to @mailing, alert: "Questo invio non è più modificabile."
return
end
@mailing.assign_attributes(mailing_params)
apply_template_if_needed
if @mailing.save
rebuild_recipients_if_needed
redirect_to @mailing, notice: "Invio aggiornato."
else
@page_title = "Modifica invio"
render :edit, status: :unprocessable_entity
end
end
def destroy
unless @mailing.draft?
redirect_to dashboard_mailings_path, alert: "Puoi eliminare solo le bozze."
return
end
@mailing.destroy!
redirect_to dashboard_mailings_path, notice: "Invio eliminato."
end
def audience
unless @mailing.editable?
redirect_to @mailing, alert: "I destinatari sono bloccati: l'invio è già partito."
return
end
@page_title = "Destinatari · #{@mailing.name}"
load_audience_options
assign_audience_preview
end
def update_audience
unless @mailing.editable?
redirect_to @mailing, alert: "I destinatari sono bloccati: l'invio è già partito."
return
end
filters = Mailings::AudienceFilters.from_params(audience_filter_params)
@mailing.assign_attributes(audience_filters: filters.to_h, audience: filters.preset)
unless @mailing.save
@page_title = "Destinatari · #{@mailing.name}"
load_audience_options
assign_audience_preview
render :audience, status: :unprocessable_entity
return
end
if Array(params[:intent]).include?("apply")
@mailing.rebuild_recipients!
redirect_to @mailing, notice: "Lista generata: #{@mailing.pending_count} da inviare, #{@mailing.skipped_count} esclusi."
else
open_sections = Array(params[:open_sections]).map(&:to_s).compact_blank
if open_sections.any?
redirect_to audience_mailing_path(@mailing, open_sections: open_sections)
else
redirect_to audience_mailing_path(@mailing)
end
end
end
def refresh_recipients
unless @mailing.editable?
redirect_to @mailing, alert: "Destinatari bloccati: l'invio è già partito."
return
end
@mailing.rebuild_recipients!
redirect_to @mailing, notice: "Lista destinatari aggiornata."
end
def update_recipients
unless @mailing.editable?
redirect_to @mailing, alert: "Non puoi più cambiare i destinatari."
return
end
selected = Array(params[:pending_ids]).map(&:to_i)
@mailing.mailing_recipients.where.not(status: %w[sent queued]).find_each do |recipient|
if selected.include?(recipient.id) && recipient.email_ok?
recipient.update!(status: "pending", skip_reason: nil)
else
reason = recipient.email_ok? ? "escluso dal check" : (recipient.skip_reason.presence || "email non valida")
recipient.update!(status: "skipped", skip_reason: reason)
end
end
redirect_to @mailing, notice: "Selezione destinatari salvata."
end
def queue
unless @mailing.draft? || @mailing.sending?
redirect_to @mailing, alert: "Questo invio è già stato concluso."
return
end
unless @mailing.tested?
redirect_to @mailing, alert: "Prima di autorizzare l'invio di massa, manda una email di prova con dati di test."
return
end
@mailing.queue_send!
redirect_to dashboard_mailings_path, notice: queue_notice(@mailing)
rescue StandardError => e
redirect_to @mailing, alert: e.message
end
def pause
unless @mailing.sending?
redirect_to @mailing, alert: "Solo un invio in corso può essere messo in pausa."
return
end
remaining = @mailing.pending_count + @mailing.queued_count
@mailing.pause_send!
redirect_to dashboard_mailings_path,
notice: "Invio in pausa. #{@mailing.sent_count} già inviate, #{remaining} pronte per quando riprendi."
rescue StandardError => e
redirect_to @mailing, alert: e.message
end
def resume
unless @mailing.paused?
redirect_to @mailing, alert: "Solo un invio in pausa può essere ripreso."
return
end
@mailing.resume_send!
redirect_to dashboard_mailings_path, notice: "Invio ripreso da dove era rimasto."
rescue StandardError => e
redirect_to @mailing, alert: e.message
end
def cancel
unless @mailing.sending? || @mailing.paused?
redirect_to @mailing, alert: "Solo un invio in corso o in pausa può essere annullato."
return
end
pending = @mailing.pending_count + @mailing.queued_count
@mailing.cancel_send!
redirect_to dashboard_mailings_path,
notice: "Invio annullato. #{@mailing.sent_count} già inviate, #{pending} in coda saltate."
rescue StandardError => e
redirect_to @mailing, alert: e.message
end
def test_send
assign_test_preview
to = @test_to.to_s.strip
unless to.match?(URI::MailTo::EMAIL_REGEXP)
redirect_to @mailing, alert: "Inserisci un indirizzo email valido per la prova."
return
end
html = @test_preview_html
subject = "[TEST#{@mailing.ab_test? ? " #{@test_variant}" : ""}] #{@test_preview_subject}"
recipient = Mailings::TestRecipient.new(mailing: @mailing, email: to)
Mailings::OutboundQueue.exclusive do
Mailings::SmtpGate.deliver do
CampaignMailer.outreach(recipient, html: html, subject: subject).deliver_now
end
Mailings::OutboundQueue.stamp!
end
@mailing.update!(test_sent_at: Time.current, test_sent_to: to)
redirect_to @mailing, notice: "Prova inviata subito a #{to}. Controlla l'output e, se è corretto, autorizza l'invio di massa."
rescue StandardError => e
redirect_to @mailing, alert: "Invio di prova non riuscito: #{e.message}"
end
def test_preview
assign_test_preview
end
def preview
redirect_to mailing_path(@mailing, recipient_id: params[:recipient_id])
end
private
def set_mailing
@mailing = Mailing.for_project(current_project).find(params[:id])
end
def load_form_collections
@mail_identities = MailIdentity.active.ordered
@mail_templates = MailTemplate.for_project(current_project)
end
def mailing_params
params.require(:mailing).permit(
:name, :mail_identity_id, :mail_template_id, :mail_template_b_id, :audience,
:subject, :body_html, :subject_b, :body_html_b, :ab_test, :ab_assignment,
:interval_seconds, :send_window_enabled, :send_window_start_tod, :send_window_end_tod, files: []
)
end
def queue_notice(mailing)
pause = mailing.interval_seconds.positive? ? " con pausa di #{mailing.interval_seconds}s" : ""
if mailing.send_window_enabled? && mailing.next_send_at && mailing.next_send_at > Time.current
"Invio avviato. La prima email partirà #{I18n.l(mailing.next_send_at, format: :long)} (fascia #{mailing.send_window_label}#{pause})."
else
"Invio avviato. Le email partono in background#{pause}."
end
end
def apply_template_if_needed
copy_template_if_blank(params.dig(:mailing, :mail_template_id), :subject, :body_html)
copy_template_if_blank(params.dig(:mailing, :mail_template_b_id), :subject_b, :body_html_b)
end
def copy_template_if_blank(template_id, subject_attr, body_attr)
return if template_id.blank?
template = MailTemplate.for_project(current_project).find_by(id: template_id)
return unless template
@mailing.public_send("#{subject_attr}=", template.subject) if @mailing.public_send(subject_attr).blank?
@mailing.public_send("#{body_attr}=", template.body_html) if @mailing.public_send(body_attr).blank?
end
def rebuild_recipients_if_needed
return unless @mailing.editable?
return unless @mailing.saved_change_to_ab_test? || @mailing.saved_change_to_ab_assignment?
@mailing.rebuild_recipients!
end
def load_audience_options
orgs = Organization.for_project(current_project)
@sport_options = orgs.where.not(sport: [nil, ""]).distinct.order(:sport).pluck(:sport)
@region_options = orgs.where.not(region: [nil, ""]).distinct.order(:region).pluck(:region)
@province_options = orgs.where.not(province: [nil, ""]).distinct.order(:province).pluck(:province)
@history_mailings = Mailing.for_project(current_project).where.not(id: @mailing.id).recent
end
def assign_audience_preview
@filters = @mailing.parsed_audience_filters
query = @mailing.audience_query
@audience_counts = query.counts
@audience_preview = query.preview
@open_sections = Array(params[:open_sections]).map(&:to_s)
end
def audience_filter_params
raw = params.fetch(:filters, ActionController::Parameters.new)
copied = raw.respond_to?(:to_unsafe_h) ? raw.to_unsafe_h : raw.to_h
Mailings::AudienceFilters::ARRAY_KEYS.each do |key|
copied[key] = Array(copied[key]) if copied.key?(key)
end
ActionController::Parameters.new(copied).permit(
:preset, :list_min, :list_max, :estimated_value, :history_kind, :history_days,
:history_mailing_id, :never_contacted, :exclude_customers, :exclude_mailed_within_days,
sports: [], team_genders: [], regions: [], provinces: [], streaming_statuses: [], statuses: [],
send_statuses: [], ab_variants: [], pipeline_stages: [],
exclude_received_mailing_ids: [], exclude_opened_mailing_ids: []
)
end
def preview_recipient
if params[:recipient_id].present?
@mailing.mailing_recipients.find_by(id: params[:recipient_id])
else
@mailing.mailing_recipients.pending.ordered.first || @mailing.mailing_recipients.ordered.first
end
end
def preview_variant
requested = params[:variant].to_s.upcase
return requested if requested.in?(%w[A B])
return "A" unless @mailing.ab_test?
preview_recipient&.ab_variant.presence_in(%w[A B]) || "A"
end
def assign_test_preview
@test_to = test_send_params[:to].presence || current_user.email
@test_variant = test_send_params[:variant].to_s.upcase.presence_in(%w[A B]) || preview_variant
@test_vars = default_test_vars.merge(test_vars_params)
extras = @test_vars
@test_preview_html = MailMerge.render(@mailing.body_for(@test_variant), extras: extras, project: current_project, ab_variant: @test_variant)
@test_preview_subject = MailMerge.render(@mailing.subject_for(@test_variant), extras: extras, project: current_project, ab_variant: @test_variant)
end
def default_test_vars
source = preview_recipient
if source
MailMerge.variables_for(**source.merge_context.merge(ab_variant: preview_variant))
else
MailMerge.catalog.keys.index_with { "" }
end
end
def test_send_params
params.permit(:to, :variant, :recipient_id)
end
def test_vars_params
raw = params[:vars]
return {} unless raw.respond_to?(:to_unsafe_h) || raw.is_a?(Hash)
allowed = MailMerge.catalog.keys
hash = raw.respond_to?(:to_unsafe_h) ? raw.to_unsafe_h : raw
hash.stringify_keys.slice(*allowed).transform_values { |value| value.to_s }
end
end