Class: Shipping::PackageShippingInsurance

Inherits:
BaseService
  • Object
show all
Defined in:
app/services/shipping/package_shipping_insurance.rb

Overview

Books Shipsurance package shipping insurance for deliveries and their shipments.

Only parcel carriers covered by the Shipsurance contract are eligible; freight
carriers are excluded. The service can book, void, and report insurance status
for shipments, as well as compute premium costs and covered declared values.

Defined Under Namespace

Classes: Result

Constant Summary collapse

SHIPSURANCE_CARRIER_DATA_BY_CARRIER =

here we only populate parcel carriers that we know are covered by our Shipsurance contract
we do not cover freight carriers per Shipsurance contract

{
  Canadapost: { id: '5', coverage_limit: 2500.0, percentage_charge: 0.45, percentage_charge_international: 0.85 },
  Canpar: { id: '80', coverage_limit: 2500.0, percentage_charge: 0.45, percentage_charge_international: 0.85 },
  FedEx: { id: '2', coverage_limit: 5000.0, percentage_charge: 0.25, percentage_charge_international: 0.25 },
  # 'FedExFreight': '37',
  Purolator: { id: '7', coverage_limit: 2500.0, percentage_charge: 0.45, percentage_charge_international: 0.85 },
  # 'RlCarriers': '31',
  # 'ShipengineSaia': '19',
  SpeedeeDelivery: { id: '17', coverage_limit: 5000.0, percentage_charge: 0.25, percentage_charge_international: 0.25 },
  UPS: { id: '3', coverage_limit: 5000.0, percentage_charge: 0.25, percentage_charge_international: 0.25 },
  # 'UPSFreight': '57',
  USPS: { id: '1', coverage_limit: 2500.0, percentage_charge: 0.45, percentage_charge_international: 0.85 } # ,
  # reoving Amazon as carrier since they promptly process claims and it is included # Amazon: { id: '111', coverage_limit: 2500.0, percentage_charge: 0.45, percentage_charge_international: 0.85 }
  # 'YrcFreight': '64'
}.freeze
INSURED_VALUE_MIN =

Insured value min.

100.0
UNSUCCESFUL_STRING =

Unsuccesful string.

'UnsuccessfulShipInsure'

Instance Attribute Summary collapse

Attributes inherited from BaseService

#options

Instance Method Summary collapse

Methods inherited from BaseService

#log_debug, #log_error, #log_info, #log_warning, #logger, #tagged_logger

Constructor Details

#initialize(_options = {}) ⇒ void

Parameters:

  • _options (Hash) (defaults to: {})

    unused options hash kept for API compatibility



65
# File 'app/services/shipping/package_shipping_insurance.rb', line 65

def initialize(_options = {}); end

Instance Attribute Details

#rendererObject (readonly)

irb(main):006:0> Shipment.label_complete.pluck('distinct carrier').sort_by{|c| c}
[
[ 0] "Canadapost",
[ 1] "Canpar",
[ 2] "FedEx",
[ 3] "FedExFreight",
[ 4] "Freightquote",
[ 5] "Purolator",
[ 6] "RlCarriers",
[ 7] "Saia",
[ 8] "SpeedeeDelivery",
[ 9] "UPS",
[10] "UPSFreight",
[11] "USPS",
[12] "YrcFreight"
]

Returns:

  • (Object)

    renderer accessor (unused by this service)



61
62
63
# File 'app/services/shipping/package_shipping_insurance.rb', line 61

def renderer
  @renderer
end

Instance Method Details

#bill_shipping_to_customer_and_not_a_store_transfer?(delivery) ⇒ Boolean

Returns true when the customer is billed for shipping and this is not a store transfer.

When billing to the customer, they pay for declared value through the carrier instead.

Parameters:

  • delivery (Delivery)

    the delivery to check

Returns:

  • (Boolean)

    true when the customer pays for shipping and it is not a store transfer



120
121
122
123
# File 'app/services/shipping/package_shipping_insurance.rb', line 120

def bill_shipping_to_customer_and_not_a_store_transfer?(delivery)
  # if billing to customer, let them pay for declared value via carrier
  delivery.bill_shipping_to_customer && !delivery.order&.is_store_transfer? # this is to be readable to me, a human
end

#book_shipping_insurance_for_shipment(shipment, options = {}) ⇒ Result

Returns insurance booking status and message.

Parameters:

  • shipment (Shipment)

    the shipment to book insurance for

  • options (Hash) (defaults to: {})

    configurable options

Options Hash (options):

  • ship_date (Date)

    ship date to record (defaults to the shipment delivery's shipped date)

Returns:

  • (Result)

    insurance booking status and message



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

def book_shipping_insurance_for_shipment(shipment, options = {})
  status = :ok
  status_message = ''

  # Outbound flow: delivery.shipped_date is the stamped ship event.
  # RMA return flow: delivery.shipped_date is nil (return deliveries never
  # transition to :shipped) — caller passes options[:ship_date] = label date.
  ship_date = options[:ship_date] || shipment.delivery.shipped_date
  return Result.new(status: :error, status_message: 'ship_date is nil — cannot report shipment') if ship_date.nil?

  shipped_date  = ship_date.strftime('%m/%d/%Y')
  insured_value = apply_carrier_coverage_limit(shipment)

  form_params = {
    recordSourceIdentifier: shipment.id.to_s,
    extShipmentTypeId:      '1',
    extCarrierId:           resolve_carrier_id(shipment.sanitized_carrier, shipment: shipment),
    carrierServiceName:     (shipment.delivery.selected_shipping_cost || shipment.delivery.shipping_option).description.to_s,
    referenceNumber:        shipment.order_reference_and_shipment_number,
    trackingNumber:         shipment.tracking_number,
    declaredValue:          insured_value.to_s,
    shipmentDate:           shipped_date,
    dsiRatePer100:          carrier_percentage_charge(resolve_insurance_carrier(shipment.sanitized_carrier, shipment: shipment), shipment.delivery.is_international?).to_s
  }

  begin
    Retryable.retryable(tries: 2, sleep: ->(n) { 4**n }, matching: [/UNSUCCESFUL_STRING/, UNSUCCESFUL_STRING]) do |retries, exception|
      Rails.logger.debug { "Shipping::PackageShippingInsurance#book_shipping_insurance_for_shipment, 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

      shipment.update(shipping_insurance_data: { shipping_insurance_record_id: response_arr.last }, is_ship_insured: true)
    end
  rescue StandardError => e
    raise unless e.message == UNSUCCESFUL_STRING
  end

  Result.new(status:, status_message:)
end

#carrier_qualifies_for_rating?(carrier) ⇒ Boolean

Checks whether a carrier name maps to a covered Shipsurance carrier.

Parameters:

  • carrier (String, Symbol, nil)

    carrier name to evaluate

Returns:

  • (Boolean)

    true if the carrier is covered



109
110
111
112
# File 'app/services/shipping/package_shipping_insurance.rb', line 109

def carrier_qualifies_for_rating?(carrier)
  # here we want to return a simple TRUE/FALSE boolean, this is to see if we can use this
  resolve_carrier_id(carrier).present?
end

#get_shipping_insurance_cost_for_delivery(delivery) ⇒ BigDecimal

Calculates the total Shipsurance premium for all shipments on a delivery.

Parameters:

  • delivery (Delivery)

    the delivery to cost

Returns:

  • (BigDecimal)

    total premium across non-blank-carrier shipments



234
235
236
# File 'app/services/shipping/package_shipping_insurance.rb', line 234

def get_shipping_insurance_cost_for_delivery(delivery)
  delivery.shipments.where.not(carrier: nil).sum { |s| get_shipping_insurance_cost_for_shipment(s) }
end

#get_shipping_insurance_cost_for_shipment(shipment) ⇒ BigDecimal

Calculates the Shipsurance premium for a single shipment.

Parameters:

  • shipment (Shipment)

    the shipment to cost

Returns:

  • (BigDecimal)

    premium for the shipment



241
242
243
244
# File 'app/services/shipping/package_shipping_insurance.rb', line 241

def get_shipping_insurance_cost_for_shipment(shipment)
  insured_value = apply_carrier_coverage_limit(shipment)
  ((insured_value / 100.0).ceil.to_f * carrier_percentage_charge(shipment.sanitized_carrier, shipment.delivery.is_international?)).round(2)
end

#get_shipping_insurance_insured_value_for_delivery(delivery) ⇒ BigDecimal

Calculates the total insured declared value for all shipments on a delivery.

Parameters:

  • delivery (Delivery)

    the delivery to evaluate

Returns:

  • (BigDecimal)

    total insured value across non-blank-carrier shipments



249
250
251
# File 'app/services/shipping/package_shipping_insurance.rb', line 249

def get_shipping_insurance_insured_value_for_delivery(delivery)
  delivery.shipments.where.not(carrier: nil).sum { |s| get_shipping_insurance_insured_value_for_shipment(s) }
end

#get_shipping_insurance_insured_value_for_shipment(shipment) ⇒ BigDecimal

Returns the insured declared value for a single shipment.

Parameters:

  • shipment (Shipment)

    the shipment to evaluate

Returns:

  • (BigDecimal)

    insured value after carrier coverage limits



256
257
258
# File 'app/services/shipping/package_shipping_insurance.rb', line 256

def get_shipping_insurance_insured_value_for_shipment(shipment)
  apply_carrier_coverage_limit(shipment)
end

Builds an HTML link to the Shipsurance recorded shipment page.

Parameters:

  • shipment (Shipment)

    the shipment to link to

Returns:

  • (ActiveSupport::SafeBuffer)

    HTML anchor tag



263
264
265
# File 'app/services/shipping/package_shipping_insurance.rb', line 263

def get_shipping_insurance_link_for_shipment(shipment)
  %(<a href='https://app.shipsurance.com/cp/ViewRecordedshipments?RecordedShipmentId=#{shipment.shipping_insurance_record_id}' target='_blank' rel='noopener'>#{shipment.shipping_insurance_record_id}</a>).html_safe
end

#process(delivery, options = {}) ⇒ Result

Returns insurance booking status and message.

Parameters:

Options Hash (options):

  • ship_date (Date)

    ship date to record for each shipment (defaults to the delivery's shipped date)

Returns:

  • (Result)

    insurance booking status and message



71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'app/services/shipping/package_shipping_insurance.rb', line 71

def process(delivery, options = {})
  status = :ok
  status_message = ''
  qualifies_res = qualifies(delivery)
  if qualifies_res.status == :ok
    shipments = delivery.shipments.completed # we look for completed shipments but in theory only shipments in state label_complete will get here. This allows us to ship insure manually completed deliveries via the admin/rails console if desired
    ship_insure_res_arr = shipments.map { |s| book_shipping_insurance_for_shipment(s, options) }
    unless ship_insure_res_arr.all? { |r| r.status == :ok }
      status = :error
      err_msgs = ship_insure_res_arr.select { |r| (r.status != :ok) && r.status_message.present? }.map(&:status_message)
      status_message_add = err_msgs.any? ? ": #{err_msgs.join('. ')}" : '.'
      status_message = "Shipping insurance cannot be booked, any booked shipments have been voided#{status_message_add}"
      shipments.select { |s| s.shipping_insurance_record_id.present? }.map { |s| void_shipping_insurance_for_shipment(s, options) }
    end
  else
    status = qualifies_res.status
    status_message = qualifies_res.status_message
  end
  Result.new(status:, status_message:)
end

#qualifies(delivery) ⇒ Result

Determines whether the delivery is eligible to book shipping insurance.

Parameters:

  • delivery (Delivery)

    the delivery to check

Returns:

  • (Result)

    :ok if eligible, or :error with a reason message



129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'app/services/shipping/package_shipping_insurance.rb', line 129

def qualifies(delivery)
  status = :ok
  status_message = ''
  carrier_present = resolve_carrier_id(delivery.carrier).present?
  any_labeled_shipments = delivery.is_amazon_seller_central_veeqo? ? delivery.shipments.manually_complete.any? : delivery.shipments.label_complete.any?
  all_labeled_shipments_have_tracking_number = delivery.is_amazon_seller_central_veeqo? ? delivery.shipments.manually_complete.all? { |s| s.tracking_number.present? } : delivery.shipments.label_complete.all? { |s| s.tracking_number.present? }
  all_labeled_shipments_do_not_have_shipping_insurance = # multiple invoicing events, yeesh, check that and do not re ship-insure. Here the operational rule is that they all get ship-insured or they all do not (via voiding).
    delivery.shipments.label_complete.all? do |s|
      !s.is_ship_insured
    end
  unless qualifies_for_rating?(delivery) && any_labeled_shipments && all_labeled_shipments_have_tracking_number && all_labeled_shipments_do_not_have_shipping_insurance
    status = :error
    status_message_arr = []
    status_message_arr << "Order #{delivery.order.reference_number} does not qualify for Shipsurance shipping insurance"
    status_message_arr << "Carrier #{delivery.carrier} is not one of our covered Shipsurance carriers" unless carrier_present
    status_message_arr << 'Delivery does not have any labeled shipments' unless any_labeled_shipments
    status_message_arr << 'Not all shipments have tracking numbers' unless all_labeled_shipments_have_tracking_number
    status_message_arr << 'When customer pays for shipping we do not ship insure via Shipsurance' if delivery.bill_shipping_to_customer
    status_message_arr << "When delivery value is less than $#{INSURED_VALUE_MIN.round(2)} we do not ship insure" if delivery.calculate_declared_value.to_f.abs < INSURED_VALUE_MIN
    status_message_arr << 'Delivery is international and via UPS or FedEx, requiring declared value for electronic customs documents' if delivery.is_international_and_ups_or_fedex?
    status_message_arr << 'Delivery shipments already have shipping insurance' unless all_labeled_shipments_do_not_have_shipping_insurance
    status_message = "#{status_message_arr.join('. ')}."
  end
  Result.new(status:, status_message:)
end

#qualifies_for_rating?(delivery) ⇒ Boolean

Checks whether the delivery can be rated for Shipsurance insurance.

Used while a delivery is still in quoting status and may become ship-labeled.

Parameters:

  • delivery (Delivery)

    the delivery to check

Returns:

  • (Boolean)

    true if the delivery qualifies for rating



98
99
100
101
102
103
# File 'app/services/shipping/package_shipping_insurance.rb', line 98

def qualifies_for_rating?(delivery)
  # here we want to return a simple TRUE/FALSE boolean, this is to see if we can use this when the delivery, in quoting status, gets ship-labeled
  carrier_present = resolve_carrier_id(delivery.carrier).present? && delivery.shipments.where.not(carrier: nil).all? { |s| resolve_carrier_id(s.sanitized_carrier, shipment: s).present? }
  !delivery.bill_shipping_to_customer? && !delivery.is_international_and_ups_or_fedex? && (delivery.supported_shipping_carrier? || delivery.is_amazon_seller_central_veeqo?) && carrier_present && (delivery.calculate_declared_value.to_f.abs > INSURED_VALUE_MIN) && !bill_shipping_to_customer_and_not_a_store_transfer?(delivery) && !(delivery.shipments.completed.any? && # if billing to customer (not including STs for which this is true), let them pay for declared value via carrier, we also need any international shipments via UPS or FedEx, SOs or STs to use declared value so we have an electronic commercial invoice via FedEx and UPS, check that and do not re ship-insure
delivery.shipments.completed.all?(&:is_ship_insured))
end

#void_shipping_insurance_for_shipment(shipment, _options) ⇒ Result

Voids a previously booked shipping insurance record for a shipment.

Parameters:

  • shipment (Shipment)

    the shipment whose insurance should be voided

  • _options (Hash)

    unused options hash kept for API compatibility

Returns:

  • (Result)

    void status and message



206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'app/services/shipping/package_shipping_insurance.rb', line 206

def void_shipping_insurance_for_shipment(shipment, _options)
  return Result.new(status: :error, status_message: 'shipped_date is nil — cannot void shipment') if shipment.delivery.shipped_date.nil?
  return Result.new(status: :error, status_message: 'shipping_insurance_record_id is nil — nothing to void') if shipment.shipping_insurance_record_id.blank?

  # Voiding uses api.shipsurance.com/VoidShipment.aspx, NOT the record endpoint.
  # dsi_recordShipment.aspx ignores void params and creates new $0 bookings instead.
  shipped_datetime = shipment.delivery.shipped_date.strftime('%m/%d/%Y %H:%M:%S')
  form_params = {
    recordedShipmentId: shipment.shipping_insurance_record_id,
    extRSVoidReasonId:  '2',
    voidDescription:    'Data entry error — resubmitting with correct declared value',
    shipmentDate:       shipped_datetime,
    extCarrierId:       resolve_carrier_id(shipment.sanitized_carrier, shipment: shipment)
  }

  sleep(0.1.seconds) # quick and dirty rate-limit guard

  raw_body     = shipsurance_client.void_shipment(form_params)
  response_arr = raw_body.split(',')
  status       = response_arr[0] == '1' ? :ok : :error
  status_message = status == :ok ? response_arr[1].to_s : raw_body
  shipment.update(shipping_insurance_data: {}, is_ship_insured: false) if status == :ok
  Result.new(status:, status_message:)
end