Class: Rma

Inherits:
ApplicationRecord show all
Includes:
Models::Auditable, Models::LiquidMethods, Models::RmaTransmittable, Models::SupportCaseLinkable
Defined in:
app/models/rma.rb,
app/subscribers/rma/creation_note_handler.rb,
app/subscribers/rma/items_added_note_handler.rb

Overview

== Schema Information

Table name: rmas
Database name: primary

id :integer not null, primary key
arrival_date :date
contact_name :string(255)
credited_date :date
customer_reference :string(255)
description :text
insure_return_shipping :boolean default(FALSE), not null
number_of_return_labels_required :integer default(0), not null
original_po_number :string(255)
payment_method :string(255)
replacement_order_type :enum default("SO")
return_declared_value_override :decimal(10, 2)
return_insurance_data :jsonb not null
return_shipping_carrier :string
returned_date :date
rma_number :string(255)
serial_number_state :string
skip_reminders :boolean default(FALSE), not null
state :string(255)
tracking_numbers :string default([]), is an Array
transmission_email :string(255) default([]), is an Array
transmission_fax :string(255) default([]), is an Array
transmission_state :string(255)
uploads_count :integer
created_at :datetime
updated_at :datetime
company_id :integer
creator_id :integer
customer_id :integer
original_invoice_id :integer
original_order_id :integer
precreate_from_delivery_id :integer
redesign_quote_id :integer
return_delivery_id :integer
return_shipping_address_id :integer
send_from_id :integer
ship_from_address_id :integer
shipping_option_id :integer
support_case_id :integer
updater_id :integer

Indexes

idx_creator_id (creator_id)
idx_rma_number (rma_number)
idx_rmas_original_order_id (original_order_id)
idx_trigram_rma_number (COALESCE((rma_number)::text, ''::text) gist_trgm_ops) USING gist
idx_tsearch_rma_number (to_tsvector('english'::regconfig, COALESCE((rma_number)::text, ''::text))) USING gin
index_rmas_on_customer_id (customer_id)
index_rmas_on_original_invoice_id (original_invoice_id)
index_rmas_on_precreate_from_delivery_id (precreate_from_delivery_id) USING hash
index_rmas_on_return_delivery_id (return_delivery_id) USING hash
index_rmas_on_shipping_option_id (shipping_option_id)
index_rmas_on_support_case_id (support_case_id)
index_rmas_on_tracking_numbers (tracking_numbers) USING gin

Foreign Keys

fk_rails_... (precreate_from_delivery_id => deliveries.id)
fk_rails_... (return_delivery_id => deliveries.id)

Defined Under Namespace

Classes: CreationNoteHandler, ItemsAddedNoteHandler, ReplacementOrderError

Constant Summary collapse

PAYMENT_METHODS =

Payment methods.

['Original Payment Method', 'Check'].freeze
DEFAULT_ORDER_REPLACEMENT_TYPE =

Default order replacement type.

'SO'
REFERENCE_NUMBER_PATTERN =

Regex pattern matching reference number.

/^RMA\d+$/i
PENDING_ORDERS_STATES =

Recognised pending orders states.

%w[pending pending_payment in_cr_hold profit_review crm_back_order pending_release_authorization
needs_serial_number_reservation].freeze
NOTIFICATIONS_RMA_STATES =

Recognised notifications rma states.

%w[auto_return_review awaiting_inspection partially_returned returned credit_in_process credited_partially_refunded].freeze
EMAILS_FOR_NOTIFICATIONS =

Emails for notifications.

%w[bwinings@warmlyyours.com ar@warmlyyours.com].freeze
URLS =

Urls.

{
  'US' => {
    'UPS'       => { url_name: 'www.ups.com/dropoff', url: 'https://www.ups.com/dropoff' },
    'FedEx'     => { url_name: 'www.fedex.com/locate', url: 'https://www.fedex.com/locate' },
    'USPS'      => { url_name: 'tools.usps.com/find-location.htm', url: 'https://tools.usps.com/find-location.htm' }
  },
  'CA' => {
    'UPS'       => { url_name: 'www.ups.com/ca/en/dropoff', url: 'https://www.ups.com/ca/en/dropoff' },
    'FedEx'     => { url_name: 'www.fedex.com/locate', url: 'https://www.fedex.com/locate' },
    'Purolator' => { url_name: 'www.purolator.com/en/shipping-locations', url: 'https://www.purolator.com/en/shipping-locations' },
    'Canpar'    => { url_name: 'www.canpar.com/en/ship/drop-off-locations.jsp', url: 'https://www.canpar.com/en/ship/drop-off-locations.jsp' },
    'Canadapost' => { url_name: 'www.canadapost-postescanada.ca/information/app/fpo/personal/findpostoffice', url: 'https://www.canadapost-postescanada.ca/information/app/fpo/personal/findpostoffice' }
  }
}.freeze
RETURN_OPEN_STATES =

RMA states in which the customer may still legitimately obtain a Canada
Post return label — i.e. goods are still expected back. Post-arrival and
terminal states (awaiting_inspection, returned, voided, credit_*) are
excluded so a stale or already-completed RMA number can't pull a new label.

%w[requested awaiting_return auto_return_review partially_returned].freeze
RETURN_SHIPPING_METHODS =

Return shipping methods.

{ '1' => 'ground', '2' => 'standard' }.freeze
PAPERLESS_LABEL_BROKER_RETAIL_URL =

USPS Label Broker drop-off finders (counter / self-service kiosk).

'https://tools.usps.com/locations/home.htm?filters=LBRORETAIL'
PAPERLESS_LABEL_BROKER_KIOSK_URL =

Returns:

  • (Object)
'https://tools.usps.com/locations/home.htm?filters=LBROSSK'
CANADA_POST_RETURNS_POLICY_ID =

Canada Post Manage Returns policy backing our CA returns (No RA number
required; the customer enters their RMA in the Return No. field and Canada
Post issues the paperless QR). The policy ID alone is accepted at any Canada
Post outlet (Canada Post's "Method 2"), so it must be printable on its own —
not just buried inside the link — for customers who print their instructions.

'PR267361'
CANADA_POST_RETURNS_POLICY_URL =

Returns:

  • (Object)
"https://www.canadapost-postescanada.ca/pr-rc/en/get-return-label?id=#{CANADA_POST_RETURNS_POLICY_ID}"
PAPERLESS_BLOCK_RE =

Markers wrapping the paperless block in the rendered RMA email body, so
Rma#inject_paperless_block! can re-splice it after labels exist.

/<!--paperless:start-->.*?<!--paperless:end-->/m

Constants included from Models::Auditable

Models::Auditable::ALWAYS_IGNORED

Constants included from Models::Schedulable

Models::Schedulable::SIMPLE_FORM_OPTIONS

Instance Attribute Summary collapse

Attributes included from Models::SupportCaseLinkable

#support_case

Belongs to collapse

Methods included from Models::Auditable

#updater

Has one collapse

Has many collapse

Delegated Instance Attributes collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Models::SupportCaseLinkable

#support_case_ref, #support_case_ref=

Methods included from Models::RmaTransmittable

#rma_available_email_addresses, #rma_available_fax_numbers

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

Returns the value of attribute allow_invoice_link.



393
394
395
# File 'app/models/rma.rb', line 393

def allow_invoice_link
  @allow_invoice_link
end

#company_idvoid (readonly)

This method returns an undefined value.

Validates company id, customer id, return shipping address id, payment method.

Validations:



358
359
# File 'app/models/rma.rb', line 358

validates :company_id, :customer_id, :return_shipping_address_id, :payment_method,
:rma_number, :ship_from_address_id, presence: true

#customer_idvoid (readonly)

This method returns an undefined value.

Validates company id, customer id, return shipping address id, payment method.

Validations:



358
359
# File 'app/models/rma.rb', line 358

validates :company_id, :customer_id, :return_shipping_address_id, :payment_method,
:rma_number, :ship_from_address_id, presence: true

#customer_referencevoid (readonly)

This method returns an undefined value.

Validates customer reference.



372
373
374
# File 'app/models/rma.rb', line 372

validates :customer_reference, presence: { if: proc { |rma|
  rma.customer.present? && rma.customer.requires_rma_reference?
}, message: 'is required for this customer' }

#original_invoice_idvoid (readonly)

This method returns an undefined value.

Validates original invoice id.



366
# File 'app/models/rma.rb', line 366

validates :original_invoice_id, numericality: { allow_nil: true }

#original_order_idvoid (readonly)

This method returns an undefined value.

Validates original order id.



363
# File 'app/models/rma.rb', line 363

validates :original_order_id, numericality: { allow_nil: true }

#payment_methodvoid (readonly)

This method returns an undefined value.

Validates company id, customer id, return shipping address id, payment method.

Validations:



358
359
# File 'app/models/rma.rb', line 358

validates :company_id, :customer_id, :return_shipping_address_id, :payment_method,
:rma_number, :ship_from_address_id, presence: true

#return_shipping_address_idvoid (readonly)

This method returns an undefined value.

Validates company id, customer id, return shipping address id, payment method.

Validations:



358
359
# File 'app/models/rma.rb', line 358

validates :company_id, :customer_id, :return_shipping_address_id, :payment_method,
:rma_number, :ship_from_address_id, presence: true

#rma_numbervoid (readonly)

This method returns an undefined value.

Validates rma number.

Validations:



355
# File 'app/models/rma.rb', line 355

validates :rma_number, uniqueness: true

#ship_from_address_idvoid (readonly)

This method returns an undefined value.

Validates company id, customer id, return shipping address id, payment method.

Validations:



358
359
# File 'app/models/rma.rb', line 358

validates :company_id, :customer_id, :return_shipping_address_id, :payment_method,
:rma_number, :ship_from_address_id, presence: true

#shipping_option_idvoid (readonly)

This method returns an undefined value.

Validates shipping option id, should validate shipping option.



369
# File 'app/models/rma.rb', line 369

validates :shipping_option_id, presence: { if: :should_validate_shipping_option?, message: 'is required to generate return labels' }

#skip_return_label_validationObject

Returns the value of attribute skip_return_label_validation.



393
394
395
# File 'app/models/rma.rb', line 393

def skip_return_label_validation
  @skip_return_label_validation
end

#skip_shipping_option_validationObject

Returns the value of attribute skip_shipping_option_validation.



393
394
395
# File 'app/models/rma.rb', line 393

def skip_shipping_option_validation
  @skip_shipping_option_validation
end

#transmission_emailvoid (readonly)

This method returns an undefined value.

Validates transmission email.



377
# File 'app/models/rma.rb', line 377

validates :transmission_email, email_format: true, allow_nil: true

#transmission_faxvoid (readonly)

This method returns an undefined value.

Validates transmission fax.



380
# File 'app/models/rma.rb', line 380

validates :transmission_fax, phone_format: true, allow_nil: true

Class Method Details

.awaiting_returnActiveRecord::Relation<Rma>

A relation of Rmas that are awaiting return. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Rma>)

See Also:



197
# File 'app/models/rma.rb', line 197

scope :awaiting_return, -> { where(state: 'awaiting_return') }

.canada_post_return_valid?(rma_number) ⇒ Boolean

Canada Post Manage Returns RA-number validation. True iff an RMA with this
number exists, is still open/eligible for return, and ships from Canada
(the only origin our Canada Post return policy can carry). Backs the
+Api::V1::CanadaPost::ReturnsController+ web service Canada Post calls
before issuing a box-free/paperless QR return label.

Parameters:

  • rma_number (String)

    the RA number the customer entered on the
    Canada Post hosted returns page.

Returns:

  • (Boolean)

    whether the number is valid for a Canada Post return.



229
230
231
232
233
# File 'app/models/rma.rb', line 229

def self.canada_post_return_valid?(rma_number)
  return false if rma_number.blank?

  canada_post_returnable.exists?(rma_number: rma_number)
end

.canada_post_returnableActiveRecord::Relation<Rma>

A relation of Rmas that are canada post returnable. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Rma>)

See Also:



216
217
218
# File 'app/models/rma.rb', line 216

scope :canada_post_returnable, lambda {
  joins(:ship_from_address).merge(Address.canada).where(state: RETURN_OPEN_STATES)
}

.contains_tracking_numberActiveRecord::Relation<Rma>

A relation of Rmas that are contains tracking number. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Rma>)

See Also:



201
202
203
204
205
206
# File 'app/models/rma.rb', line 201

scope :contains_tracking_number, ->(tracking_number) {
  left_outer_joins(:return_shipments).where.any_of(
    where.overlap(tracking_numbers: [tracking_number]),
    { shipments: { tracking_number: tracking_number } }
  ).distinct
}

.create_rma_from_delivery(d) ⇒ Object

Create rma from delivery.

Parameters:

  • d (Object)

Returns:

  • (Object)


1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
# File 'app/models/rma.rb', line 1617

def self.create_rma_from_delivery(d)
  return d.precreated_rma if d.precreated_rma.present? && !d.precreated_rma.voided?

  ship_from_address = d.destination_address.is_warehouse? ? (d.order.customer.shipping_address || d.order.customer.billing_address || d.order.customer.addresses.first) : d.destination_address
  shipping_option_id = if d.shipping_option.country == 'US'
                         1
                       elsif d.shipping_option.country == 'CA'
                         4
                       end
  rma = Rma.new(company_id: d.order.company.id,
                customer_id: d.order.customer.id,
                original_order_id: d.order.id,
                ship_from_address_id: ship_from_address.id,
                return_shipping_address_id: d.order.store.warehouse_address_id,
                number_of_return_labels_required: d.shipments.where.not(state: %w[label_voided
                                                                                  manually_voided]).size,
                shipping_option_id: shipping_option_id,
                payment_method: 'Original Payment Method',
                original_po_number: d.order.po_number,
                transmission_email: d.order.transmission_email,
                transmission_fax: d.order.transmission_fax,
                precreate_from_delivery_id: d.id,
                support_case_id: d.order.support_case_ids.first,
                contact_name: (d.order.contact.present? ? d.order.contact.full_name : d.order.customer.full_name),
                description: 'Precreated RMA',
                customer_reference: d.order.customer_reference)
  d.order.line_items.parents_only.non_shipping.each do |li|
    reason_code = li.item.sku == 'SHORTSTOP' ? 'SSR' : 'TBD'

    rma.rma_items << RmaItem.new(returned_item_id: li.item.id,
                                 returned_line_item_id: li.id,
                                 returned_item_quantity: li.quantity,
                                 returned_item_location: 'AVAILABLE',
                                 returned_reason: reason_code,
                                 liable: 'WARMLYYOURS',
                                 under_warranty: true,
                                 replacement_required: false)
  end
  added_items = rma.rma_items.select(&:new_record?)
  rma.save!
  rma.publish_event(Events::RmaItemsAdded, data: { rma_id: rma.id, added_rma_item_ids: added_items.map(&:id) })
  rma.create_return_delivery
  rma.generate_return_labels if rma.return_label_required?
  rma
end

.in_auto_return_reviewObject

In auto return review.

Returns:

  • (Object)


967
968
969
# File 'app/models/rma.rb', line 967

def self.in_auto_return_review
  where(state: 'auto_return_review')
end

.in_awaiting_inspectionObject

In awaiting inspection.

Returns:

  • (Object)


974
975
976
# File 'app/models/rma.rb', line 974

def self.in_awaiting_inspection
  where(state: 'awaiting_inspection')
end

.return_labels_select_optionsObject

Return labels select options.

Returns:

  • (Object)


629
630
631
# File 'app/models/rma.rb', line 629

def self.return_labels_select_options
  [['None', 0], ['1', 1], ['2', 2], ['3', 3], ['4', 4], ['5', 5], ['Auto', '-1']]
end

.returned_and_need_attention_immediatelyObject

Returned and need attention immediately.

Returns:

  • (Object)


981
982
983
984
985
986
987
988
# File 'app/models/rma.rb', line 981

def self.returned_and_need_attention_immediately
  returned = where(state: 'returned').where.not(returned_date: nil).order(returned_date: :desc)
  rmas_list = []
  returned.each do |rma|
    rmas_list << rma if rma.returned_date&.working_days_until(Date.current).to_i > 3
  end
  rmas_list
end

.rma_activeActiveRecord::Relation<Rma>

A relation of Rmas that are rma active. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Rma>)

See Also:



196
# File 'app/models/rma.rb', line 196

scope :rma_active, -> { where(state: %w[awaiting_return requested awaiting_inspection credit_in_process partially_returned returned]) }

.rma_count(company_id = nil, where_conditions = nil, where_not_conditions = nil) ⇒ Object

Rma count.

Parameters:

  • company_id (Object) (defaults to: nil)
  • where_conditions (Object) (defaults to: nil)
  • where_not_conditions (Object) (defaults to: nil)

Returns:

  • (Object)


956
957
958
959
960
961
962
# File 'app/models/rma.rb', line 956

def self.rma_count(company_id = nil, where_conditions = nil, where_not_conditions = nil)
  r = Rma.order(:id)
  r = r.where(company_id: company_id) unless company_id.nil?
  r = r.where(where_conditions) unless where_conditions.nil?
  r = r.where.not(where_not_conditions) unless where_not_conditions.nil?
  r.count
end

.states_for_selectObject

States for select.

Returns:

  • (Object)


1388
1389
1390
# File 'app/models/rma.rb', line 1388

def self.states_for_select
  state_machines[:state].states.sort_by(&:human_name).map { |s| [s.value.titleize, s.value] }
end

.transmission_state_for_selectObject

Transmission state for select.

Returns:

  • (Object)


1395
1396
1397
# File 'app/models/rma.rb', line 1395

def self.transmission_state_for_select
  %w[awaiting_transmission in_transmission_queue transmitted].map { |e| [e.titleize, e] }
end

.with_credit_memos_not_transmitted_and_are_not_fullyoffsetObject

With credit memos not transmitted and are not fullyoffset.

Returns:

  • (Object)


993
994
995
996
997
998
999
1000
1001
# File 'app/models/rma.rb', line 993

def self.with_credit_memos_not_transmitted_and_are_not_fullyoffset
  with_credit_memos_not_transmitted = where(id: CreditMemo.where(state: %w[printed processing_refund
                                                                           partially_offset]).where(transmission_state: 'awaiting_transmission').map(&:rma_id).uniq.compact).where.not(returned_date: nil).order(returned_date: :desc)
  rmas_list = []
  with_credit_memos_not_transmitted.each do |rma|
    rmas_list << rma if rma.returned_date&.working_days_until(Date.current).to_i > 3
  end
  rmas_list
end

.with_credit_memos_transmitted_and_are_not_fullyoffsetObject

With credit memos transmitted and are not fullyoffset.

Returns:

  • (Object)


1006
1007
1008
1009
1010
1011
1012
1013
# File 'app/models/rma.rb', line 1006

def self.with_credit_memos_transmitted_and_are_not_fullyoffset
  with_credit_memos_transmitted = where(id: CreditMemo.where(state: %w[printed processing_refund partially_offset]).where(transmission_state: 'transmitted').map(&:rma_id).uniq.compact).where.not(returned_date: nil).order(returned_date: :desc)
  rmas_list = []
  with_credit_memos_transmitted.each do |rma|
    rmas_list << rma if rma.returned_date && (Date.current - rma.returned_date).to_i > 90
  end
  rmas_list
end

.with_external_tracking_numberActiveRecord::Relation<Rma>

A relation of Rmas that are with external tracking number. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Rma>)

See Also:



198
199
200
# File 'app/models/rma.rb', line 198

scope :with_external_tracking_number, ->(tracking_number) {
  where.overlap(tracking_numbers: [tracking_number])
}

Instance Method Details

#active_rma_itemsObject

Active rma items.

Returns:

  • (Object)


1718
1719
1720
# File 'app/models/rma.rb', line 1718

def active_rma_items
  rma_items.active.reload
end

#activitiesActiveRecord::Associations::CollectionProxy

The associated activities.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


167
# File 'app/models/rma.rb', line 167

has_many :activities, as: :resource, dependent: :nullify

#all_credit_memos_fully_offset?Object

Whether all credit memos fully offset.

Returns:

  • (Object)


1148
1149
1150
# File 'app/models/rma.rb', line 1148

def all_credit_memos_fully_offset?
  credit_memos.present? && credit_memos.all?(&:fully_offset?)
end

#all_credit_memos_partially_refunded?Object

Whether all credit memos partially refunded.

Returns:

  • (Object)


1141
1142
1143
# File 'app/models/rma.rb', line 1141

def all_credit_memos_partially_refunded?
  credit_memos.present? && (credit_memos.all?(&:partially_offset?) || credit_memos.any?(&:partially_offset?))
end

#all_credit_orders_awaiting_return?Object

Whether all credit orders awaiting return.

Returns:

  • (Object)


1416
1417
1418
# File 'app/models/rma.rb', line 1416

def all_credit_orders_awaiting_return?
  credit_orders.all? { |co| co.awaiting_return? || co.cancelled? }
end

#all_items_requested?Object

Whether all items requested.

Returns:

  • (Object)


1694
1695
1696
# File 'app/models/rma.rb', line 1694

def all_items_requested?
  rma_items.reload.active.present? and rma_items.reload.active.all?(&:requested?)
end

#all_items_requested_and_ready_to_return?Object

Whether all items requested and ready to return.

Returns:

  • (Object)


1423
1424
1425
1426
1427
1428
# File 'app/models/rma.rb', line 1423

def all_items_requested_and_ready_to_return?
  all_items_requested? && return_labels_correctly_generated?
  # any_items_requested? && credit_orders.all? do |co|
  #   co.awaiting_return? || co.cancelled?
  # end && return_labels_correctly_generated?
end

#all_items_returned?Object

Whether all items returned.

Returns:

  • (Object)


1680
1681
1682
# File 'app/models/rma.rb', line 1680

def all_items_returned?
  rma_items.reload.active.all? { |rma_item| rma_item.returned? || rma_item.voided? }
end

#all_items_voided?Object

Whether all items voided.

Returns:

  • (Object)


1187
1188
1189
# File 'app/models/rma.rb', line 1187

def all_items_voided?
  rma_items.reload.all?(&:voided?)
end

#all_items_will_not_be_returned?Boolean

True when every top-level item still in play is flagged
"Will be returned? = NO", so the customer is keeping all of it and no parcel
is inbound.

Kit components are excluded because +RmaItem#expand_kit_components!+ creates
children without copying the flag: they always read false and would mask a
kit parent the user did mark. Guarded on +any?+ so a brand-new RMA with no
items yet is not mistaken for one whose items are all staying put —
+at_least_one_rma_item_if_return_label_required+ must keep firing there.
Filtered in Ruby rather than by scope so unsaved nested-attribute changes
count, matching +all_items_voided?+.

Returns:

  • (Boolean)


1204
1205
1206
1207
# File 'app/models/rma.rb', line 1204

def all_items_will_not_be_returned?
  items = rma_items.reload.reject { |ri| ri.voided? || ri.child? }
  items.any? && items.all?(&:will_not_be_returned?)
end

#all_uploadsObject

All uploads.

Returns:

  • (Object)


943
944
945
946
947
948
# File 'app/models/rma.rb', line 943

def all_uploads
  all_uploads = []
  all_uploads += uploads
  all_uploads += return_delivery&.uploads || []
  all_uploads.uniq
end

#any_credit_memo_printed?Object

Whether any credit memo printed.

Returns:

  • (Object)


1134
1135
1136
# File 'app/models/rma.rb', line 1134

def any_credit_memo_printed?
  credit_memos.present? && credit_memos.all?(&:printed?)
end

#any_items_awaiting_inspection?Object

Whether any items awaiting inspection.

Returns:

  • (Object)


1219
1220
1221
# File 'app/models/rma.rb', line 1219

def any_items_awaiting_inspection?
  rma_items.reload.any?(&:awaiting_inspection?)
end

#any_items_requested?Object

Whether any items requested.

Returns:

  • (Object)


1687
1688
1689
# File 'app/models/rma.rb', line 1687

def any_items_requested?
  rma_items.reload.active.where(state: 'requested').any?
end

#any_rejected_items?Boolean

True when any item or kit component was flagged non-resalable or not
returned during inspection. Gates the RMAITEMREJECTED notification.

Returns:

  • (Boolean)


1345
1346
1347
# File 'app/models/rma.rb', line 1345

def any_rejected_items?
  rma_items.rejected.exists?
end

#append_non_kit_credit_line(order, rma_item, good_qty, credit_percentage, from_ci_invoice) ⇒ Object

Appends a single credit line for a non-kit item, crediting +good_qty+ units.
An item linked to its original invoice line credits the invoice price (prorated
for a kit component returned on its own line); an unlinked item credits the
current catalog amount. Both are scaled by +credit_percentage+.

Parameters:

  • order (Object)
  • rma_item (Object)
  • good_qty (Object)
  • credit_percentage (Object)
  • from_ci_invoice (Object)


2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
# File 'app/models/rma.rb', line 2321

def append_non_kit_credit_line(order, rma_item, good_qty, credit_percentage, from_ci_invoice)
  if rma_item.returned_line_item.present? && (rma_item.returned_line_item.item == rma_item.returned_item)
    if rma_item.returned_line_item.parent_id.present?
      # Kit component returned on its own invoice line: credit its prorated
      # share of the original kit price from the invoice, not the component's
      # catalog cost. The prorated value always wins — whether the catalog cost
      # is higher or lower — so the credit reflects the invoice. Mirrors
      # create_kit_credit_lines, which handles the kit-parent expansion path.
      price = discounted_price = invoice_kit_component_credit_price(rma_item.returned_line_item)
    else
      price = rma_item.returned_line_item.price
      discounted_price = rma_item.returned_line_item.discounted_price
    end

    catalog_item = rma_item.returned_line_item.catalog_item
    if catalog_item.nil?
      catalog_item = rma_item.rma.customer.catalog.catalog_items.includes(:store_item).where(store_items: { item_id: rma_item.returned_item_id }).first
      raise "Can't find matching CatalogItem" if catalog_item.nil?
    end
    li_attrs = {
      catalog_item_id: catalog_item.id,
      quantity: -good_qty,
      price: price * credit_percentage,
      discounted_price: discounted_price * credit_percentage,
      credit_rma_item: rma_item
    }
  else
    catalog_item = customer.catalog.catalog_items.includes(:store_item).where(store_items: { item_id: rma_item.returned_item_id }).first
    raise "Can't find matching CatalogItem" if catalog_item.nil?

    li_attrs = {
      catalog_item_id: catalog_item.id,
      quantity: -good_qty,
      price: catalog_item.amount * credit_percentage,
      discounted_price: catalog_item.amount * credit_percentage,
      credit_rma_item: rma_item
    }
  end

  li = LineItem.new(li_attrs)
  li.do_not_calculate_tax = true if from_ci_invoice
  order.line_items << li
end

#arrival_datetimeObject

Arrival datetime.

Returns:

  • (Object)


845
846
847
# File 'app/models/rma.rb', line 845

def arrival_datetime
  rma_items.active.maximum(:arrival_datetime)
end

#auto_return_rma_itemsObject

Auto return rma items.

Returns:

  • (Object)


1666
1667
1668
# File 'app/models/rma.rb', line 1666

def auto_return_rma_items
  rma_items.where(will_not_be_returned: true, state: 'requested').find_each(&:auto_return)
end

#build_activityObject

Build activity.

Returns:

  • (Object)


1085
1086
1087
# File 'app/models/rma.rb', line 1085

def build_activity
  activities.new(resource: self, party: customer)
end

#can_be_edited?Object

Whether can be edited.

Returns:

  • (Object)


1062
1063
1064
# File 'app/models/rma.rb', line 1062

def can_be_edited?
  new_record? || requested? || auto_return_review? || awaiting_return? || awaiting_inspection? || partially_returned?
end

#can_be_received?Object

Whether can be received.

Returns:

  • (Object)


1027
1028
1029
# File 'app/models/rma.rb', line 1027

def can_be_received?
  requested? || awaiting_return? || awaiting_inspection? || partially_returned?
end

#can_be_transmitted?Object

Whether can be transmitted.

Returns:

  • (Object)


1034
1035
1036
# File 'app/models/rma.rb', line 1034

def can_be_transmitted?
  awaiting_return? || credit_in_process? || credited_partially_refunded? || credited_fully_refunded?
end

#can_be_unreturned?Object

Whether can be unreturned.

Returns:

  • (Object)


1055
1056
1057
# File 'app/models/rma.rb', line 1055

def can_be_unreturned?
  returned?
end

#can_be_unvoided?Object

Whether can be unvoided.

Returns:

  • (Object)


1048
1049
1050
# File 'app/models/rma.rb', line 1048

def can_be_unvoided?
  voided?
end

#can_be_voided?Object

Whether can be voided.

Returns:

  • (Object)


1041
1042
1043
# File 'app/models/rma.rb', line 1041

def can_be_voided?
  requested? || auto_return_review? || (awaiting_return? && no_items_returned?)
end

#can_edit_return_labels?Object

Whether can edit return labels.

Returns:

  • (Object)


1069
1070
1071
1072
1073
# File 'app/models/rma.rb', line 1069

def can_edit_return_labels?
  return false if ship_from_address.is_warehouse?

  requested? || auto_return_review? || awaiting_return?
end

#canada_post_return?Boolean

True when the customer's return is being made via Canada Post — i.e. the
selected return shipping option is Canada Post. Canada Post returns are
boxless/paperless QR returns issued by Canada Post's own Manage Returns
policy; we never generate a return label for them (ShipEngine cannot produce
a Canada Post return label, and attempting to would incur a chargeable
forward-label fallback), so the hosted QR policy link stands in for the
label. Drives both the no-label gate in +return_label_required?+ and the
customer-email block in +canada_post_return_html+.
See doc/tasks/202606121059_CANADA_POST_RETURNS_NOTIFICATION_CONTINGENCY.md.

Returns:

  • (Boolean)


1120
1121
1122
# File 'app/models/rma.rb', line 1120

def canada_post_return?
  shipping_option&.carrier == 'Canadapost'
end

#canada_post_return_htmlObject

HTML block embedded in the customer return email via the single
{{ rma.canada_post_return_html }} tag in the RMA email template. Renders
ONLY when Canada Post is the selected return shipping option
(+canada_post_return?+), and stands in for a generated return label — never
alongside one (CP returns generate no label; see +return_label_required?+).
Unlike the USPS paperless block, the artifact is a static Canada Post Manage
Returns link (no per-box QR image, no CID attachment): the customer starts
their return on Canada Post's hosted page, enters their RMA number, and
Canada Post issues the QR. Inline styles only (email-client safe).



1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
# File 'app/models/rma.rb', line 1505

def canada_post_return_html
  return '' unless canada_post_return?

  <<~HTML.html_safe
    <div style="margin:16px 0;font-family:Arial,Helvetica,sans-serif;font-size:14px;color:#222;">
      <h3 style="margin:0 0 8px;">Canada Post &mdash; Paperless QR return</h3>
      <p style="margin:0 0 8px;">You do not need to print a shipping label at home &mdash; Canada Post can create and apply it for you.</p>
      <p style="margin:0 0 8px;"><strong>Before drop-off: write your RMA number (#{ERB::Util.html_escape(rma_number)}) on the outside of the box, and print this email (or the attached return instructions) and include a copy inside the box.</strong> The label Canada Post prints may not show your RMA number, and we need it to match your returned goods to this RMA.</p>
      <p style="margin:0 0 8px;"><strong>Return Policy ID: #{CANADA_POST_RETURNS_POLICY_ID}</strong><br />Take your packaged items to any Canada Post outlet and give staff this Return Policy ID &mdash; they will print the return label for you.</p>
      <p style="margin:0 0 8px;">Or start your return online:</p>
      <p style="margin:0 0 8px;"><a href="#{CANADA_POST_RETURNS_POLICY_URL}" style="display:inline-block;padding:10px 16px;background:#d62d20;color:#ffffff;text-decoration:none;border-radius:4px;font-weight:bold;">Start your Canada Post return</a></p>
      <p style="margin:0 0 12px;font-size:12px;word-break:break-all;color:#555;">#{CANADA_POST_RETURNS_POLICY_URL}</p>
      <p style="margin:0 0 8px;">When Canada Post asks for a <strong>Return No.</strong>, enter your RMA number <strong>#{ERB::Util.html_escape(rma_number)}</strong> so we can match your return to this RMA.</p>
    </div>
  HTML
end

#capture_seeded_credit_prices(order) ⇒ Hash

Snapshots the invoice-derived prices seeded on each credit line (list/discounted
prices for plain items, prorated shares for kit components) before the autosave
chain re-prices them to catalog amount. Keyed by [rma_item_id, catalog_item_id],
which is unique per credit line. Pairs with +restore_seeded_credit_prices+.

Parameters:

  • order (Order)

    the credit order with its line items built but not yet saved

Returns:

  • (Hash)

    {[rma_item_id, catalog_item_id] => {price:, discounted_price:}}



2422
2423
2424
2425
2426
2427
2428
# File 'app/models/rma.rb', line 2422

def capture_seeded_credit_prices(order)
  order.line_items.each_with_object({}) do |li, h|
    next unless li.credit_rma_item && li.catalog_item_id

    h[[li.credit_rma_item.id, li.catalog_item_id]] = { price: li.price, discounted_price: li.discounted_price }
  end
end

#communicationsActiveRecord::Associations::CollectionProxy

The associated communications.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


170
# File 'app/models/rma.rb', line 170

has_many :communications, as: :resource, dependent: :nullify

#companyCompany?

The associated company.

Returns:



112
# File 'app/models/rma.rb', line 112

belongs_to :company, optional: true

#create_credit_order(rma_item_ids) ⇒ Object

Create credit order.

Parameters:

  • rma_item_ids (Object)

Returns:

  • (Object)


2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
# File 'app/models/rma.rb', line 2172

def create_credit_order(rma_item_ids)
  # take only lines that are returned and which don't have a credit order already
  rma_items_list = rma_items.where(id: rma_item_ids.pluck(:rma_item_id))
  items = rma_items_list.reject do |ri|
    ri.marked_for_destruction? || ri.voided? || ri.credit_order_line_item
  end
  return if items.blank?

  order = Order.new(order_type: Order::CREDIT_ORDER)
  transaction do
    order.customer = customer
    order.customer_reference = customer_reference
    if ship_from_address.present?
      order.shipping_address_id = ship_from_address_id
    else
      # Address was deleted - fall back to customer's current address
      fallback_address = customer.shipping_address || customer.billing_address || customer.addresses.first
      raise "Cannot create credit order: no valid shipping address available for RMA #{rma_number}" if fallback_address.nil?

      order.shipping_address_id = fallback_address.id
      Rails.logger.warn("RMA #{rma_number}: ship_from_address_id #{ship_from_address_id} not found, using fallback address #{fallback_address.id}")
    end
    order.shipped_date = original_invoice.try(:shipped_date) || original_invoice.try(:order).try(:shipped_date)
    order.currency = original_invoice.try(:currency) || customer.catalog.currency
    order.disable_auto_coupon = true
    order.tax_exempt = original_invoice.try(:tax_exempt).to_b
    order.rma_id = id
    order.tax_date = if rma_items.active.any?(&:replacement_required?)
                       # if there is a replacement order, then we use current tax rates so they match up
                       Date.current
                     else
                       original_invoice.try(:shipped_date) || original_invoice.try(:order).try(:shipped_date) || Date.current
                     end

    from_ci_invoice = original_invoice&.invoice_type == Invoice::CI

    items.each do |rma_item|
      credit_percentage = rma_item.credit_percentage.to_f / 100
      qty_to_receive = rma_item_ids.detect { |r| r[:rma_item_id] == rma_item.id }&.dig(:qty_to_receive) || 0

      if rma_item.returned_item&.is_kit? && rma_item.children.any?
        create_kit_credit_lines(order, rma_item, qty_to_receive, credit_percentage)
        next
      end

      # Non-kit item: credit only the resalable (good) units. The non-resalable
      # damaged portion is booked to inventory but excluded from the credit,
      # matching kit-component handling.
      good_qty = rma_item.creditable_quantity(qty_to_receive)
      next if good_qty <= 0

      append_non_kit_credit_line(order, rma_item, good_qty, credit_percentage, from_ci_invoice)
    end

    # The save below fires reset_discount (via the disable_auto_coupon toggle),
    # which re-prices every parent line to its catalog amount. Snapshot the
    # invoice-derived prices now so restore_seeded_credit_prices can put them back.
    seeded_credit_prices = capture_seeded_credit_prices(order)

    order.recalculate_shipping = false
    order.save!

    # Copy the original invoice discounts onto the credit lines. A line's discount is
    # the gap between its (list) price and its discounted/net price, both captured
    # before the save chain reset prices to catalog. For a kit component this gap is
    # already its prorated share of the kit discount, and components excluded from the
    # credit have no line — so their share is forfeited, never redistributed onto the
    # credited components. The gap is attributed back to the source line's coupons in
    # proportion to each coupon's amount.
    order.line_items.non_shipping.parents_only
         .joins(credit_rma_item: :returned_line_item)
         .includes(credit_rma_item: { returned_line_item: :line_discounts })
         .find_each do |li|
      seeded = seeded_credit_prices[[li.credit_rma_item_id, li.catalog_item_id]]
      next unless seeded

      discount_total = ((seeded[:price].to_d - seeded[:discounted_price].to_d) * li.quantity).abs
      next unless discount_total.positive?

      split_line_discount_by_coupon(discount_total, li.credit_rma_item.returned_line_item.line_discounts).each do |ld, amount|
        next if amount.zero?

        li.line_discounts.create(coupon_id: ld.coupon_id, discount_id: ld.discount_id, amount: amount)
      end
    end

    effective_dates = {}
    original_invoice&.discounts&.each { |d| effective_dates[d.coupon_id] = d.effective_date }
    order.line_items.non_shipping.map(&:line_discounts).flatten.group_by(&:coupon_id).each do |coupon_id, line_discounts|
      discount = order.discounts.create(coupon_id: coupon_id,
                                        amount: line_discounts.sum(&:amount),
                                        user_amount: line_discounts.sum(&:amount),
                                        effective_date: effective_dates[coupon_id] || Date.current)
      line_discounts.each { |ld| ld.update_attribute!(:discount_id, discount.id) }
    end

    if original_invoice&.discounts&.any? && order.discounts.empty?
      # the original order has discounts but we couldn't work out the amounts as it was too old
      # so we just flag the credit order as needing an adjustment
      order.discount_adjustment_needed = true
    end

    unless order.valid?
      # this adds the errors to the RMA so they will show up on the RMA form
      errors.add :base, "Credit Order not valid. Exc: #{order.errors.full_messages}"
    end

    # adding this so that Itemizable.set_totals will be called, which will call
    # Itemizable.calculate_discounts, which will correctly set the discounted prices
    order.force_total_reset = true
    order.save!

    if from_ci_invoice
      inherit_ci_invoice_line_values(order)
    elsif original_invoice.present?
      # The save chain above re-priced every parent line to its catalog amount via
      # reset_discount (fired by the disable_auto_coupon toggle). Restore the
      # invoice-derived prices we seeded so the credit reflects the original invoice
      # — including prorated kit-component shares — instead of current catalog cost.
      # update_columns skips the autosave/reset callbacks; tax is recomputed below
      # from the restored prices.
      restore_seeded_credit_prices(order, seeded_credit_prices)
      if has_replacement_items?
        order.refresh_tax_rate
      else
        # No replacements: the tax rate was already inherited from the invoice in
        # set_initial_tax_rate. Re-fetching via refresh_tax_rate would overwrite it
        # with current TaxJar rates, which may differ from the original transaction.
        # Only apply the inherited rate to the (now corrected) line items.
        order.reload
        order.apply_tax_rate_to_line_items
      end
    else
      order.refresh_tax_rate
    end
    order.returned!
  end
  order
end

#create_documentsObject

Create documents.

Returns:

  • (Object)


1746
1747
1748
1749
1750
# File 'app/models/rma.rb', line 1746

def create_documents
  # return unless rma_items_changed?
  # create_credit_order
  create_replacement_order
end

#create_replacement_orderObject

Create replacement order.

Returns:

  • (Object)


2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
# File 'app/models/rma.rb', line 2078

def create_replacement_order
  return unless can_be_edited?

  # take only lines that are non voided and which don't have a replacement order already

  items = rma_items.select { |ri| ri.replacement_required? && ri.replacement_order.nil? }

  return if items.empty?

  res = {}
  transaction do
    order_type = replacement_order_type || items.first.rma_reason_code&.replacement_order_type || Order::SALES_ORDER
    order = Order.new(order_type: order_type, creator: creator)
    order.customer = customer
    order.customer_reference = customer_reference
    if original_invoice.present?
      order.opportunity_id = original_invoice.order.opportunity_id if original_invoice.order.present?
      order.shipping_address_id = original_invoice.shipping_address_id
      order.currency = original_invoice.currency
    else
      order.shipping_address = customer.shipping_address
      order.currency = company.currency
    end
    order.disable_auto_coupon = true
    order.rma_id = id

    # If this RMA is linked to a support case, auto-populate the tracking email
    # with the primary participant's email so they receive shipment notifications.
    # This is important for warranty replacements being shipped directly to end consumers.
    if support_case.present?
      participant_email = support_case.primary_party&.email
      order.tracking_email = [participant_email].compact if participant_email.present?
    end

    catalog = customer.catalog
    items.each do |rma_item|
      cat_item = CatalogItem.includes(:store_item).where(catalog_id: catalog.id,
                                                         store_items: { item_id: rma_item.returned_item_id }).first
      raise ReplacementOrderError, "Unable to find replacement item in customer's catalog" if cat_item.nil?

      order.line_items << LineItem.new(catalog_item_id: cat_item.id,
                                       quantity: rma_item.returned_item_quantity,
                                       price: cat_item.amount,
                                       discounted_price: cat_item.amount,
                                       replacement_rma_item_id: rma_item.id)
    end

    errors.add :base, "Replacement Order not valid. Exc: #{order.errors.full_messages}" unless order.valid?
    order.do_not_set_totals = true
    order.save!
    order.reload

    order.retrieve_shipping_costs
    order.calculate_discounts
    order.save!
    order.reload

    # add the price match coupon
    coupon = Coupon.find_by(code: 'OPM')
    discount = Discount.new(itemizable: order, coupon_id: coupon.id, effective_date: Date.current)
    order.line_items.each do |li|
      unless (original_line = li.try(:replacement_rma_item).try(:returned_line_item)) && (li.discounted_price > original_line.discounted_price)
        next
      end

      # add a price match coupon to match the price on the original order
      coupon_amount = (li.discounted_price - original_line.discounted_price) * li.quantity
      discount.line_discounts.build(coupon_id: coupon.id, amount: -coupon_amount,
                                    line_item_id: li.id)
    end

    if discount.line_discounts.any?
      discount.amount = discount.user_amount = discount.line_discounts.to_a.sum(&:amount)
      discount.save!
    end

    items.each do |rma_item|
      rma_item.update(replacement_order_id: order.id)
    end

    order.reload.save

    # If the rma is tied to a support case, also link that order to the support case
    support_case.orders << order if support_case

    res[:order] = order
  end
  OpenStruct.new(res).freeze
end

#create_return_deliveryObject

Create return delivery.

Returns:

  • (Object)


1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
# File 'app/models/rma.rb', line 1755

def create_return_delivery
  return unless return_label_required?

  @return_packaging_overridden = false
  @return_packaging_shortfall = nil
  d = return_delivery
  if d.present?
    update_column(:return_delivery_id, nil)
    d.destroy
  end
  self.return_delivery = Delivery.create(
    rma_for_return: self,
    origin_address_id: ship_from_address_id,
    destination_address_id: customer.store.warehouse_address_id,
    bill_shipping_to_customer: customer.bill_shipping_to_customer,
    shipping_option_id: shipping_option_id,
    state: 'quoting' # here we force state quoting, overrides pre_pack
  )

  items = reject_expanded_kit_components(rma_items.reject { |ri| ri.marked_for_destruction? || ri.voided? })
  items.each do |rma_item|
    credit_percentage = rma_item.credit_percentage.to_f / 100
    if rma_item.returned_line_item.present? && (rma_item.returned_line_item.item == rma_item.returned_item)
      # it's linked to an existing line item, so get pricing from that
      if rma_item.returned_line_item.parent_id.present?
        price = discounted_price = rma_item.returned_line_item.catalog_item.amount
      else
        price = rma_item.returned_line_item.price
        discounted_price = rma_item.returned_line_item.discounted_price
      end
      catalog_item = rma_item.returned_line_item.catalog_item
      catalog_item ||= customer.catalog.catalog_items.by_skus(rma_item.returned_item.sku).first
      raise "Can't find matching CatalogItem" if catalog_item.nil?

      li = LineItem.new(catalog_item_id: catalog_item.id,
                        quantity: -rma_item.returned_item_quantity,
                        price: price * credit_percentage,
                        discounted_price: discounted_price * credit_percentage)
    else
      # it's not linked to an existing line item, so need to get pricing from current item pricing
      catalog_item = customer.catalog.catalog_items.includes(:store_item).where(store_items: { item_id: rma_item.returned_item_id }).first
      raise "Can't find matching CatalogItem" if catalog_item.nil?

      li = LineItem.new(catalog_item_id: catalog_item.id,
                        quantity: -rma_item.returned_item_quantity,
                        price: catalog_item.amount * credit_percentage,
                        discounted_price: catalog_item.amount * credit_percentage)
    end
    return_delivery.line_items << li
  end

  # Use packaging for shipping calculation but create shipments based on user's label requirement
  if original_order&.precreate_rma? && precreate_from_delivery_id == return_delivery.id
    # we are precreating this RMA, it is not a new manual RMA flow
    # grab the shipment info from the original order delivery which should now be properly packed
    return_delivery.create_shipments_from_equivalent_delivery(original_order.deliveries.first,
                                                              'awaiting_label')
  else
    # Use packaging algorithm for shipping cost calculation only
    return_delivery.search_deliveries_for_equivalent_packaging
    Rails.logger.info "RMA #{rma_number}: Packaging algorithm created #{return_delivery.shipments.count} shipments"
  end

  return_delivery.retrieve_shipping_costs

  # Handle user's label requirement vs packaging recommendation. In every mode
  # the original order's actual completed packing (when present) is the ground
  # truth for which items fit in which box, so prefer mirroring it over the
  # equivalent-packaging guess — which can return N identical max-size boxes
  # with every item dumped in box 1 (RMA60441; see
  # doc/tasks/202606052100_RMA_RETURN_PACKAGING_ALLOCATION.md).
  if number_of_return_labels_required == -1
    # Auto: mirror the original order's packing when available; otherwise keep
    # the packaging-algorithm shipments (creating a default if it made none).
    if mirror_original_order_packaging(return_delivery).nil? && return_delivery.shipments.count.zero?
      Rails.logger.warn "RMA #{rma_number}: No shipments created by packaging, creating default shipment"
      return_delivery.shipments.create!(
        weight: return_delivery.ship_weight,
        length: 12, width: 12, height: 6,
        state: 'suggested',
        container_type: 'carton'
      )
      distribute_line_items_across_shipments(return_delivery, return_delivery.shipments)
    end
  elsif number_of_return_labels_required.positive?
    # User specified a label count: mirror the original packing if it matches
    # that count exactly, otherwise honor the count by bin-packing the actual
    # returned items into that many boxes (see build_return_shipments_for_count).
    build_return_shipments_for_count(return_delivery, number_of_return_labels_required)
  end
  # If number_of_return_labels_required is 0 or nil, no return labels are required

  # Surface the resulting per-box packing on the return-labels tab instead of
  # leaving "Suggested packaging" blank; flag when the requested count
  # overrode the suggested packing.
  if (summary = build_return_packaging_summary(return_delivery)).present?
    if @return_packaging_overridden
      prefix = "Requested #{number_of_return_labels_required} labels (overrides suggestion)"
      prefix += " — ⚠ items likely need #{@return_packaging_shortfall} boxes" if @return_packaging_shortfall
      summary = "#{prefix}#{summary}"
    end
    return_delivery.update_column(:suggested_packaging_text, summary)
  end

  return_delivery.return_labels_ready
end

#creatorEmployee?

The associated creator.

Returns:



145
# File 'app/models/rma.rb', line 145

belongs_to :creator, class_name: 'Employee', optional: true

#credit_availableObject

Credit available.

Returns:

  • (Object)


1283
1284
1285
1286
# File 'app/models/rma.rb', line 1283

def credit_available
  credit = credit_total - payments.all_authorized.sum(:amount)
  credit.negative? ? 0 : credit.round(2)
end

#credit_available_before_taxObject

Credit available before tax.

Returns:

  • (Object)


1291
1292
1293
1294
# File 'app/models/rma.rb', line 1291

def credit_available_before_tax
  credit = credit_total_before_tax - payments.all_authorized.sum(:amount)
  credit.negative? ? 0 : credit.round(2)
end

#credit_deliveriesActiveRecord::Associations::CollectionProxy

The associated credit deliveries.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


190
# File 'app/models/rma.rb', line 190

has_many :credit_deliveries, through: :credit_orders, source: :deliveries, class_name: 'Delivery'

#credit_memosActiveRecord::Associations::CollectionProxy

The associated credit memos.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


158
# File 'app/models/rma.rb', line 158

has_many :credit_memos

#credit_order_line_itemsActiveRecord::Associations::CollectionProxy

The associated credit order line items.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


182
# File 'app/models/rma.rb', line 182

has_many :credit_order_line_items, through: :rma_items

#credit_order_shipmentsObject

Credit order shipments.

Returns:

  • (Object)


1226
1227
1228
# File 'app/models/rma.rb', line 1226

def credit_order_shipments
  credit_deliveries.map { |d| d.shipments.where.not(tracking_number: nil) }.flatten
end

#credit_ordersActiveRecord::Associations::CollectionProxy

The associated credit orders.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


185
186
187
# File 'app/models/rma.rb', line 185

has_many :credit_orders, lambda {
  distinct
}, through: :credit_order_line_items, source: :resource, source_type: 'Order', class_name: 'Order'

#credit_totalObject

Credit total.

Returns:

  • (Object)


1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
# File 'app/models/rma.rb', line 1233

def credit_total
  BigDecimal(0)
  total_credit_orders = BigDecimal(0)
  total_credit_orders_line_items = BigDecimal(0)
  total_rma_items_requested = BigDecimal(0)

  credit_orders.each { |o| total_credit_orders += o.total }
  credit_orders.each do |co|
    co.line_items.non_shipping.parents_only.each { |li| total_credit_orders_line_items += (li.discounted_total + li.tax_total) unless li.credit_rma_item.is_customer_fault? }
  end
  rma_items.each do |ri|
    next unless !ri.is_customer_fault? && ri.returned_line_item.present?

    li = ri.returned_line_item
    # Calculate proportional amounts for partial returns
    proportion = BigDecimal(ri.returned_item_quantity) / BigDecimal(li.quantity)
    total_rma_items_requested += (li.discounted_total * proportion) + (li.tax_total * proportion)
  end
  total = total_rma_items_requested + total_credit_orders_line_items + total_credit_orders
  total.negative? ? -total : total
end

#credit_total_before_taxObject

Credit total before tax.

Returns:

  • (Object)


1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
# File 'app/models/rma.rb', line 1258

def credit_total_before_tax
  BigDecimal(0)
  total_credit_orders = BigDecimal(0)
  total_credit_orders_line_items = BigDecimal(0)
  total_rma_items_requested = BigDecimal(0)

  credit_orders.each { |o| total_credit_orders += o.line_total }
  credit_orders.each do |co|
    co.line_items.non_shipping.parents_only.each { |li| total_credit_orders_line_items += li.discounted_total unless li.credit_rma_item.is_customer_fault? }
  end
  rma_items.each do |ri|
    next unless !ri.is_customer_fault? && ri.returned_line_item.present?

    li = ri.returned_line_item
    # Calculate proportional amount for partial returns
    proportion = BigDecimal(ri.returned_item_quantity) / BigDecimal(li.quantity)
    total_rma_items_requested += li.discounted_total * proportion
  end
  total = total_rma_items_requested + total_credit_orders_line_items + total_credit_orders
  total.negative? ? -total : total
end

Crm link.

Returns:

  • (Object)


1381
1382
1383
# File 'app/models/rma.rb', line 1381

def crm_link
  UrlHelper.instance.rma_path(self)
end

#customerCustomer?

The associated customer.

Returns:



121
# File 'app/models/rma.rb', line 121

belongs_to :customer, optional: true

#customer_nameObject

Customer name.

Returns:

  • (Object)


1165
1166
1167
# File 'app/models/rma.rb', line 1165

def customer_name
  customer.try(:full_name)
end

#earliest_return_label_ship_dateDate?

Returns the earliest ship date across generated return labels.

Returns:

  • (Date, nil)

    earliest return-label ship date



777
778
779
# File 'app/models/rma.rb', line 777

def earliest_return_label_ship_date
  return_shipments.minimum(:date_shipped)
end

#effective_return_deliveryObject

Effective return delivery.

Returns:

  • (Object)


1440
1441
1442
# File 'app/models/rma.rb', line 1440

def effective_return_delivery
  return_delivery || legacy_return_shipments.last&.delivery
end

#enqueue_external_tracking_registration_if_neededvoid

This method returns an undefined value.

Enqueues ShipEngine registration for new external numbers or numbers whose
effective carrier changed. Insurance-only and equivalent carrier edits do
not repeat registration or history polling. Generated return labels use
Shipment's equivalent after-commit callback.



797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
# File 'app/models/rma.rb', line 797

def enqueue_external_tracking_registration_if_needed
  numbers = []
  if saved_change_to_tracking_numbers?
    before, after = saved_change_to_tracking_numbers
    numbers.concat(Array(after) - Array(before))
  end

  if saved_change_to_return_shipping_carrier?
    before_carrier, after_carrier = saved_change_to_return_shipping_carrier
    numbers.concat(
      tracking_numbers.reject do |tracking_number|
        before_resolved = Heatwave::Normalizers.resolve_shipping_carrier(
          before_carrier,
          tracking_number: tracking_number
        )
        after_resolved = Heatwave::Normalizers.resolve_shipping_carrier(
          after_carrier,
          tracking_number: tracking_number
        )
        before_resolved == after_resolved
      end
    )
  end

  numbers.compact_blank.uniq.each do |tracking_number|
    RmaTrackingRegistrationWorker.perform_async(
      rma_id: id,
      tracking_number: tracking_number
    )
  end
end

#exclude_manually_initiated_event?(event) ⇒ Object

Whether exclude manually initiated event.

Parameters:

  • event (Object)

Returns:

  • (Object)


936
937
938
# File 'app/models/rma.rb', line 936

def exclude_manually_initiated_event?(event)
  %i[returned returned_or_voided credit_in_process void transmit].include?(event.to_sym)
end

#external_return_labels?Object

Whether external return labels.

Returns:

  • (Object)


1107
1108
1109
# File 'app/models/rma.rb', line 1107

def external_return_labels?
  number_of_return_labels_required.to_i.zero?
end

#full_invoice_returned?Object

Whether full invoice returned.

Returns:

  • (Object)


1155
1156
1157
1158
1159
1160
# File 'app/models/rma.rb', line 1155

def full_invoice_returned?
  return false if credit_orders.blank?
  return true if original_invoice.blank?

  (original_invoice.line_total.abs == credit_orders.first.line_total.abs) && (original_invoice.line_items.non_shipping.parents_only.sum(&:quantity).abs == credit_orders.first.line_items.non_shipping.parents_only.sum(&:quantity).abs)
end

#generate_and_send_return_email_to_customerObject

Generate and send return email to customer.

Returns:

  • (Object)


1311
1312
1313
1314
1315
1316
# File 'app/models/rma.rb', line 1311

def generate_and_send_return_email_to_customer
  return unless rma_items.active.will_be_returned.any?

  # Create draft communication - instructions will be generated and attached by controller
  send_return_email_to_customer(keep_as_draft: true)
end

#generate_return_instructions_pdfObject

Generate return instructions pdf.

Returns:

  • (Object)


1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
# File 'app/models/rma.rb', line 1964

def generate_return_instructions_pdf
  uploads.in_category('rma_return_instructions_pdf').destroy_all
  pdf_result = Pdf::Document::ReturnInstructions.call(self, output_to_file: true)
  upload = Upload.uploadify(pdf_result.pdf_file_path,
                            'rma_return_instructions_pdf',
                            self,
                            pdf_result.file_name)
  uploads << upload

  # Update draft communications with new return instructions
  update_draft_communications_with_new_attachments

  upload
end

#generate_return_labelsObject

Generate return labels.

Returns:

  • (Object)


2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
# File 'app/models/rma.rb', line 2010

def generate_return_labels
  status = :error
  errs = []
  if return_delivery&.valid_for_generating_ship_labels?
    label_result = nil
    begin
      label_result = return_delivery.generate_labels
    rescue StandardError => e
      ErrorReporting.error(e,
                    "Could not generate return labels, RMA ID: #{id}, RMA number: #{rma_number}, return_delivery ID: #{return_delivery.id}")
      status = :error
      errs << e.to_s
    end
    # Surface carrier-level rejections (e.g. PO Box guards) — Delivery#generate_labels
    # writes them to shipping_api_log but otherwise they're hidden behind the
    # generic "N labels requested but M labels generated" count message.
    errs << label_result[:status_message] if label_result.is_a?(Hash) && label_result[:status_code] == :error && label_result[:status_message].present?
    status = :ok
    unless return_labels_correctly_generated?
      status = :error
      errs += return_label_issues
    end
  end

  # Update draft communications with new labels if generation was successful
  if status == :ok
    awaiting_return
    update_draft_communications_with_new_attachments
  end

  { status: status, errors: errs.uniq }
end

#handle_return_shipment_tracking_state_updated(return_shipment) ⇒ Object

Handle return shipment tracking state updated.

Parameters:

  • return_shipment (Object)

Returns:

  • (Object)


833
834
835
836
837
838
839
840
# File 'app/models/rma.rb', line 833

def handle_return_shipment_tracking_state_updated(return_shipment)
  # method is called when return_shipment tracking_state transitions to one of:
  # state :in_transit
  # state :delivered
  # state :exception
  # state :delivery_attempt
  # state :delivered_to_collection_location
end

#has_auto_return_items?Object

Whether has auto return items.

Returns:

  • (Object)


869
870
871
# File 'app/models/rma.rb', line 869

def has_auto_return_items?
  rma_items.where(will_not_be_returned: true, state: 'requested').any?
end

#has_different_replacement_items?Object

Whether has different replacement items.

Returns:

  • (Object)


1711
1712
1713
# File 'app/models/rma.rb', line 1711

def has_different_replacement_items?
  !has_same_replacement_items?
end

#has_no_active_items?Boolean

+true+ when there are no non-voided lines (+RmaItem.active+ / +where.not(state: 'voided')+).

Returns:

  • (Boolean)


881
882
883
# File 'app/models/rma.rb', line 881

def has_no_active_items?
  rma_items.active.none?
end

#has_replacement_items?Object

Whether has replacement items.

Returns:

  • (Object)


1541
1542
1543
# File 'app/models/rma.rb', line 1541

def has_replacement_items?
  rma_items.reload.active.any?(&:replacement_required?)
end

#has_rma_items_requested?Object

Whether has rma items requested.

Returns:

  • (Object)


876
877
878
# File 'app/models/rma.rb', line 876

def has_rma_items_requested?
  rma_items.where(state: 'requested').any?
end

#has_same_replacement_items?Object

Whether has same replacement items.

Returns:

  • (Object)


1701
1702
1703
1704
1705
1706
# File 'app/models/rma.rb', line 1701

def has_same_replacement_items?
  # check that all replacement items are linked to the rma_item and match quantity
  rma_items.replacement_required.all? do |ri|
    ri.replacement_order_line_item.present? && ri.returned_item_quantity == ri.replacement_order_line_item.quantity
  end
end

#inherit_ci_invoice_line_values(order) ⇒ Object

For CI invoices (e.g. Amazon Seller Central), the marketplace sets the
per-unit price, promotional discount, and tax on the original invoice
line. Inherit those exactly on the credit order instead of recalculating
via current catalog pricing or tax rates:

  • create_credit_order seeds the correct values, but the Order autosave
    chain (reset_discount_on_auto_coupon_togglereset_discount)
    rewrites price to catalog_item.amount, and calculate_discounts
    then resets discounted_price to the same.
  • Tax on a credit must also carry the sign of the negative credit
    quantity so grand totals and taxes_grouped_by_type are negative.
  • taxable_amount is inherited HERE rather than seeded on the new line,
    because it is only ever consistent with the restored discounted_price.
    Seeding it up front left it stranded above the catalog amount the resets
    write in the meantime, and taxable_amount_not_greater_than_discounted_price
    then aborted the whole receive for any item Amazon sold above catalog.

Parameters:

  • order (Object)


2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
# File 'app/models/rma.rb', line 2381

def inherit_ci_invoice_line_values(order)
  order.reload
  order.line_items.non_shipping.includes(credit_rma_item: :returned_line_item).each do |li|
    source_line = li.credit_rma_item&.returned_line_item
    next unless source_line

    credit_pct = li.credit_rma_item.credit_percentage.to_f / 100
    source_qty = source_line.quantity.to_d
    per_unit_tax = source_qty.zero? ? 0 : source_line.tax_total.to_d / source_qty
    inherited_tax = (per_unit_tax * li.quantity.to_d * credit_pct).round(2)
    source_taxable = source_line.taxable_amount
    li.update_columns(
      price: (source_line.price.to_d * credit_pct).round(2),
      discounted_price: (source_line.discounted_price.to_d * credit_pct).round(2),
      taxable_amount: source_taxable.present? ? (source_taxable.to_d * credit_pct).round(2) : nil,
      tax_total: inherited_tax
    )
  end
end

#instructions_file_name(with_extension = true) ⇒ Object

Instructions file name.

Parameters:

  • with_extension (Object) (defaults to: true)

Returns:

  • (Object)


1526
1527
1528
# File 'app/models/rma.rb', line 1526

def instructions_file_name(with_extension = true)
  "#{rma_number}_return_instructions#{'.pdf' if with_extension}"
end

#invoiceInvoice?

The associated invoice.

Returns:



149
# File 'app/models/rma.rb', line 149

has_one  :invoice, class_name: 'Invoice', foreign_key: :id, primary_key: :original_invoice_id

#invoice_add_blocked_for_kit_component_line?(component_line_item) ⇒ Boolean

add-items UI: true if this kit component line cannot be added (the parent kit line is already on the RMA).

Parameters:

  • component_line_item (Object)

Returns:

  • (Boolean)


2598
2599
2600
2601
2602
2603
# File 'app/models/rma.rb', line 2598

def invoice_add_blocked_for_kit_component_line?(component_line_item)
  pid = component_line_item.parent_id
  return false if pid.blank?

  selected_returned_line_item_ids_for_invoice_kit_rules.include?(pid)
end

#invoice_add_blocked_for_kit_parent_line?(parent_line_item) ⇒ Boolean

add-items UI: true if this parent kit invoice line cannot be added (a component line is already on the RMA).
Callers should preload +parent_line_item.children+ (e.g. +includes(children: :item)+) so child ids are read from memory.

Parameters:

  • parent_line_item (Object)

Returns:

  • (Boolean)


2587
2588
2589
2590
2591
2592
2593
2594
# File 'app/models/rma.rb', line 2587

def invoice_add_blocked_for_kit_parent_line?(parent_line_item)
  children = parent_line_item.children
  return false if children.blank?

  child_ids = children.loaded? ? children.map(&:id) : children.ids
  selected_line_ids = selected_returned_line_item_ids_for_invoice_kit_rules
  child_ids.any? { |cid| selected_line_ids.include?(cid) }
end

#invoice_line_already_on_return_list?(line_item) ⇒ Boolean

add-items UI: true if this invoice line already has a return row (each line at most once).

Parameters:

  • line_item (Object)

Returns:

  • (Boolean)


2607
2608
2609
2610
# File 'app/models/rma.rb', line 2607

def invoice_line_already_on_return_list?(line_item)
  line_item.id.present? &&
    selected_returned_line_item_ids_for_invoice_kit_rules.include?(line_item.id)
end

#items_partially_returned?Object

Whether items partially returned.

Returns:

  • (Object)


1673
1674
1675
# File 'app/models/rma.rb', line 1673

def items_partially_returned?
  rma_items.reload.active.any?(&:returned?) && rma_items.reload.active.any?(&:requested?)
end

#labels_file_name(with_extension = true) ⇒ Object

Labels file name.

Parameters:

  • with_extension (Object) (defaults to: true)

Returns:

  • (Object)


1534
1535
1536
# File 'app/models/rma.rb', line 1534

def labels_file_name(with_extension = true)
  "#{rma_number}_return_labels#{'.pdf' if with_extension}"
end

#legacy_return_shipmentsObject

Legacy return shipments.

Returns:

  • (Object)


1433
1434
1435
# File 'app/models/rma.rb', line 1433

def legacy_return_shipments
  Shipment.label_complete.joins(:order).merge(credit_orders).order(:id)
end

#next_step_after_items_createdObject

Next step after items created.

Returns:

  • (Object)


2560
2561
2562
2563
2564
2565
2566
# File 'app/models/rma.rb', line 2560

def next_step_after_items_created
  if return_label_required?
    'return_label_options'
  else
    'show'
  end
end

#no_items_returned?Object

Whether no items returned.

Returns:

  • (Object)


1078
1079
1080
# File 'app/models/rma.rb', line 1078

def no_items_returned?
  rma_items.none?(&:returned?)
end

#ordersActiveRecord::Associations::CollectionProxy

The associated orders.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


173
# File 'app/models/rma.rb', line 173

has_many :orders, inverse_of: :rma

#original_invoiceInvoice?

The associated original invoice.

Returns:



118
# File 'app/models/rma.rb', line 118

belongs_to :original_invoice, class_name: 'Invoice', optional: true

#original_invoice_selectObject

Original invoice select.

Returns:

  • (Object)


906
907
908
909
910
# File 'app/models/rma.rb', line 906

def original_invoice_select
  return [] unless original_invoice

  [[original_invoice.selection_name_for_rmas, original_invoice.id]]
end

#original_orderOrder?

The associated original order.

Returns:



115
# File 'app/models/rma.rb', line 115

belongs_to :original_order, class_name: 'Order', optional: true

#original_order_refObject

Original order ref.

Returns:

  • (Object)


1172
1173
1174
# File 'app/models/rma.rb', line 1172

def original_order_ref
  original_order.try(:reference_number)
end

#original_order_ref=(ref) ⇒ Object

Original order ref.

Parameters:

  • ref (Object)

Returns:

  • (Object)


1180
1181
1182
# File 'app/models/rma.rb', line 1180

def original_order_ref=(ref)
  self.original_order = Order.find_by(reference_number: ref) if ref.present?
end

#original_order_selectObject

Original order select.

Returns:

  • (Object)


897
898
899
900
901
# File 'app/models/rma.rb', line 897

def original_order_select
  return [] unless original_order

  [[original_order.selection_name, original_order.id]]
end

#paperless_return?Boolean

True when this return has USPS Label Broker paperless QR codes (one or more
return shipments carry a paperless_qr_png).

Returns:

  • (Boolean)


1460
1461
1462
# File 'app/models/rma.rb', line 1460

def paperless_return?
  return_shipments.any? { |s| s.paperless_qr_png.present? }
end

#paperless_return_htmlObject

HTML block embedded in the customer return email via the single
{{ rma.paperless_return_html }} tag in the RMA email template. Returns an
empty string for non-paperless returns. Each box's QR is referenced by
Content-ID (cid:rma-qr-<upload id>); CommunicationMailer attaches the
matching paperless_qr_png uploads inline so Gmail/Outlook render them in
the body. Inline styles + table layout only (email-client safe).



1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
# File 'app/models/rma.rb', line 1470

def paperless_return_html
  qr_shipments = return_shipments.select { |s| s.paperless_qr_png.present? }
  return '' if qr_shipments.empty?

  cells = qr_shipments.each_with_index.map do |shipment, i|
    cid = "rma-qr-#{shipment.paperless_qr_png.id}"
    <<~CELL
      <td style="padding:8px;text-align:center;vertical-align:top;">
        <div style="font-weight:bold;font-size:13px;margin-bottom:4px;">Box #{i + 1}</div>
        <img src="cid:#{cid}" alt="USPS Label Broker QR code for Box #{i + 1}" width="150" style="display:block;margin:0 auto;border:0;" />
      </td>
    CELL
  end
  grid = cells.each_slice(5).map { |row| "<tr>#{row.join}</tr>" }.join

  <<~HTML.html_safe
    <div style="margin:16px 0;font-family:Arial,Helvetica,sans-serif;font-size:14px;color:#222;">
      <h3 style="margin:0 0 8px;">USPS Label Broker &mdash; Paperless QR return</h3>
      <p style="margin:0 0 8px;">No printer needed. From your smartphone, show each QR code below at the USPS counter and the clerk will scan it and print the shipping label.</p>
      <p style="margin:0 0 8px;"><strong>Write your RMA number (#{ERB::Util.html_escape(rma_number)}) and the box number on the matching box</strong> before drop-off &mdash; it is not encoded in the QR code, and we need it to match your returned goods to this RMA.</p>
      <p style="margin:0 0 12px;">Find a Post Office that offers Label Broker at the <a href="#{PAPERLESS_LABEL_BROKER_RETAIL_URL}">counter</a> or a <a href="#{PAPERLESS_LABEL_BROKER_KIOSK_URL}">self-service kiosk</a>.</p>
      <table role="presentation" cellpadding="0" cellspacing="0" border="0"><tbody>#{grid}</tbody></table>
    </div>
  HTML
end

#payment_statusObject

Payment status.

Returns:

  • (Object)


915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
# File 'app/models/rma.rb', line 915

def payment_status
  return nil unless %w[returned credit_in_process credited_partially_refunded credited_fully_refunded].include?(state)

  statuses = []
  credit_orders.each do |co|
    cm = co.try(:credit_memo)
    statuses << if co.pending_review? || co.ready_for_printing?
                  { status: 'Under Review', color: 'amber', ref: co.reference_number }
                elsif cm
                  cm.payment_status
                else
                  { status: 'Unknown', color: 'red', ref: co.reference_number }
                end
  end
  statuses
end

#paymentsActiveRecord::Associations::CollectionProxy

The associated payments.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


161
# File 'app/models/rma.rb', line 161

has_many :payments

#pending_ordersActiveRecord::Associations::CollectionProxy

The associated pending orders.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


176
# File 'app/models/rma.rb', line 176

has_many :pending_orders, -> { where(state: PENDING_ORDERS_STATES) }, class_name: 'Order'

#possible_eventsObject

Possible events.

Returns:

  • (Object)


636
637
638
# File 'app/models/rma.rb', line 636

def possible_events
  state_transitions.map(&:event).sort
end

#possible_events_for_selectObject

Possible events for select.

Returns:

  • (Object)


643
644
645
# File 'app/models/rma.rb', line 643

def possible_events_for_select
  possible_events.map { |evt| [evt.to_s.titleize, evt] }
end

#post_communication_sent_hookObject

Post communication sent hook.

Returns:

  • (Object)


1373
1374
1375
1376
# File 'app/models/rma.rb', line 1373

def post_communication_sent_hook
  transmit if awaiting_transmission?
  true
end

#precreate_from_deliveryDelivery?

The associated precreate from delivery.

Returns:



133
# File 'app/models/rma.rb', line 133

belongs_to :precreate_from_delivery, class_name: 'Delivery', optional: true

#primary_partyObject

Primary party.

Returns:

  • (Object)


2580
2581
2582
# File 'app/models/rma.rb', line 2580

def primary_party
  customer
end

#quoteQuote?

The associated quote.

Returns:



194
# File 'app/models/rma.rb', line 194

has_one :quote

#receive_item_events_for_selectObject

Receive item events for select.

Returns:

  • (Object)


650
651
652
653
654
655
656
# File 'app/models/rma.rb', line 650

def receive_item_events_for_select
  res = []
  res << ["Keep current status: #{human_state_name.titleize}", '']
  res << ['Inspect Return', 'inspect_return'] if (awaiting_return? || partially_returned?) && can_inspect_return?
  res << ['Complete Return', 'returned'] if awaiting_return? || partially_returned?
  res
end

#record_canada_post_return_inquiry(additional_info: nil, rsa_number_issue_date: nil) ⇒ void

This method returns an undefined value.

Records the external reference Canada Post forwards (the customer-entered
order/reference number — our SO number, an Amazon or Home Depot order
number, etc. — plus the RA issue date) as a note on this RMA, so staff can
cross-reference the Canada Post return to the originating retailer order.
No-op when there's nothing to record or the same reference was already noted
(Canada Post pings the endpoint repeatedly). Never raises: a note-write
failure must not fail the validation response.

Parameters:

  • additional_info (String, nil) (defaults to: nil)

    the policy's "additional information"
    value (the customer's order/reference number).

  • rsa_number_issue_date (String, nil) (defaults to: nil)

    date the RA number was issued.



247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
# File 'app/models/rma.rb', line 247

def record_canada_post_return_inquiry(additional_info: nil, rsa_number_issue_date: nil)
  additional_info = additional_info.to_s.strip.presence
  rsa_number_issue_date = rsa_number_issue_date.to_s.strip.presence
  return if additional_info.nil? && rsa_number_issue_date.nil?

  note = +'Canada Post return label requested via Manage Returns.'
  note << " Customer-provided reference: #{additional_info}." if additional_info
  note << " RA issue date: #{rsa_number_issue_date}." if rsa_number_issue_date

  return if activities.exists?(notes: note)

  activities.create(notes: note)
rescue StandardError => e
  Rails.logger.warn("[CanadaPostRA] failed to note inquiry on RMA #{id}: #{e.class}: #{e.message}")
  nil
end

#record_creation_activityvoid

This method returns an undefined value.

Records a creation note on this RMA so every RMA carries a
searchable baseline of who created it, what it links to, and the stated
reason — the context warehouse staff otherwise have to dig out of emails,
the original order, or the retailer portal. Lands on the RMA's activities
tab and, via +primary_party+, in the customer's activity feed. Invoked
async by CreationNoteHandler on Events::RmaCreated (published
after_create_commit), so a note-write failure can never break RMA
creation. Never raises.



274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
# File 'app/models/rma.rb', line 274

def record_creation_activity
  lines = ["RMA #{rma_number} created by #{creator&.full_name.presence || 'system'}."]
  refs = []
  refs << "order #{original_order.reference_number}" if original_order
  refs << "invoice #{original_invoice.reference_number}" if original_invoice
  refs << "PO #{original_po_number}" if original_po_number.present?
  refs << "customer ref #{customer_reference}" if customer_reference.present?
  lines << "Original #{refs.join(' / ')}." if refs.any?
  grouping = customer && Customer::ReportGrouping.get_report_grouping_for_customer(customer)
  lines << "Etailer channel: #{grouping}." if Customer::ReportGrouping::ETAILER_GROUPINGS.include?(grouping)
  lines << "Reason given: #{description}" if description.present?
  lines << "Support case: #{support_case.case_number}." if support_case

  note = lines.join("\n")

  # Deduped + locked: replayed or concurrent async deliveries of
  # Events::RmaCreated must not stack duplicate feed entries.
  with_lock do
    return if activities.exists?(notes: note)

    activities.create!(notes: note)
  end
rescue StandardError => e
  Rails.logger.warn("[Rma] failed to record creation note on RMA #{id}: #{e.class}: #{e.message}")
  nil
end

#record_items_added_activity(items, added_ids: nil) ⇒ void

This method returns an undefined value.

Records a note listing the RMA's current top-level items with their return
reason codes (expanded to the reason-code description), so the activities
tab answers "what is coming back and why" without opening the items tab.
Invoked async by ItemsAddedNoteHandler on Events::RmaItemsAdded
(published by create_items / bulk_add_items / create_rma_from_delivery).
No-op when the identical note was already recorded (re-saves with
unchanged items). Never raises.

Parameters:

  • items (Enumerable<RmaItem>)

    the items to list; kit component
    children are filtered out.

  • added_ids (Array<Integer>, nil) (defaults to: nil)

    ids of the lines this event actually
    added. Given them, the note separates what was just added from what was
    already on the RMA — without them (legacy events) it lists everything
    under one heading, as it always did.



316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
# File 'app/models/rma.rb', line 316

def record_items_added_activity(items, added_ids: nil)
  top_level = items.reject(&:child?)
  return if top_level.empty?

  added_ids = Array(added_ids).map(&:to_i)
  added, existing = added_ids.any? ? top_level.partition { |ri| added_ids.include?(ri.id) } : [top_level, []]
  return if added.empty?

  codes_by_code = RmaReasonCode.where(code: top_level.map(&:returned_reason)).index_by(&:code)
  describe = lambda do |ri|
    reason = ri.returned_reason.presence || ''
    reason += " (#{codes_by_code[ri.returned_reason].description})" if codes_by_code[ri.returned_reason]&.description.present?
    "- #{ri.returned_item&.sku || ''} x #{ri.returned_item_quantity}#{reason}"
  end

  heading = added_ids.any? ? "Items added to RMA #{rma_number}:" : "Items on RMA #{rma_number}:"
  parts = [heading] + added.map(&describe)
  parts += ['Already on the RMA:'] + existing.map(&describe) if existing.any?
  note = parts.join("\n")

  # Deduped + locked: replayed or concurrent async deliveries of
  # Events::RmaItemsAdded must not stack duplicate feed entries.
  with_lock do
    return if activities.exists?(notes: note)

    activities.create!(notes: note)
  end
rescue StandardError => e
  Rails.logger.warn("[Rma] failed to record items note on RMA #{id}: #{e.class}: #{e.message}")
  nil
end

#redesign_quoteQuote?

The associated redesign quote.

Returns:



130
# File 'app/models/rma.rb', line 130

belongs_to :redesign_quote, class_name: 'Quote', optional: true

#regenerate_return_documents!Object

Regenerates the customer-facing return documents WITHOUT touching the
purchased carrier labels (no void, no new label buy): each letter-format
label PDF is rebuilt from the carrier label PDF already on file, then the
return-instructions PDF is re-rendered from them. Exists so layout fixes
in the PDF generators (e.g. native-size 4x6 label pages) can be applied to
RMAs whose documents predate the fix.



1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
# File 'app/models/rma.rb', line 1985

def regenerate_return_documents!
  # Only the PDF-merge carriers need a rebuild — UPS/FedEx/USPS embed the
  # rotated PNG at native size already, and rebuilding those without the
  # PNG temp file would drop the label image entirely.
  rebuildable = return_shipments.select do |shipment|
    shipment.carrier.to_s.downcase.match?(/speedeedelivery|canadapost|canpar/)
  end
  # Preflight before deleting or regenerating anything: a merge-carrier
  # shipment without its source label PDF would lose its letter-format
  # label, so fail the whole run and surface the error instead.
  missing = rebuildable.find { |shipment| shipment.ship_label_pdf&.attachment&.path.blank? }
  raise "Cannot regenerate return documents: shipment #{missing.id} has no carrier label PDF on file" if missing

  rebuildable.each do |shipment|
    # Replace (not append to) the old letter-format labels so
    # `ship_label_pdf(true)` / `return_labels` pick up the rebuilt one.
    shipment.uploads.in_category('letter_ship_label_pdf').destroy_all
    shipment.generate_letter_ship_label_pdf(nil, shipment.carrier.to_s)
  end
  generate_return_instructions_pdf
end

#rejected_rma_itemsObject

Items (including kit components, which are rows in the same table) that
were flagged as non-resalable on inspection or were never returned.
Surfaced in the RMAITEMREJECTED notification email body.



1739
1740
1741
# File 'app/models/rma.rb', line 1739

def rejected_rma_items
  rma_items.rejected.reload
end

#restore_seeded_credit_prices(order, seeded_credit_prices) ⇒ void

This method returns an undefined value.

Re-applies the invoice-derived prices captured in +create_credit_order+ before
the +disable_auto_coupon+ toggle's +reset_discount+ re-priced every parent line
to its catalog amount. The non-CI counterpart to +inherit_ci_invoice_line_values+.
Keyed by [credit_rma_item_id, catalog_item_id] (unique per credit line, including
each kit-component line). Uses +update_columns+ so it neither re-triggers the
reset nor recalculates discounts; tax is recomputed by the caller afterward.

Parameters:

  • order (Order)

    the persisted credit order

  • seeded_credit_prices (Hash)

    maps each credit line's
    [rma_item_id, catalog_item_id] pair to its captured
    { price:, discounted_price: } invoice-derived values



2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
# File 'app/models/rma.rb', line 2442

def restore_seeded_credit_prices(order, seeded_credit_prices)
  return if seeded_credit_prices.blank?

  order.reload
  order.line_items.non_shipping.where.not(credit_rma_item_id: nil).find_each do |li|
    seeded = seeded_credit_prices[[li.credit_rma_item_id, li.catalog_item_id]]
    next unless seeded

    li.update_columns(price: seeded[:price], discounted_price: seeded[:discounted_price])
  end
end

#return_addressObject

Return address.

Returns:

  • (Object)


1409
1410
1411
# File 'app/models/rma.rb', line 1409

def return_address
  return_shipping_address.full_address(true, "\n")
end

#return_deliveryDelivery?

The associated return delivery.

Returns:



136
# File 'app/models/rma.rb', line 136

belongs_to :return_delivery, class_name: 'Delivery', optional: true

#return_instructionsObject

Return instructions.

Returns:

  • (Object)


1454
1455
1456
# File 'app/models/rma.rb', line 1454

def return_instructions
  uploads.in_category('rma_return_instructions_pdf').order(created_at: :desc).first
end

#return_label_issuesObject

Return label issues.

Returns:

  • (Object)


2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
# File 'app/models/rma.rb', line 2046

def return_label_issues
  issues = []
  return issues unless requested? || awaiting_return?

  if return_label_required?
    if number_of_return_labels_required.to_i >= 0
      num_labels_requested = number_of_return_labels_required.to_i
    elsif number_of_return_labels_required.to_i == -1
      num_labels_requested = return_delivery&.shipments&.size || 1
    end
    num_labels_generated = return_delivery&.shipments&.label_complete&.size
    if return_delivery&.return_labels_complete? || return_delivery&.valid_for_generating_ship_labels?
      issues << "#{num_labels_requested} labels requested but #{num_labels_generated} labels generated." unless num_labels_requested.positive? && num_labels_requested == num_labels_generated
    else
      issues << (return_delivery&.errors&.full_messages&.join('. ').presence || 'Return delivery could not be generated!')
    end
  end
  issues.compact
end

#return_label_required?Object Also known as: return_label_required

Whether return label required.

Returns:

  • (Object)


1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
# File 'app/models/rma.rb', line 1092

def return_label_required?
  return false if all_items_voided? # this is to prevent this validation from blocking RMAs from being voided
  # Nothing is coming back, so there is nothing to put a label on — unless one
  # was already bought. That label stays live and customer-usable, so the RMA
  # must keep reporting and validating it rather than silently disowning it.
  return false if all_items_will_not_be_returned? && !return_labels_generated?
  return false if canada_post_return? # CP returns use the hosted QR policy; we never generate a label for them

  number_of_return_labels_required.to_i.positive? || (number_of_return_labels_required == -1) # here, -1 means let Heatwave decide
end

#return_labelsObject

Return labels.

Returns:

  • (Object)


1447
1448
1449
# File 'app/models/rma.rb', line 1447

def return_labels
  return_shipments.filter_map { |s| s.ship_label_pdf(true) }
end

#return_labels_correctly_generated?Object

Whether return labels correctly generated.

Returns:

  • (Object)


2069
2070
2071
2072
2073
# File 'app/models/rma.rb', line 2069

def return_labels_correctly_generated?
  return true if skip_return_label_validation

  return_label_issues.empty?
end

#return_labels_generated?Boolean

True once at least one return label has actually been purchased.

Returns:

  • (Boolean)


1212
1213
1214
# File 'app/models/rma.rb', line 1212

def return_labels_generated?
  return_delivery&.shipments&.label_complete&.any? || false
end

#return_shipmentsActiveRecord::Associations::CollectionProxy

The associated return shipments.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


152
# File 'app/models/rma.rb', line 152

has_many :return_shipments, -> { label_complete }, source: :shipments, class_name: 'Shipment', through: :return_delivery

#return_shipping_addressAddress?

The associated return shipping address.

Returns:



124
# File 'app/models/rma.rb', line 124

belongs_to :return_shipping_address, class_name: 'Address', optional: true

#return_shipping_methodObject

Return shipping method.

Returns:

  • (Object)


1127
1128
1129
# File 'app/models/rma.rb', line 1127

def return_shipping_method
  RETURN_SHIPPING_METHODS[customer.store.id.to_s]
end

#returned_rma_itemsObject

Returned rma items.

Returns:

  • (Object)


1725
1726
1727
# File 'app/models/rma.rb', line 1725

def returned_rma_items
  rma_items.returned.reload
end

#rma_inspect_emailObject

Alias for Company#rma_inspect_email

Returns:

  • (Object)

    Company#rma_inspect_email

See Also:



391
# File 'app/models/rma.rb', line 391

delegate :rma_inspect_email, to: :company

#rma_itemsActiveRecord::Associations::CollectionProxy

The associated rma items.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


155
# File 'app/models/rma.rb', line 155

has_many :rma_items, inverse_of: :rma, dependent: :destroy, before_add: :set_rma_items_defaults

#rma_items_awaiting_inspectionObject

Rma items awaiting inspection.

Returns:

  • (Object)


1732
1733
1734
# File 'app/models/rma.rb', line 1732

def rma_items_awaiting_inspection
  rma_items.awaiting_inspection.reload
end

#rma_suggested_quotes_optionsObject

Rma suggested quotes options.

Returns:

  • (Object)


852
853
854
855
856
857
858
859
860
861
862
863
864
# File 'app/models/rma.rb', line 852

def rma_suggested_quotes_options
  quotes = []
  if customer
    quotes += customer.quotes.order('quotes.reference_number desc').map do |q|
      [q.selection_name, q.id]
    end
  end
  if redesign_quote
    new_selection = [redesign_quote.selection_name, redesign_quote.id]
    quotes |= [new_selection]
  end
  quotes
end

#send_fromEmployee?

The associated send from.

Returns:



142
# File 'app/models/rma.rb', line 142

belongs_to :send_from, class_name: 'Employee', optional: true

#send_inspection_notification_to_customerObject

Send inspection notification to customer.

Returns:

  • (Object)


1330
1331
1332
1333
# File 'app/models/rma.rb', line 1330

def send_inspection_notification_to_customer
  CommunicationBuilder.new(resource: self,
                           sender_party: send_from || customer.primary_sales_rep, template_system_code: 'RMAINSPECT').create
end

#send_rejection_notification_to_customerObject

Send rejection notification to customer.

Returns:

  • (Object)


1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
# File 'app/models/rma.rb', line 1352

def send_rejection_notification_to_customer
  # Proof photos live in two categories: `photo` for non-resalable items,
  # `not_returned_photo` for items that never came back. Attach the ones
  # that match each item's flags.
  rejected_uploads = rma_items.rejected.includes(:uploads).flat_map do |ri|
    ri.uploads.select do |upload|
      (ri.non_resalable? && upload.category == 'photo') ||
        (ri.not_returned? && upload.category == 'not_returned_photo')
    end
  end
  CommunicationBuilder.new(
    resource: self,
    sender_party: send_from || customer.primary_sales_rep,
    template_system_code: 'RMAITEMREJECTED',
    uploads: rejected_uploads
  ).create
end

#send_return_confirmation_email_to_customerObject

Send return confirmation email to customer.

Returns:

  • (Object)


1338
1339
1340
1341
# File 'app/models/rma.rb', line 1338

def send_return_confirmation_email_to_customer
  CommunicationBuilder.new(resource: self,
                           sender_party: send_from || customer.primary_sales_rep, template_system_code: 'RMARC').create
end

#send_return_email_to_customer(keep_as_draft: false) ⇒ Object

Send return email to customer.

Parameters:

  • keep_as_draft (Object) (defaults to: false)

Returns:

  • (Object)


1322
1323
1324
1325
# File 'app/models/rma.rb', line 1322

def send_return_email_to_customer(keep_as_draft: false)
  CommunicationBuilder.new(resource: self,
                           sender_party: send_from || customer.primary_sales_rep, template_system_code: 'RMA').create(keep_as_draft: keep_as_draft)
end

#serial_numbersActiveRecord::Associations::CollectionProxy

The associated serial numbers.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


179
# File 'app/models/rma.rb', line 179

has_many :serial_numbers, through: :rma_items

#service_only?Object

Whether service only.

Returns:

  • (Object)


1402
1403
1404
# File 'app/models/rma.rb', line 1402

def service_only?
  rma_items.all? { |rma_item| rma_item.returned_item.tax_class == 'svc' }
end

#set_default_notification_channelsObject

Set default notification channels.

Returns:

  • (Object)


1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
# File 'app/models/rma.rb', line 1593

def set_default_notification_channels
  return nil if customer.nil?

  notification_channels = []
  notification_channels.concat(customer.notification_channels.rmas.map(&:contact_point))
  notification_channels.concat(customer.billing_entity.notification_channels.rmas.map(&:contact_point))
  self.transmission_email ||= []
  self.transmission_fax ||= []
  notification_channels.uniq.each do |cp|
    next if ((cp.category == 'email') && transmission_email.include?(cp.detail)) || ((cp.category == 'fax') && transmission_fax.include?(cp.detail))

    case cp.category
    when 'email'
      transmission_email << cp.detail
    when 'fax'
      transmission_fax << cp.detail
    end
  end
end

#ship_from_addressAddress?

The associated ship from address.

Returns:



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

belongs_to :ship_from_address, class_name: 'Address', optional: true

#ship_from_attributesObject

Ship from attributes.

Returns:

  • (Object)


2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
# File 'app/models/rma.rb', line 2530

def ship_from_attributes
  res = {}
  # this is an RMA return label
  # address is original order address
  res[:address] = ship_from_address
  # attention_name cannot be blank!!!!
  res[:attention_name] = res[:address].person_name
  res[:attention_name] = 'Customer' if res[:attention_name].blank?
  if res[:address].company_name
    res[:name] = res[:address].company_name
    res[:name] = 'Customer' if res[:name].blank?
  else
    res[:name] = res[:address].person_name if res[:name].blank?
    res[:name] = 'Customer' if res[:name].blank?
  end
  res[:phone] =
    (original_order&.shipping_phone || customer.phone || customer.cell_phone || customer.sales_reps.first&.direct_phone || SHIPPING_SHIPPER_CONFIGURATION[customer.country.iso3.to_sym][:shipper_phone])
  res[:email] = transmission_email
  if res[:email].to_s.strip.blank?
    res[:email] =
      customer.primary_sales_rep&.email || SHIPPING_SHIPPER_CONFIGURATION[customer.country.iso3.to_sym][:shipper_email]
  end
  res[:phone] = res[:phone] if res[:phone]

  res
end

#ship_to_attributesObject

Ship to attributes.

Returns:

  • (Object)


2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
# File 'app/models/rma.rb', line 2507

def ship_to_attributes
  res = {}
  res[:address] = return_shipping_address
  if res[:address].company_name
    # attention_name cannot be blank!!!!
    res[:attention_name] = res[:address].person_name
    res[:attention_name] = 'Returns Department' if res[:attention_name].blank?
    res[:name] = res[:address].company_name
  else
    res[:attention_name] = attention_name
    res[:attention_name] = res[:address].person_name if res[:attention_name].blank?
    res[:name] = attention_name
    res[:name] = res[:address].person_name if res[:name].blank?
  end
  # sort of a kludge but for now use shipping configuration's sender phone for legacy matching
  res[:phone] = SHIPPING_SHIPPER_CONFIGURATION[customer.country.iso3.to_sym][:shipper_phone]
  res[:email] = SHIPPING_SHIPPER_CONFIGURATION[customer.country.iso3.to_sym][:shipper_email]
  res
end

#shipment_tracking_numbersArray<ShipmentTrackingNumber>

Combines generated-label and externally supplied inbound tracking numbers.

Returns:



784
785
786
787
788
789
# File 'app/models/rma.rb', line 784

def shipment_tracking_numbers
  stn = []
  stn += return_shipments.flat_map(&:shipment_tracking_number)
  stn += tracking_numbers.map { |tn_raw| ShipmentTrackingNumber.new(tn_raw) }
  stn.uniq
end

#shipping_optionShippingOption?

The associated shipping option.

Returns:



139
# File 'app/models/rma.rb', line 139

belongs_to :shipping_option, optional: true

#should_validate_shipping_option?Object

Whether should validate shipping option.

Returns:

  • (Object)


2571
2572
2573
2574
2575
# File 'app/models/rma.rb', line 2571

def should_validate_shipping_option?
  return false if skip_shipping_option_validation

  number_of_return_labels_required.present? && !number_of_return_labels_required.zero?
end

#split_line_discount_by_coupon(discount_total, source_discounts) ⇒ Hash{LineDiscount => BigDecimal}

Attributes a credit line's total discount back to the source invoice line's coupons,
split in proportion to each coupon's amount so the credit mirrors the invoice's
coupon breakdown. Returns positive amounts (the invoice's discounts are negative).

Parameters:

  • discount_total (BigDecimal)

    the line's total discount (list minus net)

  • source_discounts (Enumerable<LineDiscount>)

    the source invoice line's discounts

Returns:

  • (Hash{LineDiscount => BigDecimal})

    credit discount amount per source coupon



2408
2409
2410
2411
2412
2413
# File 'app/models/rma.rb', line 2408

def split_line_discount_by_coupon(discount_total, source_discounts)
  total_source = source_discounts.sum { |ld| ld.amount.to_d }
  return {} unless total_source.nonzero?

  source_discounts.index_with { |ld| (discount_total * (ld.amount.to_d / total_source)).round(2) }
end

#stores_for_return_selectObject

Stores for return select.

Returns:

  • (Object)


888
889
890
891
892
# File 'app/models/rma.rb', line 888

def stores_for_return_select
  return Address.none unless company_id

  Address.joins(:warehouse_store).where(stores: { company_id: company_id })
end

#suggested_contact_namesObject

Suggested contact names.

Returns:

  • (Object)


1299
1300
1301
1302
1303
1304
1305
1306
# File 'app/models/rma.rb', line 1299

def suggested_contact_names
  contact_names = []
  contact_names += customer.contacts.active.distinct.pluck(:full_name) if customer
  contact_names << original_order.contact.full_name if original_order&.contact&.full_name.present?
  contact_names << original_invoice.shipping_address.person_name if original_invoice&.shipping_address&.person_name.present?
  contact_names << contact_name if contact_name.present?
  contact_names.uniq.sort
end

#to_sObject Also known as: name, reference_number

To s.

Returns:

  • (Object)


1018
1019
1020
# File 'app/models/rma.rb', line 1018

def to_s
  rma_number
end

#unreturn_all_itemsObject

Unreturn all items.

Returns:

  • (Object)


1586
1587
1588
# File 'app/models/rma.rb', line 1586

def unreturn_all_items
  rma_items.where(state: 'returned').find_each(&:unreturn)
end

#unvoid_all_itemsObject

Unvoid all items.

Returns:

  • (Object)


1578
1579
1580
1581
# File 'app/models/rma.rb', line 1578

def unvoid_all_items
  rma_items.each(&:unvoid)
  return_shipments.update_all(tracking_state: 'not_yet_in_system') if return_shipments.present? && return_shipments.all?(&:tracking_ignore?)
end

#unvoid_all_items_and_selfObject

Unvoid all items and self.

Returns:

  • (Object)


1559
1560
1561
1562
1563
1564
1565
# File 'app/models/rma.rb', line 1559

def unvoid_all_items_and_self
  Rma.transaction do
    unvoid_all_items
    self.skip_reminders = false
    sync_state
  end
end

#update_draft_communications_with_new_attachmentsObject

Update draft communications with new attachments.

Returns:

  • (Object)


1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
# File 'app/models/rma.rb', line 1878

def update_draft_communications_with_new_attachments
  # Find draft communications for this RMA
  draft_communications = Communication.where(
    resource: self,
    state: 'draft'
  )

  return if draft_communications.empty?

  # Get the new attachments that should be included
  new_uploads = []
  if awaiting_return?
    new_uploads << return_instructions if return_instructions

    # Also include return labels if available
    return_labels_uploads = return_labels
    new_uploads += return_labels_uploads if return_labels_uploads.present?

    # Paperless QR codes: attach so CommunicationMailer can embed them inline
    # (CID) in the email body (see Rma#paperless_return_html).
    new_uploads += return_shipments.filter_map(&:paperless_qr_png)
  end

  return if new_uploads.empty?

  new_uploads = new_uploads.compact.uniq(&:id)

  # Update each draft communication
  draft_communications.each do |communication|
    communication.with_lock do
      # Remove old RMA-related attachments (return instructions, labels, QR codes)
      communication.uploads.where(
        category: %w[rma_return_instructions_pdf ship_label_pdf paperless_qr_png]
      ).each do |upload|
        communication.uploads.delete(upload)
      end

      # Add new attachments; duplicate rows can happen under concurrent submits.
      new_uploads.each do |upload|
        next if communication.uploads.exists?(id: upload.id)

        begin
          communication.uploads << upload
        rescue ActiveRecord::RecordNotUnique
          # Another request attached this upload first; join already exists.
          next
        end
      end

      # Refresh the paperless block between its comment markers — touching ONLY
      # that block, not the curated body (re-rendering the whole body would
      # regenerate the signature and trip unrelated content validations). Run
      # unconditionally: for a paperless return it injects the QR HTML (the body
      # was first rendered before labels existed, so it came out empty); for a
      # return that lost paperless support it injects an empty block, clearing
      # any stale QR/CID markup left from a prior generation.
      inject_paperless_block!(communication)

      communication.save!
    end
  end
end

#update_linked_credit_orders(allow_transition_failure: true) ⇒ Object

This pushes the credit order to the return state

Parameters:

  • allow_transition_failure (Object) (defaults to: true)


2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
# File 'app/models/rma.rb', line 2456

def update_linked_credit_orders(allow_transition_failure: true)
  Order.transaction do
    credit_orders.each do |credit_order|
      # If a shipping line already exists, don't touch
      unless credit_order.line_items.shipping_only.exists?
        # First we will copy over the shipping line from the original order
        shipping_lines = []
        credit_order.line_items.joins(credit_rma_item: :returned_line_item).find_each do |coli|
          shipping_lines += coli.credit_rma_item.returned_line_item.resource.line_items.shipping_only
        end
        shipping_lines.uniq!
        shipping_lines.each do |shipping_line|
          co_shipping_line = shipping_line.dup
          # It's always one for shipping
          co_shipping_line.quantity = co_shipping_line.qty_shipped = -1
          # Detaching to see if we can eliminate it
          co_shipping_line.shipping_cost_id = nil
          # co_shipping_line.price = -co_shipping_line.price
          # co_shipping_line.discounted_price = -co_shipping_line.discounted_price
          co_shipping_line.resource = credit_order
          co_shipping_line.delivery_id = nil # Let's see if this takes
          co_shipping_line.save!
          # Now copy the discounts from the original
          shipping_line.line_discounts.each do |ld|
            credit_order_discount = credit_order.discounts.find_by(coupon_id: ld.discount.coupon_id)
            unless credit_order_discount
              # If the discount doesn't already exist, we clone and create it
              credit_order_discount = ld.discount.dup
              credit_order_discount.itemizable = credit_order
              credit_order_discount.amount = credit_order_discount.user_amount = -credit_order_discount.amount
              credit_order_discount.save!
            end
            # Now line discount can be cloned
            co_shipping_line.line_discounts.create!(discount_id: credit_order_discount.id,
                                                    coupon_id: ld.coupon_id, amount: -ld.amount)
          end
        end
      end
      # Then mark the credit order returned
      if allow_transition_failure
        credit_order.returned
      else
        credit_order.returned!
      end
    end
  end
end

#uploadsActiveRecord::Associations::CollectionProxy

The associated uploads.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


164
# File 'app/models/rma.rb', line 164

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

#versions_for_audit_trail(_params = {}) ⇒ Object

Versions for audit trail.

Parameters:

  • _params (Object) (defaults to: {})

Returns:

  • (Object)


662
663
664
665
666
667
668
669
670
671
# File 'app/models/rma.rb', line 662

def versions_for_audit_trail(_params = {})
  query_sql = "
                (item_type = 'Rma' AND item_id = :id)
                OR (
                  item_type = 'RmaItem'
                    AND reference_data @> :rma_json
                )
              "
  RecordVersion.where(query_sql, id: id, rma_json: { rma_id: id }.to_json)
end

#versions_for_dates_trackerObject

Versions for dates tracker.

Returns:

  • (Object)


714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
# File 'app/models/rma.rb', line 714

def versions_for_dates_tracker
  shipment_id = return_shipments.first&.id
  shipment_sql = if shipment_id.present?
                   "select object_changes #>> '{" + '"tracking_state"' + ",1}' as state,timezone('America/Chicago', timestamptz(created_at))::date
                                         from versions
                                         where (object_changes->'tracking_state'->>1 in ('in_transit','delivered'))
                                         and item_type = 'Shipment'
                                         and item_id = #{shipment_id}
                                         union all"
                 else
                   ''
                 end
  query_sql = <<-SQL.squish
                #{shipment_sql}
                select object_changes #>> '{"state",1}' as state,timezone('America/Chicago', timestamptz(created_at))::date
                from versions
                where (object_changes->'state'->>1 in ('awaiting_inspection','returned','credit_in_process','credited_partially_refunded','credited_fully_refunded'))
                and item_type = 'Rma'
                and item_id = #{id}
  SQL
  # Audit trail is supplementary here — if the versions DB blips, render an empty
  # tracker rather than 500 the whole RMA tab (AppSignal #4929).
  results = RecordVersionBase.safe_read([]) do
    RecordVersion.lease_connection.execute(query_sql).to_a.map(&:symbolize_keys)
  end

  dates_tracker = if return_shipments.present?
                    { dropped_off: nil, warehouse_received: nil, submitted_for_inspection: nil, returned: nil, process_refund: nil,
                      refunded: nil, fully_refunded: nil }
                  else
                    { warehouse_received: nil, submitted_for_inspection: nil, returned: nil, process_refund: nil,
                      refunded: nil, fully_refunded: nil }
                  end
  results.each do |r|
    dates_tracker.each do |k, v|
      if r[:state] == k.to_s
        dates_tracker[k] = r[:timezone]
      elsif r[:state] == 'in_transit'
        dates_tracker[:dropped_off] = r[:timezone]
      elsif r[:state] == 'delivered'
        dates_tracker[:warehouse_received] = r[:timezone]
      elsif r[:state] == 'awaiting_inspection'
        dates_tracker[:submitted_for_inspection] = r[:timezone]
      elsif k.to_s == 'warehouse_received' && v.nil? && r[:state] == 'returned'
        dates_tracker[:warehouse_received] = r[:timezone]
      elsif k.to_s == 'submitted_for_inspection' && v.nil? && r[:state] == 'returned'
        dates_tracker[:submitted_for_inspection] = r[:timezone]
      elsif r[:state] == 'credit_in_process'
        dates_tracker[:process_refund] = r[:timezone]
      elsif r[:state] == 'credited_partially_refunded'
        dates_tracker[:refunded] = r[:timezone]
      elsif r[:state] == 'credited_fully_refunded'
        dates_tracker[:refunded] = r[:timezone]
        dates_tracker[:fully_refunded] = r[:timezone]
      end
    end
  end
  dates_tracker
end

#versions_for_state_trackerObject

Versions for state tracker.

Returns:

  • (Object)


676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
# File 'app/models/rma.rb', line 676

def versions_for_state_tracker
  where_sql = "object_changes ? 'state'"
  select_sql = %q{
                id,object_changes #>> '{"state",1}' as state,created_at,
                (lag(object_changes #>> '{"state",1}') OVER (ORDER BY id desc)) as new_state,
                (lag(created_at) OVER (ORDER BY id desc)) as new_state_created_at
              }
  # Supplementary on the RMA tab — degrade to an empty tracker (rather than 500 the page)
  # if the versions DB blips. `.to_a` forces the load inside the guard. Same surface as #4929.
  version_records = RecordVersionBase.safe_read([]) do
    versions_for_audit_trail.where(where_sql).select(select_sql).order(:id, :created_at).to_a
  end
  state_tracker = []
  version_records.each do |vr|
    NOTIFICATIONS_RMA_STATES.each do |nrs|
      state_info = {}
      next unless vr.state == nrs && vr.state != vr.new_state

      new_state_created_at = vr.new_state_created_at.present? ? vr.new_state_created_at.to_date : Date.current
      state_info[:state] = vr.state
      state_info[:date] = vr.created_at
      state_info[:days] = vr.created_at.to_date.working_days_until(new_state_created_at)
      if %w[auto_return_review awaiting_inspection].include?(vr.state)
        state_info[:sent_to] = vr.state == 'auto_return_review' ? EMAILS_FOR_NOTIFICATIONS.second : EMAILS_FOR_NOTIFICATIONS.first
        state_info[:number_of_notifications] = vr.created_at.working_days_until(new_state_created_at)
      elsif vr.created_at.working_days_until(new_state_created_at) > 3
        state_info[:sent_to] = EMAILS_FOR_NOTIFICATIONS.second
        state_info[:number_of_notifications] = vr.created_at.working_days_until(new_state_created_at) - 3
      end
      state_tracker << state_info if state_info[:number_of_notifications].present?
    end
  end
  state_tracker
end

#void_all_itemsObject

Void all items.

Returns:

  • (Object)


1570
1571
1572
1573
# File 'app/models/rma.rb', line 1570

def void_all_items
  rma_items.each(&:void)
  return_shipments.update_all(tracking_state: 'tracking_ignore') if return_shipments.present? && return_shipments.all?(&:not_yet_in_system?)
end

#void_all_items_and_selfObject

Void all items and self.

Returns:

  • (Object)


1548
1549
1550
1551
1552
1553
1554
# File 'app/models/rma.rb', line 1548

def void_all_items_and_self
  Rma.transaction do
    void_all_items
    self.skip_reminders = true # prevent email from triggering
    sync_state # some items might have been received already
  end
end