Class: Shipping::ReturnShippingInsurance

Inherits:
BaseService show all
Defined in:
app/services/shipping/return_shipping_insurance.rb

Overview

Handles Shipsurance insurance for RMA return shipments.

Two paths based on label type:

External labels (Amazon / 3rd-party tracking numbers entered by staff):

  1. start_external_tracking(rma) — registers each untracked tracking number with
    ShipEngine so we receive webhook events. Called on every RMA save that has
    insure_return_shipping: true.
  2. book_external_insurance(rma, tracking_number, occurred_at) — books Shipsurance
    using the webhook event's occurred_at as the ship date. Called by
    WebhookProcessors::ShipengineProcessor when the first real carrier movement
    event arrives (status code != AC).

Internal labels (labels we generated via our return delivery):
3. book_internal_insurance(rma) — delegates to PackageShippingInsurance for each
uninsured shipment in the return delivery. Called after label generation.

Defined Under Namespace

Classes: Result

Constant Summary collapse

CARRIER_OPTIONS =

Mirrors PackageShippingInsurance::SHIPSURANCE_CARRIER_DATA_BY_CARRIER keys so the
UI can show a carrier dropdown for external returns.

Shipping::PackageShippingInsurance::SHIPSURANCE_CARRIER_DATA_BY_CARRIER
.keys
.map { |k| [k.to_s, k.to_s] }
.freeze
TRACKING_NUMBER_GEM_TO_SHIPSURANCE_CARRIER =

Maps tracking_number gem courier codes → Shipsurance carrier name strings.
Derived from CARRIER_CODE_MAP in config/initializers/carrier_codes.rb.

::TRACKING_NUMBER_GEM_TO_SHIPSURANCE_CARRIER
INSURED_VALUE_MIN =

Minimum declared value (cents) below which we skip insurance — same threshold as
PackageShippingInsurance::INSURED_VALUE_MIN.

Shipping::PackageShippingInsurance::INSURED_VALUE_MIN
UNSUCCESFUL_STRING =
Shipping::PackageShippingInsurance::UNSUCCESFUL_STRING

Instance Method Summary collapse

Methods inherited from BaseService

#initialize, #log_debug, #log_error, #log_info, #log_warning, #logger, #options, #process, #tagged_logger

Constructor Details

This class inherits a constructor from BaseService

Instance Method Details

#book_external_insurance(rma, tracking_number, occurred_at) ⇒ Object

── Entry point B ──────────────────────────────────────────────────────────
Books Shipsurance when ShipEngine reports the first real carrier movement.
Carrier is auto-detected from the tracking number; return_shipping_carrier
is used as a fallback for unrecognised formats.
Safe to call multiple times — skips numbers already insured.



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'app/services/shipping/return_shipping_insurance.rb', line 91

def book_external_insurance(rma, tracking_number, occurred_at)
  return Result.new(status: :skip, status_message: 'Insurance not opted in') unless rma.insure_return_shipping?
  return Result.new(status: :skip, status_message: 'Already insured') if rma.return_insurance_data[tracking_number].present?

  declared_value = calculate_declared_value(rma)
  if declared_value < INSURED_VALUE_MIN
    return Result.new(status: :skip, status_message: "Declared value #{declared_value} below minimum #{INSURED_VALUE_MIN}")
  end

  carrier = effective_carrier(tracking_number, rma.return_shipping_carrier)
  carrier_id_value = carrier_id(carrier)
  unless carrier_id_value.present?
    return Result.new(status: :error, status_message: "Cannot determine Shipsurance carrier for #{tracking_number} (detected: #{carrier.inspect})")
  end

  shipment_date = occurred_at.strftime('%m/%d/%Y')
  form_params = {
    recordSourceIdentifier: "rma_#{rma.id}_#{tracking_number}",
    extShipmentTypeId:      '1',
    extCarrierId:           carrier_id_value,
    referenceNumber:        rma.rma_number,
    trackingNumber:         tracking_number,
    declaredValue:          declared_value.to_s,
    shipmentDate:           shipment_date,
    dsiRatePer100:          carrier_percentage_charge(carrier, false).to_s
  }

  status = :ok
  status_message = ''

  begin
    Retryable.retryable(tries: 2, sleep: lambda { |n| 4**n }, matching: [/#{UNSUCCESFUL_STRING}/, UNSUCCESFUL_STRING]) do |retries, exception|
      Rails.logger.debug { "[ReturnShippingInsurance] book_external_insurance retries: #{retries}, exception: #{exception}" }
      response_arr = shipsurance_client.record_shipment(form_params).split(',')

      status = response_arr[0] == '1' ? :ok : :error
      status_message = response_arr[1].to_s
      raise UNSUCCESFUL_STRING unless status == :ok

      record_id = response_arr.last
      rma.update_column(:return_insurance_data, rma.return_insurance_data.merge(tracking_number => record_id))
      Rails.logger.info "[ReturnShippingInsurance] Insured RMA #{rma.rma_number} tracking #{tracking_number} — Shipsurance ID #{record_id}"
    end
  rescue StandardError => e
    raise unless e.message == UNSUCCESFUL_STRING
  end

  Result.new(status:, status_message:)
end

#book_internal_insurance(rma) ⇒ Object

── Entry point C ──────────────────────────────────────────────────────────
Books Shipsurance for internally generated return shipments via
PackageShippingInsurance (which handles the delivery qualifies? check).



144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'app/services/shipping/return_shipping_insurance.rb', line 144

def book_internal_insurance(rma)
  return Result.new(status: :skip, status_message: 'Insurance not opted in') unless rma.insure_return_shipping?
  return Result.new(status: :skip, status_message: 'No return delivery') unless rma.return_delivery.present?

  uninsured_shipments = rma.return_delivery.shipments.label_complete.reject(&:is_ship_insured)
  return Result.new(status: :ok, status_message: 'All shipments already insured') if uninsured_shipments.empty?

  psi = Shipping::PackageShippingInsurance.new
  errors = []

  uninsured_shipments.each do |shipment|
    result = psi.book_shipping_insurance_for_shipment(shipment, {})
    if result.status != :ok
      errors << "Shipment #{shipment.id}: #{result.status_message}"
    end
  end

  if errors.any?
    Result.new(status: :error, status_message: errors.join('; '))
  else
    Result.new(status: :ok, status_message: "Insured #{uninsured_shipments.size} shipment(s)")
  end
end

#start_external_tracking(rma) ⇒ Object

── Entry point A ──────────────────────────────────────────────────────────
Registers external tracking numbers with ShipEngine for webhook delivery.
Carrier is auto-detected from the tracking number via the tracking_number gem;
rma.return_shipping_carrier is used as a fallback for unrecognised formats.
Safe to call multiple times — always attempts registration for all numbers
(ShipEngine is idempotent for duplicate registrations).



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
# File 'app/services/shipping/return_shipping_insurance.rb', line 48

def start_external_tracking(rma)
  return Result.new(status: :skip, status_message: 'Insurance not opted in') unless rma.insure_return_shipping?
  return Result.new(status: :skip, status_message: 'No tracking numbers present') if rma.tracking_numbers.blank?

  errors = []
  skipped = []
  started = []

  rma.tracking_numbers.reject(&:blank?).each do |tracking_number|
    carrier = effective_carrier(tracking_number, rma.return_shipping_carrier)
    unless carrier.present?
      skipped << tracking_number
      Rails.logger.warn "[ReturnShippingInsurance] Cannot detect carrier for #{tracking_number} and no fallback set — skipping ShipEngine registration for RMA #{rma.rma_number}"
      next
    end

    begin
      WyShipping.start_tracking_for_carrier(tracking_number, carrier)
      started << tracking_number
      Rails.logger.info "[ReturnShippingInsurance] Started ShipEngine tracking: #{tracking_number} (#{carrier}) for RMA #{rma.rma_number}"
    rescue StandardError => e
      msg = "Failed to start tracking for #{tracking_number}: #{e.message}"
      Rails.logger.error "[ReturnShippingInsurance] #{msg}"
      ErrorReporting.error(e, msg, rma_id: rma.id, tracking_number: tracking_number)
      errors << msg
    end
  end

  if errors.any?
    Result.new(status: :error, status_message: errors.join('; '))
  else
    msg_parts = []
    msg_parts << "Started tracking for #{started.size} number(s)" if started.any?
    msg_parts << "Skipped #{skipped.size} (carrier undetectable)" if skipped.any?
    Result.new(status: :ok, status_message: msg_parts.join('; '))
  end
end