Class: SpeedeeManifestShipment

Inherits:
ApplicationRecord show all
Defined in:
app/models/speedee_manifest_shipment.rb

Overview

== Schema Information

Table name: speedee_manifest_shipments
Database name: primary

id :integer not null, primary key
aod :boolean default(FALSE)
aod_code :integer
apply_package_handling :boolean default(FALSE)
barcode :string not null
bill_to_shipper_number :string default("029692"), not null
cod :boolean default(FALSE)
cod_no_check :boolean default(FALSE)
cod_value :float default(0.0)
das :boolean default(FALSE), not null
declared_value :float default(0.0)
file_version_number :string default("1"), not null
hazardous :boolean default(FALSE)
height :integer
length :integer
metro :boolean default(FALSE), not null
oversized :boolean default(FALSE)
package_handling_amount :float default(0.0)
pickup_tag :boolean default(FALSE)
reference_1 :string
reference_2 :string
reference_3 :string
reference_4 :string
ship_date :date not null
ship_from_city :string default("Lake Zurich"), not null
ship_from_company_name :string default("WarmlyYours US Warehouse"), not null
ship_from_contact_name :string default("Shipping Department")
ship_from_country :string default("US")
ship_from_email :string default("warehouseusa@warmlyyours.com")
ship_from_phone :string default("8008755285")
ship_from_shipper_number :string default("001234"), not null
ship_from_state :string default("IL"), not null
ship_from_street_1 :string default("590 Telser Rd"), not null
ship_from_street_2 :string default("Suite B")
ship_from_zip :string default("60047"), not null
ship_to_city :string not null
ship_to_company_name :string not null
ship_to_contact_name :string
ship_to_country :string
ship_to_email :string
ship_to_phone :string
ship_to_shipper_number :string
ship_to_state :string not null
ship_to_street_1 :string not null
ship_to_street_2 :string
ship_to_zip :string not null
shipment_weight :boolean default(FALSE), not null
unboxed :boolean default(FALSE), not null
weight :integer not null
width :integer
zone :integer
created_at :datetime not null
updated_at :datetime not null
ship_to_company_id :string
shipment_id :integer not null
speedee_manifest_id :integer

Indexes

index_speedee_manifest_shipments_on_aod (aod)
index_speedee_manifest_shipments_on_aod_code (aod_code)
index_speedee_manifest_shipments_on_apply_package_handling (apply_package_handling)
index_speedee_manifest_shipments_on_barcode (barcode)
index_speedee_manifest_shipments_on_bill_to_shipper_number (bill_to_shipper_number)
index_speedee_manifest_shipments_on_cod (cod)
index_speedee_manifest_shipments_on_cod_no_check (cod_no_check)
index_speedee_manifest_shipments_on_file_version_number (file_version_number)
index_speedee_manifest_shipments_on_hazardous (hazardous)
index_speedee_manifest_shipments_on_oversized (oversized)
index_speedee_manifest_shipments_on_pickup_tag (pickup_tag)
index_speedee_manifest_shipments_on_ship_date (ship_date)
index_speedee_manifest_shipments_on_ship_from_city (ship_from_city)
index_speedee_manifest_shipments_on_ship_from_shipper_number (ship_from_shipper_number)
index_speedee_manifest_shipments_on_ship_from_state (ship_from_state)
index_speedee_manifest_shipments_on_ship_from_zip (ship_from_zip)
index_speedee_manifest_shipments_on_ship_to_city (ship_to_city)
index_speedee_manifest_shipments_on_ship_to_state (ship_to_state)
index_speedee_manifest_shipments_on_ship_to_zip (ship_to_zip)
index_speedee_manifest_shipments_on_shipment_id (shipment_id)
speedee_manifest_id_aod (speedee_manifest_id,aod)
speedee_manifest_id_cod (speedee_manifest_id,cod)
speedee_manifest_id_dv (speedee_manifest_id,declared_value)
speedee_manifest_id_hazardous (speedee_manifest_id,hazardous)
speedee_manifest_id_oversized (speedee_manifest_id,oversized)
speedee_manifest_id_pickup_tag (speedee_manifest_id,pickup_tag)
speedee_manifest_id_ship_from_zip (speedee_manifest_id,ship_from_zip)

Constant Summary

Constants included from Models::Schedulable

Models::Schedulable::SIMPLE_FORM_OPTIONS

Belongs to collapse

Delegated Instance Attributes collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from ApplicationRecord

ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation

Methods included from Models::Schedulable

config

Methods included from Models::AfterCommittable

#after_commit

Methods included from Models::EventPublishable

#publish_event

Class Method Details

.build_entries!(shipments, ship_datetime) ⇒ void

This method returns an undefined value.

Creates one SpeedeeManifestShipment per package for the given shipments.
Called by create_entries_and_manifest above.

Parameters:

  • shipments (Array<Shipment>)
  • ship_datetime (Time)


279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
# File 'app/models/speedee_manifest_shipment.rb', line 279

def self.build_entries!(shipments, ship_datetime)
   = Heatwave::Configuration.fetch(:speedee_delivery_account_number)
  shipments.sort_by(&:id).each do |shipment|
    attrs = shipment.delivery ? delivery_ship_to(shipment.delivery) : standalone_ship_to(shipment.standalone_delivery)
    value = shipment.compute_shipment_declared_value
    declared_value = value > Shipping::SpeedeeDelivery::DECLARED_VALUE_MIN ? [value, Shipping::SpeedeeDelivery::DECLARED_VALUE_MAX].min : 0.0
    signature = shipment.delivery&.signature_confirmation
    # Round, don't ceil: Shipping::SpeedeeDelivery#dispatch_science_packages
    # sends `.round` to the carrier, so ceiling here made the driver summary
    # describe a shipment Spee-Dee had no record of — a 30.4 lb carton
    # printed as 31 against their 30 (SD6222187, 2026-07-31). The sheet must
    # describe the shipment AS BOOKED.
    row = new(attrs.merge(
                ship_from_shipper_number: ,
                weight: shipment.weight.round,
                length: shipment.length.ceil,
                width: shipment.width.ceil,
                height: shipment.height.ceil,
                barcode: shipment.tracking_number,
                oversized: shipment.is_speedee_oversize?,
                aod: signature.present?,
                aod_code: signature.present? ? 3 : nil, # AOD Direct = code 3
                declared_value: declared_value.round(2),
                ship_date: ship_datetime.to_date,
                bill_to_shipper_number: ,
                shipment: shipment
              ))
    row.apply_carrier_classification
    row.save!
  end
end

.create_entries_and_manifest(shipments) ⇒ Hash{Symbol=>Object}

Creates the daily manifest from API-booked shipments. Nothing is
transmitted, now or ever: under the Dispatch Science "ShipIt" API each
shipment is already registered with Spee-Dee at order-create time, and
Spee-Dee reprocesses the printed driver summary internally for billing, so
sending a CSV as well would double-register every parcel (Hunter Boelz,
Spee-Dee IT, 2026-07-17). The old FTP path was deleted rather than left
dormant — there is no rollback to a transmitting manifest.

The SpeedeeManifest and its shipments are still persisted, and they are the
sole source for the driver summary PDF and the /speedee_manifests listing:
the printed sheet must match Spee-Dee's dashboard and the parcels on the
truck, which is why a void deletes rows here (see
SpeedeeManifest.drop_voided_shipments!).
See doc/tasks/202607011617_SPEEDEE_DISPATCH_SCIENCE_MIGRATION.md.

Parameters:

Returns:

  • (Hash{Symbol=>Object})

    success flag, speedee_manifest, and message



235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
# File 'app/models/speedee_manifest_shipment.rb', line 235

def self.create_entries_and_manifest(shipments)
  return { success: false, speedee_manifest: nil, message: 'No additional shipments for deliveries in pending manifest completion to manifest!' } if shipments.blank?

  ship_datetime = Time.current
  # Scope EVERY mutation to exactly the supplied shipments. Selecting by
  # delivery state instead would be global (all pending-manifest deliveries,
  # any store) and could delete or absorb another store's / another run's
  # in-flight entries. Keying off shipment_id confines the run.
  scoped = where(shipment_id: shipments.map(&:id))
  speedee_manifest = nil
  # rescue is OUTSIDE the transaction on purpose: an exception (or the
  # count-mismatch raise below) must propagate so the whole run rolls back
  # atomically — a rescue INSIDE would let the transaction commit partial
  # entries / orphaned associations. ponytail: no advisory lock — this is a
  # manual once-a-day close; the scoping + atomic transaction already prevent
  # cross-run absorption. Add a per-store lock only if double-close shows up.
  transaction do
    scoped.destroy_all # clear any stale unmanifested rows for these shipments
    build_entries!(shipments, ship_datetime)
    created = scoped.count
    raise "Speedee manifest entry count (#{created}) does not match shipment count (#{shipments.size})" unless created == shipments.size

    # filename is the manifest identifier only — no file is written or
    # transmitted (the legacy XXXXXX.YYYYMMDDHHMMSS shape, minus .CSV).
    manifest_number = "#{Heatwave::Configuration.fetch(:speedee_delivery_account_number)}.#{ship_datetime.strftime('%Y%m%d%H%M%S')}"
    speedee_manifest = SpeedeeManifest.create!(transmitted_at: ship_datetime, filename: manifest_number)
    scoped.update_all(speedee_manifest_id: speedee_manifest.id)
    # Only order-backed Deliveries carry the manifest_completed transition;
    # StandaloneDeliveries have their own (ship_labeled) lifecycle and are
    # manifested without a state change.
    scoped.filter_map { |sms| sms.shipment&.delivery }.uniq.each(&:manifest_completed!)
  end
  { success: true, speedee_manifest:, message: '' }
rescue StandardError => e
  ErrorReporting.error(e)
  { success: false, speedee_manifest: nil, message: e.to_s }
end

.delivery_ship_to(delivery) ⇒ Hash

Ship-to columns for an order-backed Delivery.

Parameters:

Returns:

  • (Hash)


314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
# File 'app/models/speedee_manifest_shipment.rb', line 314

def self.delivery_ship_to(delivery)
  order = delivery.order
  a = order.ship_to_attributes
  {
    ship_to_company_name: a[:name],
    ship_to_contact_name: a[:attention_name],
    ship_to_street_1: a[:address].street1,
    ship_to_street_2: a[:address].street2,
    ship_to_city: a[:address].city,
    ship_to_state: a[:address].state_code,
    ship_to_zip: a[:address].zip_5,
    ship_to_country: a[:address].country.iso,
    ship_to_email: a[:email],
    ship_to_phone: PhoneNumber.parse_and_format(a[:phone], display_format: :strict_10),
    reference_1: order.reference_number,
    reference_2: order.po_number
  }
end

.metroActiveRecord::Relation<SpeedeeManifestShipment>

A relation of SpeedeeManifestShipments that are metro. Active Record Scope

Returns:

See Also:



105
# File 'app/models/speedee_manifest_shipment.rb', line 105

scope :metro, -> { where(metro: true) }

.regularActiveRecord::Relation<SpeedeeManifestShipment>

A relation of SpeedeeManifestShipments that are regular. Active Record Scope

Returns:

See Also:



104
# File 'app/models/speedee_manifest_shipment.rb', line 104

scope :regular, -> { where(metro: false, shipment_weight: false) }

.standalone_ship_to(standalone_delivery) ⇒ Hash

Ship-to columns for a StandaloneDelivery (no Order behind it). Mirrors what
WyShipping.populate_shipper_data_for_standalone_delivery sends the carrier,
so the manifest line matches the label the driver is holding.

Parameters:

Returns:

  • (Hash)


338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
# File 'app/models/speedee_manifest_shipment.rb', line 338

def self.standalone_ship_to(standalone_delivery)
  address = standalone_delivery.destination_address
  {
    # ship_to_company_name is NOT NULL — fall back to the person when the
    # destination is an individual (company_name returns nil for a person).
    ship_to_company_name: standalone_delivery.company_name.presence || standalone_delivery.person_name.presence || 'n/a',
    ship_to_contact_name: standalone_delivery.person_name,
    ship_to_street_1: address&.street1,
    ship_to_street_2: address&.street2,
    ship_to_city: address&.city,
    ship_to_state: address&.state_code,
    ship_to_zip: address&.zip_5,
    ship_to_country: address&.country&.iso,
    ship_to_email: nil,
    ship_to_phone: PhoneNumber.parse_and_format(address.try(:phone), display_format: :strict_10),
    reference_1: "STANDALONE #{standalone_delivery.id}",
    reference_2: standalone_delivery.reference_number
  }
end

.weightActiveRecord::Relation<SpeedeeManifestShipment>

A relation of SpeedeeManifestShipments that are weight. Active Record Scope

Returns:

See Also:



106
# File 'app/models/speedee_manifest_shipment.rb', line 106

scope :weight, -> { where(shipment_weight: true) }

.with_aodActiveRecord::Relation<SpeedeeManifestShipment>

A relation of SpeedeeManifestShipments that are with aod. Active Record Scope

Returns:

See Also:



111
# File 'app/models/speedee_manifest_shipment.rb', line 111

scope :with_aod, -> { where(aod: true) }

.with_codActiveRecord::Relation<SpeedeeManifestShipment>

A relation of SpeedeeManifestShipments that are with cod. Active Record Scope

Returns:

See Also:



110
# File 'app/models/speedee_manifest_shipment.rb', line 110

scope :with_cod, -> { where(cod: true) }

.with_dasActiveRecord::Relation<SpeedeeManifestShipment>

A relation of SpeedeeManifestShipments that are with das. Active Record Scope

Returns:

See Also:



114
# File 'app/models/speedee_manifest_shipment.rb', line 114

scope :with_das, -> { where(das: true) }

.with_declared_valueActiveRecord::Relation<SpeedeeManifestShipment>

A relation of SpeedeeManifestShipments that are with declared value. Active Record Scope

Returns:

See Also:



108
# File 'app/models/speedee_manifest_shipment.rb', line 108

scope :with_declared_value, -> { where('speedee_manifest_shipments.declared_value > 0.0') }

.with_hazardousActiveRecord::Relation<SpeedeeManifestShipment>

A relation of SpeedeeManifestShipments that are with hazardous. Active Record Scope

Returns:

See Also:



112
# File 'app/models/speedee_manifest_shipment.rb', line 112

scope :with_hazardous, -> { where(hazardous: true) }

.with_oversizedActiveRecord::Relation<SpeedeeManifestShipment>

A relation of SpeedeeManifestShipments that are with oversized. Active Record Scope

Returns:

See Also:



113
# File 'app/models/speedee_manifest_shipment.rb', line 113

scope :with_oversized, -> { where(oversized: true) }

.with_pickup_tagActiveRecord::Relation<SpeedeeManifestShipment>

A relation of SpeedeeManifestShipments that are with pickup tag. Active Record Scope

Returns:

See Also:



109
# File 'app/models/speedee_manifest_shipment.rb', line 109

scope :with_pickup_tag, -> { where(pickup_tag: true) }

.with_shipment_codActiveRecord::Relation<SpeedeeManifestShipment>

A relation of SpeedeeManifestShipments that are with shipment cod. Active Record Scope

Returns:

See Also:



116
# File 'app/models/speedee_manifest_shipment.rb', line 116

scope :with_shipment_cod, -> { where('1=0') }

.with_unboxedActiveRecord::Relation<SpeedeeManifestShipment>

A relation of SpeedeeManifestShipments that are with unboxed. Active Record Scope

Returns:

See Also:



115
# File 'app/models/speedee_manifest_shipment.rb', line 115

scope :with_unboxed, -> { where(unboxed: true) }

Instance Method Details

#apply_carrier_classificationvoid

This method returns an undefined value.

Freezes the carrier classification into the row at build time so the
printed sheet can't drift on reprint. Flags come from the Dispatch Science
quote's charge lines (a fee > 0 means Spee-Dee actually billed it); zone
comes from the SpeedeeZone table (the API's zone IDs don't map to the
printed ZN numbers — see #resolved_zone). Standalone parcels have no
readable breakdown (see #speedee_rate_data), so their flags stay false.



195
196
197
198
199
200
201
202
203
204
205
206
# File 'app/models/speedee_manifest_shipment.rb', line 195

def apply_carrier_classification
  fees = speedee_rate_data['extra_charges_hash'] || {}
  assign_attributes(
    zone: SpeedeeZone.get_zone_from_zip(ship_to_zip),
    das: fees['das'].to_f.positive?,
    metro: fees['metro'].to_f.positive?,
    shipment_weight: fees['shipment_weight'].to_f.positive?,
    unboxed: fees['unboxed'].to_f.positive?,
    pickup_tag: fees['pickup_tag'].to_f.positive?,
    hazardous: fees['hazardous'].to_f.positive?
  )
end

#deliveryObject

Alias for Shipment#delivery

Returns:

  • (Object)

    Shipment#delivery

See Also:



118
# File 'app/models/speedee_manifest_shipment.rb', line 118

delegate :delivery, to: :shipment

#mark_delivery_as_manifested!void

This method returns an undefined value.



122
123
124
# File 'app/models/speedee_manifest_shipment.rb', line 122

def mark_delivery_as_manifested!
  delivery.manifest_completed!
end

#orderObject

Alias for Shipment#order

Returns:

  • (Object)

    Shipment#order

See Also:



119
# File 'app/models/speedee_manifest_shipment.rb', line 119

delegate :order, to: :shipment

#parcel_parentDelivery, ...

The record this manifest line belongs to — a Delivery for order shipments,
a StandaloneDelivery for one-off CRM shipments. Both ride the same driver
summary: the driver physically takes both, so Spee-Dee's paperwork has to
list both (Ramie, 2026-07-29).

Returns:



132
133
134
# File 'app/models/speedee_manifest_shipment.rb', line 132

def parcel_parent
  shipment&.delivery || shipment&.standalone_delivery
end

#resolved_zoneInteger?

Zone for the printed sheet: the value frozen into the row at build time,
falling back to the SpeedeeZone table for rows predating the zone column.
The Dispatch Science quote's fromZone/toZone are the pricing engine's
internal sub-zone IDs (e.g. "31", "1_290"), NOT the ZN 1–8 zones Spee-Dee
prints — verified in production 2026-07-29 — so the table remains the only
source for the printed number.

Returns:

  • (Integer, nil)


183
184
185
# File 'app/models/speedee_manifest_shipment.rb', line 183

def resolved_zone
  zone || SpeedeeZone.get_zone_from_zip(ship_to_zip)
end

#shipmentShipment?

Returns:



96
# File 'app/models/speedee_manifest_shipment.rb', line 96

belongs_to :shipment, optional: true

#speedee_manifestSpeedeeManifest?

Returns:



98
# File 'app/models/speedee_manifest_shipment.rb', line 98

belongs_to :speedee_manifest, optional: true

#speedee_rate_dataHash

Carrier rate data for the parent, normalized across the two shapes:
Delivery keeps it on the selected ShippingCost record; StandaloneDelivery
keeps the chosen entry inside its shipping_rates JSON array.

Returns:

  • (Hash)

    rate data, or {} when unavailable.



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
# File 'app/models/speedee_manifest_shipment.rb', line 141

def speedee_rate_data
  parent = parcel_parent
  case parent
  when Delivery
    # base_price MUST be the label-time charge, not the pre-pack quote.
    # selected_shipping_cost holds what the rate screen quoted before the
    # warehouse packed — on 2026-07-31 that put $92.51 on the driver summary
    # against Spee-Dee's own $60.96, because the estimate assumed bigger
    # cartons than shipped. Shipping::SpeedeeDelivery#label re-rates against
    # the packed cartons and that total lands on actual_shipping_cost; the
    # Dispatch Science order's ITEMPRICE charge matches it to the cent
    # (verified against SD6222078 / SD6222187 / SD6225002).
    #
    # The extras hash still comes from the quote — it is the only per-fee
    # breakdown we hold, and apply_carrier_classification needs it to decide
    # which section of the sheet a row prints in. The order response returns
    # every accessorial at $0.00 with the whole charge rolled into ITEMPRICE.
    quoted = parent.selected_shipping_cost&.rate_data || {}
    quoted.merge('base_price' => parent.actual_shipping_cost || quoted['base_price'])
  when StandaloneDelivery
    # StandaloneDelivery#shipping_rates is an hstore ARRAY — Postgres hstore
    # stringifies values, so the nested rate_data round-trips as a String
    # (%q({"base_price" => 8.19})), not a Hash, and can't be read for a
    # breakdown. actual_shipping_cost is the reliable figure (persisted from
    # the label response). Consequence: standalone parcels contribute their
    # total to the charge columns but aren't itemized in Additional Charges.
    # Pieces / weight / stop count — what the driver actually signs for — are
    # exact either way.
    { 'base_price' => parent.actual_shipping_cost.to_f, 'extra_charges_hash' => {} }
  else
    {}
  end
end

#stop_keyString

Stop identity for the driver summary's Stop Count — one stop per distinct
ship-to address. Uses the denormalized columns on this row so it works
for both parents (the old implementation reached through to Order, which
standalone shipments don't have).

Returns:

  • (String)


214
215
216
# File 'app/models/speedee_manifest_shipment.rb', line 214

def stop_key
  [ship_to_street_1, ship_to_street_2, ship_to_city, ship_to_state, ship_to_zip].join('|')
end