Class: CommunicationRecipient
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- CommunicationRecipient
- 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
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 Schedulable
Schedulable::SIMPLE_FORM_OPTIONS
Instance Attribute Summary collapse
- #category ⇒ Object readonly
- #detail ⇒ Object readonly
-
#ignore_global_unsubscribe ⇒ Object
Returns the value of attribute ignore_global_unsubscribe.
Belongs to collapse
Methods included from Models::Auditable
Has one collapse
Has many collapse
- #communication_recipient_email_links ⇒ ActiveRecord::Relation<CommunicationRecipientEmailLink>
- #email_links ⇒ ActiveRecord::Relation<EmailLink>
- #webhook_events ⇒ ActiveRecord::Relation<WebhookEvent>
Class Method Summary collapse
-
.emails ⇒ ActiveRecord::Relation<CommunicationRecipient>
A relation of CommunicationRecipients that are emails.
-
.faxes ⇒ ActiveRecord::Relation<CommunicationRecipient>
A relation of CommunicationRecipients that are faxes.
- .states_for_select ⇒ Object
-
.undelivered ⇒ ActiveRecord::Relation<CommunicationRecipient>
A relation of CommunicationRecipients that are undelivered.
-
.unwanted ⇒ ActiveRecord::Relation<CommunicationRecipient>
A relation of CommunicationRecipients that are unwanted.
-
.with_party ⇒ ActiveRecord::Relation<CommunicationRecipient>
A relation of CommunicationRecipients that are with party.
Instance Method Summary collapse
- #combo_category ⇒ Object
- #combo_category=(val) ⇒ Object
- #customer ⇒ Object
- #deep_dup ⇒ Object
- #display_format ⇒ Object
- #display_name ⇒ Object
- #formatted_email_string ⇒ Object
- #is_email? ⇒ Boolean
- #is_fax? ⇒ Boolean
- #is_undelivered? ⇒ Boolean
- #is_unwanted? ⇒ Boolean
- #normalize_format ⇒ Object protected
- #notify_rep_email_bounced ⇒ Object
- #party ⇒ Object
-
#record_canary_trip!(ip:) ⇒ void
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).
-
#record_machine_open(machine) ⇒ void
Roll up SendGrid's per-open
sg_machine_openindicator to a sticky recipient-level signal for deliverability reporting.
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 Schedulable
Methods included from Models::AfterCommittable
Methods included from Models::EventPublishable
Instance Attribute Details
#category ⇒ Object (readonly)
127 |
# File 'app/models/communication_recipient.rb', line 127 validates :category, presence: true, inclusion: { in: [ContactPoint::FAX, ContactPoint::EMAIL] } |
#detail ⇒ Object (readonly)
128 |
# File 'app/models/communication_recipient.rb', line 128 validates :detail, presence: true |
#ignore_global_unsubscribe ⇒ Object
Returns the value of attribute ignore_global_unsubscribe.
58 59 60 |
# File 'app/models/communication_recipient.rb', line 58 def ignore_global_unsubscribe @ignore_global_unsubscribe end |
Class Method Details
.emails ⇒ ActiveRecord::Relation<CommunicationRecipient>
A relation of CommunicationRecipients that are emails. Active Record Scope
119 |
# File 'app/models/communication_recipient.rb', line 119 scope :emails, -> { where(category: ContactPoint::EMAIL) } |
.faxes ⇒ ActiveRecord::Relation<CommunicationRecipient>
A relation of CommunicationRecipients that are faxes. Active Record Scope
120 |
# File 'app/models/communication_recipient.rb', line 120 scope :faxes, -> { where(category: ContactPoint::FAX) } |
.states_for_select ⇒ Object
177 178 179 |
# File 'app/models/communication_recipient.rb', line 177 def self.states_for_select state_machines[:state].states.map(&:name) end |
.undelivered ⇒ ActiveRecord::Relation<CommunicationRecipient>
A relation of CommunicationRecipients that are undelivered. Active Record Scope
121 |
# File 'app/models/communication_recipient.rb', line 121 scope :undelivered, -> { where(state: UNDELIVERED) } |
.unwanted ⇒ ActiveRecord::Relation<CommunicationRecipient>
A relation of CommunicationRecipients that are unwanted. Active Record Scope
122 |
# File 'app/models/communication_recipient.rb', line 122 scope :unwanted, -> { where(state: UNWANTED) } |
.with_party ⇒ ActiveRecord::Relation<CommunicationRecipient>
A relation of CommunicationRecipients that are with party. Active Record Scope
118 |
# File 'app/models/communication_recipient.rb', line 118 scope :with_party, -> { joins(contact_point: :party) } |
Instance Method Details
#campaign_delivery ⇒ CampaignDelivery
53 |
# File 'app/models/communication_recipient.rb', line 53 has_one :campaign_delivery |
#combo_category ⇒ Object
198 199 200 201 202 203 |
# File 'app/models/communication_recipient.rb', line 198 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
187 188 189 190 191 192 193 194 195 196 |
# File 'app/models/communication_recipient.rb', line 187 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 |
#communication ⇒ Communication
51 |
# File 'app/models/communication_recipient.rb', line 51 belongs_to :communication, inverse_of: :communication_recipients, optional: true |
#communication_recipient_email_links ⇒ ActiveRecord::Relation<CommunicationRecipientEmailLink>
55 |
# File 'app/models/communication_recipient.rb', line 55 has_many :communication_recipient_email_links |
#contact_point ⇒ ContactPoint
52 |
# File 'app/models/communication_recipient.rb', line 52 belongs_to :contact_point, inverse_of: :communication_recipients, optional: true |
#customer ⇒ Object
235 236 237 |
# File 'app/models/communication_recipient.rb', line 235 def customer party.try(:customer) end |
#deep_dup ⇒ Object
132 133 134 135 136 |
# File 'app/models/communication_recipient.rb', line 132 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_format ⇒ Object
243 244 245 246 247 248 249 250 |
# File 'app/models/communication_recipient.rb', line 243 def display_format case category when ContactPoint::FAX PhoneNumber.parse_and_format(detail) else detail end end |
#display_name ⇒ Object
239 240 241 |
# File 'app/models/communication_recipient.rb', line 239 def display_name name || contact_point.try(:party).try(:full_name) end |
#email_links ⇒ ActiveRecord::Relation<EmailLink>
56 |
# File 'app/models/communication_recipient.rb', line 56 has_many :email_links, -> { distinct }, through: :communication_recipient_email_links |
#formatted_email_string ⇒ Object
221 222 223 224 225 226 227 228 229 |
# File 'app/models/communication_recipient.rb', line 221 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
213 214 215 |
# File 'app/models/communication_recipient.rb', line 213 def is_email? category == ContactPoint::EMAIL end |
#is_fax? ⇒ Boolean
217 218 219 |
# File 'app/models/communication_recipient.rb', line 217 def is_fax? category == ContactPoint::FAX end |
#is_undelivered? ⇒ Boolean
205 206 207 |
# File 'app/models/communication_recipient.rb', line 205 def is_undelivered? UNDELIVERED.include? state end |
#is_unwanted? ⇒ Boolean
209 210 211 |
# File 'app/models/communication_recipient.rb', line 209 def is_unwanted? UNWANTED.include? state end |
#normalize_format ⇒ Object (protected)
254 255 256 257 258 259 260 261 262 263 264 265 266 |
# File 'app/models/communication_recipient.rb', line 254 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_bounced ⇒ Object
181 182 183 184 185 |
# File 'app/models/communication_recipient.rb', line 181 def notify_rep_email_bounced return unless communication.important? SystemMailer.email_address_bounced_notification(self).deliver_later end |
#party ⇒ Object
231 232 233 |
# File 'app/models/communication_recipient.rb', line 231 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.
171 172 173 174 175 |
# File 'app/models/communication_recipient.rb', line 171 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.
154 155 156 157 158 159 |
# File 'app/models/communication_recipient.rb', line 154 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_events ⇒ ActiveRecord::Relation<WebhookEvent>
54 |
# File 'app/models/communication_recipient.rb', line 54 has_many :webhook_events |