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

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



237
238
239
240
241
242
243
244
245
246
247
248
# File 'app/helpers/rmas_helper.rb', line 237

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



315
316
317
318
319
320
321
322
323
324
# File 'app/helpers/rmas_helper.rb', line 315

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

#rma_command_options(rma) ⇒ Object



250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
# File 'app/helpers/rmas_helper.rb', line 250

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('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.present? && 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



326
327
328
329
330
331
332
333
334
335
336
337
338
339
# File 'app/helpers/rmas_helper.rb', line 326

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_milestone_backfill_dates(dates_tracker, phases) ⇒ Object

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



214
215
216
217
218
219
220
221
222
# File 'app/helpers/rmas_helper.rb', line 214

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] }.detect(&:present?)
    dates_tracker[key] = later_date if later_date
  end
end

#rma_milestone_date_caption(date) ⇒ Object



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

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) ⇒ Object

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.


105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'app/helpers/rmas_helper.rb', line 105

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



98
99
100
# File 'app/helpers/rmas_helper.rb', line 98

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

#rma_milestone_phases(dates_tracker) ⇒ Object

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



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# File 'app/helpers/rmas_helper.rb', line 133

def rma_milestone_phases(dates_tracker)
  return {} if dates_tracker.blank?

  phases = {}

  if dates_tracker.key?(:dropped_off)
    phases[:dropped_off] = if dates_tracker[:warehouse_received].present?
                             :complete
                           elsif dates_tracker[:dropped_off].present?
                             :current
                           else
                             :upcoming
                           end
  end

  if dates_tracker.key?(:warehouse_received)
    phases[:warehouse_received] = if dates_tracker[:submitted_for_inspection].present? || dates_tracker[:returned].present?
                                    :complete
                                  elsif dates_tracker[:warehouse_received].present?
                                    :current
                                  else
                                    :upcoming
                                  end
  end

  if dates_tracker.key?(:submitted_for_inspection)
    phases[:submitted_for_inspection] = if dates_tracker[:returned].present?
                                          :complete
                                        elsif dates_tracker[:submitted_for_inspection].present?
                                          :current
                                        else
                                          :upcoming
                                        end
  end

  if dates_tracker.key?(:returned)
    phases[:returned] = if dates_tracker[:process_refund].present?
                          :complete
                        elsif dates_tracker[:returned].present?
                          :current
                        else
                          :upcoming
                        end
  end

  if dates_tracker.key?(:process_refund)
    phases[:process_refund] = if dates_tracker[:refunded].present?
                                :complete
                              elsif dates_tracker[:process_refund].present?
                                :current
                              else
                                :upcoming
                              end
  end

  if dates_tracker.key?(:refunded)
    phases[:refunded] = if dates_tracker[:fully_refunded].present?
                          :complete
                        elsif dates_tracker[:refunded].present?
                          :current
                        else
                          :upcoming
                        end
  end

  # Backward sweep: promote earlier steps when later steps are already active.
  keys = phases.keys
  saw_active = false
  keys.reverse_each do |key|
    if saw_active && phases[key] != :complete
      phases[key] = :complete
    end
    saw_active = true if phases[key] == :current || phases[key] == :complete
  end

  phases
end

#rma_payment_status(rma) ⇒ Object



349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
# File 'app/helpers/rmas_helper.rb', line 349

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.



284
285
286
287
288
289
290
291
292
293
294
295
296
297
# File 'app/helpers/rmas_helper.rb', line 284

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



341
342
343
344
345
346
347
# File 'app/helpers/rmas_helper.rb', line 341

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



38
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
# File 'app/helpers/rmas_helper.rb', line 38

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

#rma_status_bar(rma, steps_phases, include_labels: false) ⇒ Object

Overall progress row: progress fill + step nodes. +steps_phases+ is +:complete+ or +:pending+ per segment.
+rma+ and +include_labels+ are part of the public API for callers; only +steps_phases+ affects markup.
rubocop:disable Lint/UnusedMethodArgument -- rma/include_labels keep +rma_status_bar_with_*+ call sites explicit



380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
# File 'app/helpers/rmas_helper.rb', line 380

def rma_status_bar(rma, steps_phases, include_labels: false)
  return if steps_phases.blank?

  width_pct = rma_overall_progress_width_percent(steps_phases)
  (:div, class: 'rma-show-main__status-bar', role: 'group', aria: { label: 'Return shipment progress by stage' }) do
    safe_join(
      [
        (:div, class: 'progress rma-show-main__progress') do
          (:div, '', class: 'progress-bar', role: 'progressbar', style: "width: #{width_pct}%;")
        end,
        (:div, class: 'rma-show-main__steps') do
          safe_join(steps_phases.map { |phase| rma_overall_progress_step(phase) })
        end
      ],
      ''
    )
  end
end

#rma_status_bar_with_labels(rma) ⇒ Object

rubocop:enable Lint/UnusedMethodArgument



400
401
402
403
# File 'app/helpers/rmas_helper.rb', line 400

def rma_status_bar_with_labels(rma)
  phases = rma_overall_progress_phases_from_milestones(rma)
  rma_status_bar(rma, phases, include_labels: true) if phases
end

#rma_status_bar_with_no_labels(rma) ⇒ Object



405
406
407
408
# File 'app/helpers/rmas_helper.rb', line 405

def rma_status_bar_with_no_labels(rma)
  phases = rma_overall_progress_phases_from_milestones(rma)
  rma_status_bar(rma, phases, include_labels: false) if phases
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).



300
301
302
303
304
305
306
307
308
309
310
311
# File 'app/helpers/rmas_helper.rb', line 300

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