Class: MaquinaNewsletters::Newsletter

Inherits:
ApplicationRecord show all
Defined in:
app/models/maquina_newsletters/newsletter.rb

Constant Summary collapse

STATUSES =
%w[draft approved scheduled sending sent failed].freeze

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.compose_datetime(date, time) ⇒ Object

Combine a “yyyy-mm-dd” date string and an “HH:MM” time string into a Time in the app zone. Returns nil when no date is given. Used by the schedule action, which gets the two parts straight from the form.



70
71
72
73
74
75
# File 'app/models/maquina_newsletters/newsletter.rb', line 70

def self.compose_datetime(date, time)
  date = date.to_s.strip
  return nil if date.empty?

  Time.zone.parse("#{date} #{time.to_s.strip.presence || "00:00"}")
end

Instance Method Details

#actionable?Boolean

Has at least one state-transition action available (drives whether the show page renders an actions footer at all). sending/sent are terminal for the UI — nothing to do but watch.

Returns:

  • (Boolean)


26
# File 'app/models/maquina_newsletters/newsletter.rb', line 26

def actionable? = !(sending? || sent?)

#approve!Object

state transitions (called by controllers / jobs)



33
34
35
# File 'app/models/maquina_newsletters/newsletter.rb', line 33

def approve!
  update!(status: "approved")
end

#approved?Boolean

Returns:

  • (Boolean)


17
# File 'app/models/maquina_newsletters/newsletter.rb', line 17

def approved? = status == "approved"

#back_to_draft!Object



45
46
47
# File 'app/models/maquina_newsletters/newsletter.rb', line 45

def back_to_draft!
  update!(status: "draft", scheduled_at: nil, batch_size: 0)
end

#draft?Boolean

status predicates

Returns:

  • (Boolean)


16
# File 'app/models/maquina_newsletters/newsletter.rb', line 16

def draft? = status == "draft"

#enqueue_sendObject

Enqueues the first batch of this newsletter’s send. A single path serves both immediate sends and future-scheduled sends, and both batch_size 0 and >0: SendBatchJob (batch 0) owns the scheduled → sending transition and recipients_count (Sidecar A1/A3/A4). There is no MarkSentIfCompleteJob.



81
82
83
84
85
# File 'app/models/maquina_newsletters/newsletter.rb', line 81

def enqueue_send
  SendBatchJob
    .set(wait_until: scheduled_at || Time.current)
    .perform_later(id, 0)
end

#failed?Boolean

Returns:

  • (Boolean)


21
# File 'app/models/maquina_newsletters/newsletter.rb', line 21

def failed? = status == "failed"

#mark_failed!Object



57
58
59
# File 'app/models/maquina_newsletters/newsletter.rb', line 57

def mark_failed!
  update!(status: "failed")
end

#mark_sent!Object



53
54
55
# File 'app/models/maquina_newsletters/newsletter.rb', line 53

def mark_sent!
  update!(status: "sent", sent_at: Time.current)
end

#recipient_batch(recipients, index) ⇒ Object

Returns the slice of recipients that belongs to the batch at index. batch_size 0 (Sidecar A4) means a single batch of everyone.



89
90
91
92
93
94
# File 'app/models/maquina_newsletters/newsletter.rb', line 89

def recipient_batch(recipients, index)
  slice_size = batch_size.zero? ? recipients.size : batch_size
  return [] if slice_size.zero?

  recipients.each_slice(slice_size).to_a[index] || []
end

#resolved_recipientsObject

Returns the array of email addresses this newsletter would be sent to: queries configured recipient model + scope, plucks the email attribute, subtracts the exclusion list. Output is downcased, deduped, and sorted.

The result is sorted (Sidecar C1) to give a STABLE order. SendBatchJob re-queries this list at the start of each batch (spec §6) and slices it by index; without a deterministic order, recipients could shift between day-apart batches and be double-sent or skipped. Sorting in Ruby (not via SQL ORDER BY) keeps the order stable across DB adapters.



105
106
107
108
109
110
111
112
113
114
# File 'app/models/maquina_newsletters/newsletter.rb', line 105

def resolved_recipients
  model_class = MaquinaNewsletters.recipient_class
  scope_name = MaquinaNewsletters.recipient_scope
  attr_name = MaquinaNewsletters.recipient_email_attr

  pool = model_class.public_send(scope_name).pluck(attr_name)
  excluded = MaquinaNewsletters::ExcludedEmail.pluck(:email)

  (pool.compact.map(&:downcase) - excluded.map(&:downcase)).uniq.sort
end

#schedule!(at:, batch_size:) ⇒ Object



37
38
39
# File 'app/models/maquina_newsletters/newsletter.rb', line 37

def schedule!(at:, batch_size:)
  update!(status: "scheduled", scheduled_at: at, batch_size: batch_size)
end

#scheduled?Boolean

Returns:

  • (Boolean)


18
# File 'app/models/maquina_newsletters/newsletter.rb', line 18

def scheduled? = status == "scheduled"

#scheduled_timeObject

The schedule form’s time dropdown pre-selects this (the time-of-day part of the stored scheduled_at). The date picker reads scheduled_at directly.



63
64
65
# File 'app/models/maquina_newsletters/newsletter.rb', line 63

def scheduled_time
  scheduled_at&.strftime("%H:%M")
end

#sending?Boolean

Returns:

  • (Boolean)


19
# File 'app/models/maquina_newsletters/newsletter.rb', line 19

def sending? = status == "sending"

#sent?Boolean

Returns:

  • (Boolean)


20
# File 'app/models/maquina_newsletters/newsletter.rb', line 20

def sent? = status == "sent"

#start_sending!Object



49
50
51
# File 'app/models/maquina_newsletters/newsletter.rb', line 49

def start_sending!
  update!(status: "sending")
end

#test_sendable?Boolean

A test email may be sent while the newsletter is still being prepared, but not once it is sending/sent or has failed.

Returns:

  • (Boolean)


30
# File 'app/models/maquina_newsletters/newsletter.rb', line 30

def test_sendable? = draft? || approved? || scheduled?

#unschedule!Object



41
42
43
# File 'app/models/maquina_newsletters/newsletter.rb', line 41

def unschedule!
  update!(status: "approved", scheduled_at: nil)
end