Aggiunge filtri avanzati per i destinatari delle campagne email.
La scelta della lista passa da un wizard a tre passi, con anteprima del conteggio e lista congelata solo dopo la conferma. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
module Mailings
|
||||
class AudienceFilters
|
||||
PRESETS = Catalog::MAILING_AUDIENCES.keys.freeze
|
||||
HISTORY_KINDS = %w[never received not_received opened not_opened recent].freeze
|
||||
VALUE_MODES = %w[any present blank].freeze
|
||||
|
||||
ARRAY_KEYS = %w[
|
||||
sports team_genders regions provinces streaming_statuses statuses
|
||||
send_statuses ab_variants pipeline_stages
|
||||
exclude_received_mailing_ids exclude_opened_mailing_ids
|
||||
].freeze
|
||||
|
||||
def self.default(preset = "to_send")
|
||||
new("preset" => preset.presence_in(PRESETS) || "to_send")
|
||||
end
|
||||
|
||||
def self.from_params(raw)
|
||||
hash = if raw.respond_to?(:to_unsafe_h)
|
||||
raw.to_unsafe_h
|
||||
else
|
||||
raw.to_h
|
||||
end
|
||||
new(hash)
|
||||
end
|
||||
|
||||
def initialize(hash = {})
|
||||
@data = normalize(hash)
|
||||
end
|
||||
|
||||
def to_h
|
||||
@data.deep_dup
|
||||
end
|
||||
|
||||
def preset
|
||||
@data["preset"]
|
||||
end
|
||||
|
||||
def [](key)
|
||||
@data[key.to_s]
|
||||
end
|
||||
|
||||
def customized?
|
||||
@data.except("preset").any? { |_key, value| value.present? && value != false && value != "any" }
|
||||
end
|
||||
|
||||
def summary_parts
|
||||
parts = [Catalog.label_for(Catalog::MAILING_AUDIENCES, preset)]
|
||||
parts << "sport: #{Array(@data["sports"]).join(", ")}" if @data["sports"].present?
|
||||
parts << "M/F: #{Array(@data["team_genders"]).map { |g| Catalog.label_for(Catalog::TEAM_GENDERS, g) }.join(", ")}" if @data["team_genders"].present?
|
||||
parts << "regione: #{Array(@data["regions"]).join(", ")}" if @data["regions"].present?
|
||||
parts << "provincia: #{Array(@data["provinces"]).join(", ")}" if @data["provinces"].present?
|
||||
parts << "n. lista #{[@data["list_min"], @data["list_max"]].compact.join("–")}" if @data["list_min"].present? || @data["list_max"].present?
|
||||
parts << "stage: #{Array(@data["pipeline_stages"]).map { |s| Catalog.label_for(Catalog::PIPELINE_STAGES, s) }.join(", ")}" if @data["pipeline_stages"].present?
|
||||
parts << history_summary if history_kind.present?
|
||||
parts << "escludi clienti" if @data["exclude_customers"]
|
||||
parts << "escludi già ricevute" if @data["exclude_received_mailing_ids"].present?
|
||||
parts << "escludi aperte" if @data["exclude_opened_mailing_ids"].present?
|
||||
parts << "escludi inviate da #{@data["exclude_mailed_within_days"]}g" if @data["exclude_mailed_within_days"].present?
|
||||
parts
|
||||
end
|
||||
|
||||
def history_kind
|
||||
@data["history_kind"]
|
||||
end
|
||||
|
||||
def history_mailing_id
|
||||
@data["history_mailing_id"]
|
||||
end
|
||||
|
||||
def history_days
|
||||
@data["history_days"]
|
||||
end
|
||||
|
||||
def organization_extra_count
|
||||
count_present(%w[team_genders provinces streaming_statuses statuses]) +
|
||||
((@data["list_min"].present? || @data["list_max"].present?) ? 1 : 0)
|
||||
end
|
||||
|
||||
def campaign_extra_count
|
||||
count_present(%w[send_statuses ab_variants pipeline_stages]) +
|
||||
((@data["estimated_value"].present? && @data["estimated_value"] != "any") ? 1 : 0)
|
||||
end
|
||||
|
||||
def exclusions_count
|
||||
n = 0
|
||||
n += 1 if @data["exclude_customers"]
|
||||
n += 1 if @data["exclude_received_mailing_ids"].present?
|
||||
n += 1 if @data["exclude_opened_mailing_ids"].present?
|
||||
n += 1 if @data["exclude_mailed_within_days"].present?
|
||||
n
|
||||
end
|
||||
|
||||
def history_active?
|
||||
history_kind.present?
|
||||
end
|
||||
|
||||
def organization_extra_active?
|
||||
organization_extra_count.positive?
|
||||
end
|
||||
|
||||
def campaign_extra_active?
|
||||
campaign_extra_count.positive?
|
||||
end
|
||||
|
||||
def exclusions_active?
|
||||
exclusions_count.positive?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def count_present(keys)
|
||||
keys.count { |key| @data[key].present? }
|
||||
end
|
||||
|
||||
|
||||
def history_summary
|
||||
case history_kind
|
||||
when "never" then "mai ricevuta una campagna"
|
||||
when "received" then "già ricevuta campagna ##{history_mailing_id}"
|
||||
when "not_received" then "non ricevuta campagna ##{history_mailing_id}"
|
||||
when "opened" then "aperta campagna ##{history_mailing_id}"
|
||||
when "not_opened" then "non aperta campagna ##{history_mailing_id}"
|
||||
when "recent" then "inviata negli ultimi #{history_days} giorni"
|
||||
end
|
||||
end
|
||||
|
||||
def normalize(hash)
|
||||
source = hash.to_h.deep_stringify_keys
|
||||
data = { "preset" => source["preset"].presence_in(PRESETS) || "to_send" }
|
||||
|
||||
ARRAY_KEYS.each do |key|
|
||||
data[key] = Array(source[key]).flatten.map { |value| value.to_s.strip }.reject(&:blank?).uniq
|
||||
data[key] = data[key].map(&:to_i) if key.end_with?("_ids")
|
||||
end
|
||||
|
||||
data["list_min"] = integer_or_nil(source["list_min"])
|
||||
data["list_max"] = integer_or_nil(source["list_max"])
|
||||
data["estimated_value"] = source["estimated_value"].presence_in(VALUE_MODES) || "any"
|
||||
data["history_kind"] = source["history_kind"].presence_in(HISTORY_KINDS)
|
||||
data["history_mailing_id"] = integer_or_nil(source["history_mailing_id"])
|
||||
data["history_days"] = integer_or_nil(source["history_days"])
|
||||
data["exclude_customers"] = ActiveModel::Type::Boolean.new.cast(source["exclude_customers"]) || false
|
||||
data["exclude_mailed_within_days"] = integer_or_nil(source["exclude_mailed_within_days"])
|
||||
data
|
||||
end
|
||||
|
||||
def integer_or_nil(value)
|
||||
return if value.blank?
|
||||
|
||||
Integer(value, exception: false)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,153 @@
|
||||
module Mailings
|
||||
class AudienceQuery
|
||||
def initialize(project, filters = {}, mailing: nil)
|
||||
@project = project
|
||||
@filters = filters.is_a?(AudienceFilters) ? filters : AudienceFilters.new(filters)
|
||||
@mailing = mailing
|
||||
end
|
||||
|
||||
def relation
|
||||
scope = Organization.for_project(@project)
|
||||
scope = apply_preset(scope)
|
||||
scope = apply_organization_filters(scope)
|
||||
scope = apply_opportunity_filters(scope)
|
||||
scope = apply_history_filters(scope)
|
||||
scope = apply_exclusions(scope)
|
||||
scope.distinct
|
||||
end
|
||||
|
||||
def counts
|
||||
{
|
||||
total: relation.unscope(:order).distinct.count("organizations.id"),
|
||||
with_email: with_email_scope(relation).unscope(:order).distinct.count("organizations.id")
|
||||
}
|
||||
end
|
||||
|
||||
def preview(limit = 15)
|
||||
relation.includes(:primary_contact).reorder(Arel.sql("organizations.list_position ASC NULLS LAST"), "organizations.name ASC").limit(limit)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def apply_preset(scope)
|
||||
case @filters.preset
|
||||
when "to_send"
|
||||
scope.where(id: opportunity_scope.where(send_status: "to_send").select(:organization_id))
|
||||
when "test_a"
|
||||
scope.where(id: opportunity_scope.where(ab_variant: "A").select(:organization_id))
|
||||
when "test_b"
|
||||
scope.where(id: opportunity_scope.where(ab_variant: "B").select(:organization_id))
|
||||
else
|
||||
scope
|
||||
end
|
||||
end
|
||||
|
||||
def apply_organization_filters(scope)
|
||||
scope = scope.where(sport: @filters["sports"]) if @filters["sports"].present?
|
||||
scope = scope.where(team_gender: @filters["team_genders"]) if @filters["team_genders"].present?
|
||||
scope = scope.where(region: @filters["regions"]) if @filters["regions"].present?
|
||||
scope = scope.where(province: @filters["provinces"]) if @filters["provinces"].present?
|
||||
scope = scope.where(streaming_status: @filters["streaming_statuses"]) if @filters["streaming_statuses"].present?
|
||||
scope = scope.where(status: @filters["statuses"]) if @filters["statuses"].present?
|
||||
scope = scope.where("organizations.list_position >= ?", @filters["list_min"]) if @filters["list_min"].present?
|
||||
scope = scope.where("organizations.list_position <= ?", @filters["list_max"]) if @filters["list_max"].present?
|
||||
scope
|
||||
end
|
||||
|
||||
def apply_opportunity_filters(scope)
|
||||
extra = opportunity_scope
|
||||
filtered = false
|
||||
if @filters["send_statuses"].present?
|
||||
extra = extra.where(send_status: @filters["send_statuses"])
|
||||
filtered = true
|
||||
end
|
||||
if @filters["ab_variants"].present?
|
||||
extra = extra.where(ab_variant: @filters["ab_variants"])
|
||||
filtered = true
|
||||
end
|
||||
if @filters["pipeline_stages"].present?
|
||||
extra = extra.where(pipeline_stage: @filters["pipeline_stages"])
|
||||
filtered = true
|
||||
end
|
||||
case @filters["estimated_value"]
|
||||
when "present"
|
||||
extra = extra.where("opportunities.estimated_value IS NOT NULL AND opportunities.estimated_value > 0")
|
||||
filtered = true
|
||||
when "blank"
|
||||
extra = extra.where("opportunities.estimated_value IS NULL OR opportunities.estimated_value = 0")
|
||||
filtered = true
|
||||
end
|
||||
|
||||
filtered ? scope.where(id: extra.select(:organization_id)) : scope
|
||||
end
|
||||
|
||||
def apply_history_filters(scope)
|
||||
case @filters.history_kind
|
||||
when "never"
|
||||
scope.where.not(id: sent_org_ids)
|
||||
when "received"
|
||||
scope.where(id: sent_org_ids(mailing_id: @filters.history_mailing_id))
|
||||
when "not_received"
|
||||
scope.where.not(id: sent_org_ids(mailing_id: @filters.history_mailing_id))
|
||||
when "opened"
|
||||
scope.where(id: opened_org_ids(mailing_id: @filters.history_mailing_id))
|
||||
when "not_opened"
|
||||
sent = sent_org_ids(mailing_id: @filters.history_mailing_id)
|
||||
opened = opened_org_ids(mailing_id: @filters.history_mailing_id)
|
||||
scope.where(id: sent).where.not(id: opened)
|
||||
when "recent"
|
||||
days = @filters.history_days.presence || 30
|
||||
scope.where(id: sent_org_ids(since: days.days.ago))
|
||||
else
|
||||
scope
|
||||
end
|
||||
end
|
||||
|
||||
def apply_exclusions(scope)
|
||||
scope = scope.where.not(status: %w[active_customer inactive_customer]) if @filters["exclude_customers"]
|
||||
if @filters["exclude_received_mailing_ids"].present?
|
||||
scope = scope.where.not(id: sent_org_ids(mailing_id: project_mailing_ids(@filters["exclude_received_mailing_ids"])))
|
||||
end
|
||||
if @filters["exclude_opened_mailing_ids"].present?
|
||||
scope = scope.where.not(id: opened_org_ids(mailing_id: project_mailing_ids(@filters["exclude_opened_mailing_ids"])))
|
||||
end
|
||||
if @filters["exclude_mailed_within_days"].present?
|
||||
scope = scope.where.not(id: sent_org_ids(since: @filters["exclude_mailed_within_days"].days.ago))
|
||||
end
|
||||
scope
|
||||
end
|
||||
|
||||
def with_email_scope(scope)
|
||||
scope.left_joins(:contacts).where(
|
||||
"NULLIF(BTRIM(organizations.email), '') IS NOT NULL OR NULLIF(BTRIM(contacts.email), '') IS NOT NULL"
|
||||
)
|
||||
end
|
||||
|
||||
def opportunity_scope
|
||||
Opportunity.where(project_id: @project.id)
|
||||
end
|
||||
|
||||
def sent_org_ids(mailing_id: nil, since: nil)
|
||||
recips = history_recipients.where(status: "sent")
|
||||
recips = recips.where(mailing_id: mailing_id) if mailing_id.present?
|
||||
recips = recips.where("mailing_recipients.sent_at >= ?", since) if since.present?
|
||||
recips.select(:organization_id)
|
||||
end
|
||||
|
||||
def opened_org_ids(mailing_id: nil)
|
||||
recips = history_recipients.where.not(opened_at: nil)
|
||||
recips = recips.where(mailing_id: mailing_id) if mailing_id.present?
|
||||
recips.select(:organization_id)
|
||||
end
|
||||
|
||||
def history_recipients
|
||||
recips = MailingRecipient.joins(:mailing).where(mailings: { project_id: @project.id })
|
||||
recips = recips.where.not(mailing_id: @mailing.id) if @mailing&.persisted?
|
||||
recips
|
||||
end
|
||||
|
||||
def project_mailing_ids(ids)
|
||||
Mailing.for_project(@project).where(id: ids).select(:id)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -36,19 +36,9 @@ class Mailings::RecipientBuilder
|
||||
end
|
||||
|
||||
def scope
|
||||
orgs = Organization.for_project(@mailing.project).includes(:contacts, :opportunities)
|
||||
org_ids =
|
||||
case @mailing.audience
|
||||
when "to_send"
|
||||
Opportunity.where(project_id: @mailing.project_id, send_status: "to_send").select(:organization_id)
|
||||
when "test_a"
|
||||
Opportunity.where(project_id: @mailing.project_id, ab_variant: "A").select(:organization_id)
|
||||
when "test_b"
|
||||
Opportunity.where(project_id: @mailing.project_id, ab_variant: "B").select(:organization_id)
|
||||
else
|
||||
orgs.select(:id)
|
||||
end
|
||||
orgs.where(id: org_ids)
|
||||
Mailings::AudienceQuery.new(@mailing.project, @mailing.parsed_audience_filters, mailing: @mailing)
|
||||
.relation
|
||||
.includes(:contacts, :opportunities)
|
||||
end
|
||||
|
||||
def assign_variant!(attrs, org, split_index)
|
||||
|
||||
Reference in New Issue
Block a user