Class: Rma

Inherits:
ApplicationRecord show all
Includes:
Models::Auditable, Models::LiquidMethods, Models::RmaTransmittable, Models::SupportCaseLinkable
Defined in:
app/models/rma.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: CodeMerger, 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 =
'https://tools.usps.com/locations/home.htm?filters=LBROSSK'
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 Schedulable

Schedulable::SIMPLE_FORM_OPTIONS

Instance Attribute Summary collapse

Belongs to collapse

Methods included from Models::SupportCaseLinkable

#support_case

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 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.



233
234
235
# File 'app/models/rma.rb', line 233

def allow_invoice_link
  @allow_invoice_link
end

#company_idObject (readonly)



211
212
# File 'app/models/rma.rb', line 211

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

#customer_idObject (readonly)



211
212
# File 'app/models/rma.rb', line 211

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

#customer_referenceObject (readonly)



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

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

#original_invoice_idObject (readonly)



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

validates :original_invoice_id, numericality: { allow_nil: true }

#original_order_idObject (readonly)



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

validates :original_order_id, numericality: { allow_nil: true }

#payment_methodObject (readonly)



211
212
# File 'app/models/rma.rb', line 211

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

#return_shipping_address_idObject (readonly)



211
212
# File 'app/models/rma.rb', line 211

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

#rma_numberObject (readonly)



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

validates :rma_number, uniqueness: true

#ship_from_address_idObject (readonly)



211
212
# File 'app/models/rma.rb', line 211

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

#shipping_option_idObject (readonly)



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

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.



233
234
235
# File 'app/models/rma.rb', line 233

def skip_return_label_validation
  @skip_return_label_validation
end

#skip_shipping_option_validationObject

Returns the value of attribute skip_shipping_option_validation.



233
234
235
# File 'app/models/rma.rb', line 233

def skip_shipping_option_validation
  @skip_shipping_option_validation
end

#transmission_emailObject (readonly)



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

validates :transmission_email, email_format: true, allow_nil: true

#transmission_faxObject (readonly)



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

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:



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

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.



172
173
174
175
176
# File 'app/models/rma.rb', line 172

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:



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

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:



144
145
146
147
148
149
# File 'app/models/rma.rb', line 144

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

.create_rma_from_delivery(d) ⇒ Object



1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
# File 'app/models/rma.rb', line 1085

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
  rma.save!
  rma.create_return_delivery
  rma.generate_return_labels if rma.return_label_required?
  rma
end

.in_auto_return_reviewObject



686
687
688
# File 'app/models/rma.rb', line 686

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

.in_awaiting_inspectionObject



690
691
692
# File 'app/models/rma.rb', line 690

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

.return_labels_select_optionsObject



454
455
456
# File 'app/models/rma.rb', line 454

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



694
695
696
697
698
699
700
701
# File 'app/models/rma.rb', line 694

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:



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

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



678
679
680
681
682
683
684
# File 'app/models/rma.rb', line 678

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



942
943
944
# File 'app/models/rma.rb', line 942

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



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

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



703
704
705
706
707
708
709
710
711
# File 'app/models/rma.rb', line 703

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



713
714
715
716
717
718
719
720
# File 'app/models/rma.rb', line 713

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

Instance Method Details

#active_rma_itemsObject



1160
1161
1162
# File 'app/models/rma.rb', line 1160

def active_rma_items
  rma_items.active.reload
end

#activitiesActiveRecord::Relation<Activity>

Returns:

See Also:



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

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

#all_credit_memos_fully_offset?Boolean

Returns:

  • (Boolean)


789
790
791
# File 'app/models/rma.rb', line 789

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

#all_credit_memos_partially_refunded?Boolean

Returns:

  • (Boolean)


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

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?Boolean

Returns:

  • (Boolean)


958
959
960
# File 'app/models/rma.rb', line 958

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

#all_items_requested?Boolean

Returns:

  • (Boolean)


1145
1146
1147
# File 'app/models/rma.rb', line 1145

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

#all_items_requested_and_ready_to_return?Boolean

Returns:

  • (Boolean)


962
963
964
965
966
967
# File 'app/models/rma.rb', line 962

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?Boolean

Returns:

  • (Boolean)


1137
1138
1139
# File 'app/models/rma.rb', line 1137

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

#all_items_voided?Boolean

Returns:

  • (Boolean)


812
813
814
# File 'app/models/rma.rb', line 812

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

#all_uploadsObject



671
672
673
674
675
676
# File 'app/models/rma.rb', line 671

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

#any_credit_memo_printed?Boolean

Returns:

  • (Boolean)


781
782
783
# File 'app/models/rma.rb', line 781

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

#any_items_awaiting_inspection?Boolean

Returns:

  • (Boolean)


816
817
818
# File 'app/models/rma.rb', line 816

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

#any_items_requested?Boolean

Returns:

  • (Boolean)


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

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)


911
912
913
# File 'app/models/rma.rb', line 911

def any_rejected_items?
  rma_items.rejected.exists?
end

#arrival_datetimeObject



601
602
603
# File 'app/models/rma.rb', line 601

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

#auto_return_rma_itemsObject



1129
1130
1131
# File 'app/models/rma.rb', line 1129

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

#build_activityObject



762
763
764
# File 'app/models/rma.rb', line 762

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

#can_be_edited?Boolean

Returns:

  • (Boolean)


748
749
750
# File 'app/models/rma.rb', line 748

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

#can_be_received?Boolean

Returns:

  • (Boolean)


728
729
730
# File 'app/models/rma.rb', line 728

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

#can_be_transmitted?Boolean

Returns:

  • (Boolean)


732
733
734
# File 'app/models/rma.rb', line 732

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

#can_be_unreturned?Boolean

Returns:

  • (Boolean)


744
745
746
# File 'app/models/rma.rb', line 744

def can_be_unreturned?
  returned?
end

#can_be_unvoided?Boolean

Returns:

  • (Boolean)


740
741
742
# File 'app/models/rma.rb', line 740

def can_be_unvoided?
  voided?
end

#can_be_voided?Boolean

Returns:

  • (Boolean)


736
737
738
# File 'app/models/rma.rb', line 736

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

#can_edit_return_labels?Boolean

Returns:

  • (Boolean)


752
753
754
755
756
# File 'app/models/rma.rb', line 752

def can_edit_return_labels?
  return false if ship_from_address.is_warehouse?

  requested? || auto_return_review? || awaiting_return?
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)

    catalog_item_id] => {price:, discounted_price:}



1763
1764
1765
1766
1767
1768
1769
# File 'app/models/rma.rb', line 1763

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::Relation<Communication>

Returns:

See Also:



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

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

#companyCompany

Returns:

See Also:



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

belongs_to :company, optional: true

#create_credit_order(rma_item_ids) ⇒ Object



1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
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
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
# File 'app/models/rma.rb', line 1540

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)
      elsif 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: -qty_to_receive,
          price: price * credit_percentage,
          discounted_price: discounted_price * credit_percentage,
          credit_rma_item: rma_item
        }
        li_attrs[:taxable_amount] = rma_item.returned_line_item.taxable_amount * credit_percentage if from_ci_invoice && rma_item.returned_line_item.taxable_amount.present?
        li = LineItem.new(li_attrs)
        li.do_not_calculate_tax = true if from_ci_invoice
        order.line_items << li
      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: -qty_to_receive,
          price: catalog_item.amount * credit_percentage,
          discounted_price: catalog_item.amount * credit_percentage,
          credit_rma_item: rma_item
        }
        li = LineItem.new(li_attrs)
        li.do_not_calculate_tax = true if from_ci_invoice
        order.line_items << li
      end
    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



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

def create_documents
  # return unless rma_items_changed?
  # create_credit_order
  create_replacement_order
end

#create_replacement_orderObject



1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
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
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
# File 'app/models/rma.rb', line 1450

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



1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
# File 'app/models/rma.rb', line 1185

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 = 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

Returns:

See Also:



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

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

#credit_availableObject



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

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

#credit_available_before_taxObject



873
874
875
876
# File 'app/models/rma.rb', line 873

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

#credit_deliveriesActiveRecord::Relation<Delivery>

Returns:

See Also:



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

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

#credit_memosActiveRecord::Relation<CreditMemo>

Returns:

See Also:



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

has_many :credit_memos

#credit_order_line_itemsActiveRecord::Relation<CreditOrderLineItem>

Returns:

  • (ActiveRecord::Relation<CreditOrderLineItem>)

See Also:



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

has_many :credit_order_line_items, through: :rma_items

#credit_order_shipmentsObject



820
821
822
# File 'app/models/rma.rb', line 820

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

#credit_ordersActiveRecord::Relation<Order>

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



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

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

#credit_totalObject



824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
# File 'app/models/rma.rb', line 824

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



846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
# File 'app/models/rma.rb', line 846

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


938
939
940
# File 'app/models/rma.rb', line 938

def crm_link
  UrlHelper.instance.rma_path(self)
end

#customerCustomer

Returns:

See Also:



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

belongs_to :customer, optional: true

#customer_nameObject



800
801
802
# File 'app/models/rma.rb', line 800

def customer_name
  customer.try(:full_name)
end

#earliest_return_label_ship_dateObject

Earliest +date_shipped+ on return shipments (matches return labels tab / audited shipment table).



581
582
583
# File 'app/models/rma.rb', line 581

def earliest_return_label_ship_date
  return_shipments.minimum(:date_shipped)
end

#effective_return_deliveryObject



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

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

#exclude_manually_initiated_event?(event) ⇒ Boolean

Returns:

  • (Boolean)


667
668
669
# File 'app/models/rma.rb', line 667

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

#external_return_labels?Boolean

Returns:

  • (Boolean)


773
774
775
# File 'app/models/rma.rb', line 773

def external_return_labels?
  number_of_return_labels_required.to_i.zero?
end

#full_invoice_returned?Boolean

Returns:

  • (Boolean)


793
794
795
796
797
798
# File 'app/models/rma.rb', line 793

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



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

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



1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
# File 'app/models/rma.rb', line 1374

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



1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
# File 'app/models/rma.rb', line 1389

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.
    if label_result.is_a?(Hash) && label_result[:status_code] == :error && label_result[:status_message].present?
      errs << label_result[:status_message]
    end
    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



592
593
594
595
596
597
598
599
# File 'app/models/rma.rb', line 592

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?Boolean

Returns:

  • (Boolean)


619
620
621
# File 'app/models/rma.rb', line 619

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

#has_different_replacement_items?Boolean

Returns:

  • (Boolean)


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

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)


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

def has_no_active_items?
  rma_items.active.none?
end

#has_replacement_items?Boolean

Returns:

  • (Boolean)


1031
1032
1033
# File 'app/models/rma.rb', line 1031

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

#has_rma_items_requested?Boolean

Returns:

  • (Boolean)


623
624
625
# File 'app/models/rma.rb', line 623

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

#has_same_replacement_items?Boolean

Returns:

  • (Boolean)


1149
1150
1151
1152
1153
1154
# File 'app/models/rma.rb', line 1149

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.


1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
# File 'app/models/rma.rb', line 1724

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)
    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),
      tax_total: inherited_tax
    )
  end
end

#instructions_file_name(with_extension = true) ⇒ Object



1023
1024
1025
# File 'app/models/rma.rb', line 1023

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

#invoiceInvoice

Returns:

See Also:



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

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).

Returns:

  • (Boolean)


1921
1922
1923
1924
1925
1926
# File 'app/models/rma.rb', line 1921

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.

Returns:

  • (Boolean)


1911
1912
1913
1914
1915
1916
1917
1918
# File 'app/models/rma.rb', line 1911

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).

Returns:

  • (Boolean)


1929
1930
1931
1932
# File 'app/models/rma.rb', line 1929

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?Boolean

Returns:

  • (Boolean)


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

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

#labels_file_name(with_extension = true) ⇒ Object



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

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

#legacy_return_shipmentsObject



969
970
971
# File 'app/models/rma.rb', line 969

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

#next_step_after_items_createdObject



1891
1892
1893
1894
1895
1896
1897
# File 'app/models/rma.rb', line 1891

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

#no_items_returned?Boolean

Returns:

  • (Boolean)


758
759
760
# File 'app/models/rma.rb', line 758

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

#ordersActiveRecord::Relation<Order>

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



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

has_many :orders, inverse_of: :rma

#original_invoiceInvoice

Returns:

See Also:



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

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

#original_invoice_selectObject



644
645
646
647
648
# File 'app/models/rma.rb', line 644

def original_invoice_select
  return [] unless original_invoice

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

#original_orderOrder

Returns:

See Also:



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

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

#original_order_refObject



804
805
806
# File 'app/models/rma.rb', line 804

def original_order_ref
  original_order.try(:reference_number)
end

#original_order_ref=(ref) ⇒ Object



808
809
810
# File 'app/models/rma.rb', line 808

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

#original_order_selectObject



638
639
640
641
642
# File 'app/models/rma.rb', line 638

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)


987
988
989
# File 'app/models/rma.rb', line 987

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).



997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
# File 'app/models/rma.rb', line 997

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 (#{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



650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
# File 'app/models/rma.rb', line 650

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::Relation<Payment>

Returns:

  • (ActiveRecord::Relation<Payment>)

See Also:



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

has_many :payments

#pending_ordersActiveRecord::Relation<Order>

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



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

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

#possible_eventsObject



458
459
460
# File 'app/models/rma.rb', line 458

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

#possible_events_for_selectObject



462
463
464
# File 'app/models/rma.rb', line 462

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

#post_communication_sent_hookObject



933
934
935
936
# File 'app/models/rma.rb', line 933

def post_communication_sent_hook
  transmit if awaiting_transmission?
  true
end

#precreate_from_deliveryDelivery

Returns:

See Also:



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

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

#primary_partyObject



1905
1906
1907
# File 'app/models/rma.rb', line 1905

def primary_party
  customer
end

#quoteQuote

Returns:

See Also:



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

has_one :quote

#receive_item_events_for_selectObject



466
467
468
469
470
471
472
# File 'app/models/rma.rb', line 466

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.



190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'app/models/rma.rb', line 190

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

#redesign_quoteQuote

Returns:

See Also:



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

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

#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.



1175
1176
1177
# File 'app/models/rma.rb', line 1175

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



1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
# File 'app/models/rma.rb', line 1783

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



954
955
956
# File 'app/models/rma.rb', line 954

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

#return_deliveryDelivery

Returns:

See Also:



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

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

#return_instructionsObject



981
982
983
# File 'app/models/rma.rb', line 981

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

#return_label_issuesObject



1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
# File 'app/models/rma.rb', line 1424

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?Boolean Also known as: return_label_required

Returns:

  • (Boolean)


766
767
768
769
770
# File 'app/models/rma.rb', line 766

def return_label_required?
  return false if all_items_voided? # this is to prevent this validation from blocking RMAs from being voided

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

#return_labelsObject



977
978
979
# File 'app/models/rma.rb', line 977

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

#return_labels_correctly_generated?Boolean

Returns:

  • (Boolean)


1444
1445
1446
1447
1448
# File 'app/models/rma.rb', line 1444

def return_labels_correctly_generated?
  return true if skip_return_label_validation

  return_label_issues.empty?
end

#return_shipmentsActiveRecord::Relation<Shipment>

Returns:

See Also:



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

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

#return_shipping_addressAddress

Returns:

See Also:



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

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

#return_shipping_methodObject



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

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

#returned_rma_itemsObject



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

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:



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

delegate :rma_inspect_email, to: :company

#rma_itemsActiveRecord::Relation<RmaItem>

Returns:

  • (ActiveRecord::Relation<RmaItem>)

See Also:



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

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

#rma_items_awaiting_inspectionObject



1168
1169
1170
# File 'app/models/rma.rb', line 1168

def rma_items_awaiting_inspection
  rma_items.awaiting_inspection.reload
end

#rma_suggested_quotes_optionsObject



605
606
607
608
609
610
611
612
613
614
615
616
617
# File 'app/models/rma.rb', line 605

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

Returns:

See Also:



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

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

#send_inspection_notification_to_customerObject



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

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



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

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



904
905
906
907
# File 'app/models/rma.rb', line 904

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



894
895
896
897
# File 'app/models/rma.rb', line 894

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::Relation<SerialNumber>

Returns:

See Also:



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

has_many :serial_numbers, through: :rma_items

#service_only?Boolean

Returns:

  • (Boolean)


950
951
952
# File 'app/models/rma.rb', line 950

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

#set_default_notification_channelsObject



1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
# File 'app/models/rma.rb', line 1065

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'
      self.transmission_email << cp.detail
    when 'fax'
      self.transmission_fax << cp.detail
    end
  end
end

#ship_from_addressAddress

Returns:

See Also:



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

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

#ship_from_attributesObject



1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
# File 'app/models/rma.rb', line 1864

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



1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
# File 'app/models/rma.rb', line 1844

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_numbersObject



585
586
587
588
589
590
# File 'app/models/rma.rb', line 585

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



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

belongs_to :shipping_option, optional: true

#should_validate_shipping_option?Boolean

Returns:

  • (Boolean)


1899
1900
1901
1902
1903
# File 'app/models/rma.rb', line 1899

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



1749
1750
1751
1752
1753
1754
# File 'app/models/rma.rb', line 1749

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



632
633
634
635
636
# File 'app/models/rma.rb', line 632

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



878
879
880
881
882
883
884
885
# File 'app/models/rma.rb', line 878

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



722
723
724
# File 'app/models/rma.rb', line 722

def to_s
  rma_number
end

#unreturn_all_itemsObject



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

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

#unvoid_all_itemsObject



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

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



1043
1044
1045
1046
1047
1048
1049
# File 'app/models/rma.rb', line 1043

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



1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
# File 'app/models/rma.rb', line 1292

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



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
# File 'app/models/rma.rb', line 1796

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::Relation<Upload>

Returns:

  • (ActiveRecord::Relation<Upload>)

See Also:



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

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

#versions_for_audit_trail(_params = {}) ⇒ Object



474
475
476
477
478
479
480
481
482
483
# File 'app/models/rma.rb', line 474

def versions_for_audit_trail(_params = {})
  query_sql = %q{
                (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



520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
# File 'app/models/rma.rb', line 520

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



485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
# File 'app/models/rma.rb', line 485

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



1051
1052
1053
1054
# File 'app/models/rma.rb', line 1051

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



1035
1036
1037
1038
1039
1040
1041
# File 'app/models/rma.rb', line 1035

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