Module: RmasHelper

Includes:
UrlsHelper
Defined in:
app/helpers/rmas_helper.rb

Overview

== Schema Information

Table name: rmas

id :integer not null, primary key
rma_number :string(255)
company_id :integer
original_order_id :integer
customer_id :integer
return_shipping_address_id :integer
customer_reference :string(255)
original_po_number :string(255)
contact_name :string(255)
description :string(255)
creator_id :integer
updater_id :integer
created_at :datetime
updated_at :datetime
payment_method :string(255)
state :string(255)
uploads_count :integer
transmission_state :string(255)
legacy_transmission_email :string(255)
legacy_transmission_fax :string(255)
return_label_required :boolean not null
ship_from_address_id :integer
delivery_id :integer
transmission_email :string(255) default([]), is an Array
transmission_fax :string(255) default([]), is an Array
serial_number_state :string

Constant Summary collapse

RMA_MILESTONE_LABELS =

Labels for +Rma#versions_for_dates_tracker+ keys (executive-facing, sentence case).

{
  dropped_off: 'In transit to warehouse',
  warehouse_received: 'Warehouse received',
  submitted_for_inspection: 'Submitted for inspection',
  returned: 'Returned',
  process_refund: 'Refund processing',
  refunded: 'Refunded'
}.freeze
RMA_MILESTONE_COMPLETED_BY =
{
  dropped_off: %i[warehouse_received],
  warehouse_received: %i[submitted_for_inspection returned],
  submitted_for_inspection: %i[returned],
  returned: %i[process_refund],
  process_refund: %i[refunded],
  refunded: %i[fully_refunded]
}.freeze
CANADA_DEFAULT_CARRIER =

Carrier whose option is pre-selected by default on Canadian RMA returns.
Matches the +carrier+ prefix of the labels built by
+ShippingOption.supported_carrier_options_for_rmas+ (e.g. "Canadapost: …").

'Canadapost'

Instance Method Summary collapse

Methods included from UrlsHelper

#catalog_breadcrumb_links, #catalog_link, #catalog_link_for_product_line, #catalog_link_for_sku, #cms_link, #delocalized_path, #path_to_sales_product_sku, #path_to_sales_product_sku_for_product_line, #path_to_sales_product_sku_for_product_line_slug, #product_line_from_catalog_link, #protocol_neutral_url, #sanitize_external_url, #valid_external_url?

Instance Method Details

#cheapest_shipping_option_id(shipping_options, shipping_costs) ⇒ Object

Returns the +shipping_option_id+ with the lowest positive quoted +cost+, or +nil+ when none qualify.

+shipping_options+ — array of +[label, shipping_option_id]+ pairs (e.g. from +ShippingOption.supported_carrier_options_for_rmas+).
+shipping_costs+ — hash keyed by option id to objects responding to +#cost+ (may be +nil+; treated as empty).



220
221
222
223
224
225
226
227
228
229
230
231
# File 'app/helpers/rmas_helper.rb', line 220

def cheapest_shipping_option_id(shipping_options, shipping_costs)
  return nil if shipping_options.blank?

  costs_by_id = shipping_costs || {}

  priced_options = shipping_options
                   .map { |(_, id)| [id, costs_by_id[id]&.cost] }
                   .select { |(_, cost)| cost.present? && cost.to_f > 0 }
                   .map { |(id, cost)| [id, cost.to_f] }

  priced_options.min_by { |(_, cost)| cost }&.first
end

#compute_returned_kit_quantity(kit_li, rma_item) ⇒ Object

Kit component qty shown on RMA line rows: scales +kit_li.quantity+ by returned subset.
Returns +0+ when the parent line has no positive quantity (avoids division by zero).



351
352
353
354
355
356
357
358
359
360
# File 'app/helpers/rmas_helper.rb', line 351

def compute_returned_kit_quantity(kit_li, rma_item)
  parent = rma_item.returned_line_item
  return 0 unless parent

  denom = parent.quantity
  return 0 if denom.nil? || !denom.to_f.positive?

  numerator = kit_li.quantity.to_i * rma_item.returned_item_quantity.to_i
  numerator / denom
end

#default_shipping_option_id(country_iso, shipping_options, shipping_costs) ⇒ Object

Returns the +shipping_option_id+ to pre-select in the RMA shipping picker.

Canadian RMAs default to the Canada Post option; every other country (and
a Canadian RMA that happens to have no Canada Post option) falls back to
the cheapest quoted rate via +cheapest_shipping_option_id+.

+country_iso+ — the RMA's origin country ISO (e.g. "CA", "US").
+shipping_options+ — array of +[label, shipping_option_id]+ pairs.
+shipping_costs+ — hash keyed by option id to objects responding to +#cost+.



247
248
249
250
251
252
253
254
255
256
# File 'app/helpers/rmas_helper.rb', line 247

def default_shipping_option_id(country_iso, shipping_options, shipping_costs)
  if country_iso == 'CA'
    canada_post = Array(shipping_options).find do |(label, _id)|
      label.to_s.start_with?("#{CANADA_DEFAULT_CARRIER}:")
    end
    return canada_post.last if canada_post
  end

  cheapest_shipping_option_id(shipping_options, shipping_costs)
end

#rma_command_options(rma) ⇒ Object



258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
# File 'app/helpers/rmas_helper.rb', line 258

def rma_command_options(rma)
  opts = []
  opts << fa_icon('right-to-bracket', text: rma.human_state_name.titleize)
  opts << link_to('Edit', edit_rma_path(rma)) if rma.can_be_edited?
  opts << link_to('Add Items', add_items_rma_path(rma)) if rma.requested?
  opts << link_to('Edit Items', edit_items_rma_path(rma)) if rma.can_be_edited? && rma.has_rma_items_requested?
  opts << link_to('Generate Return Labels', edit_return_labels_rma_path(rma)) if rma.can_be_edited? && rma.has_rma_items_requested? && rma.return_delivery.blank? && rma.number_of_return_labels_required.zero?
  opts << link_to('Generate New Return Labels', edit_return_labels_rma_path(rma)) if rma.can_be_edited? && rma.has_rma_items_requested? && rma.return_delivery.present? && !rma.number_of_return_labels_required.zero?
  if rma.requested? && !rma.number_of_return_labels_required.zero? && rma.return_delivery.present?
    opts << link_to('Reset Shipping State', reset_shipping_state_rma_path(rma),
data: { turbo_confirm: 'Are you sure you want to reset the shipping state? This will remove return label requirements and advance the RMA to awaiting_return state.', turbo_method: :put })
  end
  opts << link_to('Void RMA', void_rma_rma_path(rma), data: { turbo_confirm: 'Are you sure you want to void this RMA? This will cancel any credit orders and return labels.', turbo_method: :put }) if rma.can_be_voided?
  opts << link_to('Approve RMA', workflow_action_rma_path(rma, wf_action: 'review_complete')) if rma.auto_return_review? && user_has_role?(%w[admin accounting_manager accounting_rep])
  if rma.can_be_unreturned? && can?(
:unreturn_rma, rma
)
    opts << link_to('Unreturn RMA', unreturn_rma_rma_path(rma),
data: { turbo_confirm: 'Are you sure you want to unreturn this RMA? This will reverse the item ledger entries and make the items returnable or voidable again.', turbo_method: :put })
  end
  opts << link_to('Receive/Inspect Items', receive_items_rma_path(rma)) if rma.can_be_received? && rma.rma_items.returnable.present?
  opts << link_to('Transmit', transmit_rma_path(rma)) if rma.can_be_transmitted?
  opts << link_to('Print Serial Number Labels', print_serial_number_labels_rma_path(rma)) if rma.serial_numbers.where(print_state: 'pending_print').exists?
  if rma.voided?
    # opts << { content: 'No actions available' }
    if rma.rma_items.present?
      opts << link_to('Unvoid RMA', unvoid_rma_rma_path(rma), data: { turbo_confirm: 'Are you sure you want to unvoid this RMA?', turbo_method: :put }) if rma.can_be_unvoided?
    else
      opts << { content: 'No actions available' }
    end
  end
  opts
end

#rma_item_status_css_classes(rma_item) ⇒ Object



362
363
364
365
366
367
368
369
370
371
372
373
374
375
# File 'app/helpers/rmas_helper.rb', line 362

def rma_item_status_css_classes(rma_item)
  state = rma_item.state&.to_s
  modifier = case state
             when 'returned'
               'text-bg-success'
             when 'voided'
               'text-bg-danger'
             when 'awaiting_inspection'
               'text-bg-warning'
             else
               'text-bg-info'
             end
  "badge rounded-pill #{modifier} fw-semibold"
end

#rma_kit_grouped_lines(rows) ⇒ Object

Order return-delivery lines so each kit parent is immediately followed by its
own components, instead of the id order the +expand_kit+ trigger and the
packing algorithm leave behind (kit line stranded below its own components,
which reads as unrelated duplicate SKUs on the shipping-costs page).

+rows+ — +LineItem+s, or wrappers such as +ShipmentContent+ when a block is
given to map each row to its +LineItem+.



299
300
301
302
303
304
# File 'app/helpers/rmas_helper.rb', line 299

def rma_kit_grouped_lines(rows)
  rows.sort_by do |row|
    line = block_given? ? yield(row) : row
    [line&.parent_id || line&.id || 0, line&.parent_id ? 1 : 0]
  end
end

#rma_kit_line_badge(line_item) ⇒ Object

Pill markup flagging a return line as a kit or as one of a kit's components.
Text-labelled, not color-only, so the distinction survives grayscale.
Returns +nil+ for ordinary lines.



309
310
311
312
313
314
315
316
317
# File 'app/helpers/rmas_helper.rb', line 309

def rma_kit_line_badge(line_item)
  return if line_item.blank?

  if line_item.is_kit?
    tag.span('Kit', class: 'badge rounded-pill text-bg-info fw-normal align-middle')
  elsif line_item.parent_id.present?
    tag.span('Kit component', class: 'badge rounded-pill text-bg-light border text-body-secondary fw-normal align-middle')
  end
end

#rma_milestone_backfill_dates(dates_tracker, phases) ⇒ void

This method returns an undefined value.

Backfill nil dates for steps promoted to +:complete+ by the backward sweep.
Uses the closest subsequent step's date so the card shows when the milestone
was implicitly passed rather than "—".

Parameters:

  • dates_tracker (Hash{Symbol => Date, nil})

    mutable milestone dates

  • phases (Hash{Symbol => Symbol})

    display phase by milestone



193
194
195
196
197
198
199
200
201
# File 'app/helpers/rmas_helper.rb', line 193

def rma_milestone_backfill_dates(dates_tracker, phases)
  visible_keys = dates_tracker.keys - [:fully_refunded]
  visible_keys.each_with_index do |key, idx|
    next unless phases[key] == :complete && dates_tracker[key].blank?

    later_date = visible_keys[(idx + 1)..].lazy.map { |k| dates_tracker[k] }.find(&:present?)
    dates_tracker[key] = later_date if later_date
  end
end

#rma_milestone_date_caption(date) ⇒ String

Formats one milestone date for the CRM tracker.

Parameters:

  • date (Date, Time, ActiveSupport::TimeWithZone, nil)

    milestone date

Returns:

  • (String)

    formatted date or an em dash



207
208
209
210
211
212
213
214
# File 'app/helpers/rmas_helper.rb', line 207

def rma_milestone_date_caption(date)
  if date.present?
    d = date.respond_to?(:to_date) ? date.to_date : date
    d.strftime('%b %d, %Y')
  else
    ''
  end
end

#rma_milestone_enriched_dates(dates_tracker, rma: nil) ⇒ Hash{Symbol => Date, nil}

Dup of +versions_for_dates_tracker+ with display-only enrichment:

  1. If +warehouse_received+ is blank but +submitted_for_inspection+ has a date, copy it (receipt precedes inspection).
  2. For +dropped_off+ (in transit): prefer return label +Shipment#date_shipped+ when +rma+ is given; else backfill from +warehouse_received+ when missing.

Parameters:

  • dates_tracker (Hash{Symbol => Date, nil})

    milestone dates

  • rma (Rma, nil) (defaults to: nil)

    RMA supplying generated-label dates

Returns:

  • (Hash{Symbol => Date, nil})

    enriched display dates



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'app/helpers/rmas_helper.rb', line 119

def rma_milestone_enriched_dates(dates_tracker, rma: nil)
  return dates_tracker if dates_tracker.blank?

  h = dates_tracker.dup
  if h.key?(:warehouse_received) && h[:warehouse_received].blank? && h[:submitted_for_inspection].present?
    insp = h[:submitted_for_inspection]
    h[:warehouse_received] = insp.respond_to?(:to_date) ? insp.to_date : insp
  end
  if h.key?(:dropped_off)
    label_date = rma&.earliest_return_label_ship_date
    if label_date.present?
      h[:dropped_off] = label_date.respond_to?(:to_date) ? label_date.to_date : label_date
    elsif h[:dropped_off].blank? && h[:warehouse_received].present?
      h[:dropped_off] = h[:warehouse_received]
    end
  end
  h
end

#rma_milestone_label(key) ⇒ Object



108
109
110
# File 'app/helpers/rmas_helper.rb', line 108

def rma_milestone_label(key)
  RMA_MILESTONE_LABELS[key.to_sym] || key.to_s.humanize
end

#rma_milestone_phases(dates_tracker, state: nil) ⇒ Hash{Symbol => Symbol}

Each visible milestone key => +:complete+, +:current+ (in-progress), or +:upcoming+.
Phase is determined by checking what subsequent transition has occurred,
not simply whether the step itself has a date. +:fully_refunded+ is metadata
only and excluded from the returned hash.

After the per-step pass a backward sweep guarantees that if any later step
is +:current+ or +:complete+, every earlier step is promoted to +:complete+.
This handles state-machine skips (e.g. RMA jumps from +returned+ straight
to +credited_fully_refunded+ without passing through +credit_in_process+).

Parameters:

  • dates_tracker (Hash{Symbol => Date, nil})

    milestone dates

  • state (String, Symbol, nil) (defaults to: nil)

    current RMA state

Returns:

  • (Hash{Symbol => Symbol})

    display phase by milestone



151
152
153
154
155
156
# File 'app/helpers/rmas_helper.rb', line 151

def rma_milestone_phases(dates_tracker, state: nil)
  return {} if dates_tracker.blank?

  state_phases = rma_milestone_phases_from_state(dates_tracker, state) if state.present?
  state_phases || rma_milestone_phases_from_dates(dates_tracker)
end

#rma_payment_status(rma) ⇒ Object



385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
# File 'app/helpers/rmas_helper.rb', line 385

def rma_payment_status(rma)
  return nil if rma.payment_status.nil?

  tag.div(class: 'rma-payment-status') do
    safe_join(
      rma.payment_status.map do |status|
        text_class = rma_payment_status_text_classes(status[:color])
        tag.div(class: 'rma-payment-status__line') do
          tag1 = tag.span(class: text_class) do
            if rma.payment_status.length > 1
              "#{status[:ref]}: #{status[:status]}"
            else
              status[:status]
            end
          end
          if status[:preview]
            tag1 + tag.span(class: 'ms-1 align-middle') do
              link_to(fa_icon('file-lines'), status[:preview], target: '_blank', rel: 'noopener')
            end
          else
            tag1
          end
        end
      end
    )
  end
end

#rma_return_delivery_status_badge_class(delivery) ⇒ Object

Bootstrap +text-bg-*+ for return +Delivery+ state pills on the RMA return-labels tab.



320
321
322
323
324
325
326
327
328
329
330
331
332
333
# File 'app/helpers/rmas_helper.rb', line 320

def rma_return_delivery_status_badge_class(delivery)
  return 'text-bg-secondary' if delivery.blank?

  case delivery.state.to_s
  when 'return_labels_complete', 'shipped', 'invoiced'
    'text-bg-success'
  when 'pending_ship_labels', 'pending_ship_confirm', 'pending_manifest_completion', 'pending_carrier_confirm', 'pending_pickup_confirm'
    'text-bg-warning'
  when 'cancelled'
    'text-bg-danger'
  else
    'text-bg-secondary'
  end
end

#rma_returned_reasons_with_explanations(returned_reasons = []) ⇒ Object



377
378
379
380
381
382
383
# File 'app/helpers/rmas_helper.rb', line 377

def rma_returned_reasons_with_explanations(returned_reasons = [])
  (:ul, class: 'list-inline') do
    RmaReasonCode.where(code: returned_reasons).order(:code).each do |r|
      concat (:li, r.code, data: { 'bs-toggle': 'tooltip', title: r.description })
    end
  end
end

Safe Shipsurance / Loadsure links for an insured return shipment (+link_to+, +sanitize_external_url+,
+data-turbo-confirm+ on claim — no +html_safe+, no inline +onclick+).



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'app/helpers/rmas_helper.rb', line 39

def rma_shipsurance_links(shipment)
  id = shipment.shipping_insurance_record_id
  return if id.blank?

  enc = ERB::Util.url_encode(id.to_s)
  ltl = shipment.delivery&.ships_ltl_freight?
  view_raw, claim_raw = if ltl
                          [
                            "https://portal.loadsure.net/certificates/details/?certificateNumber=#{enc}",
                            "https://portal.loadsure.net/claims/file?id=#{enc}"
                          ]
                        else
                          [
                            "https://app.shipsurance.com/cp/ViewRecordedshipments?RecordedShipmentId=#{enc}",
                            "https://app.shipsurance.com/cp/CreateClaim?recordedShipmentId=#{enc}"
                          ]
                        end
  view_href = sanitize_external_url(view_raw)
  claim_href = sanitize_external_url(claim_raw)

  record_link = if view_href.present?
                  link_to(id.to_s, view_href, target: "_blank", rel: "noopener noreferrer")
                else
                  id.to_s
                end

  claim_suffix = if claim_href.present?
                   tag.small(class: "text-body-secondary ms-1") do
                     safe_join(
                       [
                         "(file claim ",
                         link_to(
                           "here",
                           claim_href,
                           target: "_blank",
                           rel: "noopener noreferrer",
                           data: { turbo_confirm: "Are you sure you want to file a claim?" }
                         ),
                         ")"
                       ],
                       ""
                     )
                   end
                 else
                   "".html_safe
                 end

  safe_join([record_link, claim_suffix], " ")
end

#sorted_rma_items_for_show_table(rma_items) ⇒ Object

Sort RMA lines by state machine order (same ordering as the legacy items tab table).



336
337
338
339
340
341
342
343
344
345
346
347
# File 'app/helpers/rmas_helper.rb', line 336

def sorted_rma_items_for_show_table(rma_items)
  return [] if rma_items.blank?

  order = RmaItem.state_machines[:state].states.map(&:name)
  fallback = order.length
  rma_items.to_a.sort_by do |ri|
    state = ri.state
    next fallback if state.blank?

    order.index(state.to_sym) || fallback
  end
end