Class: MailActivity

Inherits:
ApplicationRecord show all
Includes:
Models::Auditable
Defined in:
app/models/mail_activity.rb

Overview

== Schema Information

Table name: mail_activities
Database name: primary

id :integer not null, primary key
actual_shipping_cost :decimal(, )
postage_rates :hstore is an Array
service_code :string
state :string
activity_id :integer
address_id :integer
mailing_id :integer

Indexes

index_mail_activities_on_activity_id (activity_id)
index_mail_activities_on_address_id (address_id)
index_mail_activities_on_mailing_id (mailing_id)
index_mail_activities_on_state (state)

Foreign Keys

fk_rails_... (activity_id => activities.id) ON DELETE => cascade
fk_rails_... (address_id => addresses.id)
fk_rails_... (mailing_id => mailings.id)

Constant Summary

Constants included from Models::Auditable

Models::Auditable::ALWAYS_IGNORED

Constants included from Models::Schedulable

Models::Schedulable::SIMPLE_FORM_OPTIONS

Belongs to collapse

Methods included from Models::Auditable

#creator, #updater

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

Class Method Details

.completeActiveRecord::Relation<MailActivity>

A relation of MailActivities that are complete. Active Record Scope

Returns:

See Also:



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

scope :complete, -> { where("mail_activities.state = 'complete'") }

.openActiveRecord::Relation<MailActivity>

A relation of MailActivities that are open. Active Record Scope

Returns:

See Also:



51
# File 'app/models/mail_activity.rb', line 51

scope :open, -> { where("mail_activities.state = 'open'") }

.packagedActiveRecord::Relation<MailActivity>

A relation of MailActivities that are packaged. Active Record Scope

Returns:

See Also:



52
# File 'app/models/mail_activity.rb', line 52

scope :packaged, -> { where("mail_activities.state = 'packaged'") }

.postage_labeledActiveRecord::Relation<MailActivity>

A relation of MailActivities that are postage labeled. Active Record Scope

Returns:

See Also:



54
# File 'app/models/mail_activity.rb', line 54

scope :postage_labeled, -> { where("mail_activities.state = 'postage_labeled'") }

.postage_ratedActiveRecord::Relation<MailActivity>

A relation of MailActivities that are postage rated. Active Record Scope

Returns:

See Also:



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

scope :postage_rated, -> { where("mail_activities.state = 'postage_rated'") }

Instance Method Details

#activityActivity

CRM activity this mailing fulfills.

Returns:

See Also:

Validations:



33
# File 'app/models/mail_activity.rb', line 33

belongs_to :activity, optional: true

#addressAddress?

Destination address — the activity party's main address.

Returns:



360
361
362
# File 'app/models/mail_activity.rb', line 360

def address
  activity.party.main_address
end

#all_labels_pdfUpload?

Uploaded combined-labels PDF for this activity.

Returns:



337
338
339
# File 'app/models/mail_activity.rb', line 337

def all_labels_pdf
  uploads.find_by(category: 'all_labels_pdf')
end

#calculate_postage_ratesHash

Fetches postage rates from the shipping provider and stores them.

Returns:

  • (Hash)

    { status_code:, status_message:, request_response: } result



203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'app/models/mail_activity.rb', line 203

def calculate_postage_rates
  res = WyShipping.find_mail_activity_shipping_rates(self)
  rate_hashes = []
  rate_hashes = (res[:request_response][:rates] || []).sort_by { |r| r[:total_charges].to_f } if res[:request_response]
  if (res[:status_code] == :ok) && rate_hashes.any?
    self.postage_rates = rate_hashes # this will stringify the keys in the hash array
    self.service_code = rate_hashes.first[:service_code] if rate_hashes.any?
    # @mail_activity.service_code = "MediaMail" if rate_hashes.detect{|rh| rh[:service_code] == "MediaMail"}
    postage_rate_mail_activity!
    reload
  end
  res
end

#carrierString Also known as: reported_carrier

Carrier for this activity (delegates to #selected_carrier).

Returns:

  • (String)


196
197
198
# File 'app/models/mail_activity.rb', line 196

def carrier
  selected_carrier
end

#company_nameObject

Sanitized name for mailing



97
98
99
100
101
102
103
# File 'app/models/mail_activity.rb', line 97

def company_name
  cn = activity&.party&.full_name unless activity&.party&.is_person?
  cn ||= address&.party&.full_name unless address&.party&.is_person?
  return unless cn

  cn.to_s.gsub(/[^0-9a-z\s]/i, '')[0..34]
end

#default_carrierObject

The default postal carrier for the store's country (used when no rate has been selected yet)



174
175
176
# File 'app/models/mail_activity.rb', line 174

def default_carrier
  store&.country_iso3 == 'CAN' ? 'Canadapost' : 'USPS'
end

#formatted_address(separator: '<br>') ⇒ String?

Multi-line formatted destination address.

Parameters:

  • separator (String) (defaults to: '<br>')

    line separator (defaults to an HTML break)

Returns:

  • (String, nil)


352
353
354
355
356
# File 'app/models/mail_activity.rb', line 352

def formatted_address(separator: '<br>')
  return unless (a = address)

  a.full_address(true, separator, person_name)
end

#generate_all_labels_pdfvoid

This method returns an undefined value.

Combines all completed shipment labels into one uploaded PDF.

Raises:

  • (RuntimeError)

    when the combined label upload was not generated



291
292
293
294
295
296
297
298
299
300
301
# File 'app/models/mail_activity.rb', line 291

def generate_all_labels_pdf
  all_labels = shipments.completed.map(&:ship_label_pdf)
  file_name = "#{id}_all_labels_#{Time.current.strftime('%m_%d_%Y_%I_%M%p')}.pdf".downcase
  all_labels_path = Rails.application.config.x.temp_storage_path.join(file_name)
  Rails.logger.debug { "self.generate_all_labels_pdf: all_labels_path: #{all_labels_path}, all_labels: #{all_labels.inspect}" }
  PdfTools.combine(all_labels, output_file_path: all_labels_path)
  upload = Upload.uploadify(all_labels_path, 'all_labels_pdf')
  raise 'Combo label was not generated' unless upload

  uploads << upload
end

#generate_labelsHash

Purchases postage labels for all awaiting shipments via the shipping provider.

Returns:

  • (Hash)

    { status_code:, status_message: } result



219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
# File 'app/models/mail_activity.rb', line 219

def generate_labels
  # Guard the ShipEngine call: a MailActivity with no shipments awaiting a
  # label sends an empty `shipment.packages` payload, which ShipEngine
  # rejects with a `ShipEngineRb::Exceptions::ValidationError` that then
  # round-trips through `WyShipping.ship_mail_activity` -> `ErrorReporting.error`
  # to AppSignal (#5307). Surface a user-friendly status instead.
  if shipments.awaiting_label.empty?
    Rails.logger.warn("MailActivity #{id}: generate_labels called with no shipments awaiting label; skipping ShipEngine call")
    return { status_code: :error, status_message: 'No shipments are awaiting a label for this mail activity.' }
  end

  shipping_result = WyShipping.ship_mail_activity(self)
  Rails.logger.info("#{Time.current}: MailActivity ID #{id}, labels generated START LOG $$$$$$$$$$$$$$$$$$$$$$$$$$$$, status_code: #{shipping_result[:status_code]}")
  if shipping_result[:status_code] == :error
    Rails.logger.info("#{carrier}, request/response:")
    Rails.logger.info("#{begin
      shipping_result[:shipment][:ship_reply_xml]
    rescue StandardError
      nil
    end}
")
    { status_code: shipping_result[:status_code], status_message: shipping_result[:status_message] }
  else
    # only save if labels successfully generated
    # first pass get tracking numbers and charges
    Rails.logger.debug("Shipping result received", mail_activity_id: id)
    self.actual_shipping_cost = shipping_result[:shipment][:total_charges]
    save # pure hacking to get this to work
    reload # pure hacking to get this to work
    shipments.awaiting_label.each_with_index do |shipment, i|
      ship_result_label = shipping_result.dig(:shipment, :labels, i)

      Rails.logger.debug { "shipment: #{shipment.inspect}" }
      Rails.logger.debug { "shipping_result[:shipment][:labels][i]: #{ship_result_label.inspect}" }
      Rails.logger.debug { "shipping_result[:shipment][:labels][i][:tracking_number]: #{ship_result_label[:tracking_number] || 'not there'}" }

      shipment.tracking_number = ship_result_label[:tracking_number]
      shipment.shipengine_label_id = ship_result_label[:shipengine_label_id]
      shipment.label_generated!
    end
    Rails.logger.info("#{Time.current}: MailActivity ID #{id}, labels generated END LOG $$$$$$$$$$$$$$$$$$$$$$$$$$$$")
    # save this mail_activity
    save
    # second pass generate the labels from the temporary paths
    label_exceptions = []
    status_code = :ok
    shipments.label_complete.each_with_index do |shipment, i|
      ship_result_label = shipping_result.dig(:shipment, :labels, i)
      old_label_path = ship_result_label[:image]&.path
      # skip when label path is nil
      next unless old_label_path

      label_exceptions << (i + 1) unless shipment.generate_ship_label_pdf(old_label_path, carrier)
    end
    if label_exceptions.empty? && status_code == :ok
      generate_all_labels_pdf
      status_message = "Labels generated for #{shipments.completed.length} packages. Actual charge: #{store.catalogs.first.currency_symbol}#{actual_shipping_cost}. #{status_message}"
      save
      { status_code: status_code, status_message: status_message }
    else
      msg = +"Can't generate labels"
      msg << ", there was an issue with labels #{label_exceptions.map { |l| l + 1 }.join(', ')}" if label_exceptions.any?
      msg << ", status code returned:  #{status_code}" unless status_code == :ok
      msg << '.'
      { status_code: :error, status_message: msg }
    end
  end
end

#mailingMailing

Mailing batch this activity belongs to.

Returns:

See Also:

Validations:



35
# File 'app/models/mail_activity.rb', line 35

belongs_to :mailing, optional: true

#manually_complete?Boolean

Whether the activity was completed manually (no printed labels).

Returns:

  • (Boolean)


157
158
159
# File 'app/models/mail_activity.rb', line 157

def manually_complete?
  complete? and !ready_to_print_labels?
end

#nameString

Human-readable name: task type, activity id, and destination address.

Returns:

  • (String)


107
108
109
# File 'app/models/mail_activity.rb', line 107

def name
  "Mail Activity #{activity.activity_type.task_type}, ID: #{activity.id}, to: #{address.full_address(true, ', ')}"
end

#ok_to_delete?Boolean

Whether the activity can be deleted (open, with no labeled shipments).

Returns:

  • (Boolean)


343
344
345
346
347
# File 'app/models/mail_activity.rb', line 343

def ok_to_delete?
  ok = true
  ok = false unless open? && shipments.all?(&:ok_to_delete_with_mail_activity?)
  ok
end

#person_nameObject

Sanitized name for mailing



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

def person_name
  pn = activity.party.full_name if activity.party.is_person?
  pn ||= address.party.full_name if address.party&.is_person?
  return unless pn

  pn.to_s.gsub(/[^0-9a-z\s]/i, '')[0..34]
end

#ready_to_complete?Boolean

Whether any shipment has a completed label (ready to complete the activity).

Returns:

  • (Boolean)


139
140
141
# File 'app/models/mail_activity.rb', line 139

def ready_to_complete?
  shipments.any?(&:label_complete?)
end

#ready_to_label?Boolean

Whether rates are quoted and a shipment is awaiting a label.

Returns:

  • (Boolean)


133
134
135
# File 'app/models/mail_activity.rb', line 133

def ready_to_label?
  postage_rates&.any? and shipments.any?(&:awaiting_label?)
end

#ready_to_manually_complete?Boolean

Whether no shipment is labeled or awaiting a label (manual completion path).

Returns:

  • (Boolean)


145
146
147
# File 'app/models/mail_activity.rb', line 145

def ready_to_manually_complete?
  shipments.none? { |s| s.label_complete? or s.awaiting_label? }
end

#ready_to_print_labels?Boolean

Whether the combined labels PDF exists and can be printed.

Returns:

  • (Boolean)


151
152
153
# File 'app/models/mail_activity.rb', line 151

def ready_to_print_labels?
  all_labels_pdf.present? and shipments.label_complete.any?
end

#ready_to_print_postage_label?Boolean

Whether the combined postage label PDF is ready to print.

Returns:

  • (Boolean)


113
114
115
116
117
# File 'app/models/mail_activity.rb', line 113

def ready_to_print_postage_label?
  (postage_labeled? or complete?) and
    all_labels_pdf.present? and
    shipments.label_complete.any?
end

#ready_to_rate?Boolean

Whether any shipment is awaiting a rate quote.

Returns:

  • (Boolean)


127
128
129
# File 'app/models/mail_activity.rb', line 127

def ready_to_rate?
  shipments.any?(&:awaiting_label?)
end

#ready_to_reset?Boolean

Whether all shipments can be deleted so the activity can be reset.

Returns:

  • (Boolean)


121
122
123
# File 'app/models/mail_activity.rb', line 121

def ready_to_reset?
  shipments.all?(&:ok_to_delete_with_mail_activity?)
end

#selected_carrierObject

Resolve the carrier from a composite service_code like "UPS::03" or "FedEx::FEDEX_GROUND".
Falls back to the default postal carrier for legacy service codes without a prefix.



180
181
182
183
184
# File 'app/models/mail_activity.rb', line 180

def selected_carrier
  return default_carrier unless service_code&.include?('::')

  service_code.split('::').first
end

#selected_service_codeObject

Extract the raw carrier service code from a composite service_code.
"UPS::03" → "03", "USPS::Priority" → "Priority", "Priority" → "Priority"



188
189
190
191
192
# File 'app/models/mail_activity.rb', line 188

def selected_service_code
  return service_code unless service_code&.include?('::')

  service_code.split('::').last
end

#send_canada_post_manual_void_email(tracking_numbers) ⇒ void

This method returns an undefined value.

Emails admins the tracking numbers that must be voided manually on Canada Post.

Parameters:

  • tracking_numbers (Array<String>)

    tracking numbers to void



331
332
333
# File 'app/models/mail_activity.rb', line 331

def send_canada_post_manual_void_email(tracking_numbers)
  Mailer.canada_post_manual_void_email_for_mail_activity(self, tracking_numbers).deliver
end

#shipmentsActiveRecord::Relation<Shipment>

belongs_to :address, optional: true
Shipments (postage labels) created for this mail activity.

Returns:

See Also:



38
# File 'app/models/mail_activity.rb', line 38

has_many :shipments, dependent: :destroy

#storeStore?

Store the mailing is sent from.

Returns:



163
164
165
# File 'app/models/mail_activity.rb', line 163

def store
  @store ||= mailing.store
end

#store_country_iso3String?

ISO3 country code of the store's mailing address.

Returns:

  • (String, nil)


169
170
171
# File 'app/models/mail_activity.rb', line 169

def store_country_iso3
  store&.mailing_address&.country_iso3
end

#uploadsActiveRecord::Relation<Upload>

Uploaded files (e.g. combined label PDFs) for this activity.

Returns:

  • (ActiveRecord::Relation<Upload>)

See Also:



40
# File 'app/models/mail_activity.rb', line 40

has_many :uploads, as: :resource, dependent: :destroy

#void_shipmentsHash

Voids all labeled shipments with the carrier and resets the activity.

Returns:

  • (Hash)

    { status_code:, status_message: } result



305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'app/models/mail_activity.rb', line 305

def void_shipments
  if shipments.label_complete.any?
    tracking_numbers = shipments.label_complete.pluck(:tracking_number)
    shipping_result = {}
    shipping_result[:status_code] = :ok
    shipping_result = WyShipping.void_mail_activity(self)
    # going to go merrily along but send admin notification if this didn't properly void
    if shipping_result[:status_code] != :ok
      Mailer.admin_notification("mail_activity#void_shipments for mail_activity #{id} returned error shipping_result[:status_code]: #{shipping_result[:status_code]}",
"mail_activity#void_shipments for mail_activity #{id} returned error shipping_result[:status_code]: #{shipping_result[:status_code]}, shipping_result[:status_message]: #{shipping_result[:status_message]}, shipment tracking numbers: #{tracking_numbers.join(', ')}").deliver_now
      send_canada_post_manual_void_email(tracking_numbers) if carrier == 'Canadapost'
    end
    shipments.label_complete.each(&:label_voided!)
    self.postage_rates = []
    self.service_code = nil
    reset_mail_activity!
    { status_code: shipping_result[:status_code],
      status_message: "Please ensure you manually void manual shipments. Shipments voided. #{shipping_result[:status_message]}" }
  else
    { status_code: :error, status_message: "Can't void postage label(s) because the mail activity has no postage label(s)." }
  end
end