Class: SpeedeeManifestShipment
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- SpeedeeManifestShipment
- 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
-
#delivery ⇒ Object
Alias for Shipment#delivery.
-
#order ⇒ Object
Alias for Shipment#order.
Class Method Summary collapse
-
.build_entries!(shipments, ship_datetime) ⇒ void
Creates one SpeedeeManifestShipment per package for the given shipments.
-
.create_entries_and_manifest(shipments) ⇒ Hash{Symbol=>Object}
Creates the daily manifest from API-booked shipments.
-
.delivery_ship_to(delivery) ⇒ Hash
Ship-to columns for an order-backed Delivery.
-
.metro ⇒ ActiveRecord::Relation<SpeedeeManifestShipment>
A relation of SpeedeeManifestShipments that are metro.
-
.regular ⇒ ActiveRecord::Relation<SpeedeeManifestShipment>
A relation of SpeedeeManifestShipments that are regular.
-
.standalone_ship_to(standalone_delivery) ⇒ Hash
Ship-to columns for a StandaloneDelivery (no Order behind it).
-
.weight ⇒ ActiveRecord::Relation<SpeedeeManifestShipment>
A relation of SpeedeeManifestShipments that are weight.
-
.with_aod ⇒ ActiveRecord::Relation<SpeedeeManifestShipment>
A relation of SpeedeeManifestShipments that are with aod.
-
.with_cod ⇒ ActiveRecord::Relation<SpeedeeManifestShipment>
A relation of SpeedeeManifestShipments that are with cod.
-
.with_das ⇒ ActiveRecord::Relation<SpeedeeManifestShipment>
A relation of SpeedeeManifestShipments that are with das.
-
.with_declared_value ⇒ ActiveRecord::Relation<SpeedeeManifestShipment>
A relation of SpeedeeManifestShipments that are with declared value.
-
.with_hazardous ⇒ ActiveRecord::Relation<SpeedeeManifestShipment>
A relation of SpeedeeManifestShipments that are with hazardous.
-
.with_oversized ⇒ ActiveRecord::Relation<SpeedeeManifestShipment>
A relation of SpeedeeManifestShipments that are with oversized.
-
.with_pickup_tag ⇒ ActiveRecord::Relation<SpeedeeManifestShipment>
A relation of SpeedeeManifestShipments that are with pickup tag.
-
.with_shipment_cod ⇒ ActiveRecord::Relation<SpeedeeManifestShipment>
A relation of SpeedeeManifestShipments that are with shipment cod.
-
.with_unboxed ⇒ ActiveRecord::Relation<SpeedeeManifestShipment>
A relation of SpeedeeManifestShipments that are with unboxed.
Instance Method Summary collapse
-
#apply_carrier_classification ⇒ void
Freezes the carrier classification into the row at build time so the printed sheet can't drift on reprint.
- #mark_delivery_as_manifested! ⇒ void
-
#parcel_parent ⇒ Delivery, ...
The record this manifest line belongs to — a Delivery for order shipments, a StandaloneDelivery for one-off CRM shipments.
-
#resolved_zone ⇒ Integer?
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.
-
#speedee_rate_data ⇒ Hash
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_ratesJSON array. -
#stop_key ⇒ String
Stop identity for the driver summary's Stop Count — one stop per distinct ship-to address.
Methods inherited from ApplicationRecord
ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation
Methods included from Models::Schedulable
Methods included from Models::AfterCommittable
Methods included from Models::EventPublishable
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.
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) account = 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: account, 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: account, 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.
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.
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 |
.metro ⇒ ActiveRecord::Relation<SpeedeeManifestShipment>
A relation of SpeedeeManifestShipments that are metro. Active Record Scope
105 |
# File 'app/models/speedee_manifest_shipment.rb', line 105 scope :metro, -> { where(metro: true) } |
.regular ⇒ ActiveRecord::Relation<SpeedeeManifestShipment>
A relation of SpeedeeManifestShipments that are regular. Active Record Scope
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.
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 |
.weight ⇒ ActiveRecord::Relation<SpeedeeManifestShipment>
A relation of SpeedeeManifestShipments that are weight. Active Record Scope
106 |
# File 'app/models/speedee_manifest_shipment.rb', line 106 scope :weight, -> { where(shipment_weight: true) } |
.with_aod ⇒ ActiveRecord::Relation<SpeedeeManifestShipment>
A relation of SpeedeeManifestShipments that are with aod. Active Record Scope
111 |
# File 'app/models/speedee_manifest_shipment.rb', line 111 scope :with_aod, -> { where(aod: true) } |
.with_cod ⇒ ActiveRecord::Relation<SpeedeeManifestShipment>
A relation of SpeedeeManifestShipments that are with cod. Active Record Scope
110 |
# File 'app/models/speedee_manifest_shipment.rb', line 110 scope :with_cod, -> { where(cod: true) } |
.with_das ⇒ ActiveRecord::Relation<SpeedeeManifestShipment>
A relation of SpeedeeManifestShipments that are with das. Active Record Scope
114 |
# File 'app/models/speedee_manifest_shipment.rb', line 114 scope :with_das, -> { where(das: true) } |
.with_declared_value ⇒ ActiveRecord::Relation<SpeedeeManifestShipment>
A relation of SpeedeeManifestShipments that are with declared value. Active Record Scope
108 |
# File 'app/models/speedee_manifest_shipment.rb', line 108 scope :with_declared_value, -> { where('speedee_manifest_shipments.declared_value > 0.0') } |
.with_hazardous ⇒ ActiveRecord::Relation<SpeedeeManifestShipment>
A relation of SpeedeeManifestShipments that are with hazardous. Active Record Scope
112 |
# File 'app/models/speedee_manifest_shipment.rb', line 112 scope :with_hazardous, -> { where(hazardous: true) } |
.with_oversized ⇒ ActiveRecord::Relation<SpeedeeManifestShipment>
A relation of SpeedeeManifestShipments that are with oversized. Active Record Scope
113 |
# File 'app/models/speedee_manifest_shipment.rb', line 113 scope :with_oversized, -> { where(oversized: true) } |
.with_pickup_tag ⇒ ActiveRecord::Relation<SpeedeeManifestShipment>
A relation of SpeedeeManifestShipments that are with pickup tag. Active Record Scope
109 |
# File 'app/models/speedee_manifest_shipment.rb', line 109 scope :with_pickup_tag, -> { where(pickup_tag: true) } |
.with_shipment_cod ⇒ ActiveRecord::Relation<SpeedeeManifestShipment>
A relation of SpeedeeManifestShipments that are with shipment cod. Active Record Scope
116 |
# File 'app/models/speedee_manifest_shipment.rb', line 116 scope :with_shipment_cod, -> { where('1=0') } |
.with_unboxed ⇒ ActiveRecord::Relation<SpeedeeManifestShipment>
A relation of SpeedeeManifestShipments that are with unboxed. Active Record Scope
115 |
# File 'app/models/speedee_manifest_shipment.rb', line 115 scope :with_unboxed, -> { where(unboxed: true) } |
Instance Method Details
#apply_carrier_classification ⇒ void
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 |
#delivery ⇒ Object
Alias for Shipment#delivery
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 |
#order ⇒ Object
Alias for Shipment#order
119 |
# File 'app/models/speedee_manifest_shipment.rb', line 119 delegate :order, to: :shipment |
#parcel_parent ⇒ Delivery, ...
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).
132 133 134 |
# File 'app/models/speedee_manifest_shipment.rb', line 132 def parcel_parent shipment&.delivery || shipment&.standalone_delivery end |
#resolved_zone ⇒ Integer?
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.
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 |
#shipment ⇒ Shipment?
96 |
# File 'app/models/speedee_manifest_shipment.rb', line 96 belongs_to :shipment, optional: true |
#speedee_manifest ⇒ SpeedeeManifest?
98 |
# File 'app/models/speedee_manifest_shipment.rb', line 98 belongs_to :speedee_manifest, optional: true |
#speedee_rate_data ⇒ Hash
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.
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_key ⇒ String
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).
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 |