corretti bug sull'invio email
CI / scan_ruby (push) Failing after 12m46s
CI / scan_js (push) Successful in 12m35s
CI / lint (push) Failing after 12m46s

This commit is contained in:
2026-09-10 17:08:50 +02:00
parent 36b3ffe507
commit 3f7794959e
8 changed files with 59 additions and 6 deletions
+2 -1
View File
@@ -306,6 +306,7 @@ class MailingsController < ApplicationController
def load_audience_options def load_audience_options
orgs = Organization.for_project(current_project) orgs = Organization.for_project(current_project)
@sport_options = orgs.where.not(sport: [nil, ""]).distinct.order(:sport).pluck(:sport) @sport_options = orgs.where.not(sport: [nil, ""]).distinct.order(:sport).pluck(:sport)
@country_options = orgs.where.not(country: [nil, ""]).distinct.order(:country).pluck(:country)
@region_options = orgs.where.not(region: [nil, ""]).distinct.order(:region).pluck(:region) @region_options = orgs.where.not(region: [nil, ""]).distinct.order(:region).pluck(:region)
@province_options = orgs.where.not(province: [nil, ""]).distinct.order(:province).pluck(:province) @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 @history_mailings = Mailing.for_project(current_project).where.not(id: @mailing.id).recent
@@ -329,7 +330,7 @@ class MailingsController < ApplicationController
ActionController::Parameters.new(copied).permit( ActionController::Parameters.new(copied).permit(
:preset, :list_min, :list_max, :estimated_value, :history_kind, :history_days, :preset, :list_min, :list_max, :estimated_value, :history_kind, :history_days,
:history_mailing_id, :never_contacted, :exclude_customers, :exclude_mailed_within_days, :history_mailing_id, :never_contacted, :exclude_customers, :exclude_mailed_within_days,
sports: [], team_genders: [], regions: [], provinces: [], streaming_statuses: [], statuses: [], sports: [], countries: [], team_genders: [], regions: [], provinces: [], streaming_statuses: [], statuses: [],
send_statuses: [], ab_variants: [], pipeline_stages: [], send_statuses: [], ab_variants: [], pipeline_stages: [],
exclude_received_mailing_ids: [], exclude_opened_mailing_ids: [] exclude_received_mailing_ids: [], exclude_opened_mailing_ids: []
) )
+6 -1
View File
@@ -115,7 +115,12 @@ class MailingRecipient < ApplicationRecord
end end
MAX_SMTP_DEFERS = 8 MAX_SMTP_DEFERS = 8
DEFER_PATTERN = /tentativo (\d+)\// # Deve combaciare col testo generato sotto ("Da ritentare (N/8): ..."),
# altrimenti smtp_defer_count legge sempre 0 e il retry diventa infinito
# (bug osservato in produzione: un destinatario con SMTP EOF permanente
# veniva ritentato ogni 30s all'infinito, bloccando gli altri destinatari
# in errore della stessa mailing).
DEFER_PATTERN = /Da ritentare \((\d+)\//
def defer_or_fail!(error) def defer_or_fail!(error)
attempt = smtp_defer_count + 1 attempt = smtp_defer_count + 1
+2 -1
View File
@@ -5,7 +5,7 @@ module Mailings
VALUE_MODES = %w[any present blank].freeze VALUE_MODES = %w[any present blank].freeze
ARRAY_KEYS = %w[ ARRAY_KEYS = %w[
sports team_genders regions provinces streaming_statuses statuses sports countries team_genders regions provinces streaming_statuses statuses
send_statuses ab_variants pipeline_stages send_statuses ab_variants pipeline_stages
exclude_received_mailing_ids exclude_opened_mailing_ids exclude_received_mailing_ids exclude_opened_mailing_ids
].freeze ].freeze
@@ -46,6 +46,7 @@ module Mailings
def summary_parts def summary_parts
parts = [Catalog.label_for(Catalog::MAILING_AUDIENCES, preset)] parts = [Catalog.label_for(Catalog::MAILING_AUDIENCES, preset)]
parts << "sport: #{Array(@data["sports"]).join(", ")}" if @data["sports"].present? parts << "sport: #{Array(@data["sports"]).join(", ")}" if @data["sports"].present?
parts << "nazione: #{Array(@data["countries"]).join(", ")}" if @data["countries"].present?
parts << "M/F: #{Array(@data["team_genders"]).map { |g| Catalog.label_for(Catalog::TEAM_GENDERS, g) }.join(", ")}" if @data["team_genders"].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 << "regione: #{Array(@data["regions"]).join(", ")}" if @data["regions"].present?
parts << "provincia: #{Array(@data["provinces"]).join(", ")}" if @data["provinces"].present? parts << "provincia: #{Array(@data["provinces"]).join(", ")}" if @data["provinces"].present?
+1
View File
@@ -44,6 +44,7 @@ module Mailings
def apply_organization_filters(scope) def apply_organization_filters(scope)
scope = scope.where(sport: @filters["sports"]) if @filters["sports"].present? scope = scope.where(sport: @filters["sports"]) if @filters["sports"].present?
scope = scope.where(country: @filters["countries"]) if @filters["countries"].present?
scope = scope.where(team_gender: @filters["team_genders"]) if @filters["team_genders"].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(region: @filters["regions"]) if @filters["regions"].present?
scope = scope.where(province: @filters["provinces"]) if @filters["provinces"].present? scope = scope.where(province: @filters["provinces"]) if @filters["provinces"].present?
+6 -1
View File
@@ -48,11 +48,16 @@ class Mailings::OutboundQueue
end end
def next_queued def next_queued
# Sceglie il destinatario piu vecchio (per priorita/updated_at) TRA le mailing
# la cui finestra di invio e attualmente aperta. Prima si sceglieva il piu vecchio
# in assoluto: se apparteneva a una mailing con finestra chiusa, drain_one!
# restituiva :closed e si fermava, bloccando l'intera coda condivisa anche per
# le mailing senza vincoli orari (bug: coda bloccata da mailing con orario chiuso).
queued_scope.order( queued_scope.order(
Arel.sql( Arel.sql(
"CASE WHEN mailing_recipients.error_message IS NULL OR mailing_recipients.error_message = '' THEN 0 ELSE 1 END, mailing_recipients.updated_at ASC, mailing_recipients.id ASC" "CASE WHEN mailing_recipients.error_message IS NULL OR mailing_recipients.error_message = '' THEN 0 ELSE 1 END, mailing_recipients.updated_at ASC, mailing_recipients.id ASC"
) )
).first ).includes(:mailing).find { |r| Mailings::SendClock.new(r.mailing).open? }
end end
def queued_scope def queued_scope
+7 -2
View File
@@ -29,7 +29,7 @@
<section class="<%= card_class %> space-y-4 p-4 md:p-6"> <section class="<%= card_class %> space-y-4 p-4 md:p-6">
<div> <div>
<h2 class="text-base font-semibold">Chi deve ricevere questa email?</h2> <h2 class="text-base font-semibold">Chi deve ricevere questa email?</h2>
<p class="mt-1 text-sm text-zinc-500">Lista di partenza, sport, regione e chi non ha mai ricevuto una email. Il resto è opzionale.</p> <p class="mt-1 text-sm text-zinc-500">Lista di partenza, sport, nazione, regione e chi non ha mai ricevuto una email. Il resto è opzionale.</p>
</div> </div>
<div> <div>
@@ -47,12 +47,17 @@
</span> </span>
</label> </label>
<div class="grid gap-4 md:grid-cols-2"> <div class="grid gap-4 md:grid-cols-3">
<div> <div>
<label class="mb-1 block text-sm font-medium">Sport</label> <label class="mb-1 block text-sm font-medium">Sport</label>
<%= render "mailings/audience_choices", name: "filters[sports]", <%= render "mailings/audience_choices", name: "filters[sports]",
choices: @sport_options.map { |sport| [sport, sport] }, selected: @filters["sports"] %> choices: @sport_options.map { |sport| [sport, sport] }, selected: @filters["sports"] %>
</div> </div>
<div>
<label class="mb-1 block text-sm font-medium">Nazione</label>
<%= render "mailings/audience_choices", name: "filters[countries]",
choices: @country_options.map { |country| [country, country] }, selected: @filters["countries"] %>
</div>
<div> <div>
<label class="mb-1 block text-sm font-medium">Regione</label> <label class="mb-1 block text-sm font-medium">Regione</label>
<%= render "mailings/audience_choices", name: "filters[regions]", <%= render "mailings/audience_choices", name: "filters[regions]",
@@ -85,6 +85,33 @@ class Mailings::OutboundQueueTest < ActiveSupport::TestCase
assert_equal "queued", recipient_a.status assert_equal "queued", recipient_a.status
assert_match(/Da ritentare \(1\/8\)/, recipient_a.error_message) assert_match(/Da ritentare \(1\/8\)/, recipient_a.error_message)
assert_equal recipient_b.id, Mailings::OutboundQueue.next_queued.id assert_equal recipient_b.id, Mailings::OutboundQueue.next_queued.id
# Il contatore deve avanzare sullo stesso destinatario (prima il regex
# non matchava "Da ritentare (N/8)" e restava sempre tentativo 1).
begin
gate.define_singleton_method(:deliver) { raise EOFError, "end of file reached" }
assert_equal :deferred, recipient_a.deliver_queued!
ensure
gate.define_singleton_method(:deliver, original)
end
assert_match(/Da ritentare \(2\/8\)/, recipient_a.reload.error_message)
end
test "next_queued skips recipients whose send window is closed" do
open_org = create_campaign_org(name: "Club Aperto", email: "aperto@example.com")
closed_org = create_campaign_org(name: "Club Chiuso", email: "chiuso@example.com")
open_mailing = create_sending_mailing(name: "Finestra aperta")
closed_mailing = create_sending_mailing(name: "Finestra chiusa")
closed_mailing.update!(send_window_enabled: true, send_window_start_minutes: 8 * 60, send_window_end_minutes: 9 * 60)
open_mailing.update!(send_window_enabled: false)
closed_recipient = enqueue_org(closed_mailing, closed_org)
open_recipient = enqueue_org(open_mailing, open_org)
closed_recipient.update_columns(updated_at: 1.hour.ago)
travel_to Time.zone.local(2026, 9, 10, 20, 0, 0) do
assert_equal open_recipient.id, Mailings::OutboundQueue.next_queued.id
end
end end
private private
@@ -30,6 +30,14 @@ class MailingsAudienceQueryTest < ActiveSupport::TestCase
assert_not_includes ids, @volley.id assert_not_includes ids, @volley.id
end end
test "filters by country" do
@volley.update!(country: "Francia")
@acme.update!(country: "Italia")
ids = query({ "preset" => "all", "countries" => ["Francia"] }).relation.pluck(:id)
assert_includes ids, @volley.id
assert_not_includes ids, @acme.id
end
test "filters by list position range" do test "filters by list position range" do
ids = query({ "preset" => "all", "list_min" => 1, "list_max" => 5 }).relation.pluck(:id) ids = query({ "preset" => "all", "list_min" => 1, "list_max" => 5 }).relation.pluck(:id)
assert_includes ids, @acme.id assert_includes ids, @acme.id