Class: CommunicationRecipient

Inherits:
ApplicationRecord show all
Includes:
ActionView::Helpers::NumberHelper, Models::Auditable
Defined in:
app/models/communication_recipient.rb

Overview

== Schema Information

Table name: communication_recipients
Database name: primary

id :integer not null, primary key
canary_ip :string
canary_tripped_at :datetime
category :string(255)
detail :string(255)
email_method :string(3)
ip_address :string(255)
machine_clicked :boolean
machine_open :boolean
name :string(255)
state :string(25)
state_response :text
state_updated_at :datetime
tracking_disabled :boolean
user_agent :text
created_at :datetime not null
updated_at :datetime not null
communication_id :integer
contact_point_id :integer

Indexes

idx_comm_rp_contact_point_id (contact_point_id)
idx_communication_id_contact_point_id (communication_id,contact_point_id)
idx_communication_id_email_method (communication_id,email_method)
idx_communication_recipients_unique (communication_id,category,detail) UNIQUE
index_communication_recipients_on_state (state)
index_communication_recipients_on_state_updated_at (state_updated_at) USING brin

Foreign Keys

communication_recipients_communication_id_fk (communication_id => communications.id) ON DELETE => cascade

Constant Summary collapse

UNDELIVERED =

Undelivered.

%w[dropped deferred bounced].freeze
DELIVERED =

Delivered.

%w[processed delivered opened clicked spammed unsubscribed].freeze
UNWANTED =

Unwanted.

%w[spammed unsubscribed].freeze

Constants included from Models::Auditable

Models::Auditable::ALWAYS_IGNORED

Constants included from Models::Schedulable

Models::Schedulable::SIMPLE_FORM_OPTIONS

Instance Attribute Summary collapse

Belongs to collapse

Methods included from Models::Auditable

#creator, #updater

Has one collapse

Has many collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Models::Auditable

#all_skipped_columns, #audit_reference_data, #should_not_save_version, #stamp_record

Methods inherited from ApplicationRecord

ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation

Methods included from Models::Schedulable

config

Methods included from Models::AfterCommittable

#after_commit

Methods included from Models::EventPublishable

#publish_event

Instance Attribute Details

#categoryObject (readonly)

Validates category, fax, email.

Validations:



135
# File 'app/models/communication_recipient.rb', line 135

validates :category, presence: true, inclusion: { in: [ContactPoint::FAX, ContactPoint::EMAIL] }

#detailObject (readonly)

Validates detail.

Validations:

Validations (if => #is_email? ):

  • Email_format

Validations (if => #is_fax? ):

  • Phone_format


137
# File 'app/models/communication_recipient.rb', line 137

validates :detail, presence: true

#ignore_global_unsubscribeObject

Returns the value of attribute ignore_global_unsubscribe.



65
66
67
# File 'app/models/communication_recipient.rb', line 65

def ignore_global_unsubscribe
  @ignore_global_unsubscribe
end

Class Method Details

.emailsActiveRecord::Relation<CommunicationRecipient>

A relation of CommunicationRecipients that are emails. Active Record Scope

Returns:

See Also:



126
# File 'app/models/communication_recipient.rb', line 126

scope :emails, -> { where(category: ContactPoint::EMAIL) }

.faxesActiveRecord::Relation<CommunicationRecipient>

A relation of CommunicationRecipients that are faxes. Active Record Scope

Returns:

See Also:



127
# File 'app/models/communication_recipient.rb', line 127

scope :faxes, -> { where(category: ContactPoint::FAX) }

.states_for_selectObject

States for select.



188
189
190
# File 'app/models/communication_recipient.rb', line 188

def self.states_for_select
  state_machines[:state].states.map(&:name)
end

.undeliveredActiveRecord::Relation<CommunicationRecipient>

A relation of CommunicationRecipients that are undelivered. Active Record Scope

Returns:

See Also:



128
# File 'app/models/communication_recipient.rb', line 128

scope :undelivered, -> { where(state: UNDELIVERED) }

.unwantedActiveRecord::Relation<CommunicationRecipient>

A relation of CommunicationRecipients that are unwanted. Active Record Scope

Returns:

See Also:



129
# File 'app/models/communication_recipient.rb', line 129

scope :unwanted, -> { where(state: UNWANTED) }

.with_partyActiveRecord::Relation<CommunicationRecipient>

A relation of CommunicationRecipients that are with party. Active Record Scope

Returns:

See Also:



125
# File 'app/models/communication_recipient.rb', line 125

scope :with_party, -> { joins(contact_point: :party) }

Instance Method Details

#campaign_deliveryCampaignDelivery?

Returns the associated campaign delivery.

Returns:



57
# File 'app/models/communication_recipient.rb', line 57

has_one :campaign_delivery

#combo_categoryObject

Combo category.



214
215
216
217
218
219
# File 'app/models/communication_recipient.rb', line 214

def combo_category
  return ContactPoint::FAX if is_fax?

  self.email_method ||= 'to' if is_email?
  "#{ContactPoint::EMAIL}_#{email_method}"
end

#combo_category=(val) ⇒ Object

Sets the combo category.

Parameters:

  • val (Object)

    the val

  • value (Object)

    the new combo category



202
203
204
205
206
207
208
209
210
211
# File 'app/models/communication_recipient.rb', line 202

def combo_category=(val)
  map = { 'email_to' => %w[email to],
          'email_cc' => %w[email cc],
          'email_bcc' => %w[email bcc],
          'fax' => ['fax', nil] }[val]
  return unless map

  self.category = map[0]
  self.email_method = map[1]
end

#communicationCommunication?

Returns the communication this record belongs to.

Returns:

  • (Communication, nil)

    the communication this record belongs to



53
# File 'app/models/communication_recipient.rb', line 53

belongs_to :communication, inverse_of: :communication_recipients, optional: true

Returns the associated communication recipient email links.

Returns:



61
# File 'app/models/communication_recipient.rb', line 61

has_many :communication_recipient_email_links

#contact_pointContactPoint?

Returns the contact point this record belongs to.

Returns:

  • (ContactPoint, nil)

    the contact point this record belongs to



55
# File 'app/models/communication_recipient.rb', line 55

belongs_to :contact_point, inverse_of: :communication_recipients, optional: true

#customerObject

Customer.



258
259
260
# File 'app/models/communication_recipient.rb', line 258

def customer
  party.try(:customer)
end

#deep_dupObject

Returns a deep-copied, unsaved duplicate of the record.

Returns:

  • (Object)

    a deep-copied, unsaved duplicate of the record



142
143
144
145
146
# File 'app/models/communication_recipient.rb', line 142

def deep_dup
  deep_clone(except: %i[state_updated_at user_agent ip_address state_response machine_open machine_clicked canary_tripped_at canary_ip]) do |_original, copy|
    copy.state = 'ok' if copy.is_a?(CommunicationRecipient)
  end
end

#display_formatObject

Display format.



268
269
270
271
272
273
274
275
# File 'app/models/communication_recipient.rb', line 268

def display_format
  case category
  when ContactPoint::FAX
    PhoneNumber.parse_and_format(detail)
  else
    detail
  end
end

#display_nameObject

Display name.



263
264
265
# File 'app/models/communication_recipient.rb', line 263

def display_name
  name || contact_point.try(:party).try(:full_name)
end

Returns the associated email links.

Returns:

  • (ActiveRecord::Relation<EmailLink>)

    the associated email links



63
# File 'app/models/communication_recipient.rb', line 63

has_many :email_links, -> { distinct }, through: :communication_recipient_email_links

#formatted_email_stringObject

Formatted email string.



242
243
244
245
246
247
248
249
250
# File 'app/models/communication_recipient.rb', line 242

def formatted_email_string
  case category
  when ContactPoint::FAX
    n = PhoneNumber.parse_and_format(detail, display_format: :fax_dial)
    "#{n}@fax.tc"
  when ContactPoint::EMAIL
    detail
  end
end

#is_email?Boolean

Returns whether the record is email.

Returns:

  • (Boolean)

    whether the record is email



232
233
234
# File 'app/models/communication_recipient.rb', line 232

def is_email?
  category == ContactPoint::EMAIL
end

#is_fax?Boolean

Returns whether the record is fax.

Returns:

  • (Boolean)

    whether the record is fax



237
238
239
# File 'app/models/communication_recipient.rb', line 237

def is_fax?
  category == ContactPoint::FAX
end

#is_undelivered?Boolean

Returns whether the record is undelivered.

Returns:

  • (Boolean)

    whether the record is undelivered



222
223
224
# File 'app/models/communication_recipient.rb', line 222

def is_undelivered?
  UNDELIVERED.include? state
end

#is_unwanted?Boolean

Returns whether the record is unwanted.

Returns:

  • (Boolean)

    whether the record is unwanted



227
228
229
# File 'app/models/communication_recipient.rb', line 227

def is_unwanted?
  UNWANTED.include? state
end

#normalize_formatObject (protected)

Normalize format.



280
281
282
283
284
285
286
287
288
289
290
291
292
# File 'app/models/communication_recipient.rb', line 280

def normalize_format
  return if detail.blank?

  # Remove invalid characters, stick to ascii
  self.detail = detail.gsub(/\P{ASCII}/u, '').strip.downcase
  if RFC822::EMAIL.match?(detail)
    self.category = ContactPoint::EMAIL
  elsif (p = PhoneNumber.parse(detail))
    self.category = ContactPoint::FAX
    self.detail = p.to_s
  end
  true
end

#notify_rep_email_bouncedObject

Notify rep email bounced.



193
194
195
196
197
# File 'app/models/communication_recipient.rb', line 193

def notify_rep_email_bounced
  return unless communication.important?

  SystemMailer.email_address_bounced_notification(self).deliver_later
end

#partyObject

Party.



253
254
255
# File 'app/models/communication_recipient.rb', line 253

def party
  contact_point.try(:party)
end

#record_canary_trip!(ip:) ⇒ void

This method returns an undefined value.

Record that this recipient's invisible canary link was fetched — a provable
security-scanner hit, since no human can see or click an invisible link (see
Communication#canary_link_html, Api::V1::EmailCanaryController). First hit
wins; the IP is the scanner's egress, which Communication::ClickBotScorer
folds into its scanner-IP set for the whole send. Uses update_columns: a
denormalized signal that must not fire callbacks/validations or disturb the
recipient's delivery state.

Parameters:

  • ip (String, nil)

    the IP that fetched the canary



181
182
183
184
185
# File 'app/models/communication_recipient.rb', line 181

def record_canary_trip!(ip:)
  return if canary_tripped_at.present? # idempotent — first hit wins

  update_columns(canary_tripped_at: Time.current, canary_ip: ip)
end

#record_machine_open(machine) ⇒ void

This method returns an undefined value.

Roll up SendGrid's per-open sg_machine_open indicator to a sticky
recipient-level signal for deliverability reporting.

Apple Mail Privacy Protection (and corporate security scanners that load
images) fire an open for every message regardless of whether a human
looked at it — SendGrid flags those as sg_machine_open: true. A single
non-machine open, by contrast, is proof a real client rendered the
tracking pixel, so once we see one we latch machine_open to false and
never flip it back. A nil flag (SendGrid omitted it, e.g. legacy events)
carries no signal and leaves the value untouched — those count as human.

Sets the attribute only; the subsequent open_communication transition
persists it alongside the state change.

Parameters:

  • machine (Boolean, nil)

    the event's cast sg_machine_open value



164
165
166
167
168
169
# File 'app/models/communication_recipient.rb', line 164

def record_machine_open(machine)
  return if machine_open == false # confirmed-human latch wins permanently
  return if machine.nil?          # no indicator on this event — no information

  self.machine_open = machine
end

#webhook_eventsActiveRecord::Relation<WebhookEvent>

Returns the associated webhook events.

Returns:

  • (ActiveRecord::Relation<WebhookEvent>)

    the associated webhook events



59
# File 'app/models/communication_recipient.rb', line 59

has_many :webhook_events