Class: Shipping::PackageShippingInsurance

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

Overview

Service object: package shipping insurance.

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
see: https://shipsurance-partner-api.readme.io/reference/extcarrierid-lookup

{
  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'.freeze

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 = {}) ⇒ PackageShippingInsurance

Returns a new instance of PackageShippingInsurance.



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

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"
]



52
53
54
# File 'app/services/shipping/package_shipping_insurance.rb', line 52

def renderer
  @renderer
end

Instance Method Details

#bill_shipping_to_customer_and_not_a_store_transfer?(delivery) ⇒ Boolean

Returns:

  • (Boolean)


89
90
91
92
# File 'app/services/shipping/package_shipping_insurance.rb', line 89

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 = {}) ⇒ Object



120
121
122
123
124
125
126
127
128
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
154
155
156
157
158
159
160
161
# File 'app/services/shipping/package_shipping_insurance.rb', line 120

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

Returns:

  • (Boolean)


84
85
86
87
# File 'app/services/shipping/package_shipping_insurance.rb', line 84

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



188
189
190
# File 'app/services/shipping/package_shipping_insurance.rb', line 188

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



192
193
194
195
# File 'app/services/shipping/package_shipping_insurance.rb', line 192

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



197
198
199
# File 'app/services/shipping/package_shipping_insurance.rb', line 197

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



201
202
203
# File 'app/services/shipping/package_shipping_insurance.rb', line 201

def get_shipping_insurance_insured_value_for_shipment(shipment)
  apply_carrier_coverage_limit(shipment)
end


205
206
207
# File 'app/services/shipping/package_shipping_insurance.rb', line 205

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> <small>(file claim <a href='https://app.shipsurance.com/cp/CreateClaim?recordedShipmentId=#{shipment.shipping_insurance_record_id}' target='_blank' rel='noopener' onclick='return confirm("Are you sure you want to file a claim?")'>here</a>)</small>).html_safe
end

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



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'app/services/shipping/package_shipping_insurance.rb', line 56

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



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

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

Returns:

  • (Boolean)


77
78
79
80
81
82
# File 'app/services/shipping/package_shipping_insurance.rb', line 77

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



163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'app/services/shipping/package_shipping_insurance.rb', line 163

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