Class: Order

Overview

== Schema Information

Table name: orders
Database name: primary

id :integer not null, primary key
abandoned_cart_reminder :enum default("reminder_not_sent"), not null
actual_shipping_cost :decimal(10, 2)
allow_advance_credit_without_cc :boolean
amazon_checkout_session :string
attention_name_override :string(255)
bill_shipping_to_customer :boolean
billing_emails :string is an Array
bo_notification_sent_at :datetime
bulk_buy_request :boolean
cancellation_reason :string
cod_collection_type :string(255)
completion_date :date
credit_request_state :string
currency :string(255)
custom_order_agreement_status :enum default("custom_order_agreement_not_required"), not null
customer_reference :string(255)
disable_auto_coupon :boolean default(FALSE), not null
discount_adjustment_needed :boolean
do_not_reserve_stock :boolean default(FALSE), not null
early_label_metadata :jsonb
edi_auto_cancel_options :jsonb
edi_channel_order_support :string
edi_delayed_delivery_acknowledged_at :datetime
edi_is_pick_slip_required :boolean default(FALSE), not null
edi_orchestrator_partner :string
edi_order_date :date
edi_original_order_message :text
edi_original_ship_code :string
edi_po_number :string
edi_shipping_option_name :string
exclude_from_payment_check :boolean default(FALSE), not null
exported :boolean default(FALSE), not null
first_cart_view_date :datetime
first_review_request_processed_date :datetime
first_view_date :datetime
future_release_date :date
google_campaign_attribution :jsonb
google_conversion_meta :jsonb
hold_bo_release :boolean default(FALSE), not null
incorrectly_packaged_ups_canada_order :boolean
incorrectly_packaged_ups_canada_order_fixed :boolean
is_manual_hold :boolean default(FALSE), not null
jde_b_ab :integer
jde_s_ab :integer
label_instructions :string(255)
line_offset :decimal(10, 2)
line_total :decimal(10, 2)
ltl_freight :boolean
ltl_freight_guaranteed :boolean
manual_release_only :boolean
max_discount_override :decimal(4, 2)
min_profit_markup :integer default(0)
non_commissionable :boolean default(FALSE), not null
openai_ads_conversion_meta :jsonb not null
order_reception_type :string(255) default("CRM"), not null
order_type :string(255) default("SO")
override_coupon_date :date
override_coupon_date_without_limits :boolean default(FALSE), not null
override_line_lock :boolean default(FALSE), not null
packaged_items_md5_hash :string(255)
pickup :boolean
pinterest_conversion_meta :jsonb
precreate_rma :boolean default(FALSE), not null
price_match :decimal(10, 2)
pricing_program_description :string(255)
pricing_program_discount :integer
profit_total :decimal(10, 2)
purchase_label_early :boolean default(FALSE), not null
recalculate_discounts :boolean default(FALSE), not null
recalculate_shipping :boolean default(TRUE), not null
reference_number :string(255)
requested_deliver_by :date
requested_ship_before :date
requested_ship_on_or_after :date
retrieving_shipping_costs :boolean
return_label :boolean default(FALSE), not null
revenue_consolidated_at_time_of_checkout :decimal(10, 2)
review_request_processed :boolean default(FALSE), not null
review_snoozed_until :datetime
reviewed :boolean default(FALSE), not null
rma_reference :string(255)
sales_support_commission_date :date
saturday_delivery :boolean default(FALSE), not null
second_review_request_processed :boolean default(FALSE), not null
second_review_request_processed_date :datetime
shipment_instructions :text
shipment_reference_number :string
shipped_date :date
shipping_cost :decimal(10, 2)
shipping_cost_at_time_of_checkout :decimal(10, 2)
shipping_coupon :decimal(10, 2)
shipping_issue_alerted :boolean
shipping_method :string(255)
shipping_phone :string(255)
ships_economy :boolean default(FALSE), not null
signature_confirmation :boolean default(FALSE), not null
single_origin :boolean default(FALSE), not null
smartset_redemption_code :string(255)
sold_to_billing_address :integer
spiff_state :string(255)
state :string(255)
suggested_packaging_text :text
tax_date :date
tax_exempt :boolean default(FALSE), not null
tax_offset :decimal(10, 2)
tax_total :decimal(10, 2)
taxable_total :decimal(12, 2)
total :decimal(10, 2)
tracking_email :string(255) default([]), is an Array
transmission_email :string(255) default([]), is an Array
transmission_fax :string(255) default([]), is an Array
txid :uuid
uploads_count :integer default(0)
who_will_install :string
created_at :datetime
updated_at :datetime
buying_group_id :integer
contact_id :integer
creator_id :integer
customer_id :integer
default_credit_card_vault_id :integer
deleter_id :integer
edi_transaction_id :string
from_store_id :integer
local_sales_rep_id :integer
opportunity_id :integer
parent_id :integer
primary_sales_rep_id :integer
purchase_order_id :integer
quote_id :integer
resource_tax_rate_id :integer
rma_id :integer
sales_support_rep_id :integer
secondary_sales_rep_id :integer
shipping_account_number_id :integer
shipping_address_id :integer
source_id :integer
spiff_enrollment_id :integer
spiff_rep_id :integer
tax_exemption_id :integer
to_store_id :integer
tracking_event_id :uuid
updater_id :integer
visit_id :bigint

Indexes

idx_customer_id_order_type_created_at (customer_id,order_type,created_at)
idx_customer_id_order_type_state (customer_id,order_type,state)
idx_customer_id_review_snoozed_until (customer_id,review_snoozed_until)
idx_customer_id_state_shipped_date (customer_id,state,shipped_date)
idx_opportunity_id_state (opportunity_id,state)
idx_order_type_shipping_address_id (order_type,shipping_address_id)
idx_shipping_address_id_state (shipping_address_id,state)
index_orders_on_contact_id (contact_id) USING hash
index_orders_on_customer_id (customer_id) USING hash
index_orders_on_customer_reference (customer_reference) USING gin
index_orders_on_edi_po_number (edi_po_number) USING hash
index_orders_on_google_campaign_attribution (google_campaign_attribution) USING gin
index_orders_on_google_conversion_meta (google_conversion_meta) USING gin
index_orders_on_google_conversion_meta_result (((google_conversion_meta ->> 'result'::text)))
index_orders_on_opportunity_id (opportunity_id) USING hash
index_orders_on_packaged_items_md5_hash (packaged_items_md5_hash) USING hash
index_orders_on_parent_id (parent_id)
index_orders_on_purchase_order_id (purchase_order_id) USING hash
index_orders_on_quote_id (quote_id) USING hash
index_orders_on_reference_number (reference_number) UNIQUE
index_orders_on_rma_id (rma_id) USING hash
index_orders_on_sales_support_rep_id (sales_support_rep_id)
index_orders_on_shipped_date (shipped_date) USING brin
index_orders_on_shipping_address_id (shipping_address_id) USING hash
index_orders_on_smartset_redemption_code (smartset_redemption_code) WHERE (smartset_redemption_code IS NOT NULL) USING hash
index_orders_on_source_id (source_id) USING hash
index_orders_on_spiff_enrollment_id (spiff_enrollment_id) WHERE (spiff_enrollment_id IS NOT NULL) USING hash
index_orders_on_spiff_rep_id (spiff_rep_id) WHERE (spiff_rep_id IS NOT NULL) USING hash
index_orders_on_state (state) USING hash
index_orders_on_tax_exemption_id (tax_exemption_id) WHERE (tax_exemption_id IS NOT NULL) USING hash
index_orders_on_txid (txid) USING hash
index_orders_on_updated_at (updated_at) USING brin
index_orders_on_visit_id (visit_id) WHERE (visit_id IS NOT NULL) USING hash
orders_unique_edi_transaction_id_by_customer (customer_id,edi_transaction_id) UNIQUE

Foreign Keys

fk_rails_... (contact_id => parties.id) ON DELETE => nullify
fk_rails_... (parent_id => orders.id)
fk_rails_... (purchase_order_id => purchase_orders.id)
fk_rails_... (rma_id => rmas.id) ON DELETE => cascade
fk_rails_... (shipping_address_id => addresses.id)
fk_rails_... (source_id => sources.id)
fk_rails_... (visit_id => visits.id) ON DELETE => nullify
orders_tax_exemption_id_fk (tax_exemption_id => tax_exemptions.id)

Defined Under Namespace

Modules: AuthorizationSplittable, ExpressHoldAlerting Classes: AuthorizationSplitWorker, AuthorizationSplitter, AuthorizationTopupWorker, BackOrderClientNotification, ContactLookup, CrHoldReleaseReconciler, CreateVcProcurementOrdersFromCsv, DefaultTrackingEmailExtractor, FollowUpScheduler, FraudDetector, ItemNotFound, Mover, SendAbandonedCartEmails, SendExpressHoldAlerts, Splitter

Constant Summary collapse

SALES_ORDER =

Sales order.

'SO'
MARKETING_ORDER =

Marketing order.

'MO'
TECH_ORDER =

Tech order.

'TO'
CREDIT_ORDER =

Credit order.

'CO'
STORE_TRANSFER =

Store transfer.

'ST'
SHIPPING_STATES =

Recognised shipping states.

%i[awaiting_deliveries processing_deliveries].freeze
SOLD_STATES =

Recognised sold states.

%i[awaiting_deliveries processing_deliveries partially_invoiced invoiced].freeze
OPEN_FOR_CHANGE_STATES =

Recognised open for change states.

%i[cart in_shipping_estimate pending pending_payment pending_release_authorization in_cr_hold needs_serial_number_reservation cancelled fraudulent awaiting_completed_installation_plans].freeze
OPEN_FOR_TAX_CHANGE_STATES =

Recognised open for tax change states.

%i[cart pending pending_payment pending_release_authorization in_cr_hold needs_serial_number_reservation crm_back_order awaiting_completed_installation_plans].freeze
CAN_CR_HOLD_STATES =

Recognised can cr hold states.

%i[pending pending_payment awaiting_deliveries processing_deliveries profit_review crm_back_order].freeze
RELEASABLE_STATES =

Recognised releasable states.

%i[cart in_cr_hold request_carrier crm_back_order pending_release_authorization profit_review pending_payment awaiting_deliveries processing_deliveries needs_serial_number_reservation
awaiting_completed_installation_plans].freeze
CANCELABLE_STATES =

Recognised cancelable states.

%i[cart pending awaiting_return profit_review crm_back_order pending_release_authorization pending_payment in_cr_hold].freeze
ROOM_PICKABLE_STATES =

Recognised room pickable states.

%i[crm_back_order fraudulent cancelled pending pending_release_authorization pending_payment in_cr_hold].freeze
CLOSED_STATES =

Recognised closed states.

%i[shipped invoiced cancelled fraudulent].freeze
LOCKED_STATES =

Recognised locked states.

%i[cancelled awaiting_deliveries processing_deliveries shipped partially_invoiced invoiced fraudulent].freeze
CAN_REQUEST_PRE_PACK_STATES =

Recognised can request pre pack states.

%i[pending pending_payment profit_review in_cr_hold needs_serial_number_reservation].freeze
LIVE_PAYMENT_STATES =

Payment states that still hold real money against the order — an authorization
hold, a capture, or a capture with a partial refund. A currency flip must not
orphan any of these in the outgoing currency (see #live_payments_currency_consistent).

%w[authorized captured partially_refunded].freeze
RESTRICTED_ORDER_TYPES =

Recognised restricted order types.

{ CREDIT_ORDER => 'Credit Order', STORE_TRANSFER => 'Store Transfer' }.freeze
UNRESTRICTED_ORDER_TYPES =

Recognised unrestricted order types.

{ SALES_ORDER => 'Sales Order', MARKETING_ORDER => 'Marketing Order', TECH_ORDER => 'Tech Order' }.freeze
ALL_ORDER_TYPES =

Recognised all order types.

RESTRICTED_ORDER_TYPES.merge(UNRESTRICTED_ORDER_TYPES)
REFERENCE_NUMBER_PATTERN =

Regex pattern matching reference number.

Regexp.new("^(#{ALL_ORDER_TYPES.keys.join('|')})(\\d+)$", Regexp::IGNORECASE)
ALL_STATES =

Recognised all states.

Order.state_machines[:state].states.map(&:name).freeze
NON_CART_STATES =

Recognised non cart states.

ALL_STATES - [:cart]
SAME_DAY_CUTOFF_HOUR =

Customer-facing same-day-shipping cutoff, in warehouse-local hours
(noon CT for the US warehouse, noon ET for the CA warehouse). The
actual carrier pickup cutoffs in Store::CARRIER_PICKUP_CUT_OFF_TIMES
run later; noon is the conservative customer promise.

12
EARLY_LABEL_TRANSIENT_ERROR_PATTERN =

Marketplace error codes/messages that are transient (server-side, self-resolving) rather
than a bad request on our end. Amazon Buy Shipping's InternalFailure and similar 5xx-style
faults routinely clear within seconds — the normal (non-early) label purchase at ship-confirm
time succeeds without any human action if we just try again, so retrying here avoids paging
the warehouse over a blip that fixes itself.

/InternalFailure|InternalServerError|ServiceUnavailable|RequestThrottled|Timeout/i
EARLY_LABEL_CREATE_LABEL_MAX_ATTEMPTS =

Total attempts (including the first) before giving up and sending the failure notification

3
EARLY_LABEL_CREATE_LABEL_RETRY_DELAY_SECONDS =

Delay between retry attempts

2
EARLY_LABEL_RAPID_VOID_THRESHOLD_MINUTES =

Minimum time (in minutes) that must pass after early label purchase before auto-void is allowed
This prevents the race condition where label is voided before PDF is fully processed

3
AMAZON_OVER_FULFILL_PATTERN =

Regex pattern matching amazon over fulfill.

/Attempting to over-fulfill item\(s\)[\s\S]*?Existing shipmentIds for order:\[([^\]]+)\]/i

Constants included from Models::InventoryCommittable

Models::InventoryCommittable::STATES_WITH_NO_EXPIRATION

Constants included from Models::Auditable

Models::Auditable::ALWAYS_IGNORED

Constants included from Models::Schedulable

Models::Schedulable::SIMPLE_FORM_OPTIONS

Instance Attribute Summary collapse

Attributes included from Models::Profitable

#min_profit_markup

Attributes included from Models::Itemizable

#account_specialist, #coupons, #discounts, #force_total_reset, #total_reset

Belongs to collapse

Methods included from Models::Auditable

#creator, #updater

Methods included from Models::TaxableResource

#resource_tax_rate

Has one collapse

Has many collapse

Methods included from Models::Pickable

#line_discounts, #line_items

Has and belongs to many collapse

Methods included from Models::MultiRoom

#room_configurations

Delegated Instance Attributes collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Models::SourceAttributable

#has_google_ads_attribution?, #source_event_window_respected, #source_locked?, #source_locked_reason, #visit_with_google_click_id?

Methods included from Models::Lineage

#ancestors, #ancestors_ids, #children_and_roots, #descendants, #descendants_ids, #ensure_non_recursive_lineage, #family_members, #generate_full_name, #generate_full_name_array, #lineage, #lineage_array, #lineage_simple, #root, #root_id, #self_ancestors_and_descendants, #self_ancestors_and_descendants_ids, #self_and_ancestors, #self_and_ancestors_ids, #self_and_children, #self_and_descendants, #self_and_descendants_ids, #self_and_siblings, #self_and_siblings_ids, #siblings, #siblings_ids

Methods included from Models::InventoryCommittable

#can_be_committed?, #can_be_uncommitted?, #commit_line_items, #determine_commit_expiration_date, #has_committed_line_items?, #uncommit_line_items

Methods included from Models::CouponDateOverridable

#override_coupon_date_limit

Methods included from Models::LegacyRateRequest

#last_shipping_rate_request_result, #last_shipping_rate_request_result=

Methods included from Models::RmaTransmittable

#rma_available_email_addresses, #rma_available_fax_numbers

Methods included from Models::Profitable

#default_sales_markup, #profit_margins_met?, #profitable_line_items, #profitable_status, #profitable_total_discounted, #profitable_total_estimated_cost, #profitable_total_estimated_line_cost, #profitable_total_profit, #profitable_total_profit_margin, #profitable_total_profit_markup, #validate_min_profit_markup?

Methods included from Models::Auditable

#all_skipped_columns, #audit_reference_data, #should_not_save_version, #stamp_record

Methods included from Models::MultiRoom

#add_line_items_to_all_rooms, #add_lines_for_room_configuration, #all_rooms_complete?, #all_rooms_complete_or_cancelled?, #all_rooms_complete_or_cancelled_or_draft?, #all_rooms_in_design?, #all_rooms_ppd?, #any_room_ppd?, #any_rooms_in_design?, #get_operating_costs, #get_recommended_materials, #heated_sq_ft, #insulation_sq_ft, #prioritize_room, #remove_line_items_from_all_rooms, #remove_lines_for_room_configuration, #replace_line_items_in_all_rooms, #suggested_items, #suggested_services, #synchronization_targets

Methods included from Models::ShipQuotable

#carrier, #chosen_shipping_method, #days_commitment, #determine_origin_address, #everything_in_stock?, #friendly_shipping_method, #is_drop_ship?, #is_override?, #line_items_grouped_by_deliveries_quoting, #line_items_match_deliveries_if_any, #need_to_pre_pack_reasons, #one_time_shipping_address, #one_time_shipping_address=, #qualifies_for_cod?, #refresh_deliveries_quoting, #reset_deliveries_shipping_option, #reset_shipping, #retrieve_friendly_shipping_method, #retrieve_shipping_costs, #ship_quoted, #shipping_non_db_default_but_db_customer?, #shipping_signature_confirmation_non_db_default_but_db_customer?, #shipping_via_wy_but_has_shipping_account?, #ships_economy_package?, #ships_freight?, #ships_freight_but_address_not_freight_ready?, #should_ship_freight?, #should_ship_freight_but_address_not_freight_ready?, #validate_deliveries

Methods included from Models::ShipMeasurable

#cartons_total, #crates_total, #pallets_total, #ship_freight_class_from_shipments, #ship_volume_from_shipments, #ship_volume_from_shipments_in_cubic_feet, #ship_weight_from_shipments, #shipment_set, #shipments_for_measure

Methods included from Models::Pickable

#all_my_publications, #append_suggested_items, #append_suggested_materials, #control_capacity, #determine_catalog_for_picking, #determine_skus_to_filter, #discounted_shipping_total, #electrical_heating_elements, #fix_catalog, #get_material_alerts, #has_custom_products?, #invalidate_material_alerts!, #line_items_grouped_by_room_configuration, #lines_match_pick_catalog_on_flip, #meets_custom_products_threshold?, #pricing_program_discount_factor, #purge_empty_line_items, #soft_recalc, #subtotal, #subtotal_after_trade_discount_without_shipping, #subtotal_discounted_without_shipping, #subtotal_msrp_without_shipping, #switching_pick_catalog?, #synchronize_lines, #total_amps_by_product_line, #total_coverage_by_product_line, #total_linear_feet_by_product_line, #total_spec_by_product_line, #total_watts_by_product_line, #underlayments

Methods included from Models::TaxableResource

#apply_tax_rate_to_line_items, #build_tax_params, #calculate_tax_for_all_lines, #copy_tax_rate, #effective_date, #get_rates_for_line, #get_tax_rate, #manual_rate_goods, #manual_rate_services, #manual_rate_shipping, #origin_address, #refresh_tax_rate, #resource_not_taxable?, #set_initial_tax_rate, #should_refresh_tax_rate?, #state_code, #state_code_sym, #taxes_grouped_by_rate, #taxes_grouped_by_type

Methods included from Models::Itemizable

#add_line_item, #additional_items, #assign_sequence, #breakdown_of_prices, #calculate_actual_insured_value, #calculate_discounts, #calculate_shipping_cost, #coupon_search, #customer_applied_coupons, #customer_can_apply_coupon?, #discounts_changed?, #discounts_grouped_by_coupon, #discounts_subtotal, #effective_discount, #effective_shipping_discount, #has_kits?, #has_kits_or_serial_numbers?, #has_serial_numbers?, #is_credit_order?, #line_items_requiring_serial_number, #line_items_with_counters, #line_total_plus_tax, #main_rep, #perform_db_total, #purge_empty_quoting_deliveries, #purge_shipping_when_no_other_lines, #remove_line_item, #require_total_reset?, #reset_discount, #set_for_recalc, #set_signature_confirmation_on_shipping_address_change, #set_totals, #shipping_conditions_changed?, #shipping_discounted, #shipping_method_changed?, #should_recalculate_shipping?, #smartinstall_data, #smartsupport_data, #subtotal_cogs, #sync_shipping_line, #total_cogs

Methods included from Models::Notable

#quick_note

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

Instance Attribute Details

#customer_idObject (readonly)

Customer can only have one cart per Contact

Validations (if => #cart? ):

  • Uniqueness ({ scope: %i[state contact_id], message: 'Only one cart is allowed per user' })


448
# File 'app/models/order.rb', line 448

validates :customer_id, uniqueness: { scope: %i[state contact_id], message: 'Only one cart is allowed per user' }, if: :cart?

#do_not_detect_shippingObject

Returns the value of attribute do_not_detect_shipping.



478
479
480
# File 'app/models/order.rb', line 478

def do_not_detect_shipping
  @do_not_detect_shipping
end

#do_not_set_totalsObject

Returns the value of attribute do_not_set_totals.



478
479
480
# File 'app/models/order.rb', line 478

def do_not_set_totals
  @do_not_set_totals
end

#early_label_purchase_resultObject

Returns the value of attribute early_label_purchase_result.



478
479
480
# File 'app/models/order.rb', line 478

def early_label_purchase_result
  @early_label_purchase_result
end

#from_store_idvoid (readonly)

This method returns an undefined value.

Validates from store id, to store id, create.



458
# File 'app/models/order.rb', line 458

validates :from_store_id, :to_store_id, presence: { on: :create, if: proc { |o| o.order_type == STORE_TRANSFER } }

#full_shipping_address_validationObject

Returns the value of attribute full_shipping_address_validation.



478
479
480
# File 'app/models/order.rb', line 478

def full_shipping_address_validation
  @full_shipping_address_validation
end

#is_wwwObject

Returns the value of attribute is_www.



478
479
480
# File 'app/models/order.rb', line 478

def is_www
  @is_www
end

#is_www_ship_by_zipObject

Returns the value of attribute is_www_ship_by_zip.



478
479
480
# File 'app/models/order.rb', line 478

def is_www_ship_by_zip
  @is_www_ship_by_zip
end

#max_discount_overridevoid (readonly)

This method returns an undefined value.

Validates max discount override.



465
# File 'app/models/order.rb', line 465

validates :max_discount_override, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 100, allow_nil: true }

#order_typevoid (readonly)

This method returns an undefined value.

Validates order type.



451
# File 'app/models/order.rb', line 451

validates :order_type, presence: true

#reference_numbervoid (readonly)

This method returns an undefined value.

Validates reference number, cart or in shipping estimate.



455
# File 'app/models/order.rb', line 455

validates :reference_number, presence: { unless: :cart_or_in_shipping_estimate? }

#shipping_phoneObject (readonly)

skip this validation for EDI orders, let it ride

Validations (unless => #is_edi_order? ):

  • Phone_format


459
# File 'app/models/order.rb', line 459

validates :shipping_phone, phone_format: true, unless: :is_edi_order?

#to_store_idvoid (readonly)

This method returns an undefined value.

Validates from store id, to store id, create.

Validations:

  • Presence ({ on: :create, if: proc { |o| o.order_type == STORE_TRANSFER } })


458
# File 'app/models/order.rb', line 458

validates :from_store_id, :to_store_id, presence: { on: :create, if: proc { |o| o.order_type == STORE_TRANSFER } }

#tracking_emailvoid (readonly)

This method returns an undefined value.

Validates tracking email.



462
# File 'app/models/order.rb', line 462

validates :tracking_email, email_format: true

#update_shipping_address_with_contactObject

Returns the value of attribute update_shipping_address_with_contact.



478
479
480
# File 'app/models/order.rb', line 478

def update_shipping_address_with_contact
  @update_shipping_address_with_contact
end

Class Method Details

.activeActiveRecord::Relation<Order>

A relation of Orders that are active. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



634
# File 'app/models/order.rb', line 634

scope :active, -> { where.not(state: %w[pending cancelled fraudulent cart in_shipping_estimate]) }

.active_spiffsActiveRecord::Relation<Order>

A relation of Orders that are active spiffs. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



668
# File 'app/models/order.rb', line 668

scope :active_spiffs, -> { where(spiff_state: %w[awaiting_payment paid]) }

.all_awaiting_deliveriesActiveRecord::Relation<Order>

A relation of Orders that are all awaiting deliveries. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



585
# File 'app/models/order.rb', line 585

scope :all_awaiting_deliveries, -> { where(state: SHIPPING_STATES) }

.all_order_types_for_selectArray<Array(String, String)>

Every order-type code the system understands, for filter
dropdowns in admin search.

Returns:

  • (Array<Array(String, String)>)


1777
1778
1779
# File 'app/models/order.rb', line 1777

def self.all_order_types_for_select
  ALL_ORDER_TYPES.map { |code, desc| [desc, code] }
end

.assigned_to_repActiveRecord::Relation<Order>

A relation of Orders that are assigned to rep. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



613
614
615
616
617
618
619
620
621
622
623
624
625
# File 'app/models/order.rb', line 613

scope :assigned_to_rep, ->(party_or_ids) {
  # Guard against nil/blank input — without this, Rails turns the OR
  # branches into `IS NULL` predicates, which would match every order
  # missing any rep column.
  ids = Array.wrap(party_or_ids).compact
  next none if ids.empty?

  where.any_of(
    { primary_sales_rep_id: ids },
    { secondary_sales_rep_id: ids },
    { local_sales_rep_id: ids }
  )
}

.awaiting_completed_installation_plansActiveRecord::Relation<Order>

A relation of Orders that are awaiting completed installation plans. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



666
# File 'app/models/order.rb', line 666

scope :awaiting_completed_installation_plans, -> { where(state: :awaiting_completed_installation_plans) }

.awaiting_payment_spiffActiveRecord::Relation<Order>

A relation of Orders that are awaiting payment spiff. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



665
# File 'app/models/order.rb', line 665

scope :awaiting_payment_spiff, -> { where(spiff_state: 'awaiting_payment') }

.back_orderActiveRecord::Relation<Order>

A relation of Orders that are back order. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



655
# File 'app/models/order.rb', line 655

scope :back_order, -> { where(state: :crm_back_order) }

.by_company_idActiveRecord::Relation<Order>

A relation of Orders that are by company id. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



590
# File 'app/models/order.rb', line 590

scope :by_company_id, ->(company_id) { joins(customer: { catalog: :store }).where(stores: { company_id: }) }

.by_primary_rep_idActiveRecord::Relation<Order>

A relation of Orders that are by primary rep id. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



591
# File 'app/models/order.rb', line 591

scope :by_primary_rep_id, ->(rep_id) { joins(:customer).where(parties: { primary_sales_rep_id: rep_id }) }

.by_report_groupingActiveRecord::Relation<Order>

A relation of Orders that are by report grouping. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



683
# File 'app/models/order.rb', line 683

scope :by_report_grouping, ->(report_grouping) { joins(:customer).where(parties: { report_grouping: report_grouping }) }

.by_report_grouping_all_when_nilActiveRecord::Relation<Order>

A relation of Orders that are by report grouping all when nil. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



682
# File 'app/models/order.rb', line 682

scope :by_report_grouping_all_when_nil, ->(report_grouping) { report_grouping.present? ? joins(:customer).where(parties: { report_grouping: report_grouping }) : joins(:customer) }

.by_sales_rep_idActiveRecord::Relation<Order>

A relation of Orders that are by sales rep id. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



685
686
687
688
689
690
691
692
693
# File 'app/models/order.rb', line 685

scope :by_sales_rep_id, lambda { |sales_rep_id|
  next none if sales_rep_id.blank?

  joins(:customer).where.any_of(
    { parties: { primary_sales_rep_id: sales_rep_id } },
    { parties: { secondary_sales_rep_id: sales_rep_id } },
    { parties: { local_sales_rep_id: sales_rep_id } }
  )
}

.by_storeActiveRecord::Relation<Order>

A relation of Orders that are by store. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



630
# File 'app/models/order.rb', line 630

scope :by_store, ->(store) { where(currency: store.currency) }

.by_store_idActiveRecord::Relation<Order>

A relation of Orders that are by store id. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



589
# File 'app/models/order.rb', line 589

scope :by_store_id, ->(store_id) { joins(customer: :catalog).where(catalogs: { store_id: }) }

.cancelledActiveRecord::Relation<Order>

A relation of Orders that are cancelled. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



633
# File 'app/models/order.rb', line 633

scope :cancelled, -> { where(state: 'cancelled') }

.cartsActiveRecord::Relation<Order>

A relation of Orders that are carts. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



640
# File 'app/models/order.rb', line 640

scope :carts, -> { where(state: :cart) }

.co_onlyActiveRecord::Relation<Order>

A relation of Orders that are co only. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



658
# File 'app/models/order.rb', line 658

scope :co_only, -> { where(order_type: CREDIT_ORDER) }

.contains_coupon_idsActiveRecord::Relation<Order>

A relation of Orders that are contains coupon ids. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



674
# File 'app/models/order.rb', line 674

scope :contains_coupon_ids, ->(coupon_ids) { where("EXISTS (select discounts.id from discounts where discounts.coupon_id IN (?) and discounts.itemizable_type = 'Order' and discounts.itemizable_id = orders.id)", coupon_ids) }

.contains_item_idsActiveRecord::Relation<Order>

A relation of Orders that are contains item ids. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



671
672
673
# File 'app/models/order.rb', line 671

scope :contains_item_ids, ->(item_ids) {
  where("EXISTS (select li.id from line_items li inner join catalog_items ci on ci.id= li.catalog_item_id inner join store_items si on si.id = ci.store_item_id where li.resource_id = orders.id and li.resource_type = 'Order' and si.item_id IN (?))", item_ids)
}

.contains_service_itemsActiveRecord::Relation<Order>

A relation of Orders that are contains service items. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



675
# File 'app/models/order.rb', line 675

scope :contains_service_items, -> { contains_item_ids(Item.services.ids) }

.correctly_packagedActiveRecord::Relation<Order>

A relation of Orders that are correctly packaged. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



643
# File 'app/models/order.rb', line 643

scope :correctly_packaged, -> { where('orders.incorrectly_package_ups_canada_order IS NOT TRUE OR (orders.incorrectly_package_ups_canada_order IS TRUE and orders.incorrectly_packaged_ups_canada_order_fixed IS TRUE)') }

.custom_order_agreement_statuses_for_selectArray<Array(String, String)>

[label, value] pairs of every Custom-Order-Agreement status
for the CRM dropdown.

Returns:

  • (Array<Array(String, String)>)


1760
1761
1762
# File 'app/models/order.rb', line 1760

def self.custom_order_agreement_statuses_for_select
  Order.custom_order_agreement_statuses.keys.map { |e| [e.humanize, e] }
end

.customer_reference_searchActiveRecord::Relation<Order>

A relation of Orders that are customer reference search. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



698
# File 'app/models/order.rb', line 698

scope :customer_reference_search, ->(q) { where(Order[:customer_reference].matches("%#{q}%")).order([Arel.sql('orders.customer_reference <-> ?'), q]) }

.default_source_for(customer:, opportunity: nil) ⇒ Source?

Default source for a CRM-created order, per the attribution rule
decided 2026-07-23 (see
doc/tasks/202607231507_SOURCE_ATTRIBUTION_MODEL_RESTRUCTURE.md):
an order converted from an opportunity inherits the opportunity's
source (the closing call/email is influence, not acquisition); a
standalone order stamps from evidence — the customer's recent
attributable visit first, the account default last. Web orders
bypass this via #set_source_from_visit.

Parameters:

Returns:



1750
1751
1752
1753
1754
# File 'app/models/order.rb', line 1750

def self.default_source_for(customer:, opportunity: nil)
  opportunity&.source ||
    customer.most_recent_attributable_visit_source ||
    customer.source_for_opps_and_orders
end

.draft_spiffActiveRecord::Relation<Order>

A relation of Orders that are draft spiff. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



664
# File 'app/models/order.rb', line 664

scope :draft_spiff, -> { where.not(spiff_enrollment_id: nil).where(spiff_state: 'draft') }

.edi_ordersActiveRecord::Relation<Order>

A relation of Orders that are edi orders. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



697
# File 'app/models/order.rb', line 697

scope :edi_orders, -> { where.not(orders: { edi_transaction_id: nil }) }

.future_releaseActiveRecord::Relation<Order>

A relation of Orders that are future release. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



684
# File 'app/models/order.rb', line 684

scope :future_release, -> { where.not(future_release_date: nil).where.not(order_type: 'CO').where.not(state: Order::CLOSED_STATES) }

.google_conversion_acknowledgedActiveRecord::Relation<Order>

A relation of Orders that are google conversion acknowledged. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



605
606
607
608
# File 'app/models/order.rb', line 605

scope :google_conversion_acknowledged, -> {
  jsonb_where(column_name: :google_conversion_meta, json_keys: %w[result], operator: :eq, value: 'reported')
    .or(jsonb_where(column_name: :google_conversion_meta, json_keys: %w[result status], operator: :eq, value: 'reported'))
}

.google_conversion_attemptedActiveRecord::Relation<Order>

A relation of Orders that are google conversion attempted. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



599
600
601
# File 'app/models/order.rb', line 599

scope :google_conversion_attempted, -> {
  jsonb_where_exists(column_name: :google_conversion_meta, key: :attempted_at)
}

.has_manual_preset_formActiveRecord::Relation<Order>

A relation of Orders that are has manual preset form. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



676
# File 'app/models/order.rb', line 676

scope :has_manual_preset_form, -> { where("EXISTS(select 1 from uploads where uploads.resource_type = 'Order' and uploads.resource_id = orders.id and uploads.category = 'manual_smart_preset_form')") }

.heldActiveRecord::Relation<Order>

A relation of Orders that are held. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



654
# File 'app/models/order.rb', line 654

scope :held, -> { where(state: :in_cr_hold) }

.held_sales_ordersActiveRecord::Relation<Order>

A relation of Orders that are held sales orders. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



670
# File 'app/models/order.rb', line 670

scope :held_sales_orders, -> { sales_orders.where(state: %w[pending pending_payment pending_release_authorization in_cr_hold]) }

.in_progressActiveRecord::Relation<Order>

A relation of Orders that are in progress. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



644
# File 'app/models/order.rb', line 644

scope :in_progress, -> { so_only.where.not(state: %w[cart invoiced cancelled fraudulent]) }

.in_stateActiveRecord::Relation<Order>

A relation of Orders that are in state. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



588
# File 'app/models/order.rb', line 588

scope :in_state, ->(state) { where(state:) }

.incorrectly_packaged_ups_canada_orderActiveRecord::Relation<Order>

A relation of Orders that are incorrectly packaged ups canada order. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



642
# File 'app/models/order.rb', line 642

scope :incorrectly_packaged_ups_canada_order, -> { where('orders.incorrectly_package_ups_canada_order IS TRUE and orders.incorrectly_packaged_ups_canada_order_fixed IS NOT TRUE') }

.invoicedActiveRecord::Relation<Order>

A relation of Orders that are invoiced. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



637
# File 'app/models/order.rb', line 637

scope :invoiced, -> { where(state: 'invoiced') }

.like_lookupActiveRecord::Relation<Order>

A relation of Orders that are like lookup. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



680
# File 'app/models/order.rb', line 680

scope :like_lookup, ->(q) { where('orders.reference_number LIKE :term OR orders.edi_po_number LIKE :term', { term: "%#{q}%" }) }

.limit_to_fbaActiveRecord::Relation<Order>

A relation of Orders that are limit to fba. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



681
# File 'app/models/order.rb', line 681

scope :limit_to_fba, -> { joins(customer: :billing_address).where.any_of({ parties: { id: CustomerConstants::AMAZON_COM_ID } }, { addresses: { party_id: CustomerConstants::AMAZON_COM_ID } }) }

.lockedActiveRecord::Relation<Order>

A relation of Orders that are locked. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



694
# File 'app/models/order.rb', line 694

scope :locked, -> { where(state: LOCKED_STATES) }

.lookupActiveRecord::Relation<Order>

A relation of Orders that are lookup. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



677
# File 'app/models/order.rb', line 677

scope :lookup, ->(q) { where(orders: { reference_number: q }) }

.mo_onlyActiveRecord::Relation<Order>

A relation of Orders that are mo only. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



659
# File 'app/models/order.rb', line 659

scope :mo_only, -> { where(order_type: MARKETING_ORDER) }

.most_recent_firstActiveRecord::Relation<Order>

A relation of Orders that are most recent first. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



653
# File 'app/models/order.rb', line 653

scope :most_recent_first, -> { order('orders.created_at DESC') }

.non_cartsActiveRecord::Relation<Order>

A relation of Orders that are non carts. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



641
# File 'app/models/order.rb', line 641

scope :non_carts, -> { where(state: NON_CART_STATES) }

.non_creditActiveRecord::Relation<Order>

A relation of Orders that are non credit. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



639
# File 'app/models/order.rb', line 639

scope :non_credit, -> { where.not(order_type: CREDIT_ORDER) }

.not_cancelledActiveRecord::Relation<Order>

A relation of Orders that are not cancelled. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



635
# File 'app/models/order.rb', line 635

scope :not_cancelled, -> { where.not(state: 'cancelled') }

.not_in_pre_packActiveRecord::Relation<Order>

A relation of Orders that are not in pre pack. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



645
# File 'app/models/order.rb', line 645

scope :not_in_pre_pack, -> { so_only.where.not(state: %w[pre_pack]) }

.not_open_for_changeActiveRecord::Relation<Order>

A relation of Orders that are not open for change. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



628
# File 'app/models/order.rb', line 628

scope :not_open_for_change, -> { where.not(state: OPEN_FOR_CHANGE_STATES) }

.not_partially_invoicedActiveRecord::Relation<Order>

A relation of Orders that are not partially invoiced. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



638
# File 'app/models/order.rb', line 638

scope :not_partially_invoiced, -> { where.not(state: %w[invoiced partially_invoiced]) }

.not_processing_deliveriesActiveRecord::Relation<Order>

A relation of Orders that are not processing deliveries. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



636
# File 'app/models/order.rb', line 636

scope :not_processing_deliveries, -> { where.not(state: 'processing_deliveries') }

.not_soldActiveRecord::Relation<Order>

A relation of Orders that are not sold. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



587
# File 'app/models/order.rb', line 587

scope :not_sold, -> { where.not(state: SOLD_STATES) }

.open_for_changeActiveRecord::Relation<Order>

A relation of Orders that are open for change. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



626
# File 'app/models/order.rb', line 626

scope :open_for_change, -> { where(state: OPEN_FOR_CHANGE_STATES) }

.open_for_tax_updateActiveRecord::Relation<Order>

A relation of Orders that are open for tax update. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



627
# File 'app/models/order.rb', line 627

scope :open_for_tax_update, -> { where(state: OPEN_FOR_TAX_CHANGE_STATES) }

.order_count(company_id = nil, where_conditions = nil, where_not_conditions = nil) ⇒ Integer

Counts orders matching the given filters. The customer→catalog
→store join is mandatory because all callers want the
company-aware count, never the raw global count.

Parameters:

  • company_id (Integer, nil) (defaults to: nil)
  • where_conditions (Hash, String, Array, nil) (defaults to: nil)
  • where_not_conditions (Hash, String, Array, nil) (defaults to: nil)

Returns:

  • (Integer)


1789
1790
1791
1792
1793
1794
1795
# File 'app/models/order.rb', line 1789

def self.order_count(company_id = nil, where_conditions = nil, where_not_conditions = nil)
  o = Order.joins(customer: { catalog: :store }).order('orders.id')
  o = o.by_company_id(company_id) unless company_id.nil?
  o = o.where(where_conditions) unless where_conditions.nil?
  o = o.where.not(where_not_conditions) unless where_not_conditions.nil?
  o.count
end

.order_type_from_opportunity(opportunity) ⇒ String

Derive an order_type code from an Opportunity's type by
appending 'O' (so S + O = 'SO' sales order, T + O =
'TO' tech order).

Parameters:

Returns:

  • (String)


6453
6454
6455
# File 'app/models/order.rb', line 6453

def self.order_type_from_opportunity(opportunity)
  "#{opportunity.opportunity_type}O"
end

.order_type_from_quote(quote) ⇒ String

Map a Quote type code to the matching order_type code: a
tech quote (TQ) becomes a tech order (TO), a marketing
quote (MQ) becomes a marketing order (MO), etc. Defaults to
'SO' for the standard sales-quote → sales-order path.

Parameters:

Returns:

  • (String)


6443
6444
6445
# File 'app/models/order.rb', line 6443

def self.order_type_from_quote(quote)
  { TQ: 'TO', MQ: 'MO', SQ: 'SO' }[quote.quote_type.to_sym] || 'SO'
end

.overdue_to_shipActiveRecord::Relation<Order>

A relation of Orders that are overdue to ship. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



2441
2442
2443
2444
2445
2446
# File 'app/models/order.rb', line 2441

scope :overdue_to_ship, lambda {
  where("edi_orchestrator_partner LIKE 'amazon_seller%'")
    .where(state: 'awaiting_deliveries')
    .where(requested_ship_before: ...Date.current)
    .where('future_release_date IS NULL OR future_release_date <= ?', Date.current)
}

A relation of Orders that are paid spiff. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



667
# File 'app/models/order.rb', line 667

scope :paid_spiff, -> { where(spiff_state: 'paid') }

.pendingActiveRecord::Relation<Order>

A relation of Orders that are pending. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



646
# File 'app/models/order.rb', line 646

scope :pending, -> { where(state: 'pending') }

.pending_paymentActiveRecord::Relation<Order>

A relation of Orders that are pending payment. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



648
# File 'app/models/order.rb', line 648

scope :pending_payment, -> { where(state: 'pending_payment') }

.pending_payment_and_unpaid_invoicesActiveRecord::Relation<Order>

A relation of Orders that are pending payment and unpaid invoices. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



649
650
651
652
# File 'app/models/order.rb', line 649

scope :pending_payment_and_unpaid_invoices, lambda {
  unpaid_invoice_order_ids = Invoice.where(state: 'unpaid').select(:order_id)
  where(id: unpaid_invoice_order_ids).or(pending_payment)
}

.po_number_barcode(po_number:, file_path: nil) ⇒ String?

Class-method form of #po_number_barcode: render an arbitrary
PO number as a Code-128 barcode PNG. Strips non-ASCII chars
before encoding to side-step a known barby issue with smart
quotes and other paste-in unicode (toretore/barby#61).

Parameters:

  • po_number (String)
  • file_path (String, nil) (defaults to: nil)

    when given, writes PNG bytes to
    this file and returns the path; otherwise returns the bytes.

Returns:

  • (String, nil)

    PNG bytes, file path, or nil for blank input



3396
3397
3398
3399
3400
3401
3402
3403
3404
3405
3406
3407
3408
3409
3410
3411
3412
3413
3414
3415
3416
# File 'app/models/order.rb', line 3396

def self.po_number_barcode(po_number:, file_path: nil)
  return if po_number.blank?

  # we just want ASCII here, no spaces or weird cut and paste characters, see https://github.com/toretore/barby/issues/61
  po_number = po_number.to_s.scan(/\S/).join.encode(Encoding::ASCII_8BIT, invalid: :replace, undef: :replace, replace: '')
  require 'zint'
  barcode = Zint::Barcode.new(value: po_number, symbology: Zint::Constants::Symbologies::BARCODE_CODE128)
  barcode.show_hrt = 0 # barby rendered bars only; keep it that way
  png = barcode.to_memory_file(extension: '.png')

  if file_path
    File.open(file_path, 'wb') do |file|
      file.write(png)
      file.flush
      file.fsync
    end
    file_path
  else
    png
  end
end

.positive_valueActiveRecord::Relation<Order>

A relation of Orders that are positive value. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



661
# File 'app/models/order.rb', line 661

scope :positive_value, -> { where(Order[:line_total].gt(0)) }

.profit_reviewActiveRecord::Relation<Order>

A relation of Orders that are profit review. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



647
# File 'app/models/order.rb', line 647

scope :profit_review, -> { where(state: 'profit_review') }

.quick_stats_shippingActiveRecord::Relation<Order>

A relation of Orders that are quick stats shipping. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



663
# File 'app/models/order.rb', line 663

scope :quick_stats_shipping, -> { so_only.positive_value.in_state(SHIPPING_STATES).select('sum(line_total) as sum_line_total, currency').group(:currency) }

.quick_stats_soldActiveRecord::Relation<Order>

A relation of Orders that are quick stats sold. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



662
# File 'app/models/order.rb', line 662

scope :quick_stats_sold, -> { so_only.positive_value.in_state(SOLD_STATES).select('sum(line_total) as sum_line_total, currency').group(:currency) }

.reception_type_for_selectHash{String => String}

Memoised {label => value} map for the order-reception-type
filter ("Online" vs "CRM"). Each invocation returns the same
frozen-shape hash so the form caches well.

Returns:

  • (Hash{String => String})


3866
3867
3868
3869
3870
3871
3872
3873
# File 'app/models/order.rb', line 3866

def self.reception_type_for_select
  unless @reception_types
    @reception_types = {}
    @reception_types['Online'] = 'Online'
    @reception_types['CRM'] = 'CRM'
  end
  @reception_types
end

.returnable_typesActiveRecord::Relation<Order>

A relation of Orders that are returnable types. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



660
# File 'app/models/order.rb', line 660

scope :returnable_types, -> { where(order_type: [SALES_ORDER, MARKETING_ORDER, TECH_ORDER]) }

.room_not_pickableActiveRecord::Relation<Order>

A relation of Orders that are room not pickable. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



629
# File 'app/models/order.rb', line 629

scope :room_not_pickable, -> { where.not(state: ROOM_PICKABLE_STATES) }

.sales_ordersActiveRecord::Relation<Order>

A relation of Orders that are sales orders. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



669
# File 'app/models/order.rb', line 669

scope :sales_orders, -> { non_carts.so_only.active }

.selectable_order_typesArray<Array(String, String)>

Order-type codes a user is allowed to choose from in the new-
order form. Hides RESTRICTED_ORDER_TYPES (CRM-only types like
credit orders that are created via dedicated flows).

Returns:

  • (Array<Array(String, String)>)


1769
1770
1771
# File 'app/models/order.rb', line 1769

def self.selectable_order_types
  UNRESTRICTED_ORDER_TYPES.map { |code, desc| [desc, code] }
end

.so_onlyActiveRecord::Relation<Order>

A relation of Orders that are so only. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



657
# File 'app/models/order.rb', line 657

scope :so_only, -> { where(order_type: SALES_ORDER) }

.soldActiveRecord::Relation<Order>

A relation of Orders that are sold. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



586
# File 'app/models/order.rb', line 586

scope :sold, -> { where(state: SOLD_STATES) }

.st_onlyActiveRecord::Relation<Order>

A relation of Orders that are st only. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



656
# File 'app/models/order.rb', line 656

scope :st_only, -> { where(order_type: STORE_TRANSFER) }

.states_for_selectArray<Array(String, String)>

[human_name, machine_value] pairs of every order state, sorted
alphabetically by display name. Used to populate the state
filter on CRM list views.

Returns:

  • (Array<Array(String, String)>)


1810
1811
1812
# File 'app/models/order.rb', line 1810

def self.states_for_select
  state_machine.states.sort_by(&:human_name).map { |s| [s.human_name, s.value] }
end

.with_amazon_paymentsActiveRecord::Relation<Order>

A relation of Orders that are with amazon payments. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



700
# File 'app/models/order.rb', line 700

scope :with_amazon_payments, -> { joins(:payments).where(payments: { category: Payment::AMAZON_PAY }) }

.with_associationsActiveRecord::Relation<Order>

A relation of Orders that are with associations. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



631
# File 'app/models/order.rb', line 631

scope :with_associations, -> { includes(:shipments, :shipping_account_number, :shipping_address, :creator, { customer: [:buying_group, { catalog: :store }] }) }

.with_line_itemsActiveRecord::Relation<Order>

A relation of Orders that are with line items. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



632
# File 'app/models/order.rb', line 632

scope :with_line_items, -> { includes(line_items: { catalog_item: { store_item: :item } }) }

.with_paymentsActiveRecord::Relation<Order>

A relation of Orders that are with payments. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



699
# File 'app/models/order.rb', line 699

scope :with_payments, -> { joins(:payments) }

.with_reviewActiveRecord::Relation<Order>

A relation of Orders that are with review. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



695
# File 'app/models/order.rb', line 695

scope :with_review, -> { where(reviewed: true) }

.without_reviewActiveRecord::Relation<Order>

A relation of Orders that are without review. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



696
# File 'app/models/order.rb', line 696

scope :without_review, -> { where(reviewed: false) }

Instance Method Details

#accounting_hold_order?Object

Whether accounting hold order.

Returns:

  • (Object)


2478
2479
2480
# File 'app/models/order.rb', line 2478

def accounting_hold_order?
  customer.on_hold || payment_method_requires_authorization? || potential_fraud?
end

#activitiesActiveRecord::Associations::CollectionProxy

The associated activities.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


361
# File 'app/models/order.rb', line 361

has_many :activities, as: :resource, dependent: :nullify

#add_customer_to_campaignObject

After-save hook: when an order's source changes to a source that's
tied to a marketing campaign, enroll the customer in that campaign
so subsequent campaign emails reach them. No-op when the source
isn't campaign-linked or hasn't actually changed.



1818
1819
1820
1821
1822
1823
1824
# File 'app/models/order.rb', line 1818

def add_customer_to_campaign
  return unless saved_change_to_source_id?
  return unless source
  return unless source.linked_to_campaign?

  source.add_customer_to_campaign(customer)
end

#add_item(sku, qty) ⇒ Array<Hash>?

Convenience for #add_multiple_items with a single sku/qty pair.
No-op when the order is in a locked state (post-pending).

Parameters:

  • sku (String)
  • qty (Integer)

Returns:



3241
3242
3243
3244
3245
3246
# File 'app/models/order.rb', line 3241

def add_item(sku, qty)
  return if editing_locked?

  sku_array = [{ sku:, qty: }]
  add_multiple_items(sku_array)
end

#add_multiple_items(sku_array = []) ⇒ Array<Hash>

Add a batch of SKUs to the order in one save. Each entry is a
Hash with :sku and optional :qty (defaulting to 1):
[{sku: 'UDG4-4999', qty: 1}, {sku: 'SS-01', qty: 2}]. Bails
without raising when the order is locked or the cart already
has 100+ lines (bot suspicion). Triggers tier-2 / auto-coupon
recalculation on save and recovers from
index_discounts_unique_per_itemizable collisions by
reloading discounts and retrying.

Parameters:

  • sku_array (Array<Hash>) (defaults to: [])

Returns:

  • (Array<Hash>)

    one summary hash per added line:
    {id:, sku:, name:, category:, quantity:}

Raises:



3199
3200
3201
3202
3203
3204
3205
3206
3207
3208
3209
3210
3211
3212
3213
3214
3215
3216
3217
3218
3219
3220
3221
3222
3223
3224
3225
3226
3227
3228
3229
3230
3231
3232
3233
# File 'app/models/order.rb', line 3199

def add_multiple_items(sku_array = [])
  return if editing_locked?
  return if line_items.size > 100 # Then we suspect it's a bot adding things to the cart

  added_items = []
  sku_array.each do |hsh|
    sku = hsh[:sku]
    qty = hsh[:qty] || 1
    room_configuration_id = hsh[:room_configuration_id]
    ci = catalog.catalog_items.public_catalog_items.by_skus(sku).first

    raise Order::ItemNotFound, "#{sku} not found" if ci.blank?

    # Persist each line_item inline. Without this, add_line_item's
    # `line_items.reload` at the top of the next iteration discards the
    # in-memory build from this one — only the final SKU in the batch
    # would survive the post-loop save!.
    li = add_line_item(catalog_item_id: ci.id, quantity: qty, room_configuration_id:)
    added_items << { id: li.id, sku: li.sku, name: li.name, category: li.reported_category_name, quantity: li.quantity }
  end
  if added_items.present?
    self.recalculate_shipping = true # doesn't hurt to set it
    self.recalculate_discounts = true # ensure tier2 and auto-apply discounts are calculated
    self.force_total_reset = true
    begin
      save!
    rescue ActiveRecord::RecordNotUnique => e
      raise unless e.message.include?('index_discounts_unique_per_itemizable')

      discounts.reload
      save!
    end
  end
  added_items
end

#adjusted_actual_shipping_costFloat

Like #calculate_actual_shipping_cost but applies per-delivery
accounting adjustments (recoveries, fuel surcharge corrections).

Returns:

  • (Float)


6183
6184
6185
# File 'app/models/order.rb', line 6183

def adjusted_actual_shipping_cost
  deliveries.invoiced.to_a.sum { |d| d.adjusted_actual_shipping_cost.to_f }
end

#all_deliveries_cancelable?(current_user = nil) ⇒ Object

Whether all deliveries cancelable.

Parameters:

  • current_user (Object) (defaults to: nil)

Returns:

  • (Object)


1301
1302
1303
# File 'app/models/order.rb', line 1301

def all_deliveries_cancelable?(current_user = nil)
  deliveries.active.empty? || deliveries.active.all? { |d| d.cancelable?(current_user) }
end

#all_deliveries_invoiced?Object

Whether all deliveries invoiced.

Returns:

  • (Object)


2888
2889
2890
# File 'app/models/order.rb', line 2888

def all_deliveries_invoiced?
  deliveries.active.present? && deliveries.active.all?(&:invoiced?)
end

#all_funds_available?(ignore_cod = false) ⇒ Object

Whether all funds available.

Parameters:

  • ignore_cod (Object) (defaults to: false)

Returns:

  • (Object)


2881
2882
2883
# File 'app/models/order.rb', line 2881

def all_funds_available?(ignore_cod = false)
  balance(ignore_cod) <= 0
end

#all_funds_not_available_and_shippable?Object (protected)

Whether all funds not available and shippable.

Returns:

  • (Object)


6403
6404
6405
# File 'app/models/order.rb', line 6403

def all_funds_not_available_and_shippable?
  !all_funds_available?(true) && shipping_address && chosen_shipping_method.present?
end

#all_items_in_stockBoolean

True when every line item has enough on-hand inventory at its
delivery's warehouse to fulfil immediately. Computed via
LineItem.inventory_check, which considers committed inventory,
transit, and minimum-on-hand thresholds.

Returns:

  • (Boolean)


2918
2919
2920
# File 'app/models/order.rb', line 2918

def all_items_in_stock
  stock_status == :ok
end

#all_participant_idsArray<Integer>

Every party that should see this order in their CRM activity feed:
all opportunity participants plus the customer and contact.

Returns:

  • (Array<Integer>)

    deduplicated party ids



1441
1442
1443
1444
1445
1446
1447
# File 'app/models/order.rb', line 1441

def all_participant_ids
  party_ids = []
  party_ids += opportunity.all_participants.ids if opportunity
  party_ids << customer_id
  party_ids << contact_id
  party_ids.compact.uniq
end

#all_payments_are_valid?Object

Whether all payments are valid.

Returns:

  • (Object)


1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
# File 'app/models/order.rb', line 1308

def all_payments_are_valid?
  return true unless payments.bread_payments.any?

  if payments.bread_payments.where(state: 'authorized').sum(:amount) >= total
    true
  else
    errors.add :base, 'Financed order cannot be modified to have a higher total than the existing payment. Please create a new order for the extra items or extra cost.'
    false
  end
end

#all_rooms_not_orderable?Object

Whether all rooms not orderable.

Returns:

  • (Object)


2398
2399
2400
# File 'app/models/order.rb', line 2398

def all_rooms_not_orderable?
  room_configurations.any? && room_configurations.all? { |rc| !rc.orderable? }
end

#all_support_casesArray<Integer>

Convenience for support_case_ids. Kept so the CRM views can use
a single accessor whether the support cases came from a HABTM or
via a custom join.

Returns:

  • (Array<Integer>)


1920
1921
1922
1923
1924
# File 'app/models/order.rb', line 1920

def all_support_cases
  support_case_ids
  linked_support_cases.ids
  SupportCase.where(id: support_case_ids).order('support_cases.case_number desc')
end

#all_uploadsActiveRecord::Relation<Upload>

Every Upload attached anywhere in the order tree — order
itself, its deliveries, and the shipments under those deliveries.
Pre-loads the polymorphic resource so the CRM's documents tab
can render attribution without N+1.

Returns:

  • (ActiveRecord::Relation<Upload>)


6227
6228
6229
6230
6231
6232
6233
6234
6235
6236
6237
# File 'app/models/order.rb', line 6227

def all_uploads
  ret_uploads = if delivery_ids.present?
                  shipment_ids = Shipment.where(delivery_id: delivery_ids).ids
                  conditions = ["(resource_type = 'Order' and resource_id = :order_id)", "(resource_type = 'Delivery' and resource_id IN (:delivery_ids))"]
                  conditions << "(resource_type = 'Shipment' and resource_id IN (:shipment_ids))" if shipment_ids.any?
                  Upload.where(conditions.join(' OR '), order_id: id, delivery_ids:, shipment_ids:)
                else
                  uploads
                end
  ret_uploads.includes(:resource).order(Upload[:created_at].desc)
end

#allows_edi_split?Object

Whether allows edi split.

Returns:

  • (Object)


5798
5799
5800
# File 'app/models/order.rb', line 5798

def allows_edi_split?
  edi_channel_order_support.present? && edi_channel_order_support.in?(%w[SPLIT_ORDERS SPLIT_ORDER_LINES])
end

#already_has_smartinstall_request?Object

Whether already has smartinstall request.

Returns:

  • (Object)


3453
3454
3455
# File 'app/models/order.rb', line 3453

def already_has_smartinstall_request?
  activities.where(activity_type_id: ActivityTypeConstants::LEAD_SSI).any?
end

#amazon_buy_shipping_eligible?Boolean

Check if order is eligible for Amazon Buy Shipping early label purchase

Returns:

  • (Boolean)


5004
5005
5006
5007
5008
5009
5010
5011
5012
5013
5014
5015
# File 'app/models/order.rb', line 5004

def amazon_buy_shipping_eligible?
  return false unless is_edi_order?
  return false unless edi_orchestrator_partner&.start_with?('amazon_seller')

  begin
    orchestrator = Edi::Amazon::Orchestrator.new(edi_orchestrator_partner.to_sym)
    orchestrator&.buy_shipping_enabled?
  rescue StandardError => e
    Rails.logger.warn("[EarlyLabel] Error checking Amazon Buy Shipping eligibility: #{e.message}")
    false
  end
end

#amzbs_packing_slip_included?Boolean

Amazon Buy Shipping label PDFs include a packing slip page, so no
separate upload is needed when an AMZBS shipping option is selected.

Returns:

  • (Boolean)


2666
2667
2668
# File 'app/models/order.rb', line 2666

def amzbs_packing_slip_included?
  deliveries.any? { |d| d.shipping_option&.carrier == 'AmazonSeller' }
end

#any_rooms_not_orderable?Object

Whether any rooms not orderable.

Returns:

  • (Object)


2389
2390
2391
2392
2393
# File 'app/models/order.rb', line 2389

def any_rooms_not_orderable?
  # (self.order_reception_type == "Online" or online_order?) and self.room_configurations.any?{|rc| (rc.room_layout_attached? and !rc.complete?)}
  # here we are now allowing direct order of online rooms via HW so relax the test for any orders with uncompleted rooms that have layouts
  room_configurations.any? { |rc| !rc.orderable? }
end

#applies_for_smartinstallBoolean

Eligibility check for the SmartInstall service add-on (paid
in-home installation). Currently hard-coded to false pending the
service refactor mentioned inline; the legacy logic checks that
the customer is a homeowner inside service range, has selected
heated items, and doesn't already carry SmartInstall.

Returns:

  • (Boolean)


3436
3437
3438
3439
3440
3441
# File 'app/models/order.rb', line 3436

def applies_for_smartinstall
  return false # added by Roman Aug 19th until service refactor
  return true if installation_is_within_range? && customer.is_homeowner? && has_selected_heated_items? && doesnt_already_has_smartinstall?

  false
end

#apply_tier2_pricing?Boolean

Whether or not to apply the tier2 pricing (customer discount) by default

Returns:

  • (Boolean)


3574
3575
3576
# File 'app/models/order.rb', line 3574

def apply_tier2_pricing?
  is_sales_order?
end

#attention_nameString?

"ATTN: …" line written on shipping labels. Returns the explicit
attention_name_override if the user set one, otherwise falls
back to #inherited_attention_name (the shipping address's
person name when the customer is an individual).

Returns:

  • (String, nil)


3819
3820
3821
# File 'app/models/order.rb', line 3819

def attention_name
  attention_name_override || inherited_attention_name
end

#attention_name=(value) ⇒ Object

Setter that stores the override unless the caller is sending the
literal sentinel string "inherited_attention_name" (used by the
CRM form to mean "go back to the inherited value"), in which case
we leave the override untouched.

Parameters:

  • value (String)


3829
3830
3831
3832
3833
# File 'app/models/order.rb', line 3829

def attention_name=(value)
  return if inherited_attention_name && value =~ /inherited_attention_name/i

  self.attention_name_override = value
end

#auto_reserve_serial_numbersObject

Walks every line item that requires serial-number reservation
(heating mats, cables) and asks the line to grab the next available
serial-number block from inventory. No-op for credit orders, which
don't ship physical product.



1725
1726
1727
1728
1729
# File 'app/models/order.rb', line 1725

def auto_reserve_serial_numbers
  return unless can_auto_reserve_serial_numbers?

  line_items.select(&:require_reservation?).each(&:auto_reserve_serial_numbers)
end

#awaiting_future_deliveries?Object

Whether awaiting future deliveries.

Returns:

  • (Object)


5837
5838
5839
# File 'app/models/order.rb', line 5837

def awaiting_future_deliveries?
  awaiting_deliveries? && deliveries.for_future_release.present?
end

#balance(ignore_cod = false, excluding_payment: nil) ⇒ BigDecimal

Outstanding amount on the order: total of all deliveries minus
whatever payments have been authorised against each delivery
(capped per delivery so over-authorisation on one delivery doesn't
cancel out a balance owed on another). Always 0 for store
transfers (no money changes hands) and for COD-funded orders
unless ignore_cod: true is passed.

Parameters:

  • ignore_cod (Boolean) (defaults to: false)

    include COD deliveries in the balance

  • excluding_payment (Payment, nil) (defaults to: nil)

    forwarded to each delivery's
    Models::Payable#total_payments_authorized; drops that payment's
    authorized amount from the per-delivery obligation (its captured
    portion stays). Used by the reauth flow so an over-sized auth being
    replaced doesn't make the order look paid.

Returns:

  • (BigDecimal)

    non-negative balance owed



2789
2790
2791
2792
2793
2794
2795
2796
2797
2798
2799
2800
# File 'app/models/order.rb', line 2789

def balance(ignore_cod = false, excluding_payment: nil)
  return BigDecimal('0.0') if is_store_transfer? || (!ignore_cod && funded_by_cod?)

  total = deliveries.sum(:total) || BigDecimal('0.0')
  bal = total
  deliveries.each do |dq|
    dq_total = dq.total || BigDecimal('0.0')
    dq_auth = dq.total_payments_authorized(currency, excluding_payment: excluding_payment) || BigDecimal('0.0')
    bal -= [dq_auth, dq_total].min
  end
  bal < 0 ? BigDecimal('0.0') : bal
end

#belongs_to_smartservice_group?Object

Whether belongs to smartservice group.

Returns:

  • (Object)


2973
2974
2975
# File 'app/models/order.rb', line 2973

def belongs_to_smartservice_group?
  is_smartfit_service? or is_smartinstall_service? or is_smartguide_service? or is_smartfix_service?
end

#billing_addressObject

Alias for Customer#billing_address

Returns:

  • (Object)

    Customer#billing_address

See Also:



424
# File 'app/models/order.rb', line 424

delegate :catalog, :billing_address, :billing_entity, :company, to: :customer

#billing_entityObject

Alias for Customer#billing_entity

Returns:

  • (Object)

    Customer#billing_entity

See Also:



424
# File 'app/models/order.rb', line 424

delegate :catalog, :billing_address, :billing_entity, :company, to: :customer

#build_activityActivity

Builds (but does not save) a new Activity record attached to
this order with the order's primary party already populated.
Used by the CRM activity-form helpers to seed a new activity
without round-tripping through nested form params.

Returns:



1911
1912
1913
# File 'app/models/order.rb', line 1911

def build_activity
  activities.build resource: self, party: primary_party
end

#build_early_label_carrier_info(label_result, delivery = nil) ⇒ Hash

Build carrier info hash for early label EDI confirm (Walmart only)

Delegates to ShipCodeMapper for carrier name + methodCode derivation,
matching the behavior of the normal (non-early) ship confirm flow
in ConfirmMessageProcessor#build_order_line_status -> ShipCodeMapper#carrier_info.

Parameters:

  • label_result (Hash)

    with :carrier, :service_type

  • delivery (Delivery) (defaults to: nil)

Returns:

  • (Hash)

    with :carrierName (Hash), :methodCode (String)



5756
5757
5758
5759
5760
5761
5762
5763
5764
5765
5766
5767
5768
5769
5770
5771
5772
5773
# File 'app/models/order.rb', line 5756

def build_early_label_carrier_info(label_result, delivery = nil)
  actual_carrier = label_result[:carrier] || 'OTHER'

  orchestrator = Edi::Walmart::Orchestrator.new(edi_orchestrator_partner.to_sym)
  mapper = orchestrator.ship_code_mapper

  carrier_code = mapper.carrier_code(actual_carrier)
  carrier_name = if carrier_code
                   { carrier: carrier_code }
                 else
                   { otherCarrier: actual_carrier.to_s }
                 end

  description = delivery&.selected_shipping_cost&.description
  method = mapper.extract_method_from_delivery(description, carrier_code || actual_carrier)

  { carrierName: carrier_name, methodCode: method }
end

#build_early_label_tracking_url(carrier, tracking_number) ⇒ String?

Build tracking URL for early label

Parameters:

  • carrier (String)
  • tracking_number (String)

Returns:

  • (String, nil)


5780
5781
5782
5783
5784
5785
5786
5787
5788
5789
5790
5791
5792
5793
# File 'app/models/order.rb', line 5780

def build_early_label_tracking_url(carrier, tracking_number)
  return nil if tracking_number.blank?

  case carrier&.downcase
  when /fedex/
    "https://www.fedex.com/fedextrack/?trknbr=#{tracking_number}"
  when /ups/
    "https://www.ups.com/track?tracknum=#{tracking_number}"
  when /usps/
    "https://tools.usps.com/go/TrackConfirmAction?tLabels=#{tracking_number}"
  when /ontrac/
    "https://www.ontrac.com/trackingdetail.asp?tracking=#{tracking_number}"
  end
end

#buying_groupBuyingGroup?

The associated buying group.

Returns:



310
# File 'app/models/order.rb', line 310

belongs_to :buying_group, optional: true

#calculate_actual_shipping_costFloat

Sum of actual carrier-billed shipping cost on every invoiced
delivery (i.e. what we paid the carrier, not what the customer
was charged). Used by accounting reports to track shipping margin.

Returns:

  • (Float)


6175
6176
6177
# File 'app/models/order.rb', line 6175

def calculate_actual_shipping_cost
  deliveries.invoiced.to_a.sum { |d| d.actual_shipping_cost.to_f }
end

#can_auto_reserve_serial_numbers?Object

Whether can auto reserve serial numbers.

Returns:

  • (Object)


1717
1718
1719
# File 'app/models/order.rb', line 1717

def can_auto_reserve_serial_numbers?
  order_type != 'CO'
end

#can_be_cancelled?Object

Whether can be cancelled.

Returns:

  • (Object)


4238
4239
4240
4241
4242
4243
# File 'app/models/order.rb', line 4238

def can_be_cancelled?
  return false if editing_locked?
  return false if is_edi_order? && cancellation_reason.blank?

  cancelable?
end

#can_be_returned?Object

Whether can be returned.

Returns:

  • (Object)


2150
2151
2152
# File 'app/models/order.rb', line 2150

def can_be_returned?
  is_regular_order?
end

#can_cr_hold?(current_user = nil) ⇒ Object

Whether can cr hold.

Parameters:

  • current_user (Object) (defaults to: nil)

Returns:

  • (Object)


2457
2458
2459
# File 'app/models/order.rb', line 2457

def can_cr_hold?(current_user = nil)
  CAN_CR_HOLD_STATES.include?(state.to_sym) && all_deliveries_cancelable?(current_user)
end

#can_edit_future_release_date?Object

Whether can edit future release date.

Returns:

  • (Object)


1354
1355
1356
# File 'app/models/order.rb', line 1354

def can_edit_future_release_date?
  awaiting_deliveries? && deliveries.for_future_release.any?
end

#cancel_deliveries_pre_packObject

Cancels the in-progress packaging-estimate work on every
pre_pack delivery. Called when the order is being held or
cancelled so the warehouse stops trying to compute box dimensions
for an order that won't ship.



5900
5901
5902
5903
5904
# File 'app/models/order.rb', line 5900

def cancel_deliveries_pre_pack
  deliveries.reload.each do |delivery|
    delivery.cancel_estimated_packaging if delivery.pre_pack?
  end
end

#cancel_or_destroyObject

Carts get destroyed (no audit trail to keep). Real orders go
through the state-machine cancel event so cancellations are
logged and reversed properly.



4082
4083
4084
# File 'app/models/order.rb', line 4082

def cancel_or_destroy
  cart? ? destroy : cancel
end

#cancelable?Object

Whether cancelable.

Returns:

  • (Object)


2464
2465
2466
# File 'app/models/order.rb', line 2464

def cancelable?
  CANCELABLE_STATES.include?(state.to_sym) && deliveries.non_quoting.empty?
end

#cannot_delete_reason(account = nil) ⇒ String?

Human-readable explanation of why an order cannot currently be
deleted, or nil when #ok_to_delete? would let it through.
Used by the CRM delete-button confirmation dialog.

Parameters:

  • account (Account, nil) (defaults to: nil)

    currently-acting account (used to
    bypass the "non-draft only by admin" rule for admins)

Returns:

  • (String, nil)


3306
3307
3308
3309
3310
3311
3312
3313
3314
3315
3316
# File 'app/models/order.rb', line 3306

def cannot_delete_reason( = nil)
  if editing_locked?
    'Order cannot be deleted because it is in pending processing or beyond or referenced by authorizations.'
  elsif payments.any?(&:captured?)
    'Order cannot be deleted because it is referenced by authorizations.'
  elsif line_items.any?(&:has_linked_unvoided_rma_item?)
    'Order cannot be deleted until all linked RMA items have been voided.'
  elsif pending? || &.is_admin?
    'Orders that are non-draft can only be deleted by an admin'
  end
end

#cart_identifierString

Stable identifier suitable for URLs and abandoned-cart emails:
the reference number once the order has one, otherwise a SC<id>
("Shopping Cart") fallback so brand-new carts still have a slug.

Returns:

  • (String)


4250
4251
4252
# File 'app/models/order.rb', line 4250

def cart_identifier
  reference_number || "SC#{id}"
end

#cart_or_in_shipping_estimate?Object

Whether cart or in shipping estimate.

Returns:

  • (Object)


4257
4258
4259
# File 'app/models/order.rb', line 4257

def cart_or_in_shipping_estimate?
  cart? || in_shipping_estimate?
end

#catalogObject

Alias for Customer#catalog

Returns:

  • (Object)

    Customer#catalog

See Also:



424
# File 'app/models/order.rb', line 424

delegate :catalog, :billing_address, :billing_entity, :company, to: :customer

#check_payments_statusvoid

This method returns an undefined value.

Reconcile the order's payments with the upstream gateways
(Stripe, PayPal). Captures funds we know were taken outside
Heatwave, kicks expired authorizations back to pending_payment,
and re-syncs prepayments to deliveries. No-op for orders whose
only payments are non-gateway types (PO, store credit, check,
echeck, etc.) because those don't have anything to reconcile.



1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
# File 'app/models/order.rb', line 1573

def check_payments_status
  # Sometimes we manually capture the funds in paypal or stripe and HW doesn't know about it. Or the authorization is expired, etc.
  # This method checks that and puts the payment in the right status

  return if payments.blank?
  return if exclude_from_payment_check

  # Skip payment verification for orders that only have non-gateway payment types
  # These payment types don't require reauthorization or gateway status checks:
  # - PO/VPO: Terms-based, pre-approved credit
  # - Advance Replacement & RMA Credit: Internal credits from RMA process
  # - Store Credit: Internal customer credit balance
  # - CHECK/CASH/WIRE: Manual payments processed offline
  # - ECHECK: Cannot be captured through gateway, requires manual processing
  # - BREAD/PLAID: No check methods implemented yet
  non_gateway_payment_types = [
    Payment::PO,
    Payment::VPO,
    Payment::ADV_REPL,
    Payment::RMA_CREDIT,
    Payment::STORE_CREDIT,
    Payment::CHECK,
    Payment::CASH,
    Payment::WIRE,
    Payment::ECHECK,
    Payment::BREAD,
    Payment::PLAID
  ]

  return if payments.all? { |p| p.category.in?(non_gateway_payment_types) }

  # First credit cards
  payments.credit_cards.each(&:check_cc_payment_status)

  # Next Paypal payments
  payments.paypal_payments.each(&:check_paypal_payment_status)

  # Next paypal invoice
  payments.paypal_invoices.each(&:check_paypal_invoice_payment_status)

  # re-sync the prepayments to deliveries
  deliveries.each(&:relink_payments)

  # Reload to ensure we have fresh data after payment status checks
  # Payment status checks may have changed payment states, and we need
  # fresh delivery totals and payment associations
  reload
  deliveries.reload

  # after re-authorizing, check there are enough funds to cover the order total
  if all_funds_available?
    # nothing to do as all funds are available. This means the total of the order is either authorized or captured already. But not only captured.
  elsif can_pending_payment? && deliveries.none?(&:pre_pack?) && deliveries.none?(&:pending_manifest_completion?)
    OrdersMailer.order_with_insuficient_payment(self).deliver_later unless pending_payment?
    pending_payment!
  end
end

#check_sales_repObject (protected)

Validation callback that prevents an order from listing the same
rep as both primary and secondary, and from setting a secondary
rep without a primary. Skipped for store transfers (which have
no customer-side rep concept).



6390
6391
6392
6393
6394
6395
6396
6397
6398
# File 'app/models/order.rb', line 6390

def check_sales_rep
  # Skip sales rep validation for orders without customers (e.g., Store Transfers use from_store/to_store instead)
  return if customer.nil?

  errors.add('Sales rep', 'can only be primary or secondary sales rep for a given customer at a time') if (primary_sales_rep == secondary_sales_rep) && primary_sales_rep
  return unless secondary_sales_rep && !primary_sales_rep

  errors.add('Order', 'must first have a primary sales rep to have have a secondary sales rep')
end

#ci_invoice_credit_order?Object

Whether ci invoice credit order.

Returns:

  • (Object)


3520
3521
3522
# File 'app/models/order.rb', line 3520

def ci_invoice_credit_order?
  order_type == CREDIT_ORDER && rma&.original_invoice&.invoice_type == Invoice::CI
end

#clear_shipped_dateObject

Resets the shipped_date column when an order needs to be
reverted to a pre-shipped state (e.g. cancellation after a partial
ship). No-op when nothing was set.



3353
3354
3355
# File 'app/models/order.rb', line 3353

def clear_shipped_date
  update_attribute(:shipped_date, nil) if shipped_date.present?
end

#closed_state?Object

Whether closed state.

Returns:

  • (Object)


2428
2429
2430
# File 'app/models/order.rb', line 2428

def closed_state?
  CLOSED_STATES.include?(state.to_sym)
end

#communicationsActiveRecord::Associations::CollectionProxy

The associated communications.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


391
# File 'app/models/order.rb', line 391

has_many :communications, as: :resource, dependent: :nullify

#companyObject

Alias for Customer#company

Returns:

  • (Object)

    Customer#company

See Also:



424
# File 'app/models/order.rb', line 424

delegate :catalog, :billing_address, :billing_entity, :company, to: :customer

#company_review_urlObject

Reviews.io dynamic link for company review using the order reference as order_id
and the customer CN number as customer_identifier (for CRM context).



1379
1380
1381
# File 'app/models/order.rb', line 1379

def company_review_url
  Api::ReviewsIo::DynamicLinkBuilder.for_order(self)
end

#company_review_url_for_confirmation(email: nil) ⇒ Object

Reviews.io dynamic link for the order confirmation / thank-you page.
Uses the customer email instead of the CN identifier.

Parameters:

  • email (Object) (defaults to: nil)


1386
1387
1388
# File 'app/models/order.rb', line 1386

def company_review_url_for_confirmation(email: nil)
  Api::ReviewsIo::DynamicLinkBuilder.for_order_confirmation(self, email: email)
end

#complete?Object

Whether complete.

Returns:

  • (Object)


2711
2712
2713
# File 'app/models/order.rb', line 2711

def complete?
  invoiced?
end

#completed_regular_deliveriesArray<Delivery>

Active deliveries that have actually shipped (parcel or freight)
vs ones that completed in some other way (warehouse pickup,
service-only). Used to decide partial-vs-full ship status.

Returns:



6085
6086
6087
# File 'app/models/order.rb', line 6085

def completed_regular_deliveries
  deliveries.active.select(&:completed_regular_delivery?)
end

#completely_shipped?Object

Whether completely shipped.

Returns:

  • (Object)


6099
6100
6101
# File 'app/models/order.rb', line 6099

def completely_shipped?
  deliveries.active.all?(&:completed_regular_delivery?)
end

#consolidate_revenueObject

Snapshot the order's total and shipping cost at the moment the
customer completed checkout. The Google Ads conversion ping later
sends these values, and any discrepancy with the live total
(which may be re-tier'd by promotions) is investigated.



6152
6153
6154
6155
6156
6157
6158
6159
# File 'app/models/order.rb', line 6152

def consolidate_revenue
  # Method to save the order total that the customer paid at the time of checkout and compare this value to the
  # value we send as a conversion to google ads.
  return unless online_order?

  update_column(:revenue_consolidated_at_time_of_checkout, total)
  update_column(:shipping_cost_at_time_of_checkout, shipping_cost || 0.0)
end

#contactContact?

The associated contact.

Returns:



298
# File 'app/models/order.rb', line 298

belongs_to :contact, inverse_of: :orders, optional: true

#contact_comboObject

Used by dynamic contact lookup on order creation allowing interaction with the tom-select input on account_managers.html.erb
Contact can either be existing (single integer value for contact_id) or new contact



1321
1322
1323
1324
1325
# File 'app/models/order.rb', line 1321

def contact_combo
  return unless contact_id

  "Contact|#{contact_id}"
end

#contact_combo=(val) ⇒ Object

Used by dynamic contact lookup on order creation allowing interaction with the tom-select input on account_managers.html.erb
Contact can either be existing (single integer value for contact_id) or new contact if val is in format Customer|customer_id|full_name

Parameters:

  • val (Object)


1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
# File 'app/models/order.rb', line 1330

def contact_combo=(val)
  if val.blank?
    self.contact = nil
    return
  end
  contact_name, self.contact_id = val.split('|')
  return unless contact_id.blank? && name.present?

  self.contact = Contact.create(name: contact_name.squish.titleize, customer_id:)
end

#contact_combo_for_selectArray<Array(String, String)>

Active contacts under this order's customer formatted for a Rails
select helper, with each value encoded as "Contact|<id>" so the
CRM "contact" field can distinguish between picking an existing
contact and typing a new name.

Returns:

  • (Array<Array(String, String)>)


1347
1348
1349
# File 'app/models/order.rb', line 1347

def contact_combo_for_select
  customer.contacts.where(inactive: false).map { |cnt| [cnt.to_s, "Contact|#{cnt.id}"] }
end

#copy_customer_repsObject

In-memory mirror of #copy_invoice_reps that pulls reps from the
current customer's reps_collaboration setup. Called during order
construction so a fresh order inherits whoever owns the customer.



1431
1432
1433
1434
1435
# File 'app/models/order.rb', line 1431

def copy_customer_reps
  self.primary_sales_rep_id = primary_sales_rep.try(:id)
  self.secondary_sales_rep_id = secondary_sales_rep.try(:id)
  self.local_sales_rep_id = local_sales_rep.try(:id)
end

#copy_invoice_repsObject

Pulls primary / secondary / local sales-rep ids off the order's
invoice and writes them onto the order itself. Used when an order
is invoiced after rep reassignment so the order's commission split
matches the invoice that actually shipped.



1418
1419
1420
1421
1422
1423
1424
1425
1426
# File 'app/models/order.rb', line 1418

def copy_invoice_reps
  return unless (invoice = invoices.first)

  update(
    primary_sales_rep_id: invoice.primary_sales_rep_id,
    secondary_sales_rep_id: invoice.secondary_sales_rep_id,
    local_sales_rep_id: invoice.local_sales_rep_id
  )
end

#copy_items_from(order) ⇒ Boolean

Copy non-shipping line items from another order (typically a guest cart)
into this one, then destroy the source if it is itself a cart.

Atomicity matters: this is the guest -> account cart transfer path that
runs silently on login (Authenticable#handle_cart_transfer). The previous
implementation called add_multiple_items and then unconditionally
order.destroy if order.cart? — so when add_multiple_items raised
Order::ItemNotFound, returned early on editing_locked? / 100-item guard,
or hit a save failure, the source cart was destroyed while the items
never made it into the target. The customer's items vanished at checkout
with no error to the user and (often) no exception report.

Now: bail out early on no-op conditions, wrap copy + destroy in a single
transaction, and on any failure leave both carts untouched and report the
error so we can see it in AppSignal.

Parameters:

  • order (Object)

Returns:

  • (Boolean)

    true if items were copied (or there was nothing to
    copy and the empty source was cleaned up); false if the operation was
    aborted to preserve data.



1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
# File 'app/models/order.rb', line 1500

def copy_items_from(order)
  return false if order.nil?

  # Preserve room_configuration_id so the cart UI keeps grouping merged items
  # under their original room after a guest→account cart transfer.
  sku_array = order.line_items.non_shipping.parents_only.map do |li|
    { sku: li.sku, qty: li.quantity, room_configuration_id: li.room_configuration_id }
  end

  if sku_array.empty?
    order.destroy! if order.cart?
    return true
  end

  if editing_locked?
    ErrorReporting.warning(
      'Order#copy_items_from aborted: target cart is editing_locked',
      source: :web, source_cart_id: order.id, target_cart_id: id, target_state: state
    )
    return false
  end

  if line_items.size + sku_array.size > 100
    ErrorReporting.warning(
      'Order#copy_items_from aborted: would exceed 100 line items',
      source: :web, source_cart_id: order.id, target_cart_id: id,
      existing: line_items.size, incoming: sku_array.size
    )
    return false
  end

  copied = false
  ActiveRecord::Base.transaction do
    copied = add_multiple_items(sku_array).present?
    unless copied
      ErrorReporting.warning(
        'Order#copy_items_from aborted: add_multiple_items returned no items (race with editing_locked? or 100-item guard)',
        source: :web, source_cart_id: order.id, target_cart_id: id,
        target_state: state, target_line_item_count: line_items.size
      )
      raise ActiveRecord::Rollback
    end

    order.destroy! if order.cart?
  end
  copied
rescue Order::ItemNotFound,
       ActiveRecord::RecordInvalid,
       ActiveRecord::RecordNotSaved,
       ActiveRecord::RecordNotDestroyed => e
  ErrorReporting.error(
    e,
    source: :web, source_cart_id: order&.id, target_cart_id: id,
    skus: sku_array.pluck(:sku)
  )
  false
end

#copy_shipping_reference_number_to_deliveriesObject

After-save callback: propagate the order's
shipment_reference_number (used by Amazon FBA shipments and by
marketplace seller programs as the carrier BOL) onto every
delivery. Skipped unless the column actually changed AND has a
value.



1844
1845
1846
1847
1848
1849
1850
# File 'app/models/order.rb', line 1844

def copy_shipping_reference_number_to_deliveries
  # The shipment reference (or FBA ID) can be used as carrier_bol number when present
  # Only run when shipment_reference_number actually changed and is present
  return unless saved_change_to_shipment_reference_number? && shipment_reference_number.present?

  deliveries.update_all(carrier_bol: shipment_reference_number)
end

#countryCountry?

The Country that owns this order's billing entity (resolved
through customer → catalog → store → country). Used by tax,
shipping, and locale-aware code paths. Returns nil if any link
in the chain is missing rather than raising.

Returns:



2702
2703
2704
2705
2706
# File 'app/models/order.rb', line 2702

def country
  customer.catalog.store.country
rescue StandardError
  nil
end

#create_credit_memovoid

This method returns an undefined value.

Spawn a credit-memo invoice off this order (e.g. when the order is
being voided after invoicing) and run it through TaxJar to back
out any tax already reported.



3505
3506
3507
3508
# File 'app/models/order.rb', line 3505

def create_credit_memo
  CreditMemo.new_credit_memo_from_order(self)
  credit_memo.evaluate_taxjar_submission
end

#create_smartfix_ticketObject

Open a SmartFix service ticket (paid repair visit) and schedule
the service-confirmation activity.



3632
3633
3634
# File 'app/models/order.rb', line 3632

def create_smartfix_ticket
  new_ss_ticket('SmartFix', ActivityTypeConstants::SSFIX_SERVICE_CONFIRM)
end

#create_smartguide_ticketObject

Open a SmartGuide service ticket (consultation). Picks the
on-site vs remote variant based on whether the order contains the
SGS_ONSITE_FIXRATE SKU.



3639
3640
3641
3642
3643
3644
3645
# File 'app/models/order.rb', line 3639

def create_smartguide_ticket
  if line_items.joins(:item).merge(Item.where(sku: 'SGS_ONSITE_FIXRATE')).any?
    new_ss_ticket('SmartGuide', ActivityTypeConstants::SGS_ONSITE_PREPLAN_MEET)
  else
    new_ss_ticket('SmartGuide', ActivityTypeConstants::SGS_REMOTE_PREPLAN_MEET)
  end
end

#create_smartinstall_ticketObject

Open a SmartInstall service ticket against this order via the
shared #new_ss_ticket helper and schedule the kickoff
pre-installation phone activity.



3626
3627
3628
# File 'app/models/order.rb', line 3626

def create_smartinstall_ticket
  new_ss_ticket('SmartInstall', ActivityTypeConstants::SSI_PREPLAN_MEET)
end

#credit_memoCreditMemo?

The associated credit memo.

Returns:



351
# File 'app/models/order.rb', line 351

has_one :credit_memo, foreign_key: :credit_order_id

#credit_memosActiveRecord::Associations::CollectionProxy

The associated credit memos.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


373
# File 'app/models/order.rb', line 373

has_many :credit_memos, foreign_key: :original_order_id

CRM-facing path to this order's show page (relative URL, suitable
for use in CRM emails, Slack notifications, and audit logs).

Returns:

  • (String)


3276
3277
3278
# File 'app/models/order.rb', line 3276

def crm_link
  UrlHelper.instance.order_path(self)
end

#currency_symbolString

Currency symbol ($, , £, C$, …) for the order's currency,
resolved through the money gem's locale-aware Currency table.

Returns:

  • (String)


2534
2535
2536
# File 'app/models/order.rb', line 2534

def currency_symbol
  Money::Currency.new(currency).symbol
end

#custom_shipping_labelsObject

Pulls all custom ship labels from the order, deliveries and shipments



4095
4096
4097
4098
4099
4100
4101
4102
4103
4104
4105
# File 'app/models/order.rb', line 4095

def custom_shipping_labels
  label_array = []
  label_array += uploads.custom_ship_labels
  deliveries.each do |d|
    label_array += d.uploads.custom_ship_labels
    d.shipments.each do |shipment|
      label_array += shipment.uploads.custom_ship_labels
    end
  end
  label_array.compact.uniq
end

#customerCustomer?

The associated customer.

Returns:



286
# File 'app/models/order.rb', line 286

belongs_to :customer, inverse_of: :orders, optional: true

#customer_qualifies_for_free_online_shipping?Object

Whether customer qualifies for free online shipping.

Returns:

  • (Object)


2170
2171
2172
# File 'app/models/order.rb', line 2170

def customer_qualifies_for_free_online_shipping?
  discounts.free_online_shipping.present?
end

#deep_dupOrder

Deep-clones the order (line items, discounts, EDI documents) but
resets the identity / lifecycle columns so the duplicate can be
saved as a fresh order. Records the source order's id on the copy
via #parent_id, marks the copy pending, and suppresses
shipping auto-detect so the caller can reconfigure shipping
explicitly. Used by "Duplicate Order" CRM action and by order
splits.

Returns:

  • (Order)

    the unsaved duplicate



719
720
721
722
723
724
725
726
727
728
729
730
# File 'app/models/order.rb', line 719

def deep_dup
  deep_clone(
    include: %i[discounts edi_documents],
    except: %i[txid reference_number created_at creator_id edi_transaction_id edi_po_number]
  ) do |original, copy|
    if copy.is_a?(Order)
      copy.state = 'pending'
      copy.do_not_detect_shipping = true
      copy.parent_id = original.id
    end
  end
end

#default_billing_emailsArray<String>

Default email recipients for billing notifications: every email
contact point flagged as a billing notification channel for the
customer (or their billing entity), plus the tracking email,
contact emails, and any per-order overrides on billing_emails.
De-duplicates and sorts so the form renders deterministically.

Returns:

  • (Array<String>)


3153
3154
3155
3156
3157
3158
3159
3160
# File 'app/models/order.rb', line 3153

def default_billing_emails
  emails = []
  billing_customer = customer.billing_entity
  emails += NotificationChannel.joins(:contact_point).merge(ContactPoint.emails).where(customer_id: [customer_id, billing_customer&.id].compact.uniq).pluck(ContactPoint[:detail])
  emails << billing_customer.contact_points.transmittable.emails.pick(:detail) if billing_customer.profile && (billing_customer.profile.homeowner? || billing_customer.profile.direct_pro?)

  emails.compact.uniq.sort
end

#default_credit_card_vaultCreditCardVault?

The associated default credit card vault.

Returns:



323
# File 'app/models/order.rb', line 323

belongs_to :default_credit_card_vault, class_name: 'CreditCardVault', optional: true

#deferred_payments_captured_and_ready_for_shipping?Object

Whether deferred payments captured and ready for shipping.

Returns:

  • (Object)


2133
2134
2135
# File 'app/models/order.rb', line 2133

def deferred_payments_captured_and_ready_for_shipping?
  paypal_invoices_paid? && ready_for_shipping?
end

#deletable?Object

Whether deletable.

Returns:

  • (Object)


2526
2527
2528
# File 'app/models/order.rb', line 2526

def deletable?
  %w[pending pending_payment crm_back_order in_cr_hold].include? state
end

#deliveriesActiveRecord::Associations::CollectionProxy

The associated deliveries.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


412
# File 'app/models/order.rb', line 412

has_many :deliveries, -> { order(:origin_address_id) }, dependent: :destroy, autosave: true

#delivery_activitiesActiveRecord::Associations::CollectionProxy

The associated delivery activities.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


364
# File 'app/models/order.rb', line 364

has_many :delivery_activities, class_name: 'Activity', through: :deliveries, source: :activities

#direct_shipmentsActiveRecord::Associations::CollectionProxy

The associated direct shipments.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


379
# File 'app/models/order.rb', line 379

has_many :direct_shipments, class_name: 'Shipment'

#doesnt_already_has_smartinstall?Object

Whether doesnt already has smartinstall.

Returns:

  • (Object)


3446
3447
3448
# File 'app/models/order.rb', line 3446

def doesnt_already_has_smartinstall?
  line_items.smartinstall_items.empty?
end

#download_and_store_early_label_pdf_atomic(shipper, label_result, is_amazon: false) ⇒ Upload?

SAFEGUARD A: Atomic PDF download and storage
Downloads and stores the label PDF with retries, ensuring it's persisted before returning.
This prevents the race condition where void happens before PDF is saved.

Amazon: label data is returned inline from create_label — store directly.
Walmart: requires a separate download_label API call with retries.

Parameters:

Returns:

  • (Upload, nil)

    The persisted upload or nil if all attempts failed



5424
5425
5426
5427
5428
5429
5430
5431
5432
5433
5434
5435
5436
5437
# File 'app/models/order.rb', line 5424

def download_and_store_early_label_pdf_atomic(shipper, label_result, is_amazon: false)
  tracking_number = label_result[:tracking_number]
  carrier = label_result[:carrier]
  marketplace = is_amazon ? 'Amazon' : 'Walmart'

  if Rails.env.development?
    Rails.logger.info('[EarlyLabel] Development mode - creating mock label PDF')
    return store_mock_early_label_pdf(tracking_number, carrier)
  end

  return store_amazon_label_pdf_atomic(label_result, tracking_number, marketplace) if is_amazon

  store_walmart_label_pdf_atomic(shipper, label_result, tracking_number, carrier, marketplace)
end

#drop_ship_purchase_ordersActiveRecord::Associations::CollectionProxy

The associated drop ship purchase orders.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


418
# File 'app/models/order.rb', line 418

has_many :drop_ship_purchase_orders, -> { order(:id) }, through: :deliveries

#early_label_failure_notification(error_message) ⇒ Object

Send EDI admin notification when early label purchase fails
This ensures failures are not silent and the team is notified

Parameters:

  • error_message (String)

    Description of the failure



4898
4899
4900
4901
4902
4903
4904
4905
4906
4907
4908
4909
4910
4911
4912
4913
4914
4915
4916
4917
4918
4919
4920
4921
4922
4923
4924
4925
4926
4927
# File 'app/models/order.rb', line 4898

def early_label_failure_notification(error_message)
  marketplace = edi_orchestrator_partner&.start_with?('amazon_seller') ? 'Amazon' : 'Walmart'
  portal = marketplace == 'Amazon' ? 'Amazon Seller Central' : 'Walmart Seller Portal'

  subject = "Early Label Purchase FAILED - Order #{reference_number}"
  message = <<~MSG
    Early label purchase failed for #{marketplace} order #{reference_number}.

    ORDER DETAILS:
    - Order: #{reference_number}
    - PO Number: #{edi_po_number}
    - EDI Partner: #{edi_orchestrator_partner}
    - Order Link: https://#{CRM_HOSTNAME}/en-US/orders/#{id}

    ERROR:
    #{error_message}

    ACTION REQUIRED:
    The early label was NOT purchased automatically. The warehouse will need to:
    1. Purchase the label manually at ship-label time, OR
    2. Purchase via #{portal} and upload as manual_ship_label

    This order will proceed through normal warehouse flow without early tracking.
  MSG

  EdiMailer.notify_edi_admin_of_warning(subject, message).deliver_later
  Rails.logger.warn("[EarlyLabel] Sent failure notification for order #{reference_number}")
rescue StandardError => e
  Rails.logger.error("[EarlyLabel] Failed to send failure notification: #{e.message}")
end

#early_label_flash_messageHash

Generate flash messages based on early label purchase result
Called by controllers after order transitions to awaiting_deliveries

Returns:

  • (Hash)

    Hash with :type and :message keys, or nil if no message needed



5100
5101
5102
5103
5104
5105
5106
5107
5108
5109
5110
5111
5112
5113
5114
5115
5116
5117
5118
5119
5120
5121
5122
5123
5124
5125
# File 'app/models/order.rb', line 5100

def early_label_flash_message
  return nil if early_label_purchase_result.blank?

  result = early_label_purchase_result

  marketplace = edi_orchestrator_partner&.start_with?('amazon_seller') ? 'Amazon' : 'Walmart'

  if result[:success]
    if result[:already_purchased]
      {
        type: :info,
        message: "Early shipping label already purchased - Tracking: #{result[:tracking_number]} (#{result[:carrier]})"
      }
    else
      {
        type: :success,
        message: "Early shipping label purchased successfully! Tracking: #{result[:tracking_number]} (#{result[:carrier]}) - Tracking has been sent to #{marketplace}."
      }
    end
  else
    {
      type: :error,
      message: "Failed to purchase early shipping label: #{result[:error]}. The order has been released but no tracking was sent to #{marketplace}."
    }
  end
end

#early_label_purchase_enabled_for_partner?Boolean

Checks whether the partner's orchestrator has early label purchase enabled.
Walmart uses early_label_purchase_enabled?, Amazon uses buy_shipping_enabled?.

Returns:

  • (Boolean)


5019
5020
5021
5022
5023
5024
5025
5026
5027
5028
5029
5030
# File 'app/models/order.rb', line 5019

def early_label_purchase_enabled_for_partner?
  if edi_orchestrator_partner&.start_with?('walmart_seller')
    Edi::Walmart::Orchestrator.new(edi_orchestrator_partner.to_sym).early_label_purchase_enabled?
  elsif edi_orchestrator_partner&.start_with?('amazon_seller')
    Edi::Amazon::Orchestrator.new(edi_orchestrator_partner.to_sym).buy_shipping_enabled?
  else
    false
  end
rescue StandardError => e
  Rails.logger.warn("[EarlyLabel] Error checking early label purchase enabled: #{e.message}")
  false
end

#early_label_purchased_recently?Boolean

Check if early label was purchased recently (within the rapid void threshold)
Used by SAFEGUARD B to prevent race conditions

Returns:

  • (Boolean)


4769
4770
4771
4772
4773
4774
# File 'app/models/order.rb', line 4769

def early_label_purchased_recently?
  return false if early_label_purchased_at.blank?

  minutes_since_purchase = (Time.current - early_label_purchased_at) / 60
  minutes_since_purchase < EARLY_LABEL_RAPID_VOID_THRESHOLD_MINUTES
end

#early_label_shipments_match?(delivery) ⇒ Hash

Check if current shipments match the early label shipments
Used to detect if warehouse staff changed the packing

Parameters:

Returns:

  • (Hash)

    { match: true/false, reason: String if mismatch }



5363
5364
5365
5366
5367
5368
5369
5370
5371
5372
5373
5374
5375
5376
5377
5378
5379
5380
5381
5382
5383
5384
5385
5386
5387
5388
5389
5390
5391
5392
5393
5394
5395
5396
5397
5398
5399
5400
5401
5402
5403
5404
5405
5406
5407
5408
5409
5410
5411
# File 'app/models/order.rb', line 5363

def early_label_shipments_match?(delivery)
  return { match: true } unless has_early_purchased_label?

  current_shipments = delivery.shipments.non_voided.to_a
  early_count = early_label_shipments_count || 0
  early_data = early_label_shipments_data || []

  # Check shipment count
  if current_shipments.size != early_count
    return {
      match: false,
      reason: "Shipment count changed: early label was for #{early_count} shipment(s), now #{current_shipments.size}"
    }
  end

  # Check dimensions/weights for significant changes
  current_shipments.each_with_index do |shipment, idx|
    early_shipment = early_data[idx]
    next unless early_shipment

    # Check for significant dimension changes (more than 20% or 2 inches)
    %i[length width height].each do |dim|
      early_val = early_shipment[dim.to_s]&.to_f || 0
      current_val = shipment.send(dim)&.to_f || 0
      diff = (early_val - current_val).abs

      if diff > 2 && diff > (early_val * 0.2)
        return {
          match: false,
          reason: "Shipment #{idx + 1} #{dim} changed significantly: was #{early_val.round(1)}in, now #{current_val.round(1)}in"
        }
      end
    end

    # Check for significant weight changes (more than 20% or 1 lb)
    early_weight = early_shipment['weight']&.to_f || 0
    current_weight = shipment.weight&.to_f || 0
    weight_diff = (early_weight - current_weight).abs

    if weight_diff > 1 && weight_diff > (early_weight * 0.2)
      return {
        match: false,
        reason: "Shipment #{idx + 1} weight changed significantly: was #{early_weight.round(1)}lbs, now #{current_weight.round(1)}lbs"
      }
    end
  end

  { match: true }
end

#early_label_uploadUpload?

Get the early label upload (PDF) if it exists

Returns:



5081
5082
5083
# File 'app/models/order.rb', line 5081

def early_label_upload
  uploads.in_category('early_ship_label').first
end

#echecks_requiring_authorizationArray<Payment>

eCheck payments on this order that the fraud-review pipeline has
flagged as needing accounting review before the order can leave
CR hold. Driven by payment.authorization_review[:required].

Returns:



2495
2496
2497
# File 'app/models/order.rb', line 2495

def echecks_requiring_authorization
  payments.all_authorized.where(category: Payment::ECHECK).select { |pp| pp.authorization_review[:required] == true }
end

#edi_cancellation_reason_descriptionString

Customer-facing description of the EDI cancellation reason on the
order. Canadian Tire wants the raw 3-char code on the wire;
everywhere else we render "<code>: human-readable reason" so
warehouse staff can read it at a glance.

Returns:

  • (String)


3881
3882
3883
3884
3885
3886
3887
3888
3889
# File 'app/models/order.rb', line 3881

def edi_cancellation_reason_description
  if is_canadian_tire?
    # just return 3 character reason code for Canadian Tire
    cancellation_reason
  else
    # here we only want to return a description for when we use a code, fallback to humanize
    "#{cancellation_reason}: #{(edi_cancellation_reasons.find { |arr| arr.last == cancellation_reason }&.first || cancellation_reason).to_s.humanize.downcase}"
  end
end

#edi_cancellation_reasonsArray<Array(String, String)>

Allowed cancellation-reason codes for the order's EDI partner,
formatted as [[label, code], …] for a Rails select. Each EDI
partner has its own enumeration; falls through to a generic set
for partners that don't pin codes.

Returns:

  • (Array<Array(String, String)>)


3897
3898
3899
3900
3901
3902
3903
3904
3905
3906
3907
3908
3909
3910
3911
3912
3913
3914
3915
3916
3917
3918
3919
3920
3921
3922
3923
3924
3925
3926
3927
3928
3929
3930
3931
3932
3933
3934
3935
3936
3937
3938
3939
3940
3941
3942
3943
3944
3945
3946
3947
3948
3949
3950
3951
3952
3953
3954
3955
3956
3957
3958
3959
3960
3961
3962
3963
3964
3965
3966
3967
3968
3969
3970
3971
3972
3973
3974
3975
3976
3977
3978
3979
3980
3981
3982
3983
3984
3985
3986
3987
3988
3989
3990
3991
3992
3993
3994
3995
3996
3997
3998
3999
4000
4001
4002
4003
4004
4005
4006
4007
4008
4009
4010
4011
4012
4013
4014
4015
4016
4017
4018
4019
4020
4021
4022
4023
4024
4025
4026
4027
4028
4029
4030
4031
4032
4033
4034
4035
4036
4037
4038
4039
4040
4041
4042
4043
4044
4045
4046
4047
4048
4049
4050
4051
4052
# File 'app/models/order.rb', line 3897

def edi_cancellation_reasons
  if is_amazon_com?
    [
      ['Shipping 100 percent of ordered product', '00'],
      ['Canceled due to missing/invalid SKU', '02'],
      ['Canceled out of stock', '03'],
      ['Buyer request', '99'],
      ['Canceled due to duplicate Amazon Ship ID', '04'],
      ['Canceled due to missing/invalid Bill To Location Code', '05'],
      ['Canceled due to missing/invalid Ship From Location Code', '06'],
      ['Canceled due to missing/invalid Customer Ship to Name', '07'],
      ['Canceled due to missing/invalid Customer Ship to Address Line 1 ', '08'],
      ['Canceled due to missing/invalid Customer Ship to City', '09'],
      ['Canceled due to missing/invalid Customer Ship to State', '10'],
      ['Canceled due to missing/invalid Customer Ship to Postal Code ', '11'],
      ['Canceled due to missing/invalid Customer Ship to Country Code ', '12'],
      ['Canceled due to missing/invalid Shipping Carrier/Shipping Method ', '13'],
      ['Canceled due to missing/invalid Unit Price', '20'],
      ['Canceled due to missing/invalid Ship to Address Line 2', '21'],
      ['Canceled due to missing/invalid Ship to Address Line 3', '22'],
      ['Canceled due to Tax Nexus Issue', '50'],
      ['Canceled due to Restricted SKU/Qty', '51'],
      ['Canceled due to USPS >$400', '53'],
      ['Canceled due to Missing AmazonShipID', '54'],
      ['Canceled due to Missing AmazonOrderID', '55'],
      ['Canceled due to Missing LineItemId', '56'],
      ['Canceled due to discontinued item', '71']
    ]
  elsif is_home_depot_usa?
    [
      ['Bad SKU', 'bad_sku'],
      ["Cancelled at Merchant's Request", 'merchant_request'],
      ['Invalid Item Cost', 'invalid_item_cost'],
      ['Invalid method of shipment', 'invalid_ship_method'],
      ['Merchant detected fraud', 'merchant_detected_fraud'],
      ['Order Info Missing', 'info_missing'],
      ['Out of Stock', 'out_of_stock'],
      ['Product Has Been Discontinued', 'discontinued'],
      ['Supplier detected fraud', 'supplier_detected_fraud']
    ]
  elsif is_home_depot_can?
    [
      ['Backorder Cancellation', 'backorder_cancel'],
      ['Bad Address', 'bad_address'],
      ['Bad SKU', 'bad_sku'],
      ["Cancelled at Merchant's Request", 'merchant_request'],
      ['Cannot fulfill the order in time', 'fulfill_time_expired'],
      ['Cannot Ship as Ordered', 'cannot_meet_all_reqs'],
      ['Cannot ship to Country', 'cant_shipto_country'],
      ['Cannot ship to PO Box', 'cannot_shipto_POBOX'],
      ['Customer Refused Delivery', 'customer_refused'],
      ['Duplicate Order', 'duplicate_order'],
      ['Order Entry Error', 'order_entry_error'],
      ['Order Info Missing', 'info_missing'],
      ['Other', 'other'],
      ['Product Has Been Discontinued', 'discontinued']
    ]
  elsif is_costco_ca?
    [
      ['Bad Address', 'bad_address'],
      ['Bad SKU', 'bad_sku'],
      ["Cancelled at Merchant's Request", 'merchant_request'],
      ['Carrier does not service delivery location', 'carrier_does_not_service_area'],
      ['Customer Changed Mind', 'customer_request'],
      ['Duplicate Order', 'duplicate_order'],
      ['Minimum Order Not Met', 'min_order_not_met'],
      ['Order Entry Error', 'order_entry_error'],
      ['Order Info Missing', 'info_missing'],
      ['Out of Stock', 'out_of_stock'],
      ['Product Has Been Discontinued', 'discontinued'],
      ['To close order and allow reissue', 'close_and_reissue'],
      ['Unable to contact recipient', 'unable_to_contact_recipient']
    ]
  elsif is_part_of_rona?
    [
      ['Bad Address', 'bad_address'],
      ['Bad SKU', 'bad_sku'],
      ["Cancelled at Merchant's Request", 'merchant_request'],
      ['Cannot fulfill the order in time', 'fulfill_time_expired'],
      ['Cannot Ship as Ordered', 'cannot_meet_all_reqs'],
      ['Customer Changed Mind', 'customer_request'],
      ['Invalid Item Cost', 'invalid_item_cost'],
      ['Order Info Missing', 'info_missing'],
      ['Out of Stock', 'out_of_stock'],
      ['Product Has Been Discontinued', 'discontinued']
    ]
  elsif is_part_of_lowes_com?
    [
      ['Bad SKU', 'bad_sku'],
      ["Cancelled at Merchant's Request", 'merchant_request'],
      ['Minimum Order Not Met', 'min_order_not_met'],
      ['Other', 'other'],
      ['Invalid Item Cost', 'invalid_item_cost'],
      ['Out of Stock', 'out_of_stock'],
      ['Product Has Been Discontinued', 'discontinued']
    ]
  elsif is_walmart_ca?
    [
      ['Backorder Cancellation', 'backorder_cancel'],
      ['Bad SKU', 'bad_sku'],
      ["Cancelled at Merchant's Request", 'merchant_request'],
      ['Customer Changed Mind', 'customer_request'],
      ['Out of Stock', 'out_of_stock'],
      ['Product Has Been Discontinued', 'discontinued']
    ]
  elsif edi_orchestrator_partner == 'walmart_seller_us'
    [
      ['Backorder Cancellation', 'backorder_cancel'],
      ['Bad SKU', 'bad_sku'],
      ["Cancelled at Merchant's Request", 'merchant_request'],
      ['Customer Changed Mind', 'customer_request'],
      ['Out of Stock', 'out_of_stock'],
      ['Product Has Been Discontinued', 'discontinued']
    ]
  elsif is_canadian_tire?
    [
      ['Out of Stock', 'W01'],
      ['Not Enough Stock', 'W13'],
      ['Discontinued Item', 'A83'],
      ['Incorrect Address', 'A03'],
      ['Invalid Ship Instructions', '051'],
      ["Can't Ship on Time", 'D50'],
      ['Cancelled at Retailer Request', 'ABN'],
      ['Other', 'A13'],
      ['Bad Sku', 'A80'],
      ['Cannot ship to country', 'A05'],
      ['Cannot ship to PO box', 'A06'],
      ['Cannot ship USPS', 'A82'],
      ['Carrier does not service delivery location', 'D01'],
      ['Duplicate order', 'A07'],
      ['Invalid UOM', 'SOW'],
      ['Item Recall', 'IV1'],
      ['Minimum order not met', 'MIN'],
      ['Order entry error', 'W05'],
      ['Order info missing', 'B14'],
      ['Preorder cancellation', 'POA'],
      ['Fraud', '030'],
      ['To close order and allow reissue', 'RUN'],
      ['Unable to contact recipient', 'A58']
    ]
  else
    %w[
      bad_address
      bad_sku
      merchant_request
      carrier_wont_svc_loc
      customer_request
      customer_refused
      duplicate_order
      info_missing
      out_of_stock
      discontinued
      close_and_reissue
    ]
  end
end

#edi_communication_logsActiveRecord::Associations::CollectionProxy

The associated edi communication logs.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


409
# File 'app/models/order.rb', line 409

has_many :edi_communication_logs, through: :edi_documents

#edi_documentsActiveRecord::Associations::CollectionProxy

The associated edi documents.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


406
# File 'app/models/order.rb', line 406

has_many :edi_documents, dependent: :destroy

#edi_force_price_matchArray<Array<String>>

Force every parent line item with an EDI unit cost to match it on
both the MSRP price and the discounted price. Used to align an
EDI-imported order back to the partner's quoted price after the
CRM has re-sourced or repriced items.

Returns:

  • (Array<Array<String>>)

    per-line validation errors, empty
    if all updates succeeded



2293
2294
2295
2296
2297
2298
2299
# File 'app/models/order.rb', line 2293

def edi_force_price_match
  errs = []
  line_items.parents_only.where.not(edi_unit_cost: nil).find_each do |li|
    errs << li.errors.full_messages unless li.update(price: li.edi_unit_cost) && li.update(discounted_price: li.edi_unit_cost)
  end
  errs
end

#edi_orchestratorEdi::BaseOrchestrator?

The Edi::BaseOrchestrator subclass responsible for this order's
EDI partner, if it has one. Returns nil for non-EDI orders.

Returns:



6326
6327
6328
6329
# File 'app/models/order.rb', line 6326

def edi_orchestrator
  # simple order to orchestrator mapping, nil otherwise
  Edi::BaseOrchestrator.orchestrator_for_customer_id(customer_id) if edi_transaction_id
end

#edi_price_matcheable?Object

Whether edi price matcheable.

Returns:

  • (Object)


2276
2277
2278
2279
2280
2281
2282
2283
2284
# File 'app/models/order.rb', line 2276

def edi_price_matcheable?
  return false unless is_edi_order?

  target_lines = line_items.goods.parents_only
  return false if target_lines.blank?
  return false unless target_lines.all?(&:edi_unit_cost)

  target_lines.any? { |li| li.price != li.edi_unit_cost || li.discounted_price != li.edi_unit_cost }
end

#editing_locked?Object

Whether editing locked.

Returns:

  • (Object)


2421
2422
2423
# File 'app/models/order.rb', line 2421

def editing_locked?
  (LOCKED_STATES.include?(state.to_sym) || purchase_order&.should_lock_linked_order?).to_b
end

#effective_date_for_couponDate

Date used by the coupon engine to evaluate "is this coupon
currently valid?" against start_date/end_date windows. Carts
use today; existing orders use the order's override_coupon_date
(if any) or the created_at so the coupon evaluation is stable
after the order is placed even if the coupon's window later moves.

Returns:

  • (Date)


1672
1673
1674
1675
1676
# File 'app/models/order.rb', line 1672

def effective_date_for_coupon
  return Date.current if cart?

  override_coupon_date.presence || created_at.try(:to_date) || Date.current
end

#email_for_order_confirmationString?

First non-blank email for the order's confirmation flow,
checking (in order): the contact/customer's stored email, then
the customer's email, then the contact's account email, then the
customer's account email. Returns nil if every lookup fails.

Returns:

  • (String, nil)


3801
3802
3803
3804
3805
3806
3807
3808
3809
3810
3811
# File 'app/models/order.rb', line 3801

def email_for_order_confirmation
  party_for_order_confirmation.email || customer.email || (begin
    party_for_order_confirmation..email
  rescue StandardError
    nil
  end) || (begin
    customer..email
  rescue StandardError
    nil
  end)
end

#email_options_for_tracking_emailArray<String>

Choices for the "tracking emails" multi-select on the order form:
every email the customer has on file, plus whatever's currently
set on tracking_email (which may include free-typed addresses
not yet on the customer record).

Returns:

  • (Array<String>)


4167
4168
4169
# File 'app/models/order.rb', line 4167

def email_options_for_tracking_email
  [customer.all_emails, tracking_email].flatten.compact.uniq
end

#empty?Object

Whether empty.

Returns:

  • (Object)


2519
2520
2521
# File 'app/models/order.rb', line 2519

def empty?
  line_items.non_shipping.empty?
end

#encrypted_idString

Reversibly encrypted form of the order's database id, used in
public-facing URLs (payment link, cart-recovery emails) so the id
doesn't appear directly in logs or share links.

Returns:

  • (String)


6029
6030
6031
# File 'app/models/order.rb', line 6029

def encrypted_id
  Encryption.encrypt_string(id.to_s)
end

#errors_with_deliveries_errorsArray<String>

Combined errors from the order itself and from each of its
deliveries, suitable for surfacing to the CRM "save failed"
banner so users see every reason at once.

Returns:

  • (Array<String>)


6192
6193
6194
# File 'app/models/order.rb', line 6192

def errors_with_deliveries_errors
  (errors.full_messages + deliveries.map { |d| d.errors.full_messages }).flatten
end

#estimate_next_available_date_from_out_of_stock_itemsDate?

Latest "next available" date across every fully out-of-stock
line item (or nil if all items have stock). Used to estimate when
a back-ordered order can ship. Bounded recursion (max_depth: 10)
protects against pathological substitution chains in the catalog.

Returns:

  • (Date, nil)


5946
5947
5948
5949
5950
5951
5952
5953
5954
# File 'app/models/order.rb', line 5946

def estimate_next_available_date_from_out_of_stock_items
  # this returns nil if no items are out of stock, or latest available store item's next available date
  line_items.goods.select do |li|
    li.stock_status == :none
  end.filter_map do |li|
    # Use depth-limited version to prevent infinite recursion
    li.catalog_item.store_item.next_available_with_depth_limit(max_depth: 10)&.next_available_date
  end.max
end

#find_gbraidString?

Same fallback chain as #find_gclid for the Android-app
web-to-app gbraid token used by Google Ads.

Returns:

  • (String, nil)


6270
6271
6272
6273
6274
6275
6276
# File 'app/models/order.rb', line 6270

def find_gbraid
  visit&.marketing_meta_gbraid ||
    quote&.visit&.marketing_meta_gbraid ||
    opportunity&.visit&.marketing_meta_gbraid ||
    customer&.visit&.marketing_meta_gbraid ||
    customer&.visits&.last_marketing_value(:gbraid)
end

#find_gclidString?

Best-effort Google Click ID for this order. Walks the order's
visit, then the quote's visit, then the opportunity's visit,
then the customer-level captures so a conversion ping always has
an attribution token if any was ever recorded.

Returns:

  • (String, nil)


6245
6246
6247
6248
6249
6250
6251
6252
# File 'app/models/order.rb', line 6245

def find_gclid
  visit&.marketing_meta_gclid ||
    quote&.visit&.marketing_meta_gclid ||
    opportunity&.visit&.marketing_meta_gclid ||
    customer&.gclid ||
    customer&.visit&.marketing_meta_gclid ||
    customer&.visits&.last_marketing_value(:gclid)
end

#find_msclkidString?

Microsoft Ads (Bing) click identifier, captured by the Tracker into
Visit#marketing_meta['msclkid'] (JSONB-only). Keys the offline-conversion
upload in MicrosoftAds::ConversionReporter.

Mirrors #find_oppref but adds a landing-page fallback: visits created
before the msclkid-capture deploy carry the id in ?msclkid= on
landing_page (not yet in marketing_meta), so parse it back out across
the ~90-day offline-conversion window.

Returns:

  • (String, nil)


6308
6309
6310
6311
6312
6313
6314
6315
6316
6317
6318
6319
6320
# File 'app/models/order.rb', line 6308

def find_msclkid
  visit_msclkid = lambda do |v|
    next nil unless v

    (v.marketing_meta || {})['msclkid'].presence || v.landing_page_params['msclkid'].presence
  end

  visit_msclkid.call(visit) ||
    visit_msclkid.call(quote&.visit) ||
    visit_msclkid.call(opportunity&.visit) ||
    visit_msclkid.call(customer&.visit) ||
    customer&.visits&.last_marketing_value(:msclkid)
end

#find_opprefString?

OpenAI Ads (ChatGPT) click identifier, captured by the Tracker into
Visit#marketing_meta['oppref'] (JSONB-only — no scalar column). Used
by the CAPI reporter to populate the event's top-level oppref field,
which OpenAI requires us to forward ourselves on server-side events.

Fallback chain mirrors #find_gclid but queries the JSONB blob instead
of a scalar column; the last-resort SQL lookup uses GIN-indexed
marketing_meta @> '{"oppref": ...}' containment.

Returns:

  • (String, nil)


6288
6289
6290
6291
6292
6293
6294
6295
6296
# File 'app/models/order.rb', line 6288

def find_oppref
  visit_oppref = ->(v) { (v&.marketing_meta || {})['oppref'].presence }

  visit_oppref.call(visit) ||
    visit_oppref.call(quote&.visit) ||
    visit_oppref.call(opportunity&.visit) ||
    visit_oppref.call(customer&.visit) ||
    customer&.visits&.last_marketing_value(:oppref)
end

#find_wbraidString?

Same fallback chain as #find_gclid for the iOS-app web-to-app
wbraid token used by Google Ads.

Returns:

  • (String, nil)


6258
6259
6260
6261
6262
6263
6264
# File 'app/models/order.rb', line 6258

def find_wbraid
  visit&.marketing_meta_wbraid ||
    quote&.visit&.marketing_meta_wbraid ||
    opportunity&.visit&.marketing_meta_wbraid ||
    customer&.visit&.marketing_meta_wbraid ||
    customer&.visits&.last_marketing_value(:wbraid)
end

#fire_early_label_edi_confirm(delivery, label_result) ⇒ Object

Fire EDI ship confirm with early label tracking info.
This sends tracking to the marketplace immediately, without waiting for warehouse processing.
Branches for Amazon (packageDetail format) vs Walmart (orderShipment format).

Parameters:

  • delivery (Delivery)
  • label_result (Hash)

    Result from create_label



5601
5602
5603
5604
5605
5606
5607
5608
5609
5610
5611
5612
5613
5614
5615
5616
5617
5618
5619
5620
# File 'app/models/order.rb', line 5601

def fire_early_label_edi_confirm(delivery, label_result)
  return unless is_edi_order?

  Rails.logger.info("[EarlyLabel] Firing EDI ship confirm for order #{reference_number}")

  begin
    if edi_orchestrator_partner&.start_with?('amazon_seller')
      # Buy Shipping V2 purchaseShipment automatically notifies Amazon about the
      # shipment — a separate confirmShipment via the Orders API is redundant and
      # fails with "PackageToUpdateNotFound" because Buy Shipping manages packages
      # in a different subsystem than the Orders API expects.
      Rails.logger.info("[EarlyLabel] Skipping EDI ship confirm for Amazon Buy Shipping order #{reference_number} — Buy Shipping V2 handles notification automatically")
    else
      fire_early_label_edi_confirm_walmart(delivery, label_result)
    end
  rescue StandardError => e
    Rails.logger.error("[EarlyLabel] Failed to fire EDI ship confirm for order #{reference_number}: #{e.message}")
    Rails.logger.error(e.backtrace.first(5).join("\n"))
  end
end

#fire_early_label_edi_confirm_amazon(delivery, label_result) ⇒ Object

Amazon early label ship confirm — mirrors Edi::Amazon::ConfirmMessageProcessor#acknowledge_order
but builds the message from order data since no shipment record exists yet.

Parameters:

  • delivery (Object)
  • label_result (Object)


5626
5627
5628
5629
5630
5631
5632
5633
5634
5635
5636
5637
5638
5639
5640
5641
5642
5643
5644
5645
5646
5647
5648
5649
5650
5651
5652
5653
5654
5655
5656
5657
5658
5659
5660
5661
5662
5663
5664
5665
5666
5667
5668
5669
5670
5671
5672
5673
5674
5675
5676
5677
# File 'app/models/order.rb', line 5626

def fire_early_label_edi_confirm_amazon(delivery, label_result)
  orchestrator = Edi::Amazon::Orchestrator.new(edi_orchestrator_partner.to_sym)

  label_data = label_result.respond_to?(:to_h) ? label_result.to_h : label_result
  tracking_number = label_data[:tracking_number]
  effective_carrier = label_data[:carrier]

  carrier_code = Edi::Amazon::ConfirmMessageProcessor::CARRIER_TO_CARRIER_CODE_MAP_HASH[effective_carrier.to_sym] || 'Other'

  raw_message = edi_original_order_message
  raise 'edi_original_order_message is blank — cannot build ship confirm' if raw_message.blank?

  order_hash = JSON.parse(raw_message).with_indifferent_access
  order_items = (order_hash[:OrderItems] || []).map do |item|
    { orderItemId: item[:OrderItemId], quantity: (item[:QuantityOrdered] || 1).to_i }
  end

  message = {
    packageDetail: {
      packageReferenceId: '1',
      carrierCode: carrier_code,
      carrierName: effective_carrier,
      shippingMethod: delivery.shipping_method_friendly,
      trackingNumber: tracking_number,
      shipDate: Time.current.iso8601,
      orderItems: order_items
    },
    codCollectionMethod: 'DirectPayment',
    marketplaceId: orchestrator.marketplace
  }

  ecl = EdiCommunicationLog.create!(
    partner: orchestrator.partner,
    category: 'order_confirm',
    data: message.to_json,
    data_type: 'json',
    file_info: {
      order_id: id,
      reference_number: reference_number,
      lines_confirmed: order_items.size,
      early_label: true
    },
    transaction_id: edi_transaction_id,
    transmit_datetime: Time.current
  )
  ecl.edi_documents.create!(order: self)

  Rails.logger.info("[EarlyLabel] Created Amazon EDI ship confirm ECL #{ecl.id} for order #{reference_number}")

  # Immediately send via ConfirmMessageSender (POSTs to orders/v0/orders/{orderId}/shipmentConfirmation)
  orchestrator.confirm_message_sender.process(ecl)
end

#fire_early_label_edi_confirm_walmart(delivery, label_result) ⇒ Object

Walmart early label ship confirm — original Walmart-specific flow

Parameters:

  • delivery (Object)
  • label_result (Object)


5682
5683
5684
5685
5686
5687
5688
5689
5690
5691
5692
5693
5694
5695
5696
5697
5698
5699
5700
5701
5702
5703
5704
5705
5706
5707
5708
5709
5710
5711
5712
5713
5714
5715
5716
5717
5718
5719
5720
5721
5722
5723
5724
5725
5726
5727
5728
5729
5730
5731
5732
5733
5734
5735
5736
5737
5738
5739
5740
5741
5742
5743
5744
5745
# File 'app/models/order.rb', line 5682

def fire_early_label_edi_confirm_walmart(delivery, label_result)
  orchestrator = Edi::Walmart::Orchestrator.new(edi_orchestrator_partner.to_sym)

  raw_message = edi_original_order_message
  raise 'edi_original_order_message is blank — cannot build Walmart ship confirm' if raw_message.blank?

  order_hash = JSON.parse(raw_message).with_indifferent_access
  order_lines = order_hash.dig(:orderLines, :orderLine) || []

  label_data = label_result.respond_to?(:to_h) ? label_result.to_h : label_result
  carrier = label_data[:carrier]
  tracking_number = label_data[:tracking_number]

  ship_datetime = Time.current.iso8601
  carrier_info = build_early_label_carrier_info(label_data, delivery)
  tracking_url = build_early_label_tracking_url(carrier, tracking_number)

  shipped_order_lines = order_lines.map do |line|
    line_number = line[:lineNumber]
    quantity = line.dig(:orderLineQuantity, :amount) || '1'

    {
      lineNumber: line_number,
      orderLineStatuses: {
        orderLineStatus: [
          {
            status: 'Shipped',
            statusQuantity: { unitOfMeasurement: 'EACH', amount: quantity.to_s },
            trackingInfo: {
              shipDateTime: ship_datetime,
              carrierName: carrier_info[:carrierName],
              methodCode: carrier_info[:methodCode],
              trackingNumber: tracking_number,
              trackingURL: tracking_url
            }.compact
          }
        ]
      }
    }
  end

  message = {
    orderShipment: { orderLines: { orderLine: shipped_order_lines } }
  }

  ecl = EdiCommunicationLog.create!(
    partner: orchestrator.partner,
    category: 'order_confirm',
    data: message.to_json,
    data_type: 'json',
    file_info: {
      order_id: id,
      reference_number: reference_number,
      lines_confirmed: shipped_order_lines.size,
      early_label: true
    },
    transaction_id: edi_transaction_id,
    transmit_datetime: Time.current
  )
  ecl.edi_documents.create!(order: self)

  Rails.logger.info("[EarlyLabel] Created Walmart EDI ship confirm ECL #{ecl.id} for order #{reference_number}")
  ecl.process
end

#first_po_numberString?

First non-blank PO number across the order's payments. Used in
places that need just one PO for display where #po_number would
otherwise return a comma-separated list.

Returns:

  • (String, nil)


2839
2840
2841
# File 'app/models/order.rb', line 2839

def first_po_number
  payments.find(&:po_number)&.po_number
end

#first_tracking_emailString?

Single-string variant of #tracking_email (which is an array
column). Used in carrier API payloads that accept exactly one
notification email per shipment.

Returns:

  • (String, nil)


6108
6109
6110
# File 'app/models/order.rb', line 6108

def first_tracking_email
  tracking_email&.first
end

#fix_future_release_dateHash

Reconciles requested_ship_on_or_after, future_release_date,
and requested_ship_before so they don't contradict each other:
bumps future_release_date forward to honour the
ship-on-or-after date, and clears requested_ship_before when
the user explicitly sets a future-release date past it.
Returns a { message: "…" } hash describing any change so the
CRM can flash the user.

Returns:

  • (Hash)


6205
6206
6207
6208
6209
6210
6211
6212
6213
6214
6215
6216
6217
6218
6219
# File 'app/models/order.rb', line 6205

def fix_future_release_date
  res = {}
  if requested_ship_on_or_after && (requested_ship_on_or_after > Date.current) && (future_release_date.nil? || (future_release_date < requested_ship_on_or_after)) # check if we are shipping too soon and don't have a future date set or have one set too soon
    res[:message] = "Order ship on or after date was set past today (#{requested_ship_on_or_after}) and order future release date is set to #{future_release_date || 'none'}, so order future release date was updated to match."
    update(future_release_date: requested_ship_on_or_after)
  end
  # If future_release_date is after requested_ship_before, the user explicitly set a future hold.
  # Clear requested_ship_before since it conflicts with the intentional future release.
  # (Previously this would override the user's future_release_date, causing unexpected immediate release)
  if requested_ship_before && future_release_date && (future_release_date > requested_ship_before)
    res[:message] = "Order future release date (#{future_release_date}) is after the ship before date (#{requested_ship_before}). Clearing ship before date to respect the future release hold."
    update(requested_ship_before: nil)
  end
  res
end

#formatted_po_numberString

po_number with the customer's preferred prefix applied (e.g.
"PO# 12345" vs bare "12345"), driven by
customer.include_po_prefix?. Used in CRM list views and emails.

Returns:

  • (String)


3383
3384
3385
# File 'app/models/order.rb', line 3383

def formatted_po_number
  po_number(include_po_prefix: include_po_prefix?)
end

#from_storeStore?

The associated from store.

Returns:



335
# File 'app/models/order.rb', line 335

belongs_to :from_store, class_name: 'Store', optional: true

#fully_funded_by_advance_replacement?Object

Whether fully funded by advance replacement.

Returns:

  • (Object)


2866
2867
2868
# File 'app/models/order.rb', line 2866

def fully_funded_by_advance_replacement?
  deliveries.any? && deliveries.all? { |dq| dq.payments.any? && dq.payments.all? { |pp| pp.category == Payment::ADV_REPL } }
end

#funded_by_advance_replacement?Object

Whether funded by advance replacement.

Returns:

  • (Object)


2873
2874
2875
# File 'app/models/order.rb', line 2873

def funded_by_advance_replacement?
  deliveries.any? { |_dq| payments.any? { |pp| pp.category == Payment::ADV_REPL } }
end

#funded_by_cod?Object

Whether funded by cod.

Returns:

  • (Object)


2859
2860
2861
# File 'app/models/order.rb', line 2859

def funded_by_cod?
  deliveries.all?(&:funded_by_cod?) && deliveries.any?
end

#funds_available_and_ready_for_warehouse?Object (protected)

Whether funds available and ready for warehouse.

Returns:

  • (Object)


6410
6411
6412
# File 'app/models/order.rb', line 6410

def funds_available_and_ready_for_warehouse?
  all_funds_available? && ready_for_warehouse?
end

#funds_shortfall_report(excluding_payment: nil) ⇒ String

Human-readable, per-delivery breakdown of why #all_funds_available? is
false: each under-covered delivery's total vs. authorized funds and the
shortfall. Lets ops distinguish pricing drift (a total inflated past the
authorization) from a genuine underpayment, instead of chasing a bare
"insufficient funds" error (SO728077, defect H).

Parameters:

  • excluding_payment (Payment, nil) (defaults to: nil)

Returns:

  • (String)

    empty when every delivery is fully covered



2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
# File 'app/models/order.rb', line 2810

def funds_shortfall_report(excluding_payment: nil)
  deliveries.filter_map do |dq|
    dq_total = dq.total || BigDecimal('0.0')
    dq_auth = dq.total_payments_authorized(currency, excluding_payment:) || BigDecimal('0.0')
    short = dq_total - dq_auth
    next if short <= 0

    format('delivery #%s short $%.2f (total $%.2f vs authorized $%.2f)', dq.id, short, dq_total, dq_auth)
  end.join('; ')
end

#generate_spiff_training_activityActivity

Schedules a 7-day-out follow-up training activity for the primary
sales rep so they walk a SPIFF-eligible customer through their
first WarmlyYours order. Called automatically when
#needs_spiff_training? returns true.

Returns:



1981
1982
1983
# File 'app/models/order.rb', line 1981

def generate_spiff_training_activity
  Activity.create(activity_type_id: ActivityTypeConstants::SPIFFACTTRAIN, target_datetime: 7.days.from_now, assigned_resource: primary_sales_rep, party: customer, resource: self)
end

#get_expected_ship_date_timeTime

Estimated delivery time = scheduled ship time +
carrier-committed transit days from the selected shipping
option. Falls back to a 4-working-day commitment when no rate
has been picked yet.

Returns:

  • (Time)


5989
5990
5991
5992
# File 'app/models/order.rb', line 5989

def get_expected_ship_date_time
  days_committment = (deliveries.first&.selected_shipping_cost&.days_commitment || 4.0).ceil # put in some fallback of 4 days
  days_committment.working.days.since(get_scheduled_ship_date_time)
end

#get_scheduled_ship_date_timeTime

When the order is expected to be picked up by the carrier. Honors
future_release_date when set, falls back to today's 15:55 cutoff
in the store's local timezone, and rolls forward to the next
working day at 10:00 if we're already past cutoff or on a
weekend. Back-orders use the latest stock-available date.

Returns:

  • (Time)


5963
5964
5965
5966
5967
5968
5969
5970
5971
5972
5973
5974
5975
5976
5977
5978
5979
5980
5981
# File 'app/models/order.rb', line 5963

def get_scheduled_ship_date_time
  order_ship_date_time = future_release_date if future_release_date.present? && (future_release_date > Time.current)
  if crm_back_order?
    order_ship_date_time = estimate_next_available_date_from_out_of_stock_items || 10.working.days.since(Time.current) # is 10 days a good fallback?
  elsif order_ship_date_time.nil?
    Time.use_zone(store.time_zone_string) do # WY Canada is Eastern Time, Wy US is Central
      today_cut_off_time = Time.zone.parse('15:55:00')
      order_ship_date = (future_release_date || today_cut_off_time).strftime('%Y-%m-%d') # use future release date if present or today_cut_off_time
      order_ship_time = today_cut_off_time.strftime('%R:%S')
      if Time.current > today_cut_off_time || today_cut_off_time.on_weekend?
        early_pickup_time = Time.zone.parse('10:00:00')
        order_ship_date = 1.working.day.since(early_pickup_time).strftime('%Y-%m-%d')
        order_ship_time = early_pickup_time.strftime('%R:%S')
      end
      order_ship_date_time = Time.zone.parse("#{order_ship_date} #{order_ship_time}")
    end
  end
  order_ship_date_time.to_time
end

#has_authorized_payment?Object

Whether has authorized payment.

Returns:

  • (Object)


1561
1562
1563
# File 'app/models/order.rb', line 1561

def has_authorized_payment?
  payments.all_authorized.any?
end

#has_committed_serial_number_reservations?Object

Whether has committed serial number reservations.

Returns:

  • (Object)


3127
3128
3129
# File 'app/models/order.rb', line 3127

def has_committed_serial_number_reservations?
  line_items.any? { |li| li.require_reservation? && !li.all_reserved_serial_numbers_available? }
end

#has_custom_packing_slip?Object

Whether has custom packing slip.

Returns:

  • (Object)


2660
2661
2662
# File 'app/models/order.rb', line 2660

def has_custom_packing_slip?
  uploads.in_category('custom_packing_slip_pdf').present? || amzbs_packing_slip_included?
end

#has_early_purchased_label?Boolean

Check if order has an active (non-voided) early-purchased label

Returns:

  • (Boolean)


4932
4933
4934
# File 'app/models/order.rb', line 4932

def has_early_purchased_label?
  early_label_tracking_number.present? && early_label_voided_at.nil?
end

#has_incomplete_reservations?Object

Whether has incomplete reservations.

Returns:

  • (Object)


3113
3114
3115
# File 'app/models/order.rb', line 3113

def has_incomplete_reservations?
  has_unreserved_line_items? || has_committed_serial_number_reservations?
end

#has_selected_heated_items?Object

Whether has selected heated items.

Returns:

  • (Object)


3460
3461
3462
# File 'app/models/order.rb', line 3460

def has_selected_heated_items?
  smartinstall_data.present?
end

#has_shipping_method?Object

Whether has shipping method.

Returns:

  • (Object)


2673
2674
2675
# File 'app/models/order.rb', line 2673

def has_shipping_method?
  chosen_shipping_method.present?
end

#has_unreserved_line_items?Object

Whether has unreserved line items.

Returns:

  • (Object)


3120
3121
3122
# File 'app/models/order.rb', line 3120

def has_unreserved_line_items?
  line_items.any? { |li| li.require_reservation? && !li.fully_reserved? }
end

#has_web_rooms_needing_installation_plans?Object

Whether has web rooms needing installation plans.

Returns:

  • (Object)


3134
3135
3136
# File 'app/models/order.rb', line 3134

def has_web_rooms_needing_installation_plans?
  room_configurations.any? { |rc| rc.web_room? && rc.room_layout_attached? && !rc.complete? && !rc.installation_plans_attached? }
end

#hold_for_early_label_mismatch!(_delivery, reason) ⇒ Object

Puts the order on CR hold with a descriptive note when early label
purchase cannot proceed because the selected shipping rate doesn't
match available marketplace rates.

Parameters:

  • _delivery (Delivery)

    Unused — kept for caller signature symmetry

  • reason (String)


5060
5061
5062
5063
5064
5065
5066
5067
5068
5069
5070
5071
5072
5073
5074
5075
5076
# File 'app/models/order.rb', line 5060

def hold_for_early_label_mismatch!(_delivery, reason)
  note_text = <<~NOTE.strip
    ⚠ Early label purchase BLOCKED: #{reason}

    Please select a valid marketplace shipping rate (e.g. an AMZBS rate for Amazon orders) on the Shipping tab, then release the order.
  NOTE

  if can_cr_hold?
    cr_hold
    quick_note(note_text) if respond_to?(:quick_note)
  else
    quick_note(note_text) if respond_to?(:quick_note)
    Rails.logger.warn("[EarlyLabel] Cannot CR-hold order #{reference_number} (state=#{state}) — note added instead")
  end

  early_label_failure_notification(reason)
end

#hold_order_reasonsArray<String>

Human-readable list of reasons this order is currently being held
by the CR/fraud/EDI pipeline — every line item the CRM hold-page
surfaces to the user. An empty array means the order is releasable.
Combines validation across customer state, billing/shipping data,
EDI partner constraints (price match, shipping option, packing
slip), and freight readiness.

Returns:

  • (Array<String>)


2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
# File 'app/models/order.rb', line 2560

def hold_order_reasons
  res = []
  res << 'Order was manually held and so can only be manually released.' if is_manual_hold?
  res << 'Customer is in a state requiring orders to be held (lead_qualify, guest, bankrupt or closed)' if customer.lead_qualify? || customer.guest? || customer.bankrupt? || customer.closed?
  res << 'Logged in customer placing order has qc orders flag set' if is_online? && customer.qc_orders? && !CurrentScope.employee_logged_in?
  res << 'Customer has no billing address' if billing_address.nil?
  res << 'Order has no shipping method' unless has_shipping_method?
  if all_rooms_not_orderable? # we should let empty rooms through as long as other rooms in the order are orderable, case in point a tiny corner/closet room that we could not fit heating elements into
    res << 'Rooms/Heated Spaces are not orderable, empty line items'
  end
  res << 'Order ships via freight but shipping address is not freight ready, please set shipping address freight fields and recalculate shipping' if ships_freight_but_address_not_freight_ready?
  res << 'Deliveries are awaiting packaging estimates' if deliveries.any?(&:pre_pack?)
  if is_edi_order?
    res << "EDI order price match discrepancy, expects line total to be #{price_match}" if price_match.present? && price_match != line_total
    res << 'EDI order missing line number, all non shipping lines must have an edi line number' if line_items.parents_only.non_shipping.where(edi_line_number: nil).present?
    # Skip shipping option validation when order is being cancelled - shipping method doesn't matter for cancellations
    if cancellation_reason.blank?
      if edi_original_ship_code.to_s.upcase.exclude?('UNSP') && edi_shipping_option_name.present? && deliveries.any? { |d| !d.shipping_option_matches?(edi_shipping_option_name) }
        # unless carrier is unspecified ie UNSP, force match on mapped edi_shipping_option_name
        res << "EDI order must use original shipping option: #{edi_shipping_option_name}"
      end
      # For Walmart SWW orders, allow any WalmartSeller shipping option even if edi_shipping_option_name wasn't set
      # This handles orders created before the 'sww' shipping option name was added
      walmart_sww_ok = edi_orchestrator_partner&.start_with?('walmart_seller') &&
                       deliveries.all? { |d| d.shipping_option&.carrier == 'WalmartSeller' }
      # For Amazon Buy Shipping orders, allow any AmazonSeller shipping option
      amazon_amzbs_ok = edi_orchestrator_partner&.start_with?('amazon_seller') &&
                        deliveries.all? { |d| d.shipping_option&.carrier == 'AmazonSeller' }
      marketplace_ok = walmart_sww_ok || amazon_amzbs_ok
      res << "EDI order must use original shipping option matched from: #{edi_original_ship_code}" if edi_original_ship_code.present? && edi_shipping_option_name.blank? && !marketplace_ok
    end
    if deliveries.any? { |d| (!d.signature_confirmation && signature_confirmation?) || (d.signature_confirmation && !signature_confirmation?) }
      res << "EDI order deliveries must use EDI order signature_confirmation option: #{signature_confirmation}"
    end

    if edi_is_pick_slip_required? && !has_custom_packing_slip?
      link_snippet = ''
      encoded_po = ERB::Util.url_encode(edi_po_number.to_s)
      packing_slip_url =
        if customer&.is_houzz?
          "https://www.houzz.com//printBuyerOrder//orderId=#{encoded_po}"
        elsif customer&.is_amazon_seller_central?
          "https://sellercentral.amazon.com/orders/packing-slip?orderId=#{encoded_po}"
        elsif customer&.is_amazon_vendor_central?
          "https://vendorcentral.amazon.com/hz/vendor/members/df/orders?id=#{encoded_po}"
        end
      if packing_slip_url
        escaped_url = ERB::Util.html_escape(packing_slip_url)
        link_snippet = ", get it manually here: <a href=\"#{escaped_url}\" target=\"_blank\" rel=\"noopener noreferrer\"><i class=\"fa-sharp fa-solid fa-arrow-up-right-from-square\"></i> #{escaped_url}</a>"
      end
      res << "This EDI order requires a custom packing slip, but it failed to get attached#{link_snippet}"
    end
    # Home depot canada rule + item ERT240-1.5x35, remove trap when order has been found.  BYPASS in notes for false positive
    if customer.id == 10_358 && line_items.any? { |li| li.item_id == 1910 } && !activities.notes_only.where(Activity[:notes].matches('%BYPASS%')).exists?
      res << 'EDI order for HDC contains item: ERT240-1.5x35, contact india to ensure not a duplicate of PO 0088974632'
    end
    if edi_auto_cancel_options.present? && cancellation_reason.present?
      res << "EDI order is pending cancellation via EDI for reason: #{cancellation_reason}, if it is canceled, Heatwave will send a rejection acknowledgement message due to #{cancellation_reason}."
      bad_vendor_skus_msg = nil
      bad_merchant_skus_msg = nil
      bad_vendor_skus_msg = "bad vendor SKUs: #{edi_auto_cancel_options[:bad_vendor_skus].join(', ')}" if edi_auto_cancel_options[:bad_vendor_skus]&.any?
      bad_merchant_skus_msg = "bad merchant SKUs: #{edi_auto_cancel_options[:bad_merchant_skus].join(', ')}" if edi_auto_cancel_options[:bad_merchant_skus]&.any?
      res << "The following bad SKUs need to be addressed because order line items could not be created for them: #{[bad_vendor_skus_msg, bad_merchant_skus_msg].compact.join(', ')}" if bad_vendor_skus_msg || bad_merchant_skus_msg
    end
    if shipping_cost.to_f > 0.0 && customer&.bill_shipping_to_customer?
      res << "EDI order should have shipping cost of $0.00, since EDI customer is set to bill shipping to customer, but order shipping cost is: $#{format('%.2f',
                                                                                                                                                          shipping_cost.to_f)}. Please ensure to use the EDI customer shipping account or, worst case, have an admin or manager add either a EDI_SHIPPING_ADJ or FS-A coupon."
    end
  end
  # NOTE: "Shipping too late" is now a warning, not a blocking hold reason.
  # It's displayed via shipping_date_warnings but doesn't prevent order release.
  # Users can still ship late orders after acknowledging the warning.
  res << 'Cannot combine service items with goods in the same order. Please split this order.' if line_items.services.with_positive_qty.any? && line_items.goods.with_positive_qty.any?
  if (moqv = minimum_order_quantity_violations).present?
    res += moqv.map(&:name)
  end
  res += errors.full_messages unless ready_for_shipping?
  res.uniq
end

#hold_orders?Object

Whether hold orders.

Returns:

  • (Object)


2541
2542
2543
# File 'app/models/order.rb', line 2541

def hold_orders?
  hold_order_reasons.present?
end

#human_state_nameString

Human-friendly version of the state machine state. Mostly defers
to AASM/state_machines super, but rewrites the bland
"awaiting deliveries" into "awaiting service delivery" for orders
whose only outstanding deliveries are services (warranty visits,
SmartInstall) so the CRM list distinguishes them at a glance.

Raises ArgumentError on a state the machine doesn't declare, which is
deliberate: the order_state enum makes such a value unstorable, so seeing
one means a programmer assigned garbage in memory and the loud failure is the
point. AppSignal #3274 was the persisted case, and the enum is its fix.

Returns:

  • (String)


2742
2743
2744
2745
2746
# File 'app/models/order.rb', line 2742

def human_state_name
  return 'awaiting service delivery' if deliveries.any?(&:service_ready_to_fulfill?) && awaiting_deliveries?

  super
end

#include_po_prefix?Object

Alias for Customer#include_po_prefix?

Returns:

  • (Object)

    Customer#include_po_prefix?

See Also:



702
703
# File 'app/models/order.rb', line 702

delegate :include_po_prefix?, :is_home_depot_usa?, :is_home_depot_can?, :is_walmart_ca?, :is_amazon_com?, :is_costco_ca?, :is_part_of_rona?, :is_part_of_lowes_com?, :is_houzz?, :is_wasn4_or_wat0f?, :is_canadian_tire?, :is_amazon_seller_central?,
to: :customer

#includes_schedulable_service?Object

Whether includes schedulable service.

Returns:

  • (Object)


2980
2981
2982
# File 'app/models/order.rb', line 2980

def includes_schedulable_service?
  belongs_to_smartservice_group?
end

#inherited_attention_nameString?

Default attention-line value derived from the shipping address's
person name, but only when the customer is a person (not a
company); otherwise the company name should be used as the line 1
name and ATTN can stay blank.

Returns:

  • (String, nil)


3841
3842
3843
3844
3845
# File 'app/models/order.rb', line 3841

def inherited_attention_name
  n = nil
  n = shipping_address.person_name if shipping_address && customer&.is_person?
  n
end

#installation_country_isoString?

ISO 2-letter country code of the installation site (US, CA, …).

Returns:

  • (String, nil)


4157
4158
4159
# File 'app/models/order.rb', line 4157

def installation_country_iso
  opportunity.presence&.installation_country_iso
end

#installation_country_iso3String?

ISO 3-letter country code of the installation site (USA, CAN, …).

Returns:

  • (String, nil)


4150
4151
4152
# File 'app/models/order.rb', line 4150

def installation_country_iso3
  opportunity.presence&.installation_country_iso3
end

#installation_is_within_range?Object

Whether installation is within range.

Returns:

  • (Object)


3467
3468
3469
3470
3471
3472
3473
# File 'app/models/order.rb', line 3467

def installation_is_within_range?
  zip_code = shipping_address&.zip
  distance = SmartServicesController.helpers.calculate_distance_from_lz(zip_code)
  return true if distance.present? && (distance <= 100) # within 100 miles from office

  false
end

#installation_postal_codeString?

Postal code where the order will be installed (from the linked
opportunity's installation address, not the shipping address).
Used by service-area lookups for SmartInstall eligibility.

Returns:

  • (String, nil)


4135
4136
4137
# File 'app/models/order.rb', line 4135

def installation_postal_code
  opportunity.presence&.installation_postal_code
end

#installation_state_codeString?

State/province code of the installation site (see
#installation_postal_code).

Returns:

  • (String, nil)


4143
4144
4145
# File 'app/models/order.rb', line 4143

def installation_state_code
  opportunity.presence&.installation_state_code
end

#invoice_balanceBigDecimal

Sum of Invoice#balance across every invoice on the order — the
outstanding A/R amount. Differs from #balance in that this
looks at invoices once they exist; balance looks at deliveries.

Returns:

  • (BigDecimal)


2826
2827
2828
2829
2830
2831
2832
# File 'app/models/order.rb', line 2826

def invoice_balance
  bal = BigDecimal('0.0')
  invoices.to_a.each do |i|
    bal += i.balance
  end
  bal
end

#invoiced_local_sales_repParty?

The associated invoiced local sales rep.

Returns:



332
# File 'app/models/order.rb', line 332

belongs_to :invoiced_local_sales_rep, class_name: 'Party', foreign_key: :local_sales_rep_id, optional: true

#invoiced_primary_sales_repParty?

The associated invoiced primary sales rep.

Returns:



326
# File 'app/models/order.rb', line 326

belongs_to :invoiced_primary_sales_rep, class_name: 'Party', foreign_key: :primary_sales_rep_id, optional: true

#invoiced_secondary_sales_repParty?

The associated invoiced secondary sales rep.

Returns:



329
# File 'app/models/order.rb', line 329

belongs_to :invoiced_secondary_sales_rep, class_name: 'Party', foreign_key: :secondary_sales_rep_id, optional: true

#invoicesActiveRecord::Associations::CollectionProxy

The associated invoices.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


382
# File 'app/models/order.rb', line 382

has_many :invoices

#is_amazon_com?Object

Alias for Customer#is_amazon_com?

Returns:

  • (Object)

    Customer#is_amazon_com?

See Also:



702
703
# File 'app/models/order.rb', line 702

delegate :include_po_prefix?, :is_home_depot_usa?, :is_home_depot_can?, :is_walmart_ca?, :is_amazon_com?, :is_costco_ca?, :is_part_of_rona?, :is_part_of_lowes_com?, :is_houzz?, :is_wasn4_or_wat0f?, :is_canadian_tire?, :is_amazon_seller_central?,
to: :customer

#is_amazon_seller_central?Object

Alias for Customer#is_amazon_seller_central?

Returns:

  • (Object)

    Customer#is_amazon_seller_central?

See Also:



702
703
# File 'app/models/order.rb', line 702

delegate :include_po_prefix?, :is_home_depot_usa?, :is_home_depot_can?, :is_walmart_ca?, :is_amazon_com?, :is_costco_ca?, :is_part_of_rona?, :is_part_of_lowes_com?, :is_houzz?, :is_wasn4_or_wat0f?, :is_canadian_tire?, :is_amazon_seller_central?,
to: :customer

#is_canadian_tire?Object

Alias for Customer#is_canadian_tire?

Returns:

  • (Object)

    Customer#is_canadian_tire?

See Also:



702
703
# File 'app/models/order.rb', line 702

delegate :include_po_prefix?, :is_home_depot_usa?, :is_home_depot_can?, :is_walmart_ca?, :is_amazon_com?, :is_costco_ca?, :is_part_of_rona?, :is_part_of_lowes_com?, :is_houzz?, :is_wasn4_or_wat0f?, :is_canadian_tire?, :is_amazon_seller_central?,
to: :customer

#is_costco_ca?Object

Alias for Customer#is_costco_ca?

Returns:

  • (Object)

    Customer#is_costco_ca?

See Also:



702
703
# File 'app/models/order.rb', line 702

delegate :include_po_prefix?, :is_home_depot_usa?, :is_home_depot_can?, :is_walmart_ca?, :is_amazon_com?, :is_costco_ca?, :is_part_of_rona?, :is_part_of_lowes_com?, :is_houzz?, :is_wasn4_or_wat0f?, :is_canadian_tire?, :is_amazon_seller_central?,
to: :customer

#is_crm?Object

Whether is crm.

Returns:

  • (Object)


3850
3851
3852
# File 'app/models/order.rb', line 3850

def is_crm?
  order_reception_type == 'CRM'
end

#is_edi_order?Object

Whether is edi order.

Returns:

  • (Object)


2260
2261
2262
# File 'app/models/order.rb', line 2260

def is_edi_order?
  edi_transaction_id.present?
end

#is_fba?Object

Whether is fba.

Returns:

  • (Object)


2314
2315
2316
# File 'app/models/order.rb', line 2314

def is_fba?
  is_store_transfer? && customer&.is_amazon_com?
end

#is_from_myprojects?Object

Whether is from myprojects.

Returns:

  • (Object)


2692
2693
2694
# File 'app/models/order.rb', line 2692

def is_from_myprojects?
  !customer_reference.to_s.strip.empty?
end

#is_home_depot_can?Object

Alias for Customer#is_home_depot_can?

Returns:

  • (Object)

    Customer#is_home_depot_can?

See Also:



702
703
# File 'app/models/order.rb', line 702

delegate :include_po_prefix?, :is_home_depot_usa?, :is_home_depot_can?, :is_walmart_ca?, :is_amazon_com?, :is_costco_ca?, :is_part_of_rona?, :is_part_of_lowes_com?, :is_houzz?, :is_wasn4_or_wat0f?, :is_canadian_tire?, :is_amazon_seller_central?,
to: :customer

#is_home_depot_usa?Object

Alias for Customer#is_home_depot_usa?

Returns:

  • (Object)

    Customer#is_home_depot_usa?

See Also:



702
703
# File 'app/models/order.rb', line 702

delegate :include_po_prefix?, :is_home_depot_usa?, :is_home_depot_can?, :is_walmart_ca?, :is_amazon_com?, :is_costco_ca?, :is_part_of_rona?, :is_part_of_lowes_com?, :is_houzz?, :is_wasn4_or_wat0f?, :is_canadian_tire?, :is_amazon_seller_central?,
to: :customer

#is_houzz?Object

Alias for Customer#is_houzz?

Returns:

  • (Object)

    Customer#is_houzz?

See Also:



702
703
# File 'app/models/order.rb', line 702

delegate :include_po_prefix?, :is_home_depot_usa?, :is_home_depot_can?, :is_walmart_ca?, :is_amazon_com?, :is_costco_ca?, :is_part_of_rona?, :is_part_of_lowes_com?, :is_houzz?, :is_wasn4_or_wat0f?, :is_canadian_tire?, :is_amazon_seller_central?,
to: :customer

#is_marketing_order?Object

Whether is marketing order.

Returns:

  • (Object)


3555
3556
3557
# File 'app/models/order.rb', line 3555

def is_marketing_order?
  order_type == MARKETING_ORDER
end

#is_online?Object

Whether is online.

Returns:

  • (Object)


3857
3858
3859
# File 'app/models/order.rb', line 3857

def is_online?
  order_reception_type == 'Online'
end

#is_part_of_lowes_com?Object

Alias for Customer#is_part_of_lowes_com?

Returns:

  • (Object)

    Customer#is_part_of_lowes_com?

See Also:



702
703
# File 'app/models/order.rb', line 702

delegate :include_po_prefix?, :is_home_depot_usa?, :is_home_depot_can?, :is_walmart_ca?, :is_amazon_com?, :is_costco_ca?, :is_part_of_rona?, :is_part_of_lowes_com?, :is_houzz?, :is_wasn4_or_wat0f?, :is_canadian_tire?, :is_amazon_seller_central?,
to: :customer

#is_part_of_rona?Object

Alias for Customer#is_part_of_rona?

Returns:

  • (Object)

    Customer#is_part_of_rona?

See Also:



702
703
# File 'app/models/order.rb', line 702

delegate :include_po_prefix?, :is_home_depot_usa?, :is_home_depot_can?, :is_walmart_ca?, :is_amazon_com?, :is_costco_ca?, :is_part_of_rona?, :is_part_of_lowes_com?, :is_houzz?, :is_wasn4_or_wat0f?, :is_canadian_tire?, :is_amazon_seller_central?,
to: :customer

#is_price_editable?Boolean

Tells whether or not the order can have its line item discounted and msrp price editable by the
price editable concerns, this method is called by the ability check first.

Returns:

  • (Boolean)


2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
# File 'app/models/order.rb', line 2156

def is_price_editable?
  case order_type
  when Order::STORE_TRANSFER
    !editing_locked?
  when Order::CREDIT_ORDER
    !(ready_for_printing? || printed?)
  else
    false
  end
end

#is_regular_order?Object

Whether is regular order.

Returns:

  • (Object)


3562
3563
3564
# File 'app/models/order.rb', line 3562

def is_regular_order?
  [SALES_ORDER, MARKETING_ORDER, TECH_ORDER].include?(order_type)
end

#is_remote_service?Object

Whether is remote service.

Returns:

  • (Object)


3024
3025
3026
# File 'app/models/order.rb', line 3024

def is_remote_service?
  belongs_to_smartservice_group? && line_items.any? { |a| a.sku.include?('REMOTE') }
end

#is_rma_replacement?Object

Whether is rma replacement.

Returns:

  • (Object)


3534
3535
3536
# File 'app/models/order.rb', line 3534

def is_rma_replacement?
  rma && [SALES_ORDER, MARKETING_ORDER, TECH_ORDER].include?(order_type)
end

#is_rma_return?Object

Whether is rma return.

Returns:

  • (Object)


3513
3514
3515
# File 'app/models/order.rb', line 3513

def is_rma_return?
  order_type == CREDIT_ORDER
end

#is_sales_order?Object

Whether is sales order.

Returns:

  • (Object)


3541
3542
3543
# File 'app/models/order.rb', line 3541

def is_sales_order?
  order_type == SALES_ORDER
end

#is_smartfit_service?Object

Whether is smartfit service.

Returns:

  • (Object)


2987
2988
2989
# File 'app/models/order.rb', line 2987

def is_smartfit_service?
  line_items.smartfit_items.any?
end

#is_smartfix_service?Object

Whether is smartfix service.

Returns:

  • (Object)


3001
3002
3003
# File 'app/models/order.rb', line 3001

def is_smartfix_service?
  line_items.smartfix_items.any?
end

#is_smartguide_service?Object

Whether is smartguide service.

Returns:

  • (Object)


3008
3009
3010
# File 'app/models/order.rb', line 3008

def is_smartguide_service?
  line_items.smartguide_items.any?
end

#is_smartinstall_service?Object

Whether is smartinstall service.

Returns:

  • (Object)


2994
2995
2996
# File 'app/models/order.rb', line 2994

def is_smartinstall_service?
  line_items.smartinstall_items.any?
end

#is_store_transfer?Object

Whether is store transfer.

Returns:

  • (Object)


3527
3528
3529
# File 'app/models/order.rb', line 3527

def is_store_transfer?
  order_type == STORE_TRANSFER
end

#is_subject_to_minimum_qty_rules?Object

Whether is subject to minimum qty rules.

Returns:

  • (Object)


3569
3570
3571
# File 'app/models/order.rb', line 3569

def is_subject_to_minimum_qty_rules?
  [SALES_ORDER].include?(order_type)
end

#is_tech_order?Object

Whether is tech order.

Returns:

  • (Object)


3548
3549
3550
# File 'app/models/order.rb', line 3548

def is_tech_order?
  order_type == TECH_ORDER
end

#is_walmart_ca?Object

Alias for Customer#is_walmart_ca?

Returns:

  • (Object)

    Customer#is_walmart_ca?

See Also:



702
703
# File 'app/models/order.rb', line 702

delegate :include_po_prefix?, :is_home_depot_usa?, :is_home_depot_can?, :is_walmart_ca?, :is_amazon_com?, :is_costco_ca?, :is_part_of_rona?, :is_part_of_lowes_com?, :is_houzz?, :is_wasn4_or_wat0f?, :is_canadian_tire?, :is_amazon_seller_central?,
to: :customer

#is_warehouse_pickup?Object

Whether is warehouse pickup.

Returns:

  • (Object)


3106
3107
3108
# File 'app/models/order.rb', line 3106

def is_warehouse_pickup?
  shipping_address&.is_warehouse
end

#is_wasn4_or_wat0f?Object

Alias for Customer#is_wasn4_or_wat0f?

Returns:

  • (Object)

    Customer#is_wasn4_or_wat0f?

See Also:



702
703
# File 'app/models/order.rb', line 702

delegate :include_po_prefix?, :is_home_depot_usa?, :is_home_depot_can?, :is_walmart_ca?, :is_amazon_com?, :is_costco_ca?, :is_part_of_rona?, :is_part_of_lowes_com?, :is_houzz?, :is_wasn4_or_wat0f?, :is_canadian_tire?, :is_amazon_seller_central?,
to: :customer

#job_nameString?

Best-known job name for this order: the originating quote's
opportunity name, falling back to the order's own opportunity
name, or nil if the order isn't linked to either. Used by
warehouse pack-list headings and job-folder titles.

Returns:

  • (String, nil)


2122
2123
2124
2125
2126
2127
2128
# File 'app/models/order.rb', line 2122

def job_name
  if quote
    quote.opportunity.name
  elsif opportunity
    opportunity.name
  end
end

#linked_support_casesActiveRecord::Associations::CollectionProxy

The associated linked support cases.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


394
# File 'app/models/order.rb', line 394

has_many :linked_support_cases, through: :room_configurations, source: :support_cases

#live_payments_currency_consistentvoid (protected)

This method returns an undefined value.

Blocks flipping an order's currency while it still holds a live payment
(authorized OR captured) in the outgoing currency. A USD guest cart that
becomes a Canadian order (SO727885, 2026-07) must not silently keep a
USD/CAD-mismatched payment — the money has to be voided or refunded and
re-taken in the new currency first. Mirrors
Catalog::AssignCatalogToCustomer#error_check. Only fires on a currency
change, so pre-existing cross-currency payments are undisturbed until
someone actively re-currencies them.



6376
6377
6378
6379
6380
6381
6382
6383
6384
# File 'app/models/order.rb', line 6376

def live_payments_currency_consistent
  return unless persisted?

  stale = payments.where(state: LIVE_PAYMENT_STATES).where.not(currency:).where.not(currency: nil)
  return if stale.none?

  errors.add(:currency,
             "cannot change to #{currency} while a live payment in #{stale.distinct.pluck(:currency).join(', ')} exists — void or refund it first")
end

#local_sales_repEmployee?

Local (on-site) sales rep on the order. Same closed-vs-open
semantics as #primary_sales_rep.

Returns:



1887
1888
1889
1890
1891
# File 'app/models/order.rb', line 1887

def local_sales_rep
  return invoiced_local_sales_rep if order_closed?

  invoiced_local_sales_rep || customer&.local_sales_rep
end

#log_early_label_event(event_type, message, details = {}) ⇒ Object

Log an event to the early label API log
Used for debugging and visibility into what happened during early label purchase

Parameters:

  • event_type (String)

    Type of event (e.g., 'api_call', 'api_response', 'error')

  • message (String)

    Description of the event

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

    Optional additional details



4826
4827
4828
4829
4830
4831
4832
4833
4834
4835
# File 'app/models/order.rb', line 4826

def log_early_label_event(event_type, message, details = {})
  @early_label_api_log ||= []
  @early_label_api_log << {
    timestamp: Time.current.iso8601,
    event: event_type,
    message: message,
    details: details
  }
  Rails.logger.info("[EarlyLabel] #{event_type}: #{message} #{details.presence&.to_json}")
end

#mark_serial_coupons_as_usedObject

Burn any single-use coupon serial numbers that were applied to
this order so they can't be redeemed again. Called when the
order transitions out of pending-payment.



6011
6012
6013
# File 'app/models/order.rb', line 6011

def mark_serial_coupons_as_used
  discounts.includes(:coupon_serial_number).joins(:coupon_serial_number).find_each { |d| d.coupon_serial_number.update_attribute!(:used, true) }
end

#marketplace_early_label_eligible?Boolean

Check if order is eligible for marketplace early label purchase (Walmart SWW or Amazon Buy Shipping)

Returns:

  • (Boolean)


4981
4982
4983
# File 'app/models/order.rb', line 4981

def marketplace_early_label_eligible?
  walmart_sww_eligible? || amazon_buy_shipping_eligible?
end

#material_alertsActiveRecord::Associations::CollectionProxy

The associated material alerts.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


403
# File 'app/models/order.rb', line 403

has_many :material_alerts, dependent: :destroy

#merge_shipper_api_log(shipper) ⇒ Object

Merge raw HTTP API call logs from the shipper into the order's API log.
Works for both Walmart SWW and Amazon Buy Shipping carriers.

Parameters:

  • shipper (Object)


4840
4841
4842
4843
4844
4845
4846
4847
4848
4849
4850
4851
4852
4853
4854
4855
4856
4857
4858
4859
4860
4861
4862
4863
4864
4865
4866
4867
4868
4869
4870
4871
# File 'app/models/order.rb', line 4840

def merge_shipper_api_log(shipper)
  return unless shipper.respond_to?(:api_log) && shipper.api_log.present?

  prefix = case shipper
           when Shipping::AmazonSeller  then 'amzbs_http'
           when Shipping::WalmartSeller then 'sww_http'
           else 'marketplace_http'
           end

  @early_label_api_log ||= []
  @early_label_api_log_offset ||= 0
  new_entries = shipper.api_log[@early_label_api_log_offset..]
  @early_label_api_log_offset = shipper.api_log.length

  (new_entries || []).each do |entry|
    @early_label_api_log << {
      timestamp: entry[:timestamp],
      event: "#{prefix}_#{entry[:operation]}",
      message: "HTTP #{entry[:method]} #{entry[:url]}",
      details: {
        operation: entry[:operation],
        method: entry[:method],
        url: entry[:url],
        request_payload: entry[:request_payload],
        response_status: entry[:response_status],
        response_body: entry[:response_body],
        success: entry[:success],
        error: entry[:error]
      }
    }
  end
end

#messaging_logsActiveRecord::Associations::CollectionProxy

The associated messaging logs.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


400
# File 'app/models/order.rb', line 400

has_many :messaging_logs, dependent: :destroy, as: :resource

#minimum_order_quantity_violationsArray<Alert>

MOQ (minimum-order-quantity) alerts triggered by the current
line items — e.g. "this control requires a minimum of 5 units".
Surfaced in #hold_order_reasons.

Returns:

  • (Array<Alert>)


2653
2654
2655
# File 'app/models/order.rb', line 2653

def minimum_order_quantity_violations
  Item::Materials::Checks::Moq.new.process(container: self).alerts
end

#move_deliveries_from_quoting!Object

Promote any deliveries currently in quoting (rate-shopping)
state into ready_to_ship, copying the order-level shipment /
label / future-release fields onto each delivery first. Skips
pre-pack deliveries (which still need warehouse packing data
before they can be released).



5807
5808
5809
5810
5811
5812
5813
5814
5815
5816
5817
5818
5819
5820
5821
5822
5823
# File 'app/models/order.rb', line 5807

def move_deliveries_from_quoting!
  deliveries.quoting.each do |delivery|
    next if delivery.pre_pack?

    delivery.shipment_instructions = shipment_instructions
    delivery.label_instructions = label_instructions
    delivery.future_release_date = future_release_date
    delivery.manual_release_only = manual_release_only
    delivery.do_not_reserve_stock = do_not_reserve_stock
    # Save before ready_to_ship! because ready_to_ship! calls reload inside an advisory lock,
    # which would discard unsaved changes (including future_release_date).
    # Without this save, orders with future release dates incorrectly go to at_warehouse
    # instead of future_release state.
    delivery.save!
    delivery.ready_to_ship!
  end
end

#move_deliveries_to_quoting!Object

Force every delivery on the order back into the quoting state
so the rate-shop can be re-run from scratch (e.g. after a
shipping address change). Cancels deliveries first if they're
past the cancelable threshold. Wrapped in a transaction so a
mid-loop failure rolls everything back.



5872
5873
5874
5875
5876
5877
5878
5879
5880
5881
5882
5883
5884
5885
5886
5887
5888
5889
5890
5891
5892
5893
5894
# File 'app/models/order.rb', line 5872

def move_deliveries_to_quoting!
  deliveries.reload.each do |delivery|
    Rails.logger.info("[Order] Moving delivery #{delivery.id} from #{delivery.state} to quoting")
    begin
      if delivery.can_back_to_quoting?
        delivery.back_to_quoting!
        Rails.logger.info("[Order] Successfully moved delivery #{delivery.id} to quoting")
      else
        Rails.logger.warn("[Order] Cannot move delivery #{delivery.id} to quoting - transition not allowed from state: #{delivery.state}")
        # Try to force it by calling cancel first
        if delivery.cancelable?
          delivery.cancel
          delivery.back_to_quoting! if delivery.can_back_to_quoting?
          Rails.logger.info("[Order] Successfully moved delivery #{delivery.id} to quoting after cancel")
        end
      end
    rescue StandardError => e
      Rails.logger.error("[Order] Error moving delivery #{delivery.id} to quoting: #{e.message}")
      Rails.logger.error(e.backtrace.first(5).join("\n"))
      raise # Re-raise to rollback the transaction
    end
  end
end

#move_service_case_from_pending_service_paymentObject

When a SmartService order's payment finally clears, flip the
associated SmartService support case out of
pending_service_payment and into the case_open state so the
service team can pick it up.



5829
5830
5831
5832
# File 'app/models/order.rb', line 5829

def move_service_case_from_pending_service_payment
  service_case = support_cases&.services&.pending_service_payment&.first
  service_case.presence&.case_open
end

#nameString

Human label like "Order #SO123456 (PO# 0042)" (or with PO#s
for multi-PO orders). Used in mailer subjects, CRM headers, and
support-case titles.

Returns:

  • (String)


3253
3254
3255
3256
3257
3258
3259
3260
3261
# File 'app/models/order.rb', line 3253

def name
  po_numbers_text = ''
  po_numbers = po_number
  if po_numbers.present?
    po_numbers_text = " (PO# #{po_numbers})" unless po_numbers.index(',')
    po_numbers_text = " (PO#s #{po_numbers})" if po_numbers.index(',')
  end
  "Order ##{reference_number}#{po_numbers_text}"
end

#need_to_recalculate_shippingObject

Flag the order so the next save re-runs the carrier rate-shop and
re-detects shipping options. Clears the suppression flag set by
earlier flows that wanted to bypass detection.



6164
6165
6166
6167
6168
# File 'app/models/order.rb', line 6164

def need_to_recalculate_shipping
  self.recalculate_shipping = true
  self.do_not_detect_shipping = false
  logger.debug "Deliverable, need_to_recalculate_shipping ID: #{id}, self.recalculate_shipping: #{recalculate_shipping}, self.do_not_detect_shipping: #{do_not_detect_shipping}"
end

#needs_attention_issuesArray<String>

Human-readable list of mismatches between the order's state and
its deliveries' states (e.g. order is awaiting_deliveries but
some deliveries are still quoting). Surfaces in the CRM
"Needs Attention" panel so dispatch can hand-fix them.

Returns:

  • (Array<String>)


6000
6001
6002
6003
6004
6005
6006
# File 'app/models/order.rb', line 6000

def needs_attention_issues
  issues = []
  issues << "order is #{state.humanize.downcase} but deliveries are still in quoting, need to hold the order and recalculate shipping" if SHIPPING_STATES.include?(state.to_sym) && deliveries.any?(&:quoting?)
  issues << "order is #{state.humanize.downcase} and deliveries in state Awaiting PO fulfillment, need to ensure PO fulfillment process proceeds" if deliveries.any?(&:awaiting_po_fulfillment?)
  issues << "order is #{state.humanize.downcase} and deliveries in state Processing PO fulfillment, need to ensure PO fulfillment process completed" if deliveries.any?(&:processing_po_fulfillment?)
  issues
end

#needs_spiff_training?Object

Whether needs spiff training.

Returns:

  • (Object)


1971
1972
1973
# File 'app/models/order.rb', line 1971

def needs_spiff_training?
  spiff_enrollment.present? && spiff_enrollment.spiff.is_active? && customer.orders.one? && customer.activities.where(activity_type_id: ActivityTypeConstants::SPIFFACTTRAIN).empty?
end

#new_customer_online_orderBoolean

True when this is a brand-new online customer's first order —
a key segmentation flag for new-customer email flows and
acquisition reporting.

Returns:

  • (Boolean)


2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
# File 'app/models/order.rb', line 2323

def new_customer_online_order
  # puts "!!!new_customer_online_order, new_customer_order: #{new_customer_order}, self.customer.reception_type.downcase: #{self.customer.reception_type.downcase}, self.customer.account.present?: #{self.customer.account.present?}"
  # Check that this is a new customer/order and an online order
  Rails.logger.info "new_customer_online_order: id: #{id}, reference_number: #{reference_number}"
  if new_customer_order && online_order?
    Rails.logger.info "new_customer_online_order: TRUE id: #{id}, reference_number: #{reference_number}"
    true
  else
    Rails.logger.info "new_customer_online_order: FALSE id: #{id}, reference_number: #{reference_number}"
    false
  end
end

#new_customer_orderBoolean

True when this is the customer's only non-management-held order,
i.e. their first real purchase. Used by the welcome email flow
and by SmartInstall enrollment to skip "first-time customer"
bonuses for repeat customers.

Returns:

  • (Boolean)


2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
# File 'app/models/order.rb', line 2342

def new_customer_order
  # puts "!!!new_customer_order, self.customer.orders.length: #{self.customer.orders.length}, !in_management_hold?: #{!in_management_hold?}"
  Rails.logger.info "new_customer_order: id: #{id}, reference_number: #{reference_number}"
  # Check that this is a new customer/order and not in manager hold (in the case of Canada situation above)
  if (customer.orders.length == 1) && !in_management_hold?
    Rails.logger.info "new_customer_order: TRUE id: #{id}, reference_number: #{reference_number}"
    true
  else
    Rails.logger.info "new_customer_order: FALSE id: #{id}, reference_number: #{reference_number}"
    false
  end
end

#new_ss_ticket(service, activity_type) ⇒ Object

Shared backend for #create_smartinstall_ticket,
#create_smartfix_ticket, and #create_smartguide_ticket:
builds the SmartService support case, attaches participants and
rooms, and schedules the kickoff activity. No-op if a service
ticket already exists on the order.

Parameters:

  • service (String)

    human-readable service label

  • activity_type (Integer)

    ActivityTypeConstants id for
    the kickoff activity



3656
3657
3658
3659
3660
3661
3662
3663
3664
3665
3666
3667
3668
3669
3670
3671
3672
3673
3674
3675
3676
3677
3678
3679
# File 'app/models/order.rb', line 3656

def new_ss_ticket(service, activity_type)
  return if support_cases.services.any?

  ticket = support_cases.new(
    case_type: 'SmartService',
    assigned_to_id: Employee::SCOTT,
    state: 'new',
    service_address_id: shipping_address.id,
    description: "#{service}: Order id #{id}. #{service} request for a project.",
    priority: 'High',
    service:
  )
  ticket.build_participants_and_rooms(order_id: id)
  return unless ticket.save!

  # ticket.order_ids = [self.id] this is already being done in the build_participants_and_rooms method
  Activity.create(lock_target_datetime: false,
                  activity_type_id: activity_type,
                  target_datetime: 1.working.days.from_now,
                  assigned_resource: Employee.find(Employee::SCOTT), # Assigned by default to JL
                  party: customer,
                  resource: ticket,
                  notes: "Call Customer to confirm date and time of the #{service} service.")
end

#next_six_months_with_end_datesArray<Array(String, String)>

Six-month label/value pairs for end-of-month dates starting
this month — [["October 2026", "2026-10-31"], …]. Powers the
ship-by month dropdown on EDI orders.

Returns:

  • (Array<Array(String, String)>)


2720
2721
2722
2723
2724
2725
2726
2727
2728
# File 'app/models/order.rb', line 2720

def next_six_months_with_end_dates
  today = Time.zone.today
  (0..5).map do |i|
    date = today >> i
    month_name = date.strftime('%B %Y')
    end_of_month = date.end_of_month.strftime('%Y-%m-%d')
    [month_name, end_of_month]
  end
end

#next_warehouse_ship_date(now: Time.current) ⇒ Date?

Earliest date the warehouse will hand the order to a carrier,
honoring the same-day cutoff and the company holiday calendar.
nil when the warehouse company can't be resolved.

Parameters:

  • now (ActiveSupport::TimeWithZone, Time) (defaults to: Time.current)

    override for tests

Returns:

  • (Date, nil)


2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
# File 'app/models/order.rb', line 2245

def next_warehouse_ship_date(now: Time.current)
  company = store&.company
  return nil unless company

  local_today = now.in_time_zone(company.working_hours_timezone).to_date
  case shipping_cutoff_status(now: now)
  when :same_day then local_today
  when :after_cutoff then company.next_business_day(local_today + 1)
  when :non_working_day then company.next_business_day(local_today)
  end
end

#notify_pre_pack_cancellationvoid

This method returns an undefined value.

When an order is cancelled while it still has pre-pack
deliveries (waiting on warehouse packing data), email each
affected warehouse so they don't waste pack time on a dead order.
Publishes one Events::DeliveryPrePackCancelled per affected delivery
from after_all_transactions_commit; the async audience_member re-queries
by id, so a delivery destroyed later in the same transaction
(purge_empty_quoting_deliveries) is a clean no-op (AppSignal #4958).



5850
5851
5852
5853
5854
5855
5856
5857
5858
5859
5860
5861
5862
5863
5864
5865
# File 'app/models/order.rb', line 5850

def notify_pre_pack_cancellation
  pre_pack_delivery_ids = deliveries.reload.where(state: "pre_pack").ids
  return unless pre_pack_delivery_ids.any?

  cancelled_by_id = PaperTrail.request.whodunnit
  ActiveRecord.after_all_transactions_commit do
    pre_pack_delivery_ids.each do |delivery_id|
      Rails.configuration.event_store.publish(
        Events::DeliveryPrePackCancelled.new(data: { delivery_id:, cancelled_by_id: }),
        stream_name: "Delivery-#{delivery_id}"
      )
    rescue StandardError => e
      ErrorReporting.error(e)
    end
  end
end

#ok_to_delete?(account = nil) ⇒ Object

Whether ok to delete.

Parameters:

  • account (Object) (defaults to: nil)

Returns:

  • (Object)


3294
3295
3296
3297
# File 'app/models/order.rb', line 3294

def ok_to_delete?( = nil)
  &.is_admin? ||
    (cancelled? && (line_items.empty? || reference_number.blank?))
end

#online_order?Object

Whether online order.

Returns:

  • (Object)


2381
2382
2383
2384
# File 'app/models/order.rb', line 2381

def online_order?
  (res = creator.blank? || !creator.is_employee?) && customer.
  res
end

#open_activities_counterInteger

Number of open follow-up activities tied to this order, for the
CRM's per-order activity badge. Only counts activities that
default-visible filters surface.

Returns:

  • (Integer)


6057
6058
6059
# File 'app/models/order.rb', line 6057

def open_activities_counter
  activities.open_activities.visible_by_default.size
end

#opportunitiesArray<Opportunity>

Every opportunity tied to the order — directly via
#opportunity and indirectly via each RoomConfiguration's
opportunity. Used by the CRM activity feed to roll the order up
under each linked job.

Returns:



2040
2041
2042
2043
2044
2045
# File 'app/models/order.rb', line 2040

def opportunities
  opps = []
  opps << opportunity
  opps += room_configurations.map(&:opportunity)
  opps.uniq.compact
end

#opportunityOpportunity?

The associated opportunity.

Returns:



295
# File 'app/models/order.rb', line 295

belongs_to :opportunity, inverse_of: :orders, optional: true

#opportunity_nameString?

Name of the linked Opportunity, or nil if the order isn't
tied to one. Free-typed in the CRM order form; setting it
auto-finds-or-creates the opportunity.

Returns:

  • (String, nil)


2360
2361
2362
# File 'app/models/order.rb', line 2360

def opportunity_name
  opportunity.try(:name)
end

#opportunity_name=(val) ⇒ Object

Setter that finds or creates an Opportunity on the customer
with the given name and links the order to it. Passing nil or
blank detaches the opportunity entirely.

Parameters:

  • val (String, nil)


2369
2370
2371
2372
2373
2374
2375
2376
# File 'app/models/order.rb', line 2369

def opportunity_name=(val)
  if val.present?
    opp = customer.opportunities.find_or_create_by(name: val.strip)
    self.opportunity = opp
  else
    self.opportunity = nil
  end
end

#order_closed?Object

Whether order closed.

Returns:

  • (Object)


1855
1856
1857
# File 'app/models/order.rb', line 1855

def order_closed?
  invoiced? || cancelled? || fraudulent? || printed?
end

#order_emailsArray<String>

Every email address relevant to this order: the customer + all
their contacts, the transmission email, the tracking email, the
bound contact's emails, and any per-order billing overrides.
De-duplicated and sorted.

Returns:

  • (Array<String>)


3168
3169
3170
3171
3172
3173
3174
3175
3176
# File 'app/models/order.rb', line 3168

def order_emails
  emails = []
  emails += customer.contacts_and_self_contact_points_by_category('email').pluck(:detail)
  emails += transmission_email
  emails += tracking_email
  emails += contact.contact_points.emails.pluck(:detail) if contact
  emails += billing_emails || []
  emails.compact.uniq.sort
end

#order_type_titleString

Human label for order_type (e.g. 'SO''Sales Order').
Falls back to 'Unknown' for codes not in ALL_ORDER_TYPES.

Returns:

  • (String)


1801
1802
1803
# File 'app/models/order.rb', line 1801

def order_type_title
  ALL_ORDER_TYPES[order_type] || 'Unknown'
end

#original_sourceSource?

The associated original source.

Returns:



320
# File 'app/models/order.rb', line 320

belongs_to :original_source, class_name: 'Source', optional: true

#overdue_to_ship?Boolean

Returns whether this order is currently an overdue-to-ship slip.

Returns:

  • (Boolean)

    whether this order is currently an overdue-to-ship slip.



2449
2450
2451
# File 'app/models/order.rb', line 2449

def overdue_to_ship?
  self.class.overdue_to_ship.exists?(id)
end

#partially_shipped?Object

Whether partially shipped.

Returns:

  • (Object)


6092
6093
6094
# File 'app/models/order.rb', line 6092

def partially_shipped?
  deliveries.active.any?(&:completed_regular_delivery?)
end

#participants_options_for_selectArray<Array(String, Integer)>?

[name, party_id] pairs of every opportunity participant on
this order, suitable for a Rails select helper. nil when no
opportunity is linked.

Returns:

  • (Array<Array(String, Integer)>, nil)


4224
4225
4226
# File 'app/models/order.rb', line 4224

def participants_options_for_select
  opportunity&.opportunity_participants&.map { |scp| [scp.party.full_name, scp.party_id] }
end

#party_for_order_confirmationParty

The party the order-confirmation email goes to: the contact when
one is bound, else the customer. Mirrors #primary_party but
carries different intent — the confirmation flow specifically
wants whoever placed the order, even if it's a contact under a
company account.

Returns:



3791
3792
3793
# File 'app/models/order.rb', line 3791

def party_for_order_confirmation
  contact || customer
end

#pay_on_spiff?Object

Whether pay on spiff.

Returns:

  • (Object)


1710
1711
1712
# File 'app/models/order.rb', line 1710

def pay_on_spiff?
  spiff_enrollment.present? && (spiff_enrollment.spiff.eligible_reward(self) > 0)
end

#payment_methodString?

Category code (Payment::CREDIT_CARD, Payment::PO, …) of the
first authorised payment on the order — a quick "how was this
paid?" hint. Returns nil if there are no authorised payments.

Returns:

  • (String, nil)


2753
2754
2755
2756
2757
# File 'app/models/order.rb', line 2753

def payment_method
  payments.all_authorized.first.category
rescue StandardError
  nil
end

#payment_method_requires_authorization?Boolean

True when any payment on the order is awaiting human
authorization (e.g. unreviewed eCheck or fraud-flagged card).

Returns:

  • (Boolean)


2486
2487
2488
# File 'app/models/order.rb', line 2486

def payment_method_requires_authorization?
  payments_requiring_authorization.any?
end

#payment_options(source) ⇒ Array<String>

Allowed payment-method codes for this order, ordered by the
canonical sort for the given UI surface. The source selects
which surface is asking:

  • 'cart' / 'online_order_payment' — public checkout
  • 'online_order_invoices' — pay-an-invoice flow
  • 'crm' — internal CRM order entry

Parameters:

  • source (String)

Returns:

  • (Array<String>)


2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
# File 'app/models/order.rb', line 2013

def payment_options(source)
  payment_options = Payment.payment_options(billing_entity, self)
  payment_options.uniq!
  sorted_options = []
  if %w[cart online_order_payment].include?(source)
    all_payment_options = if includes_schedulable_service?
                            [Payment::CREDIT_CARD]
                          else
                            [Payment::CREDIT_CARD, Payment::PAYPAL, Payment::PO, Payment::VPO, Payment::CHECK, Payment::WIRE]
                          end
  elsif source == 'online_order_invoices'
    all_payment_options = [Payment::CREDIT_CARD]
  elsif source == 'crm'
    all_payment_options = [Payment::CREDIT_CARD, Payment::PO, Payment::VPO, Payment::RMA_CREDIT, Payment::STORE_CREDIT, Payment::PAYPAL_INVOICE, Payment::ECHECK, Payment::ADV_REPL, Payment::CHECK, Payment::WIRE, Payment::CASH]
  end
  all_payment_options.each do |po|
    sorted_options << po if payment_options.include?(po)
  end
  sorted_options
end

#paymentsActiveRecord::Associations::CollectionProxy

The associated payments.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


358
# File 'app/models/order.rb', line 358

has_many :payments, dependent: :nullify, inverse_of: :order

#payments_requiring_authorizationArray<Payment>

Authorised payments that haven't yet been management-approved
AND whose authorisation-review pipeline has flagged them as
requiring sign-off (any reason — fraud risk, eCheck, large
amount, etc.). Used by #payment_method_requires_authorization?.

Returns:



2505
2506
2507
# File 'app/models/order.rb', line 2505

def payments_requiring_authorization
  payments.all_authorized.select { |pp| !pp.payment_approved? && (pp.authorization_review[:required] == true) }
end

#payments_with_fraud_reportArray<Payment>

Non-voided payments that carry a fraud report and haven't been
explicitly approved by management. Subset of #payments used by
#potential_fraud_reasons and the fraud-review UI.

Returns:



1703
1704
1705
# File 'app/models/order.rb', line 1703

def payments_with_fraud_report
  payments.non_voided.select { |a| a.fraud_report.present? && !a.payment_approved? }
end

#paypal_invoices_paid?Object

Whether paypal invoices paid.

Returns:

  • (Object)


2140
2141
2142
2143
2144
2145
# File 'app/models/order.rb', line 2140

def paypal_invoices_paid?
  active_paypal_invoices = payments.paypal_invoices.where.not(state: %w[declined voided expired cancelled])
  return true if active_paypal_invoices.empty?

  active_paypal_invoices.all?(&:captured?)
end

#po_number(include_po_prefix: false, limit: nil) ⇒ String

Comma-joined PO number(s) across the order's authorised /
captured payments. Store transfers use the linked
PurchaseOrder reference number instead. Pass
include_po_prefix: true to prepend "PO# ", or limit: N to
cap the number of payment rows scanned (avoids scanning
thousands of payments on a heavily-amended order).

Parameters:

  • include_po_prefix (Boolean) (defaults to: false)
  • limit (Integer, nil) (defaults to: nil)

Returns:

  • (String)


3367
3368
3369
3370
3371
3372
3373
3374
3375
3376
# File 'app/models/order.rb', line 3367

def po_number(include_po_prefix: false, limit: nil)
  query = payments.where(state: %w[authorized captured]).where.not(po_number: [nil, ''])
  query = query.limit(limit) if limit
  po_numbers = query.distinct.pluck(:po_number)
  po_numbers = po_numbers.map(&:strip).uniq
  res = po_numbers.join(', ')
  res.insert(0, 'PO# ') if res.present? && include_po_prefix
  res = purchase_order.reference_number if is_store_transfer?
  res
end

#po_number_barcode(file_path: nil) ⇒ String

Instance-level wrapper around po_number_barcode that uses
this order's PO number. Used by warehouse pack-list PDFs to
render a scannable Code-128 barcode.

Parameters:

  • file_path (String, nil) (defaults to: nil)

    write to this path if given,
    otherwise return the PNG bytes

Returns:

  • (String)

    PNG bytes or the file path



3425
3426
3427
# File 'app/models/order.rb', line 3425

def po_number_barcode(file_path: nil)
  self.class.po_number_barcode(po_number:, file_path:)
end

#po_numbersArray<String>

Distinct PO numbers across all payments on the order.

Returns:

  • (Array<String>)


3266
3267
3268
# File 'app/models/order.rb', line 3266

def po_numbers
  payments.where.not(po_number: nil).distinct.pluck(:po_number)
end

#potential_fraud?Object

Whether potential fraud.

Returns:

  • (Object)


1681
1682
1683
# File 'app/models/order.rb', line 1681

def potential_fraud?
  payments.all_authorized.any? { |pp| pp.try(:fraud_report).try(:potential_fraud?) && !pp.payment_approved? }
end

#potential_fraud_reasonsArray<String>

Aggregate list of fraud-report reasons across every unapproved
payment on the order. Surfaced in the CR-hold UI so reviewers
see at a glance why the order tripped the fraud filter.

Returns:

  • (Array<String>)

    deduplicated reason codes



1690
1691
1692
1693
1694
1695
1696
# File 'app/models/order.rb', line 1690

def potential_fraud_reasons
  reasons = []
  payments_with_fraud_report.each do |payment|
    reasons += payment.fraud_report.potential_fraud_reasons
  end
  reasons.uniq
end

#precreated_rma_credit_availableBigDecimal

Remaining RMA credit pre-authorised against this order, computed
as line_total_plus_tax − already-applied RMA credit payments.
Floored at 0 so the figure never goes negative when an RMA credit
exceeds the new order's total.

Returns:

  • (BigDecimal)


2849
2850
2851
2852
2853
2854
# File 'app/models/order.rb', line 2849

def precreated_rma_credit_available
  total_available = line_total_plus_tax
  spent = payments.all_authorized.where(category: Payment::RMA_CREDIT).sum(:amount)
  available = total_available - spent
  available < 0 ? BigDecimal('0.0') : available
end

#preset_jobsActiveRecord::Associations::CollectionProxy

The associated preset jobs.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


397
# File 'app/models/order.rb', line 397

has_many :preset_jobs, inverse_of: :order

#prevent_recalculate_shipping?Object

Whether prevent recalculate shipping.

Returns:

  • (Object)


4174
4175
4176
# File 'app/models/order.rb', line 4174

def prevent_recalculate_shipping?
  is_credit_order? || SOLD_STATES.include?(state.to_sym) || retrieving_shipping_costs?
end

#primary_partyParty

The party who's the "face" of this order — the contact when one
is bound (e.g. dealer with multiple contacts), otherwise the
customer. Used everywhere the CRM needs a single party for
activity attribution and recipient resolution.

Returns:



2307
2308
2309
# File 'app/models/order.rb', line 2307

def primary_party
  contact || customer
end

#primary_rep_nameString

Display string for the primary sales rep, falling back to the
literal "Unassigned" so list views never show a blank cell.

Returns:

  • (String)


3142
3143
3144
# File 'app/models/order.rb', line 3142

def primary_rep_name
  primary_sales_rep.try(:full_name) || 'Unassigned'
end

#primary_sales_repEmployee?

Primary sales rep on the order. For closed orders we return
whatever was stamped on the invoice at close time (even if
blank) so commissions match what was reported. For open orders
we fall back to the customer's current primary if the order
itself doesn't override.

Returns:



1866
1867
1868
1869
1870
1871
# File 'app/models/order.rb', line 1866

def primary_sales_rep
  # if it's closed always return what's stored (even if blank)
  return invoiced_primary_sales_rep if order_closed?

  invoiced_primary_sales_rep || customer&.primary_sales_rep
end

#product_review_urlString?

Reviews.io product-review landing URL pre-filled with this
order's customer/email/skus. Returns nil when the order has no
customer email or no product SKUs (no review surface to render).

Returns:

  • (String, nil)


1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
# File 'app/models/order.rb', line 1395

def product_review_url
  return nil if customer&.email.blank?

  product_skus = line_items.non_shipping.joins(:item).pluck('items.sku').compact.uniq
  return nil unless product_skus.any?

  store_key = 'warmlyyours-com'
  params = {
    store: store_key,
    user: ERB::Util.url_encode(customer.full_name.presence || 'Customer'),
    order_id: ERB::Util.url_encode(reference_number),
    email: ERB::Util.url_encode(customer.email),
    products: ERB::Util.url_encode(product_skus.join(';')),
    type: 'product_review',
    rating: 5
  }
  "https://www.reviews.io/store/landing_new_review?#{params.map { |k, v| "#{k}=#{v}" }.join('&')}"
end

#prune_cart_roomsObject

Drop room configurations from the cart whose line items have
all been removed. Cart-only — orders keep their room
associations even when room contents move around.



3491
3492
3493
3494
3495
3496
3497
3498
# File 'app/models/order.rb', line 3491

def prune_cart_rooms
  # this method clears out associated rooms from the cart if none of the room line items remain
  # this is only for carts since we want to keep room association when linking rooms to quotes and orders in the crm before the rooms have been designed
  rcs = room_configurations
  rcs.each do |rc|
    remove_room_configuration(rc) if line_items.where(room_configuration_id: rc.id).empty?
  end
end

#public_pathString?

Public-portal path to the order ("My Account" view) when the
customer has a portal account, otherwise nil.

Returns:

  • (String, nil)


3284
3285
3286
3287
3288
# File 'app/models/order.rb', line 3284

def public_path
  return nil if customer&..blank?

  UrlHelper.instance.my_order_path(self)
end

Public, tokenised URL a customer can hit to pay an order without
signing in. The id is encrypted via Encryption.encrypt_string
so the URL doesn't leak sequential ids.

Returns:

  • (String)


6020
6021
6022
# File 'app/models/order.rb', line 6020

def public_payment_link
  "https://#{WEB_HOSTNAME}/pay-order/#{encrypted_id}"
end

#purchase_early_label_if_requestedHash?

Purchase shipping label early if requested and order is eligible
Called from after_transition to awaiting_deliveries

Stores result in early_label_purchase_result for controller to display flash messages
Logs all API calls to early_label_api_log for debugging/visibility
Sends EDI admin notification on any failure

Returns:

  • (Hash, nil)

    Result hash with success/error, or nil if skipped/lock-not-acquired



4286
4287
4288
4289
4290
4291
4292
4293
4294
4295
4296
# File 'app/models/order.rb', line 4286

def purchase_early_label_if_requested
  return unless purchase_label_early?

  # Unpersisted records (tests) skip the lock — no DB row to lock against
  return purchase_early_label_locked unless persisted?

  Order.with_advisory_lock("early_label_purchase_#{id}", timeout_seconds: 0, disable_query_cache: true) do
    reload
    purchase_early_label_locked
  end || nil
end

#purchase_orderPurchaseOrder?

The associated purchase order.

Returns:



314
# File 'app/models/order.rb', line 314

belongs_to :purchase_order, optional: true

#quoteQuote?

The associated quote.

Returns:



292
# File 'app/models/order.rb', line 292

belongs_to :quote, inverse_of: :orders, optional: true

#quotesArray<Quote>

Every quote tied to the order — directly via #quote and
indirectly via the most recent completed quote on each linked
room configuration.

Returns:



2052
2053
2054
2055
2056
2057
# File 'app/models/order.rb', line 2052

def quotes
  qs = []
  qs << quote
  qs += room_configurations.map { |rc| rc.quotes.completed_quotes.last }
  qs.uniq.compact
end

#ready_for_pending_payment?Object

Whether ready for pending payment.

Returns:

  • (Object)


2966
2967
2968
# File 'app/models/order.rb', line 2966

def ready_for_pending_payment?
  ready_for_shipping?
end

#ready_for_service?Object

Whether ready for service.

Returns:

  • (Object)


3031
3032
3033
# File 'app/models/order.rb', line 3031

def ready_for_service?
  belongs_to_smartservice_group?
end

#ready_for_shipping?Object

Whether ready for shipping.

Returns:

  • (Object)


3038
3039
3040
3041
3042
3043
3044
3045
3046
3047
3048
3049
3050
3051
3052
3053
3054
3055
3056
3057
3058
3059
3060
3061
3062
3063
3064
3065
3066
3067
3068
3069
3070
3071
3072
3073
3074
3075
3076
3077
3078
3079
3080
3081
3082
3083
3084
3085
3086
3087
3088
3089
3090
3091
3092
3093
3094
3095
3096
3097
3098
3099
3100
3101
# File 'app/models/order.rb', line 3038

def ready_for_shipping?
  return false if shipping_address.nil?

  if has_unreserved_line_items?
    errors.add :base, "One or more line items require a serial number reservation. Line items: #{line_items.select { |li| li.require_reservation? && !li.fully_reserved? }.map(&:sku).join(', ')}"
    return false
  end
  if has_committed_serial_number_reservations?
    errors.add :base, 'Not all reserved serial numbers are available'
    return false
  end
  if is_sales_order? && price_match && price_match != line_total
    errors.add :base, "EDI order price match discrepancy, expects line total to be #{price_match}"
    return false
  end
  if is_sales_order? && customer&.customer_record&.always_custom_packing_list? && edi_is_pick_slip_required? && uploads.in_category('custom_packing_slip_pdf').blank?
    houzz_snippet = +''
    if customer&.is_houzz? && edi_po_number.present?
      encoded_po = ERB::Util.url_encode(edi_po_number.to_s)
      escaped_url = ERB::Util.html_escape("https://www.houzz.com//printBuyerOrder//orderId=#{encoded_po}")
      houzz_snippet = ", get it here: <a href=\"#{escaped_url}\" target=\"_blank\" rel=\"noopener noreferrer\"><i class=\"fa-sharp fa-solid fa-arrow-up-right-from-square\"></i> #{escaped_url}</a>"
    end
    errors.add :base, "Customer requires a custom packing slip pdf for all orders#{houzz_snippet}".html_safe
    return false
  end
  # enforce that all Canadian Tire orders must ship to a store and use the store name format
  if is_sales_order? && customer&.billing_entity&.is_canadian_tire? && !((shipping_address.company_name || shipping_address.person_name) =~ CustomerConstants::CANADIAN_TIRE_STORE_NAME_REGEX).nil? != true
    errors.add :base, CustomerConstants::CANADIAN_TIRE_STORE_NAME_ERROR_MESSAGE
    return false
  end

  unless shipping_address_valid_and_not_po_box_if_present
    errors.add :base, 'Shipping address invalid'
    return false
  end
  if deliveries.any?(&:incurs_oversized_penalty?)
    errors.add :base, 'Shipping packages exceed oversized limits or include a pallet and would incur big penalties, please review packaging and consider shipping via LTL freight.'
    return false
  end

  # if shipping_address.is_warehouse
  #   errors.add :base, 'Warehouse pickup, manager must release from management hold'
  #   return false
  # end
  if shipping_address.is_warehouse || (order_type == Order::STORE_TRANSFER) || %w[USA
                                                                                  CAN].exclude?(shipping_address.country_iso3) || shipping_address.verified_for_shipping || shipping_address.disable_address_correction? || shipping_address.override_all_address_validation?
    return true
  end

  shipping_address.require_carrier_validation = true
  carrier
  'Purolator' if shipping_address.country_iso3 == 'CAN'
  shipping_address.validate_with_carrier = carrier
  shipping_address.do_not_accept_legacy_verified = true
  res = shipping_address.external_validation_with_carrier(true)
  unless res || is_www || errors.to_a.any? { |e| e.index('Customer must be contacted to obtain a valid shipping address') }
    # don't show this validation error for www
    errors.add :base,
               "Order carrier validation#{if carrier.present?
                                            " with #{carrier}"
                                          end} must pass for order to be released. Carrier validation can't be skipped by disabling address correction, but in exceptional circumstances can be skipped by a system administrator. In worst cases, customer may need to be contacted to obtain a valid shipping address. "
  end
  res
end

#ready_for_warehouse?Object

Whether ready for warehouse.

Returns:

  • (Object)


2959
2960
2961
# File 'app/models/order.rb', line 2959

def ready_for_warehouse?
  all_items_in_stock && ready_for_shipping?
end

#recipient_nameString?

Best-known recipient name for emails and shipping labels:
company name on the shipping address, then the person name on
the address, finally the customer's full name.

Returns:

  • (String, nil)


1363
1364
1365
# File 'app/models/order.rb', line 1363

def recipient_name
  shipping_address&.company_name || shipping_address&.person_name || customer&.full_name
end

Activity records corresponding to #related_activities_ids.

Returns:



2414
2415
2416
# File 'app/models/order.rb', line 2414

def related_activities
  Activity.where(id: related_activities_ids)
end

Combined ids of every Activity hung directly off this order
plus those hung off its deliveries — the ids the CRM "all
activity for this order" feed should display.

Returns:

  • (Array<Integer>)


2407
2408
2409
# File 'app/models/order.rb', line 2407

def related_activities_ids
  [activities.ids, delivery_activities.ids].flatten
end

#release_order_or_holdObject

Release the order to the warehouse unless something requires
accounting/management review first (#accounting_hold_order?),
in which case the order stays on hold for manual release.



2471
2472
2473
# File 'app/models/order.rb', line 2471

def release_order_or_hold
  release_order unless accounting_hold_order?
end

#remove_room_configuration(rc) ⇒ Hash

Detach a RoomConfiguration from the order, switching the
order's bound opportunity/quote to whichever sibling room is
left (or nil if removing the last one). Recomputes discounts
and saves. Returns {status: Boolean, message: String} for the
CRM flash-message stack.

Parameters:

Returns:

  • (Hash)


2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
# File 'app/models/order.rb', line 2067

def remove_room_configuration(rc)
  order_label = cart? ? 'cart' : 'order'
  if rc && room_configuration_ids.include?(rc.id)
    room_configurations.delete rc
    if opportunity_id == rc.opportunity_id
      update(opportunity: opportunities[1]) # this will be nil if no room_configurations, as desired.
    end
    if rc.quote_ids.include?(quote_id)
      update(quote: quotes[1]) # this will be nil if no room_configurations, as desired.
    end
    reset_discount(reset_item_pricing: false)
    res = save
    msg = if res
            "Room/Heated Space #{rc.name} has been removed from #{order_label}."
          else
            "Room/Heated Space #{rc.name} has been removed from #{order_label}."
          end
  else
    res = false
    msg = "Room/Heated Space #{rc.name} is not in the #{order_label}."
  end
  { status: res, message: msg }
end

#require_cc_for_advance_credit?Object

Whether require cc for advance credit.

Returns:

  • (Object)


1470
1471
1472
1473
1474
1475
1476
1477
1478
# File 'app/models/order.rb', line 1470

def require_cc_for_advance_credit?
  if allow_advance_credit_without_cc?
    false
  elsif rma && rma.rma_items.active.all?(&:will_not_be_returned?)
    false
  else
    customer.credit_card_vaults.valid.visible.empty? && rma.rma_items.any?(&:is_customer_fault?)
  end
end

#requires_intervention?Object

Whether requires intervention.

Returns:

  • (Object)


2548
2549
2550
# File 'app/models/order.rb', line 2548

def requires_intervention?
  crm_back_order? || in_cr_hold? || pending_review? || pending_release_authorization? || pending_payment?
end

#reset_cartObject

Wipe a cart back to empty: destroy all line items, quoting
deliveries, and discounts, clear the shipping address, and prune
any rooms that no longer have line items. Used by the public
site's "empty cart" button.



3479
3480
3481
3482
3483
3484
3485
3486
# File 'app/models/order.rb', line 3479

def reset_cart
  line_items.destroy_all
  deliveries.quoting.destroy_all
  discounts.destroy_all
  self.shipping_address = nil
  save
  prune_cart_rooms
end

#restricted_order_type?Object

Whether restricted order type.

Returns:

  • (Object)


1734
1735
1736
# File 'app/models/order.rb', line 1734

def restricted_order_type?
  RESTRICTED_ORDER_TYPES.key?(order_type)
end

#reviewerObject

Returns the reviewer info for Reviews.io invitation
Prefers contact over customer if present



1369
1370
1371
1372
1373
1374
1375
# File 'app/models/order.rb', line 1369

def reviewer
  reviewer_party = contact.presence || customer
  {
    email: reviewer_party&.email,
    name: reviewer_party&.full_name
  }
end

#rmaRma?

The associated rma.

Returns:



301
# File 'app/models/order.rb', line 301

belongs_to :rma, inverse_of: :orders, optional: true

#rma_cancelObject

Hard-cancel an RMA replacement order: destroy all active
deliveries first (so their inventory commits release) then
destroy the order itself, all in one transaction.



4123
4124
4125
4126
4127
4128
# File 'app/models/order.rb', line 4123

def rma_cancel
  Order.transaction do
    deliveries.active.each(&:destroy)
    destroy
  end
end

#rma_itemsActiveRecord::Associations::CollectionProxy

The associated rma items.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


385
# File 'app/models/order.rb', line 385

has_many :rma_items, inverse_of: :replacement_order

#rmasActiveRecord::Associations::CollectionProxy

The associated rmas.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


370
# File 'app/models/order.rb', line 370

has_many :rmas, foreign_key: :original_order_id

#sales_support_repEmployee?

The associated sales support rep.

Returns:



289
# File 'app/models/order.rb', line 289

belongs_to :sales_support_rep, class_name: 'Employee', optional: true

#save_early_label_api_logObject

Save the accumulated API log to the order's early_label_metadata
Called at the end of purchase_early_label_if_requested (success or failure)



4875
4876
4877
4878
4879
4880
4881
4882
4883
4884
4885
4886
4887
4888
4889
4890
4891
4892
# File 'app/models/order.rb', line 4875

def save_early_label_api_log
  return if @early_label_api_log.blank?

  # Append to existing log if present, or create new
  existing_log = early_label_api_log || []
  new_log = existing_log + @early_label_api_log

  # Keep only the last 50 entries to prevent unbounded growth
  new_log = new_log.last(50) if new_log.length > 50

  # Ensure early_label_metadata is a hash before merging
   =  || {}
  update_column(:early_label_metadata, .merge('api_log' => new_log))
  @early_label_api_log = nil
rescue StandardError => e
  Rails.logger.error("[EarlyLabel] Failed to save API log: #{e.message}")
  Rails.logger.error(e.backtrace.first(3).join("\n"))
end

#schedule_follow_up_activityBoolean

Run the FollowUpScheduler service against this order.
The scheduler decides whether the customer is due for a
follow-up call/email and creates the activity if so.

Returns:

  • (Boolean)

    true if an activity was actually scheduled



3323
3324
3325
3326
# File 'app/models/order.rb', line 3323

def schedule_follow_up_activity
  result = Order::FollowUpScheduler.new.process(self)
  result.activity_scheduled?
end

#search_textString? (protected)

Indexed search text for full-text matching: order reference,
PO numbers, and RMA reference. Carts return nil so they don't
pollute the search index.

Returns:

  • (String, nil)


6426
6427
6428
6429
6430
6431
6432
6433
6434
# File 'app/models/order.rb', line 6426

def search_text
  return nil if cart?

  st = []
  st << reference_number
  st += payments.pluck(:po_number)
  st << rma_reference
  st.join(' ')
end

#secondary_sales_repEmployee?

Secondary sales rep on the order. Same closed-vs-open semantics
as #primary_sales_rep.

Returns:



1877
1878
1879
1880
1881
# File 'app/models/order.rb', line 1877

def secondary_sales_rep
  return invoiced_secondary_sales_rep if order_closed?

  invoiced_secondary_sales_rep || customer&.secondary_sales_rep
end

#selected_shipping_cost_supports_early_label?(selected_sc, delivery) ⇒ Boolean

Checks whether the delivery's selected shipping cost is compatible with
the marketplace early-label flow. Today this means AMZBS or SWW; long-term
it may also include Heatwave native ship-labels.

Parameters:

Returns:

  • (Boolean)


5039
5040
5041
5042
5043
5044
5045
5046
5047
5048
5049
5050
5051
5052
# File 'app/models/order.rb', line 5039

def selected_shipping_cost_supports_early_label?(selected_sc, delivery)
  return false unless delivery.present? && selected_sc.present?

  is_amazon = edi_orchestrator_partner&.start_with?('amazon_seller')
  is_walmart = edi_orchestrator_partner&.start_with?('walmart_seller')

  if is_amazon
    selected_sc.is_amzbs?
  elsif is_walmart
    selected_sc.is_sww?
  else
    false
  end
end

#selection_nameString

Select-2 / Tom Select dropdown label — like #to_label but
uses "not shipped" when the order hasn't shipped yet.

Returns:

  • (String)


4116
4117
4118
# File 'app/models/order.rb', line 4116

def selection_name
  "#{reference_number} #{customer.full_name} (#{shipped_date.present? ? shipped_date.to_fs(:crm_default) : 'not shipped'})"
end

#send_back_order_notificationObject

Email the back-order team that this order moved into back-order
state and needs follow-up with the customer.



3723
3724
3725
# File 'app/models/order.rb', line 3723

def send_back_order_notification
  OrdersMailer.back_order_notification(self).deliver_later
end

#send_early_label_void_alert(reason:, details:) ⇒ Object

Send alert email when early label void fails or is blocked
SAFEGUARD C: Ensures operations team is notified of label issues

Parameters:

  • reason (String)

    Brief description of why alert is being sent

  • details (String)

    Additional details about the failure



4781
4782
4783
4784
4785
4786
4787
4788
4789
4790
4791
4792
4793
4794
4795
4796
4797
4798
4799
4800
4801
4802
4803
4804
4805
4806
4807
4808
4809
4810
4811
4812
4813
4814
4815
4816
4817
4818
# File 'app/models/order.rb', line 4781

def send_early_label_void_alert(reason:, details:)
  marketplace = edi_orchestrator_partner&.start_with?('amazon_seller') ? 'Amazon' : 'Walmart'
  portal = marketplace == 'Amazon' ? 'Amazon Seller Central' : 'Walmart Seller Portal'

  Rails.logger.warn("[EarlyLabel] Sending void alert for order #{reference_number}: #{reason}")

  SystemMailer.generic_mailer(
    subject: "[URGENT] Early Label Void Issue - Order #{reference_number}",
    body: <<~BODY,
      An early label void issue occurred that requires attention.

      ORDER DETAILS:
      - Order: #{reference_number}
      - Tracking: #{early_label_tracking_number}
      - Carrier: #{early_label_carrier}
      - Label Purchased: #{early_label_purchased_at&.strftime('%Y-%m-%d %H:%M:%S %Z')}
      - EDI Partner: #{edi_orchestrator_partner}
      - PO Number: #{edi_po_number}
      - Marketplace: #{marketplace}

      ISSUE:
      - Reason: #{reason}
      - Details: #{details}

      ACTION REQUIRED:
      1. Check the order in Heatwave: https://#{CRM_HOSTNAME}/en-US/orders/#{id}
      2. Check the label status in #{portal}
      3. If the label exists in #{marketplace} but not in Heatwave, manually void it in #{portal}
      4. Update the order status as needed

      This is an automated alert from the Early Label Purchase system.
    BODY
    to: ORDERS_EMAIL,
    from: ADMINISTRATOR_EMAIL
  ).deliver_later
rescue StandardError => e
  Rails.logger.error("[EarlyLabel] Failed to send void alert email: #{e.message}")
end

#send_online_order_confirmationHash{Symbol => Symbol, String}

Build and send the ONLINE_ORDER_CONFIRM email via
CommunicationBuilder, then pin the recipient address to the
order's tracking_email array so subsequent ship/tracking
emails go to the same place. Returns a {status_code:, status_message:} hash for the controller's flash stack.

Returns:

  • (Hash{Symbol => Symbol, String})


3758
3759
3760
3761
3762
3763
3764
3765
3766
3767
3768
3769
3770
3771
3772
3773
3774
3775
3776
3777
3778
3779
3780
3781
3782
# File 'app/models/order.rb', line 3758

def send_online_order_confirmation
  if email_for_order_confirmation
    sender = customer.primary_sales_rep
    co = CommunicationBuilder.new(
      resource: self,
      sender_party: sender,
      sender: sender || INFO_EMAIL,
      recipient_party: party_for_order_confirmation,
      emails: email_for_order_confirmation,
      recipient_name: party_for_order_confirmation.name,
      template_system_code: 'ONLINE_ORDER_CONFIRM',
      bcc: sender&.email,
      merge_options: { order: to_liquid }
    ).create
    if co.draft?
      { status_code: :error, status_message: co.errors_to_s }
    else
      # IMPORTANT!!! THIS MUST BE SET HERE
      update_column(:tracking_email, [email_for_order_confirmation])
      { status_code: :ok, status_message: "Order confirmation e-mail sent to #{party_for_order_confirmation.name} #{email_for_order_confirmation}." }
    end
  else
    { status_code: :error, status_message: "Can't send: order/customer e-mail is blank!" }
  end
end

#send_payment_automatically_authorized_notificationObject

Notify accounting that the auto-authoriser approved a payment
without human review — useful for spot-checking the rules engine.



3747
3748
3749
# File 'app/models/order.rb', line 3747

def send_payment_automatically_authorized_notification
  OrdersMailer.payment_automatically_authorized(self).deliver_later
end

#send_profit_review_notificationObject

Email the management profit-review queue when this order's
margin falls below the configured threshold and needs sign-off.



3735
3736
3737
# File 'app/models/order.rb', line 3735

def send_profit_review_notification
  OrdersMailer.order_profit_review_notification(self).deliver_later
end

#send_ready_for_pickup_emailHash{Symbol => Symbol, String}

Send the warehouse-pickup-ready email (ORDER_PICKUP template)
used for "your order is ready at the will-call counter" notices.

Returns:

  • (Hash{Symbol => Symbol, String})


6124
6125
6126
# File 'app/models/order.rb', line 6124

def send_ready_for_pickup_email
  send_tracking_template_email 'ORDER_PICKUP'
end

#send_release_authorization_notificationObject

Email management when an order needs explicit release-from-CR-hold
authorisation (e.g. fraud-flagged payment, large order, etc.).



3741
3742
3743
# File 'app/models/order.rb', line 3741

def send_release_authorization_notification
  OrdersMailer.release_authorization_required(self).deliver_later
end

#send_request_carrier_assignment_notificationObject

Notify dispatch when an order is waiting on a manual carrier
assignment (rate-shop returned no eligible options).



3729
3730
3731
# File 'app/models/order.rb', line 3729

def send_request_carrier_assignment_notification
  DeliveryMailer.request_carrier_assignment_notification(self).deliver_later
end

#send_tracking_emailHash{Symbol => Symbol, String}

Send the standard ORDER_TRACKING email with the carrier's
tracking link.

Returns:

  • (Hash{Symbol => Symbol, String})


6116
6117
6118
# File 'app/models/order.rb', line 6116

def send_tracking_email
  send_tracking_template_email 'ORDER_TRACKING'
end

#send_tracking_template_email(template_code) ⇒ Hash{Symbol => Symbol, String}

Generic helper that drives both #send_tracking_email and
#send_ready_for_pickup_email: builds the communication via
CommunicationBuilder for the named system template and sends
it to every address in tracking_email. Returns a controller-
friendly {status_code:, status_message:} hash.

Parameters:

  • template_code (String)

Returns:

  • (Hash{Symbol => Symbol, String})


6136
6137
6138
6139
6140
6141
6142
6143
6144
6145
6146
# File 'app/models/order.rb', line 6136

def send_tracking_template_email(template_code)
  return { status_code: :error, status_message: "Can't send #{template_code}: no tracking e-mail present!" } if tracking_email.blank?

  sender = customer.primary_sales_rep
  co = CommunicationBuilder.new(resource: self, sender_party: sender, sender: (sender.nil? ? INFO_EMAIL : nil), recipient_party: customer, emails: tracking_email.join(','), template_system_code: template_code).create
  if co.draft?
    { status_code: :error, status_message: co.errors_to_s }
  else
    { status_code: :ok, status_message: "#{template_code} e-mail sent." }
  end
end

#service_only_order?Object

Whether service only order.

Returns:

  • (Object)


2512
2513
2514
# File 'app/models/order.rb', line 2512

def service_only_order?
  line_items.non_shipping.any? && line_items.non_shipping.all?(&:is_service?)
end

#set_currencyObject (protected)

Before-validation callback: pin the order's currency to the
customer's store currency when not already set, so currency
never silently defaults to USD on non-US orders.



6362
6363
6364
# File 'app/models/order.rb', line 6362

def set_currency
  self.currency ||= customer&.store&.currency
end

#set_custom_order_agreementObject

Checks for the presence of custom products in excess of $2k



5907
5908
5909
5910
5911
5912
5913
# File 'app/models/order.rb', line 5907

def set_custom_order_agreement
  return unless is_sales_order?
  return if custom_order_agreement_bypass?
  return unless meets_custom_products_threshold?

  update_column(:custom_order_agreement_status, Order.custom_order_agreement_statuses['custom_order_agreement_required'])
end

#set_default_tracking_email(force: false) ⇒ Object

Populate tracking_email with addresses extracted by
DefaultTrackingEmailExtractor (customer + contact +
opportunity participants). No-op when an address is already set
unless force: true is passed.

Parameters:

  • force (Boolean) (defaults to: false)


4073
4074
4075
4076
4077
# File 'app/models/order.rb', line 4073

def set_default_tracking_email(force: false)
  return unless force || tracking_email.empty?

  self.tracking_email = Order::DefaultTrackingEmailExtractor.new(self).emails
end

#set_min_profit_markupObject

Before-validation callback: pin the order's minimum-profit
markup percentage. Sales orders default to the company-wide
default_sales_markup; everything else (RMA replacements,
marketing, tech orders) gets 0 so they don't trip the
profit-review hold.



4059
4060
4061
4062
4063
4064
4065
# File 'app/models/order.rb', line 4059

def set_min_profit_markup
  self.min_profit_markup = if is_sales_order?
                             default_sales_markup
                           else
                             0
                           end
end

#set_opportunity_sourceObject

After-save hook: when the order's source changes (and isn't the
generic "unknown source"), propagate that source up to its
opportunity. Keeps opportunity-level acquisition reporting
consistent with whatever source the order ended up with.



1830
1831
1832
1833
1834
1835
1836
1837
# File 'app/models/order.rb', line 1830

def set_opportunity_source
  return unless source && saved_change_to_source_id?
  return if source&.unknown_source?
  return unless opportunity

  # If you made it this far, then you can update the opportunity source
  opportunity.update_column(:source_id, source.id)
end

#set_shipped_dateObject

Stamp the order's shipped_date with today, but only when blank
— once shipped, the date is canonical and shouldn't drift on a
re-save. Called by the after-ship state-machine transition.



3346
3347
3348
# File 'app/models/order.rb', line 3346

def set_shipped_date
  update_attribute(:shipped_date, Date.current) if shipped_date.blank?
end

#ship_from_attributes(delivery = nil) ⇒ Hash

Build the "ship from" address/phone/email/attention block for a
carrier API call. Defaults to the order's store warehouse, but
when delivery is supplied it uses the delivery's origin
address (which differs for dropship deliveries) and applies any
shipping-account-number override (used when the customer is
billing shipping to their own carrier account).

Parameters:

  • delivery (Delivery, nil) (defaults to: nil)

Returns:

  • (Hash)

    keys: :address, :phone, :email, :attention_name, :name



3690
3691
3692
3693
3694
3695
3696
3697
3698
3699
3700
3701
3702
3703
3704
3705
3706
3707
3708
3709
3710
3711
3712
3713
3714
3715
3716
3717
3718
3719
# File 'app/models/order.rb', line 3690

def ship_from_attributes(delivery = nil)
  res = {}
  # Here we use delivery, if present, to extract the origin address for the ship from, as well as the contact info. This is for drop shippers, but should also cover when shipper is WY US or Canada
  res[:address] = delivery&.origin_address || store.warehouse_address
  # sort of a kludge but for now use shipping configuration's sender phone for legacy matching
  res[:phone] = delivery&.origin_address&.party&.phone || SHIPPING_SHIPPER_CONFIGURATION[country.iso3.to_sym][:shipper_phone]
  res[:email] = delivery&.origin_address&.party&.email || SHIPPING_SHIPPER_CONFIGURATION[country.iso3.to_sym][:shipper_email]
  # post rb_any_ship_from, we actually want the ship_from to be the physical ship from and handle the shipper separately
  if bill_shipping_to_customer && delivery&.chosen_shipping_method&.
    if delivery.chosen_shipping_method..address
      # here, we implement override of ship from address to be the address linked in the shipping_account_number, if any.
      res[:address] = delivery.chosen_shipping_method..address
    end
    if begin
      delivery.chosen_shipping_method..phone
    rescue StandardError
      false
    end
      # here, we implement override of ship from phone to be the phone linked in the shipping_account_number, if any.
      res[:phone] = delivery.chosen_shipping_method..phone.detail
    end
  end
  # attention_name cannot be blank!!!!
  res[:attention_name] = res[:address].person_name
  res[:attention_name] = 'Shipping Department' if res[:attention_name].blank?
  res[:name] = (res[:address].company_name || res[:address].person_name)
  res[:phone] = res[:phone].scan(/[0-9]/).join if res[:phone]

  res
end

#ship_to_attributesHash

Build the "ship to" hash for carrier API calls: address, phone,
email, and ATTN/name lines, with sensible cascading fallbacks
(attention_name → address person name → literal "Customer",
phone falls through customer's phones to the rep's, etc.) so the
carrier never receives a blank field.

Returns:

  • (Hash)

    keys: :address, :phone, :email, :attention_name, :name



3585
3586
3587
3588
3589
3590
3591
3592
3593
3594
3595
3596
3597
3598
3599
3600
3601
3602
3603
3604
3605
3606
3607
3608
3609
3610
3611
3612
# File 'app/models/order.rb', line 3585

def ship_to_attributes
  res = {}
  res[:address] = shipping_address
  # attention_name cannot be blank!!!!
  res[:attention_name] = attention_name
  res[:attention_name] = res[:address].person_name if res[:attention_name].blank?
  res[:attention_name] = 'Customer' if res[:attention_name].blank?
  if res[:address].company_name
    res[:name] = res[:address].company_name
    res[:name] = 'Customer' if res[:name].blank?
  else
    res[:name] = attention_name
    res[:name] = res[:address].person_name if res[:name].blank?
    res[:name] = 'Customer' if res[:name].blank?
  end
  res[:phone] = shipping_phone.presence ||
                customer.phone ||
                customer.cell_phone ||
                customer.primary_sales_rep&.direct_phone ||
                customer.store.contact_number ||
                SHIPPING_SHIPPER_CONFIGURATION.dig(country.iso3.to_sym, :shipper_phone)
  res[:email] = first_tracking_email&.strip.presence ||
                customer.primary_sales_rep&.email ||
                customer.store.contact_email ||
                SHIPPING_SHIPPER_CONFIGURATION.dig(country.iso3.to_sym, :shipper_email)
  # need something here
  res
end

#ship_weightFloat

Aggregate physical ship weight across all non-shipping line
items, floored at 0.1 lb so carrier rate APIs that reject zero
weights don't blow up. Memoised so freight quote calls don't
re-walk the line items.

Returns:

  • (Float)


6076
6077
6078
# File 'app/models/order.rb', line 6076

def ship_weight
  @ship_weight ||= [0.1, line_items.includes(:item).non_shipping.parents_only.sum(&:total_shipping_weight).round(1)].max
end

#shipmentsActiveRecord::Associations::CollectionProxy

The associated shipments.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


415
# File 'app/models/order.rb', line 415

has_many :shipments, -> { order(:id) }, through: :deliveries

#shipping_account_numberShippingAccountNumber

DELIVERY REFACTOR HERE



311
# File 'app/models/order.rb', line 311

belongs_to :shipping_account_number, optional: true

#shipping_addressAddress?

The associated shipping address.

Returns:



341
# File 'app/models/order.rb', line 341

belongs_to :shipping_address, class_name: 'Address', validate: true, optional: true

#shipping_address_valid_and_not_po_box_if_present(rate_shopping = false) ⇒ Boolean

Validates the shipping address: must be present, must pass its
own AR validations, and must not be a PO box for the chosen
carrier (FedEx/UPS reject them; USPS accepts them). Pass
rate_shopping: true to skip the carrier check during the
initial shop-rates pass — the carrier isn't picked yet so we
can't enforce its specific rules.

Parameters:

  • rate_shopping (Boolean) (defaults to: false)

Returns:

  • (Boolean)


2939
2940
2941
2942
2943
2944
2945
2946
2947
2948
2949
2950
2951
2952
2953
2954
# File 'app/models/order.rb', line 2939

def shipping_address_valid_and_not_po_box_if_present(rate_shopping = false)
  res = false
  if shipping_address.nil?
    errors.add(:shipping_address, 'must be selected')
  elsif shipping_address.present?
    carrer_to_use = carrier
    carrer_to_use = nil if rate_shopping
    if shipping_address.valid? && shipping_address.not_a_po_box?(carrer_to_use)
      res = true
    else
      res = false
      shipping_address.errors.full_messages.each { |msg| errors.add(:shipping_address, msg) }
    end
  end
  res
end

#shipping_cutoff_advance_order?Object

Whether shipping cutoff advance order.

Returns:

  • (Object)


2202
2203
2204
# File 'app/models/order.rb', line 2202

def shipping_cutoff_advance_order?
  line_items.goods.parents_only.any? { |a| !a.in_stock? }
end

#shipping_cutoff_next_day?Object

Whether shipping cutoff next day.

Returns:

  • (Object)


2195
2196
2197
# File 'app/models/order.rb', line 2195

def shipping_cutoff_next_day?
  line_items.goods.parents_only.any? { |a| a.in_stock? && a.ships_via_freight? } # next day
end

#shipping_cutoff_same_day?Object

Whether shipping cutoff same day.

Returns:

  • (Object)


2188
2189
2190
# File 'app/models/order.rb', line 2188

def shipping_cutoff_same_day?
  line_items.goods.parents_only.all? { |a| a.in_stock? && !a.ships_via_freight? } # same day
end

#shipping_cutoff_status(now: Time.current) ⇒ Symbol

Whether the warehouse can still ship this order today.

Branches on the warehouse company's working hours + holiday
calendar (USA/CAN), not the visitor's local clock.

Parameters:

  • now (ActiveSupport::TimeWithZone, Time) (defaults to: Time.current)

    override for tests

Returns:

  • (Symbol)

    one of:

    • :same_day — working day, before today's cutoff
    • :after_cutoff — working day, but past today's cutoff
    • :non_working_day — weekend or company holiday
    • :unknown — no resolvable warehouse company


2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
# File 'app/models/order.rb', line 2223

def shipping_cutoff_status(now: Time.current)
  company = store&.company
  return :unknown unless company

  local_now = now.in_time_zone(company.working_hours_timezone)
  company.with_working_hours_config do
    if !WorkingHours.working_day?(local_now.to_date)
      :non_working_day
    elsif local_now.hour >= SAME_DAY_CUTOFF_HOUR
      :after_cutoff
    else
      :same_day
    end
  end
end

#shipping_date_warningsObject

Note:

requested_ship_before is automatically advanced when the user revisits the shipping form,
so no warning is needed when it's in the past.

Warnings about shipping dates that don't block order release
These are displayed to users but don't prevent state transitions



2644
2645
2646
# File 'app/models/order.rb', line 2644

def shipping_date_warnings
  []
end

#should_commit_stock?Object

Whether should commit stock.

Returns:

  • (Object)


1293
1294
1295
# File 'app/models/order.rb', line 1293

def should_commit_stock?
  (future_release_date.present? && !do_not_reserve_stock?) || all_funds_available?
end

#smartservice_ticketSupportCase?

The SmartService ticket currently waiting on payment for this
order, or nil if there isn't one. Used by the post-payment
hook that flips the ticket open (#move_service_case_from_pending_service_payment).

Returns:



3017
3018
3019
# File 'app/models/order.rb', line 3017

def smartservice_ticket
  support_cases&.services&.pending_service_payment&.first
end

#sms_enabled_numbersArray<String>

SMS-capable phone numbers for every order participant
(customer, contact, opportunity participants), formatted in the
E.164-style our SMS provider expects.

Returns:

  • (Array<String>)


1454
1455
1456
# File 'app/models/order.rb', line 1454

def sms_enabled_numbers
  ContactPoint.where(party_id: all_participant_ids).sms_numbers.order(:detail).map(&:formatted_for_sms).uniq
end

#sms_messagesActiveRecord::Relation<SmsMessage>

SMS conversation thread tied to this order — every inbound /
outbound message exchanged with any participant phone returned
by #sms_enabled_numbers.

Returns:



1463
1464
1465
# File 'app/models/order.rb', line 1463

def sms_messages
  SmsMessage.for_numbers(sms_enabled_numbers)
end

#sold_to_billing_addressAddress?

The associated sold to billing address.

Returns:



344
# File 'app/models/order.rb', line 344

belongs_to :sold_to_billing_address, class_name: 'Address', foreign_key: :sold_to_billing_address, optional: true

#sourceSource?

The associated source.

Returns:



317
# File 'app/models/order.rb', line 317

belongs_to :source, optional: true

#spiff_enrollmentSpiffEnrollment?

The associated spiff enrollment.

Returns:



307
# File 'app/models/order.rb', line 307

belongs_to :spiff_enrollment, optional: true

#spiff_repContact?

The associated spiff rep.

Returns:



304
# File 'app/models/order.rb', line 304

belongs_to :spiff_rep, class_name: 'Contact', optional: true, inverse_of: :spiff_orders

#spiff_rewardString

Eligible SPIFF reward amount formatted as "%.2f". Assumes the
order is enrolled in a SPIFF; raises if spiff_enrollment is
nil.

Returns:

  • (String)


1999
2000
2001
# File 'app/models/order.rb', line 1999

def spiff_reward
  '%.2f' % spiff_enrollment.spiff.eligible_reward(self)
end

#state_description(describe_state = nil) ⇒ String?

User-facing description for a state name, optionally for a
state other than the current one. Pulled from
OrderConstants::STATE_DESCRIPTION so descriptions are managed
in one place.

Parameters:

  • describe_state (String, nil) (defaults to: nil)

    state to describe; defaults
    to this order's current state

Returns:

  • (String, nil)


1901
1902
1903
# File 'app/models/order.rb', line 1901

def state_description(describe_state = nil)
  OrderConstants::STATE_DESCRIPTION[describe_state || state]
end

#state_listArray<Symbol>

The ordered set of states this order can be in given its type
and current data. Credit orders follow a fixed sequence; sales /
store-transfer / etc. orders compute the state list dynamically
based on which states they've actually visited (so the CRM
progress bar only shows applicable steps).

Returns:

  • (Array<Symbol>)


1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
# File 'app/models/order.rb', line 1933

def state_list
  if order_type == CREDIT_ORDER
    transition %i[cart pending pending_payment pending_release_authorization crm_back_order awaiting_deliveries in_cr_hold created awaiting_return] => :fraudulent, if: :can_be_cancelled?
    %i[created awaiting_return pending_review ready_for_printing printed cancelled]
  else
    states = []
    states << :cart if cart?
    states << :pending
    states << :pending_payment
    states << :crm_back_order if crm_back_order?
    states << :pending_release_authorization if pending_release_authorization?
    states << :in_cr_hold if in_cr_hold?
    states << :needs_serial_number_reservation
    states << :awaiting_deliveries
    states << :processing_deliveries
    states << :partially_invoiced
    states << :invoiced
    if begin
      cancelled?
    rescue StandardError
      false
    end
      states << :cancelled
    end
    if begin
      fraudulent?
    rescue StandardError
      false
    end
      states << :fraudulent
    end
    states
  end
end

#stock_statusSymbol

Combined inventory status across all line items: :ok,
:partial, or :none. Computed by LineItem.inventory_check.

Returns:

  • (Symbol)


2926
2927
2928
# File 'app/models/order.rb', line 2926

def stock_status
  LineItem.inventory_check(self)
end

#stop_for_pre_pack?Object

Whether stop for pre pack.

Returns:

  • (Object)


5928
5929
5930
5931
5932
5933
5934
5935
5936
5937
5938
# File 'app/models/order.rb', line 5928

def stop_for_pre_pack?
  return false unless can_request_estimated_packaging?
  return false if customer.is_e_commerce_misc?
  return false if room_configurations.present? && !room_configurations.all?(&:transmittable_and_settled?)
  return false if deliveries.all? { |d| d.shipments.packed.any? && d.all_lines_allocated_to_shipments? }

  res = need_to_pre_pack_reasons
  return res if res&.any?

  false
end

#stop_for_profit_review?Object

Whether stop for profit review.

Returns:

  • (Object)


5918
5919
5920
5921
5922
5923
# File 'app/models/order.rb', line 5918

def stop_for_profit_review?
  # Etailer orders are immune
  return false if customer.is_e_commerce_misc?

  !profit_margins_met?
end

#storeStore?

The Store this order is fulfilled from. Store transfers carry
an explicit from_store; everything else inherits the
customer's store.

Returns:



1286
1287
1288
# File 'app/models/order.rb', line 1286

def store
  from_store || customer&.store
end

#store_additional_early_label_pdfs(additional_labels) ⇒ Object

Persist the secondary label PDFs returned by a multi-package
marketplace label purchase. Each entry is {label_data:, tracking_number:}; missing or blank entries are skipped, and a
single-PDF failure doesn't abort the rest of the loop.

Parameters:

  • additional_labels (Array<Hash>)


5550
5551
5552
5553
5554
5555
5556
5557
5558
5559
# File 'app/models/order.rb', line 5550

def store_additional_early_label_pdfs(additional_labels)
  additional_labels.each_with_index do |label, idx|
    next unless label[:label_data].present? && label[:tracking_number].present?

    store_early_label_pdf(label[:label_data], label[:tracking_number])
    Rails.logger.info("[EarlyLabel] Stored additional package #{idx + 2} label PDF (tracking: #{label[:tracking_number]})")
  rescue StandardError => e
    Rails.logger.warn("[EarlyLabel] Failed to store additional package #{idx + 2} label PDF: #{e.message}")
  end
end

#store_amazon_label_pdf_atomic(label_result, tracking_number, marketplace) ⇒ Object

Amazon: label data is returned inline — store it and verify persistence.
For multi-package orders, also stores additional package labels.

Parameters:

  • label_result (Object)
  • tracking_number (Object)
  • marketplace (Object)


5444
5445
5446
5447
5448
5449
5450
5451
5452
5453
5454
5455
5456
5457
5458
5459
5460
5461
5462
5463
5464
5465
5466
5467
5468
5469
5470
5471
5472
# File 'app/models/order.rb', line 5444

def store_amazon_label_pdf_atomic(label_result, tracking_number, marketplace)
  label_data = label_result[:label_data]

  if label_data.blank?
    Rails.logger.warn('[EarlyLabel] Amazon label data not returned inline; label PDF will be attached at ship-label time')
    send_early_label_void_alert(
      reason: 'Amazon label data missing from purchase response',
      details: "Label was purchased but no inline label data was returned. " \
               "The label exists in #{marketplace}'s system but PDF is not stored locally."
    )
    return nil
  end

  upload = store_early_label_pdf(label_data, tracking_number)

  if upload&.persisted?
    Rails.logger.info("[EarlyLabel] Amazon label PDF stored as upload #{upload.id}")
    store_additional_early_label_pdfs(label_result[:additional_labels]) if label_result[:additional_labels].present?
    return upload
  end

  Rails.logger.error("[EarlyLabel] Amazon label PDF storage failed for order #{reference_number}")
  send_early_label_void_alert(
    reason: 'Amazon label PDF storage failed',
    details: "Label data was returned but could not be persisted. " \
             "The label exists in #{marketplace}'s system but is not stored locally."
  )
  nil
end

#store_early_label_pdf(label_data, tracking_number) ⇒ Upload?

Store early label PDF on the order

Parameters:

  • label_data (String)

    Raw PDF data

  • tracking_number (String)

Returns:



5523
5524
5525
5526
5527
5528
5529
5530
5531
5532
5533
5534
5535
5536
5537
5538
5539
5540
5541
5542
# File 'app/models/order.rb', line 5523

def store_early_label_pdf(label_data, tracking_number)
  filename = "early_ship_label_#{tracking_number}_#{Time.current.to_i}.pdf"

  upload = Upload.uploadify_from_data(
    file_name: filename,
    data: label_data,
    category: 'early_ship_label',
    resource: self
  )

  if upload&.persisted?
    uploads << upload
    Rails.logger.info("[EarlyLabel] Stored early label PDF as upload #{upload.id}")
  end

  upload
rescue StandardError => e
  Rails.logger.error("[EarlyLabel] Failed to store early label PDF: #{e.message}")
  nil
end

#store_idObject

Alias for Store#id

Returns:

  • (Object)

    Store#store_id

See Also:



3270
# File 'app/models/order.rb', line 3270

delegate :id, to: :store, prefix: true

#store_mock_early_label_pdf(tracking_number, _carrier) ⇒ Object

Store a mock label PDF for development/testing
Uses a pre-generated mock PDF since the sandbox doesn't return real label PDFs

Parameters:

  • tracking_number (String)
  • _carrier (String)

    Unused — kept for caller signature symmetry



5566
5567
5568
5569
5570
5571
5572
5573
5574
5575
5576
5577
5578
5579
5580
5581
5582
5583
5584
5585
5586
5587
5588
5589
5590
5591
5592
5593
# File 'app/models/order.rb', line 5566

def store_mock_early_label_pdf(tracking_number, _carrier)
  mock_pdf_path = Rails.root.join('test/sww_label_mock_pdf.pdf')

  unless File.exist?(mock_pdf_path)
    Rails.logger.error("[EarlyLabel] Mock PDF not found at #{mock_pdf_path}")
    return nil
  end

  mock_pdf_content = File.binread(mock_pdf_path)
  filename = "early_ship_label_#{tracking_number}_#{Time.current.to_i}.pdf"

  upload = Upload.uploadify_from_data(
    file_name: filename,
    data: mock_pdf_content,
    category: 'early_ship_label',
    resource: self
  )

  if upload&.persisted?
    uploads << upload
    Rails.logger.info("[EarlyLabel] Stored mock early label PDF as upload #{upload.id}")
  end

  upload
rescue StandardError => e
  Rails.logger.error("[EarlyLabel] Failed to store mock early label PDF: #{e.message}")
  nil
end

#store_walmart_label_pdf_atomic(shipper, _label_result, tracking_number, carrier, marketplace) ⇒ Object

Walmart: download label via API with retries, then store and verify persistence

Parameters:

  • shipper (Object)
  • _label_result (Object)
  • tracking_number (Object)
  • carrier (Object)
  • marketplace (Object)


5480
5481
5482
5483
5484
5485
5486
5487
5488
5489
5490
5491
5492
5493
5494
5495
5496
5497
5498
5499
5500
5501
5502
5503
5504
5505
5506
5507
5508
5509
5510
5511
5512
5513
5514
5515
5516
# File 'app/models/order.rb', line 5480

def store_walmart_label_pdf_atomic(shipper, _label_result, tracking_number, carrier, marketplace)
  max_attempts = 3
  delays = [1, 2, 3]

  max_attempts.times do |attempt|
    delay = delays[attempt] || 1
    sleep(delay)

    Rails.logger.info("[EarlyLabel] Attempting PDF download (attempt #{attempt + 1}/#{max_attempts}) for tracking #{tracking_number}")

    download_result = shipper.download_label(carrier, tracking_number)
    merge_shipper_api_log(shipper)

    if download_result[:success] && download_result[:label_data].present?
      upload = store_early_label_pdf(download_result[:label_data], tracking_number)

      if upload&.persisted?
        Rails.logger.info("[EarlyLabel] PDF successfully stored (attempt #{attempt + 1}) as upload #{upload.id}")
        return upload
      else
        Rails.logger.warn("[EarlyLabel] PDF download succeeded but storage failed (attempt #{attempt + 1})")
      end
    else
      Rails.logger.warn("[EarlyLabel] PDF download failed (attempt #{attempt + 1}): #{download_result[:error]}")
    end
  end

  Rails.logger.error("[EarlyLabel] All PDF download attempts failed for order #{reference_number}, tracking #{tracking_number}")

  send_early_label_void_alert(
    reason: 'PDF download failed after all retries',
    details: "Label was created successfully but PDF could not be downloaded after #{max_attempts} attempts. " \
             "The label exists in #{marketplace}'s system but is not stored locally."
  )

  nil
end

#suggested_shipmentsActiveRecord::Relation?

Shipments still in the suggested state — i.e. proposed by
Shipping::DeterminePackaging but not yet confirmed by the
warehouse. Used by the early-label flow to know what to label.

Returns:

  • (ActiveRecord::Relation, nil)


6066
6067
6068
# File 'app/models/order.rb', line 6066

def suggested_shipments
  shipments.where(state: 'suggested') if respond_to? :shipments
end

#support_case_idInteger?

Single support-case binding for the CRM order-setup form's Tom
Select picker, mirroring Rma#support_case_id. Backed by the HABTM
support_cases association so the form can reuse the shared
lookup_support_cases typeahead (which keys options by id).

Returns:

  • (Integer, nil)

    id of the (first) linked support case



1656
1657
1658
# File 'app/models/order.rb', line 1656

def support_case_id
  support_case_ids.first
end

#support_case_id=(id) ⇒ Object

Parameters:

  • id (String, Integer, nil)

    support-case id selected in the picker



1661
1662
1663
# File 'app/models/order.rb', line 1661

def support_case_id=(id)
  self.support_case_ids = Array(id.presence).map(&:to_i)
end

#support_case_refString

Comma-separated list of attached support case numbers. Used in
the CRM order form's free-typed support-case field so users can
paste in a list of case numbers without juggling ids.

Returns:

  • (String)


1636
1637
1638
# File 'app/models/order.rb', line 1636

def support_case_ref
  support_cases.map(&:case_number).join(', ')
end

#support_case_ref=(support_case_ref) ⇒ Object

Setter that resolves a comma-separated list of case numbers
into the matching support-case ids and writes them to
support_case_ids. Tolerates spaces in the input.

Parameters:

  • support_case_ref (String)


1645
1646
1647
1648
# File 'app/models/order.rb', line 1645

def support_case_ref=(support_case_ref)
  refs = support_case_ref.delete(' ').split(',')
  self.support_case_ids = SupportCase.where(case_number: refs).ids
end

#support_casesActiveRecord::Associations::CollectionProxy

The associated support cases.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


422
# File 'app/models/order.rb', line 422

has_and_belongs_to_many :support_cases

#suppress_edi_duplicate_warning?Boolean

Suppresses duplicate order warnings for off-book delivery arrangements.
Returns true if warnings should be suppressed (acknowledged date is in the future and order not shipped).

Returns:

  • (Boolean)


2266
2267
2268
2269
2270
2271
# File 'app/models/order.rb', line 2266

def suppress_edi_duplicate_warning?
  return false if edi_delayed_delivery_acknowledged_at.blank?
  return false if shipped? # Auto-clear when shipped

  edi_delayed_delivery_acknowledged_at.to_date >= Date.current
end

#sync_opportunityObject

After-save hook: tell the linked opportunity to re-evaluate its
state machine (the opportunity may need to advance from
"active" to "sold" once the order is invoiced).



4181
4182
4183
# File 'app/models/order.rb', line 4181

def sync_opportunity
  opportunity&.sync_state
end

#technical_support_repTechnicalSupportRep?

The associated technical support rep.

Returns:

  • (TechnicalSupportRep, nil)


354
# File 'app/models/order.rb', line 354

has_one :technical_support_rep, through: :opportunity

#terms_available?Object (protected)

Whether terms available.

Returns:

  • (Object)


6417
6418
6419
# File 'app/models/order.rb', line 6417

def terms_available?
  billing_entity.terms_credit_limit >= total
end

#to_labelString

Compact label string used by autocomplete dropdowns and SMS
threads — [REF#] Customer (shipped-date).

Returns:

  • (String)


4090
4091
4092
# File 'app/models/order.rb', line 4090

def to_label
  "[#{reference_number}] #{customer.full_name} (#{shipped_date.to_fs(:crm_default)})"
end

#to_liquidLiquid::OrderDrop

Wrap the order in a Liquid::OrderDrop so it can be safely
rendered inside customer-facing email templates without exposing
internal model methods.

Returns:



3619
3620
3621
# File 'app/models/order.rb', line 3619

def to_liquid
  Liquid::OrderDrop.new self
end

#to_sString

Returns #cart_identifier.

Returns:



4108
4109
4110
# File 'app/models/order.rb', line 4108

def to_s
  cart_identifier
end

#to_storeStore?

The associated to store.

Returns:



338
# File 'app/models/order.rb', line 338

belongs_to :to_store, class_name: 'Store', optional: true

#total_codBigDecimal, Float

Cash-On-Delivery amount the carrier should collect from the
consignee. Returns 0 when the order isn't COD-funded; otherwise
subtracts any already-authorised pre-payments so we don't ask
the carrier to collect twice.

Returns:

  • (BigDecimal, Float)


3334
3335
3336
3337
3338
3339
3340
3341
# File 'app/models/order.rb', line 3334

def total_cod
  # return 0 if the order is not funded by cod
  return 0.0 unless funded_by_cod?

  # else we need to deduct any pre_payments from the order total to work out what the cod amount should be
  # in case the balance was partially paid by another payment method
  total - total_payments_authorized
end

#total_moneyString

Order total formatted as a "%.2f" string for display where the
template needs a fixed two-decimal value (CSV exports, EDI
payloads).

Returns:

  • (String)


1990
1991
1992
# File 'app/models/order.rb', line 1990

def total_money
  '%.2f' % total
end

#total_payments_authorizedBigDecimal

Sum of authorised payment amounts across deliveries, capping
each delivery at its own total so over-authorisation on one
delivery doesn't shift coverage to another. The complement of
#balance.

Returns:

  • (BigDecimal)


2765
2766
2767
2768
2769
2770
2771
2772
2773
# File 'app/models/order.rb', line 2765

def total_payments_authorized
  amount = BigDecimal('0.0')
  deliveries.each do |dq|
    dq_total = dq.total || BigDecimal('0.0')
    dq_auth = dq.total_payments_authorized || BigDecimal('0.0')
    amount += [dq_auth, dq_total].min
  end
  amount < 0 ? BigDecimal('0.0') : amount
end

#track_profit?Object

Whether track profit.

Returns:

  • (Object)


6048
6049
6050
# File 'app/models/order.rb', line 6048

def track_profit?
  profitable_line_items.present? && is_regular_order?
end

#tracking_email_addressString

Inbound-email address that uniquely identifies this order — when
a tracking-email reply lands at this address, ActionMailbox
decrypts the id and re-attaches the message to the order.

Returns:

  • (String)


2682
2683
2684
2685
2686
2687
# File 'app/models/order.rb', line 2682

def tracking_email_address
  domain = Rails.application.config.x.email_domain
  prefix = 'ord'
  encrypted_id = Encryption.encrypt_string(id.to_s)
  "#{prefix}+id#{encrypted_id}@#{domain}"
end

#uncommit_undelivered_line_itemsObject

Release inventory commits for line items that aren't attached to
a delivery (typically left over after a delivery is destroyed
mid-flow). Otherwise stock would stay reserved against a phantom
commit and warehouse pickers couldn't see it as available.



2896
2897
2898
2899
2900
# File 'app/models/order.rb', line 2896

def uncommit_undelivered_line_items
  orphaned = line_items.where(delivery_id: nil)
                       .joins(:inventory_commits).distinct
  Item::InventoryCommitter.crm_uncommit(orphaned) if orphaned.any?
end

#undiscounted_shipping_totalBigDecimal

Sum of the shipping ('shp') line items at their raw (pre-discount) price.
Unlike the shipping_cost column — the post-discount net, polluted by any
live shipping credit (FS or the economy match itself) — this is a stable
base for computing shipping credits (see
Delivery#economy_shipping_match_amount).

Returns:

  • (BigDecimal)


2181
2182
2183
# File 'app/models/order.rb', line 2181

def undiscounted_shipping_total
  line_items.shipping_only.to_a.sum(BigDecimal('0')) { |li| (li.price || 0).to_d * li.quantity.to_i }
end

#unfulfilled_dropship_itemsBoolean

True when any dropship line still lacks a fully-receipted
supplier purchase-order item — i.e. the supplier hasn't shipped
the order yet from their warehouse. Used to decide whether the
order can transition out of awaiting_po_fulfillment.

Returns:

  • (Boolean)


2908
2909
2910
# File 'app/models/order.rb', line 2908

def unfulfilled_dropship_items
  line_items.dropship.any? { |li| li.purchase_order_item.nil? || !li.purchase_order_item.fully_receipted? }
end

#update_customer_statusObject (protected)

After-save callback: re-evaluate the customer's lifecycle state
(lead → prospect → customer → returning customer) now that this
order may have moved them up or down.



6355
6356
6357
# File 'app/models/order.rb', line 6355

def update_customer_status
  customer&.set_status
end

#update_linked_po_if_stBoolean

For store-transfer orders, sync the linked PurchaseOrder record
to the order's first delivery so accounting sees the same dates,
quantities, and costs on both sides of the transfer.

Returns:

  • (Boolean)

    true if the PO was created/updated and is valid



6038
6039
6040
6041
6042
6043
# File 'app/models/order.rb', line 6038

def update_linked_po_if_st
  return unless is_store_transfer? && deliveries.first

  po = PurchaseOrder.new_or_update_st_po_from_delivery(deliveries.first)
  po.present? && po.valid?
end

#update_sales_support_rep(new_sales_support_rep_id, new_commission_date = nil) ⇒ Boolean

Reassign the sales-support rep on the order (and optionally
update the support-commission date). For invoiced orders we
save with validate: false so a rep change after invoicing
doesn't accidentally fail validations that don't matter at
this point in the lifecycle.

Parameters:

  • new_sales_support_rep_id (Integer)
  • new_commission_date (Date, nil) (defaults to: nil)

Returns:

  • (Boolean)


2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
# File 'app/models/order.rb', line 2100

def update_sales_support_rep(new_sales_support_rep_id, new_commission_date = nil)
  # Allow updating sales_support_rep_id and commission date even for invoiced orders
  if invoiced?
    # For invoiced orders, update only the sales_support_rep_id and commission date without other validations
    self.sales_support_rep_id = new_sales_support_rep_id
    self.sales_support_commission_date = new_commission_date
    save(validate: false)
  else
    # For non-invoiced orders, use normal update process
    update(
      sales_support_rep_id: new_sales_support_rep_id,
      sales_support_commission_date: new_commission_date
    )
  end
end

#uploadsActiveRecord::Associations::CollectionProxy

The associated uploads.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


367
# File 'app/models/order.rb', line 367

has_many :uploads, -> { order(created_at: :desc) }, as: :resource, dependent: :destroy

#versions_for_audit_trail(_params = {}) ⇒ ActiveRecord::Relation<RecordVersion>

Aggregate RecordVersion (PaperTrail) rows for the order's
full audit trail — the order itself plus every line item,
delivery, and discount that points back to it. Surfaced in the
CRM "Audit" tab.

Parameters:

  • _params (Object) (defaults to: {})

Returns:



4192
4193
4194
4195
4196
4197
4198
4199
4200
4201
4202
4203
4204
4205
4206
4207
4208
4209
4210
4211
4212
4213
4214
4215
4216
4217
# File 'app/models/order.rb', line 4192

def versions_for_audit_trail(_params = {})
  query_sql = %q{
                (item_type = 'Order' AND item_id = :id)
                OR (
                  item_type = 'LineItem'
                    AND reference_data @> '{"resource_type": "Order"}'
                    AND reference_data @> :resource_id_json
                )
                OR (
                  item_type = 'Delivery'
                    AND reference_data @> :order_id_json
                )
                OR (
                  item_type = 'Discount'
                    AND reference_data @> :itemizable_id_json
                    AND reference_data @> '{"itemizable_type": "Order"}'
                )
              }
  RecordVersion.where(
    query_sql,
    id:,
    resource_id_json: { resource_id: id }.to_json,
    order_id_json: { order_id: id }.to_json,
    itemizable_id_json: { itemizable_id: id }.to_json
  )
end

#visitVisit?

The associated visit.

Returns:



347
# File 'app/models/order.rb', line 347

belongs_to :visit, optional: true

#void_credit_rma_itemObject

Void every RMA credit-item linked to this order's line items.
Used when a credit order is being cancelled — the linked RMA
items should no longer be redeemable against future orders.



4231
4232
4233
# File 'app/models/order.rb', line 4231

def void_credit_rma_item
  line_items.filter_map(&:credit_rma_item).uniq.each(&:void!)
end

#void_early_label!(reason: 'Manual void requested', reset_flag: true) ⇒ Hash

Void the early label with a custom reason (public method for external callers)
Also resets purchase_label_early flag so the next ship-label goes through normal flow

Parameters:

  • reason (String) (defaults to: 'Manual void requested')

    The reason for voiding

  • reset_flag (Boolean) (defaults to: true)

    Whether to reset purchase_label_early flag (default: true)

Returns:

  • (Hash)

    Result with :success and optional :error



4942
4943
4944
4945
4946
4947
4948
4949
4950
4951
4952
4953
4954
4955
4956
4957
4958
4959
4960
4961
4962
4963
4964
4965
4966
4967
4968
4969
4970
4971
4972
4973
4974
4975
4976
# File 'app/models/order.rb', line 4942

def void_early_label!(reason: 'Manual void requested', reset_flag: true)
  return { success: true, message: 'No early label to void' } unless has_early_purchased_label?

  Rails.logger.info("[EarlyLabel] Voiding early label for order #{reference_number}: tracking=#{early_label_tracking_number}, reason=#{reason}")

  begin
    client, marketplace_type = build_early_label_void_client
    void_api_result = if marketplace_type == :amazon
                        client.cancel_shipment(early_label_sww_label_id)
                      else
                        client.discard_label(early_label_carrier, early_label_tracking_number)
                      end

    if void_api_result.success
      Rails.logger.info("[EarlyLabel] Successfully voided early label for order #{reference_number}")

      void_additional_early_label_packages(client) if marketplace_type == :amazon

      update_attrs = { early_label_voided_at: Time.current, early_label_void_reason: reason }
      update_attrs[:purchase_label_early] = false if reset_flag
      update!(update_attrs)
      void_early_label_upload
      Rails.logger.info('[EarlyLabel] Reset purchase_label_early flag') if reset_flag
      { success: true }
    else
      Rails.logger.error("[EarlyLabel] Failed to void early label for order #{reference_number}: #{void_api_result.error}")
      update!(early_label_void_reason: "#{reason} (API failed: #{void_api_result.error})")
      { success: false, error: void_api_result.error }
    end
  rescue StandardError => e
    Rails.logger.error("[EarlyLabel] Error voiding early label for order #{reference_number}: #{e.message}")
    update!(early_label_void_reason: "#{reason} (Exception: #{e.message})")
    { success: false, error: e.message }
  end
end

#void_early_label_if_existsHash?

Void early-purchased label when order is pulled back from awaiting_deliveries
Called from before_transition to cancelled/fraudulent/in_cr_hold

SAFEGUARD B: Blocks rapid void if label was just purchased (within threshold)
SAFEGUARD C: Sends alert email if void fails

Returns:

  • (Hash, nil)

    Result hash with success/error or nil if no label to void



4604
4605
4606
4607
4608
4609
4610
4611
4612
4613
4614
4615
4616
4617
4618
4619
4620
4621
4622
4623
4624
4625
4626
4627
4628
4629
4630
4631
4632
4633
4634
4635
4636
4637
4638
4639
4640
4641
4642
4643
4644
4645
4646
4647
4648
4649
4650
4651
4652
4653
4654
4655
4656
4657
4658
4659
4660
4661
4662
4663
4664
4665
4666
4667
4668
4669
4670
4671
4672
4673
4674
4675
4676
4677
4678
4679
4680
4681
4682
4683
4684
# File 'app/models/order.rb', line 4604

def void_early_label_if_exists
  unless has_early_purchased_label?
    cleanup_failed_early_label_state if purchase_label_early? && early_label_tracking_number.blank?
    return
  end

  # SAFEGUARD B: Check for rapid state transition
  if early_label_purchased_recently?
    minutes_since = ((Time.current - early_label_purchased_at) / 60).round(1)
    wait_minutes = (EARLY_LABEL_RAPID_VOID_THRESHOLD_MINUTES - minutes_since).ceil

    Rails.logger.warn("[EarlyLabel] Rapid void blocked for order #{reference_number}: label purchased #{minutes_since} min ago (threshold: #{EARLY_LABEL_RAPID_VOID_THRESHOLD_MINUTES} min)")

    errors.add(:base, "Early label was purchased #{minutes_since} minutes ago. Please wait #{wait_minutes} more minute(s) before holding/canceling, or void the label manually first.")
    throw :halt
  end

  Rails.logger.info("[EarlyLabel] Auto-voiding early label for order #{reference_number}: tracking=#{early_label_tracking_number}")

  # Initialize API log for void operation
  @early_label_api_log = []
  log_early_label_event('void_start', "Starting void for tracking #{early_label_tracking_number}")

  begin
    client, marketplace_type = build_early_label_void_client
    void_api_result = if marketplace_type == :amazon
                        client.cancel_shipment(early_label_sww_label_id)
                      else
                        client.discard_label(early_label_carrier, early_label_tracking_number)
                      end

    log_early_label_event('api_response', 'void/discard response', {
      success: void_api_result.success,
      error: void_api_result.error
    })

    if void_api_result.success
      Rails.logger.info("[EarlyLabel] Successfully voided early label for order #{reference_number}")
      log_early_label_event('void_success', "Label voided successfully")

      void_additional_early_label_packages(client) if marketplace_type == :amazon

      save_early_label_api_log
      update!(
        early_label_voided_at: Time.current,
        early_label_void_reason: 'Order transitioned out of awaiting_deliveries'
      )
      void_early_label_upload
      { success: true }
    else
      Rails.logger.error("[EarlyLabel] Failed to void early label for order #{reference_number}: #{void_api_result.error}")
      log_early_label_event('void_failed', "Void API failed: #{void_api_result.error}")
      save_early_label_api_log

      # SAFEGUARD C: Send alert for void failure
      marketplace = edi_orchestrator_partner&.start_with?('amazon_seller') ? 'Amazon' : 'Walmart'
      send_early_label_void_alert(
        reason: "#{marketplace} API void failed",
        details: void_api_result.error
      )

      update!(early_label_void_reason: "Void attempted but API failed: #{void_api_result.error}")
      { success: false, error: void_api_result.error }
    end
  rescue StandardError => e
    Rails.logger.error("[EarlyLabel] Error voiding early label for order #{reference_number}: #{e.message}")
    log_early_label_event('void_exception', "Exception: #{e.message}")
    save_early_label_api_log

    # SAFEGUARD C: Send alert for void exception (unless it's our own rapid-void exception)
    unless e.message.include?('Cannot auto-void early label')
      send_early_label_void_alert(
        reason: 'Void exception',
        details: e.message
      )
    end

    update!(early_label_void_reason: "Void failed with exception: #{e.message}")
    { success: false, error: e.message }
  end
end

#void_early_label_uploadObject

Re-categorize the early label PDF as voided so it no longer appears
as an active attachment but is kept for audit trail (mirrors the
ship_label_pdf -> voided_ship_label_pdf pattern in Shipment)



5088
5089
5090
5091
5092
5093
5094
# File 'app/models/order.rb', line 5088

def void_early_label_upload
  uploads.in_category('early_ship_label').update_all(category: 'voided_early_ship_label')
  Rails.logger.info("[EarlyLabel] Re-categorized early label PDF to voided_early_ship_label on order #{reference_number}")
rescue StandardError => e
  Rails.logger.warn("[EarlyLabel] Failed to re-categorize early label PDF: #{e.message}")
  # Non-fatal — metadata void is what matters
end

#void_payments(report_fraud = false) ⇒ Object

Void every authorised payment on the order (and refuse to act on
captured ones — those need manual intervention). Used when an
order is cancelled or marked fraudulent; pass report_fraud: true
to flag the void to the gateway as a chargeback risk.

Parameters:

  • report_fraud (Boolean) (defaults to: false)

Raises:

  • (StandardError)

    if any payment can't be voided



6338
6339
6340
6341
6342
6343
6344
6345
6346
6347
6348
# File 'app/models/order.rb', line 6338

def void_payments(report_fraud = false)
  payments.each do |pp|
    if pp.authorized?
      res = pp.gateway_class.new(pp).void(report_fraud)
      raise StandardError, "Unable to void authorized payment #{pp.id} for order #{reference_number}" unless res.success
    elsif pp.captured?
      # TODO: Handle captured payments.
      raise StandardError, "Unable to void captured payments #{pp.id} for order #{reference_number}"
    end
  end
end

#vouchersActiveRecord::Associations::CollectionProxy

The associated vouchers.

Returns:

  • (ActiveRecord::Associations::CollectionProxy)


388
# File 'app/models/order.rb', line 388

has_many :vouchers

#walmart_sww_eligible?Boolean

Check if order is eligible for Walmart Ship with Walmart early label purchase

Returns:

  • (Boolean)


4988
4989
4990
4991
4992
4993
4994
4995
4996
4997
4998
4999
# File 'app/models/order.rb', line 4988

def walmart_sww_eligible?
  return false unless is_edi_order?
  return false unless edi_orchestrator_partner&.start_with?('walmart_seller')

  begin
    orchestrator = Edi::Walmart::Orchestrator.new(edi_orchestrator_partner.to_sym)
    orchestrator&.ship_with_walmart_enabled?
  rescue StandardError => e
    Rails.logger.warn("[EarlyLabel] Error checking SWW eligibility: #{e.message}")
    false
  end
end

#warrantiesActiveRecord::Relation<Warranty>

Warranty registrations against this order (kept if the order is ever
hard-deleted — the registration is the customer's record, not ours).

Returns:

See Also:



376
# File 'app/models/order.rb', line 376

has_many :warranties, dependent: :nullify, inverse_of: :order