Class: Shipping::PackageShippingInsurance

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

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',
  # 'Saia': '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'
}
INSURED_VALUE_MIN =
100.0
UNSUCCESFUL_STRING =
'UnsuccessfulShipInsure'

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods inherited from BaseService

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

Constructor Details

#initialize(options = {}) ⇒ PackageShippingInsurance

Returns a new instance of PackageShippingInsurance.



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

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



47
48
49
# File 'app/services/shipping/package_shipping_insurance.rb', line 47

def renderer
  @renderer
end

Instance Method Details

#bill_shipping_to_customer_and_not_a_store_transfer?(delivery) ⇒ Boolean

Returns:

  • (Boolean)


87
88
89
90
# File 'app/services/shipping/package_shipping_insurance.rb', line 87

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



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

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

  return Result.new(status: :error, status_message: 'shipped_date is nil — cannot report shipment') if shipment.delivery.shipped_date.nil?

  # shipped_date: use shipped_date (same as invoicing date in normal flow).
  # If they diverge it usually means a delivery was stuck in a state transition.
  shipped_date  = shipment.delivery.shipped_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: lambda { |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)


82
83
84
85
# File 'app/services/shipping/package_shipping_insurance.rb', line 82

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



184
185
186
# File 'app/services/shipping/package_shipping_insurance.rb', line 184

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



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

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



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

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



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

def get_shipping_insurance_insured_value_for_shipment(shipment)
  apply_carrier_coverage_limit(shipment)
end


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

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



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'app/services/shipping/package_shipping_insurance.rb', line 52

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 { |r| r.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



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

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)


73
74
75
76
77
78
79
80
# File 'app/services/shipping/package_shipping_insurance.rb', line 73

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? do |s|
s.is_ship_insured
end)
end

#void_shipping_insurance_for_shipment(shipment, options) ⇒ Object



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

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