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 Classes: AuthorizationSplitWorker, AuthorizationSplitter, BackOrderClientNotification, ContactLookup, CreateVcProcurementOrdersFromCsv, DefaultTrackingEmailExtractor, FollowUpScheduler, FraudDetector, Mover, SendAbandonedCartEmails, 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
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_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 Schedulable

Schedulable::SIMPLE_FORM_OPTIONS

Instance Attribute Summary collapse

Attributes included from Models::Profitable

#min_profit_markup

Attributes included from Models::Itemizable

#force_total_reset, #total_reset

Belongs to collapse

Methods included from Models::Auditable

#creator, #updater

Methods included from Models::TaxableResource

#resource_tax_rate

Methods included from Models::Itemizable

#account_specialist

Has one collapse

Has many collapse

Methods included from Models::Pickable

#line_discounts, #line_items

Methods included from Models::Itemizable

#coupons, #discounts

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_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, #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, #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 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' })


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

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.



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

def do_not_detect_shipping
  @do_not_detect_shipping
end

#do_not_set_totalsObject

Returns the value of attribute do_not_set_totals.



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

def do_not_set_totals
  @do_not_set_totals
end

#early_label_purchase_resultObject

Returns the value of attribute early_label_purchase_result.



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

def early_label_purchase_result
  @early_label_purchase_result
end

#from_store_idObject (readonly)



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

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.



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

def full_shipping_address_validation
  @full_shipping_address_validation
end

#is_wwwObject

Returns the value of attribute is_www.



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

def is_www
  @is_www
end

#is_www_ship_by_zipObject

Returns the value of attribute is_www_ship_by_zip.



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

def is_www_ship_by_zip
  @is_www_ship_by_zip
end

#max_discount_overrideObject (readonly)



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

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

#order_typeObject (readonly)



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

validates :order_type, presence: true

#reference_numberObject (readonly)



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

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


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

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

#to_store_idObject (readonly)



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

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

#tracking_emailObject (readonly)



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

validates :tracking_email, email_format: true

#update_shipping_address_with_contactObject

Returns the value of attribute update_shipping_address_with_contact.



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

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:



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

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:



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

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:



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

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


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

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:



483
484
485
486
487
488
489
490
491
492
493
494
495
# File 'app/models/order.rb', line 483

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



553
554
555
556
557
558
559
560
561
# File 'app/models/order.rb', line 553

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:



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

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:



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

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:



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

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

.cartsActiveRecord::Relation<Order>

A relation of Orders that are carts. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



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

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:



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

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:



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

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:



541
542
543
# File 'app/models/order.rb', line 541

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:



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

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:



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

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


1544
1545
1546
# File 'app/models/order.rb', line 1544

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:



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

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

.draft_spiffActiveRecord::Relation<Order>

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

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



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

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:



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

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:



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

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:



475
476
477
478
# File 'app/models/order.rb', line 475

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:



469
470
471
# File 'app/models/order.rb', line 469

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

scope :like_lookup, ->(q) { where('orders.reference_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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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)


1573
1574
1575
1576
1577
1578
1579
# File 'app/models/order.rb', line 1573

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)


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

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)


5828
5829
5830
# File 'app/models/order.rb', line 5828

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

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

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



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

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:



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

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:



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

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:



519
520
521
522
# File 'app/models/order.rb', line 519

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



2965
2966
2967
2968
2969
2970
2971
2972
2973
2974
2975
2976
2977
2978
2979
2980
2981
2982
2983
2984
2985
2986
# File 'app/models/order.rb', line 2965

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 'barby'
  require 'barby/barcode/code_128'
  require 'barby/outputter/png_outputter'
  barcode = Barby::Code128B.new(po_number)
  png = barcode.to_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:



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

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:



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

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:



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

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:



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

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})


3391
3392
3393
3394
3395
3396
3397
3398
# File 'app/models/order.rb', line 3391

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:



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

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:



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

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:



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

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


1553
1554
1555
# File 'app/models/order.rb', line 1553

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:



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

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:



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

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:



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

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


1594
1595
1596
# File 'app/models/order.rb', line 1594

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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

Instance Method Details

#accounting_hold_order?Boolean

Returns:

  • (Boolean)


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

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

#activitiesActiveRecord::Relation<Activity>

Returns:

See Also:



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

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.



1602
1603
1604
1605
1606
1607
1608
# File 'app/models/order.rb', line 1602

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:



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

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:



2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794
2795
2796
2797
2798
2799
2800
2801
2802
2803
2804
2805
2806
# File 'app/models/order.rb', line 2772

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)


5621
5622
5623
# File 'app/models/order.rb', line 5621

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

Returns:

  • (Boolean)


1130
1131
1132
# File 'app/models/order.rb', line 1130

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

#all_deliveries_invoiced?Boolean

Returns:

  • (Boolean)


2514
2515
2516
# File 'app/models/order.rb', line 2514

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

#all_funds_available?(ignore_cod = false) ⇒ Boolean

Returns:

  • (Boolean)


2510
2511
2512
# File 'app/models/order.rb', line 2510

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

#all_funds_not_available_and_shippable?Boolean (protected)

Returns:

  • (Boolean)


5794
5795
5796
# File 'app/models/order.rb', line 5794

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)


2544
2545
2546
# File 'app/models/order.rb', line 2544

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



1262
1263
1264
1265
1266
1267
1268
# File 'app/models/order.rb', line 1262

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?Boolean

Returns:

  • (Boolean)


1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
# File 'app/models/order.rb', line 1134

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?Boolean

Returns:

  • (Boolean)


2126
2127
2128
# File 'app/models/order.rb', line 2126

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


1701
1702
1703
1704
1705
# File 'app/models/order.rb', line 1701

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


5665
5666
5667
5668
5669
5670
5671
5672
5673
5674
5675
# File 'app/models/order.rb', line 5665

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?Boolean

Returns:

  • (Boolean)


5254
5255
5256
# File 'app/models/order.rb', line 5254

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?Boolean

Returns:

  • (Boolean)


3017
3018
3019
# File 'app/models/order.rb', line 3017

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)


4479
4480
4481
4482
4483
4484
4485
4486
4487
4488
4489
4490
# File 'app/models/order.rb', line 4479

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)


2339
2340
2341
# File 'app/models/order.rb', line 2339

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

#any_rooms_not_orderable?Boolean

Returns:

  • (Boolean)


2120
2121
2122
2123
2124
# File 'app/models/order.rb', line 2120

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)


3006
3007
3008
3009
3010
3011
# File 'app/models/order.rb', line 3006

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)


3105
3106
3107
# File 'app/models/order.rb', line 3105

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)


3350
3351
3352
# File 'app/models/order.rb', line 3350

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)


3360
3361
3362
3363
3364
# File 'app/models/order.rb', line 3360

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.



1530
1531
1532
1533
1534
# File 'app/models/order.rb', line 1530

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?Boolean

Returns:

  • (Boolean)


5290
5291
5292
# File 'app/models/order.rb', line 5290

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



2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
# File 'app/models/order.rb', line 2450

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?Boolean

Returns:

  • (Boolean)


2590
2591
2592
# File 'app/models/order.rb', line 2590

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:



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

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

#billing_entityObject

Alias for Customer#billing_entity

Returns:

  • (Object)

    Customer#billing_entity

See Also:



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

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:



1692
1693
1694
# File 'app/models/order.rb', line 1692

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)



5215
5216
5217
5218
5219
5220
5221
5222
5223
5224
5225
5226
5227
5228
5229
5230
5231
5232
# File 'app/models/order.rb', line 5215

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)


5239
5240
5241
5242
5243
5244
5245
5246
5247
5248
5249
5250
5251
5252
# File 'app/models/order.rb', line 5239

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



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

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)


5613
5614
5615
# File 'app/models/order.rb', line 5613

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

#can_auto_reserve_serial_numbers?Boolean

Returns:

  • (Boolean)


1522
1523
1524
# File 'app/models/order.rb', line 1522

def can_auto_reserve_serial_numbers?
  order_type != 'CO'
end

#can_be_cancelled?Boolean

Returns:

  • (Boolean)


3756
3757
3758
3759
3760
3761
# File 'app/models/order.rb', line 3756

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

  cancelable?
end

#can_be_returned?Boolean

Returns:

  • (Boolean)


1919
1920
1921
# File 'app/models/order.rb', line 1919

def can_be_returned?
  is_regular_order?
end

#can_cr_hold?(current_user = nil) ⇒ Boolean

Returns:

  • (Boolean)


2154
2155
2156
# File 'app/models/order.rb', line 2154

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?Boolean

Returns:

  • (Boolean)


1176
1177
1178
# File 'app/models/order.rb', line 1176

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.



5353
5354
5355
5356
5357
# File 'app/models/order.rb', line 5353

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.



3607
3608
3609
# File 'app/models/order.rb', line 3607

def cancel_or_destroy
  cart? ? destroy : cancel
end

#cancelable?Boolean

Returns:

  • (Boolean)


2158
2159
2160
# File 'app/models/order.rb', line 2158

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)


2875
2876
2877
2878
2879
2880
2881
2882
2883
2884
2885
# File 'app/models/order.rb', line 2875

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)


3768
3769
3770
# File 'app/models/order.rb', line 3768

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

#cart_or_in_shipping_estimate?Boolean

Returns:

  • (Boolean)


3772
3773
3774
# File 'app/models/order.rb', line 3772

def cart_or_in_shipping_estimate?
  cart? || in_shipping_estimate?
end

#catalogObject

Alias for Customer#catalog

Returns:

  • (Object)

    Customer#catalog

See Also:



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

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.



1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
# File 'app/models/order.rb', line 1387

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).



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

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?Boolean

Returns:

  • (Boolean)


3072
3073
3074
# File 'app/models/order.rb', line 3072

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.



2922
2923
2924
# File 'app/models/order.rb', line 2922

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

#closed_state?Boolean

Returns:

  • (Boolean)


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

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

#communicationsActiveRecord::Relation<Communication>

Returns:

See Also:



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

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

#companyObject

Alias for Customer#company

Returns:

  • (Object)

    Customer#company

See Also:



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

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).



1201
1202
1203
# File 'app/models/order.rb', line 1201

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.



1207
1208
1209
# File 'app/models/order.rb', line 1207

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

#complete?Boolean

Returns:

  • (Boolean)


2375
2376
2377
# File 'app/models/order.rb', line 2375

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:



5529
5530
5531
# File 'app/models/order.rb', line 5529

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

#completely_shipped?Boolean

Returns:

  • (Boolean)


5537
5538
5539
# File 'app/models/order.rb', line 5537

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.



5590
5591
5592
5593
5594
5595
5596
5597
# File 'app/models/order.rb', line 5590

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

Returns:

See Also:



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

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



1147
1148
1149
1150
1151
# File 'app/models/order.rb', line 1147

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



1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
# File 'app/models/order.rb', line 1155

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


1172
1173
1174
# File 'app/models/order.rb', line 1172

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.



1252
1253
1254
1255
1256
# File 'app/models/order.rb', line 1252

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.



1239
1240
1241
1242
1243
1244
1245
1246
1247
# File 'app/models/order.rb', line 1239

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.

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.



1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
# File 'app/models/order.rb', line 1317

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.



1628
1629
1630
1631
1632
1633
1634
# File 'app/models/order.rb', line 1628

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:



2369
2370
2371
2372
2373
# File 'app/models/order.rb', line 2369

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.



3063
3064
3065
3066
# File 'app/models/order.rb', line 3063

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.



3163
3164
3165
# File 'app/models/order.rb', line 3163

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.



3170
3171
3172
3173
3174
3175
3176
# File 'app/models/order.rb', line 3170

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.



3157
3158
3159
# File 'app/models/order.rb', line 3157

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

#credit_memoCreditMemo

Returns:

See Also:



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

has_one :credit_memo, foreign_key: :credit_order_id

#credit_memosActiveRecord::Relation<CreditMemo>

Returns:

See Also:



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

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)


2849
2850
2851
# File 'app/models/order.rb', line 2849

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)


2216
2217
2218
# File 'app/models/order.rb', line 2216

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

#custom_shipping_labelsObject

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



3620
3621
3622
3623
3624
3625
3626
3627
3628
3629
3630
# File 'app/models/order.rb', line 3620

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

Returns:

See Also:



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

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

#customer_qualifies_for_free_online_shipping?Boolean

Returns:

  • (Boolean)


1936
1937
1938
# File 'app/models/order.rb', line 1936

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



587
588
589
590
591
592
593
594
595
596
597
598
# File 'app/models/order.rb', line 587

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


2731
2732
2733
2734
2735
2736
2737
2738
# File 'app/models/order.rb', line 2731

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



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

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

#deferred_payments_captured_and_ready_for_shipping?Boolean

Returns:

  • (Boolean)


1908
1909
1910
# File 'app/models/order.rb', line 1908

def deferred_payments_captured_and_ready_for_shipping?
  paypal_invoices_paid? && ready_for_shipping?
end

#deletable?Boolean

Returns:

  • (Boolean)


2208
2209
2210
# File 'app/models/order.rb', line 2208

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

#deliveriesActiveRecord::Relation<Delivery>

Returns:

See Also:



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

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

#delivery_activitiesActiveRecord::Relation<Activity>

Returns:

See Also:



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

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

#direct_shipmentsActiveRecord::Relation<Shipment>

Returns:

See Also:



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

has_many :direct_shipments, class_name: 'Shipment'

#doesnt_already_has_smartinstall?Boolean

Returns:

  • (Boolean)


3013
3014
3015
# File 'app/models/order.rb', line 3013

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



4895
4896
4897
4898
4899
4900
4901
4902
4903
4904
4905
4906
4907
4908
# File 'app/models/order.rb', line 4895

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::Relation<DropShipPurchaseOrder>

Returns:

  • (ActiveRecord::Relation<DropShipPurchaseOrder>)

See Also:



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

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



4373
4374
4375
4376
4377
4378
4379
4380
4381
4382
4383
4384
4385
4386
4387
4388
4389
4390
4391
4392
4393
4394
4395
4396
4397
4398
4399
4400
4401
4402
# File 'app/models/order.rb', line 4373

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



4575
4576
4577
4578
4579
4580
4581
4582
4583
4584
4585
4586
4587
4588
4589
4590
4591
4592
4593
4594
4595
4596
4597
4598
4599
4600
# File 'app/models/order.rb', line 4575

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)


4494
4495
4496
4497
4498
4499
4500
4501
4502
4503
4504
4505
# File 'app/models/order.rb', line 4494

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)


4245
4246
4247
4248
4249
4250
# File 'app/models/order.rb', line 4245

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 }



4834
4835
4836
4837
4838
4839
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
4872
4873
4874
4875
4876
4877
4878
4879
4880
4881
4882
# File 'app/models/order.rb', line 4834

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:



4556
4557
4558
# File 'app/models/order.rb', line 4556

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:



2186
2187
2188
# File 'app/models/order.rb', line 2186

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)


3406
3407
3408
3409
3410
3411
3412
3413
3414
# File 'app/models/order.rb', line 3406

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


3422
3423
3424
3425
3426
3427
3428
3429
3430
3431
3432
3433
3434
3435
3436
3437
3438
3439
3440
3441
3442
3443
3444
3445
3446
3447
3448
3449
3450
3451
3452
3453
3454
3455
3456
3457
3458
3459
3460
3461
3462
3463
3464
3465
3466
3467
3468
3469
3470
3471
3472
3473
3474
3475
3476
3477
3478
3479
3480
3481
3482
3483
3484
3485
3486
3487
3488
3489
3490
3491
3492
3493
3494
3495
3496
3497
3498
3499
3500
3501
3502
3503
3504
3505
3506
3507
3508
3509
3510
3511
3512
3513
3514
3515
3516
3517
3518
3519
3520
3521
3522
3523
3524
3525
3526
3527
3528
3529
3530
3531
3532
3533
3534
3535
3536
3537
3538
3539
3540
3541
3542
3543
3544
3545
3546
3547
3548
3549
3550
3551
3552
3553
3554
3555
3556
3557
3558
3559
3560
3561
3562
3563
3564
3565
3566
3567
3568
3569
3570
3571
3572
3573
3574
3575
3576
3577
# File 'app/models/order.rb', line 3422

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_lowes_ca?
    [
      ['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::Relation<EdiCommunicationLog>

Returns:

See Also:



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

has_many :edi_communication_logs, through: :edi_documents

#edi_documentsActiveRecord::Relation<EdiDocument>

Returns:

See Also:



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

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



2033
2034
2035
2036
2037
2038
2039
# File 'app/models/order.rb', line 2033

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:



5740
5741
5742
5743
# File 'app/models/order.rb', line 5740

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?Boolean

Returns:

  • (Boolean)


2016
2017
2018
2019
2020
2021
2022
2023
2024
# File 'app/models/order.rb', line 2016

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?Boolean

Returns:

  • (Boolean)


2146
2147
2148
# File 'app/models/order.rb', line 2146

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)


1486
1487
1488
1489
1490
# File 'app/models/order.rb', line 1486

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)


3332
3333
3334
3335
3336
3337
3338
3339
3340
3341
3342
# File 'app/models/order.rb', line 3332

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


3692
3693
3694
# File 'app/models/order.rb', line 3692

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

#empty?Boolean

Returns:

  • (Boolean)


2204
2205
2206
# File 'app/models/order.rb', line 2204

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)


5476
5477
5478
# File 'app/models/order.rb', line 5476

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


5630
5631
5632
# File 'app/models/order.rb', line 5630

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)


5393
5394
5395
5396
5397
5398
5399
5400
5401
# File 'app/models/order.rb', line 5393

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)


5708
5709
5710
5711
5712
5713
5714
# File 'app/models/order.rb', line 5708

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)


5683
5684
5685
5686
5687
5688
5689
5690
# File 'app/models/order.rb', line 5683

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_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)


5726
5727
5728
5729
5730
5731
5732
5733
5734
# File 'app/models/order.rb', line 5726

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)


5696
5697
5698
5699
5700
5701
5702
# File 'app/models/order.rb', line 5696

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



5064
5065
5066
5067
5068
5069
5070
5071
5072
5073
5074
5075
5076
5077
5078
5079
5080
5081
5082
5083
# File 'app/models/order.rb', line 5064

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.



5087
5088
5089
5090
5091
5092
5093
5094
5095
5096
5097
5098
5099
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
5126
5127
5128
5129
5130
5131
5132
5133
5134
5135
5136
5137
5138
# File 'app/models/order.rb', line 5087

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



5141
5142
5143
5144
5145
5146
5147
5148
5149
5150
5151
5152
5153
5154
5155
5156
5157
5158
5159
5160
5161
5162
5163
5164
5165
5166
5167
5168
5169
5170
5171
5172
5173
5174
5175
5176
5177
5178
5179
5180
5181
5182
5183
5184
5185
5186
5187
5188
5189
5190
5191
5192
5193
5194
5195
5196
5197
5198
5199
5200
5201
5202
5203
5204
# File 'app/models/order.rb', line 5141

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)


2481
2482
2483
# File 'app/models/order.rb', line 2481

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)


5546
5547
5548
# File 'app/models/order.rb', line 5546

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)


5643
5644
5645
5646
5647
5648
5649
5650
5651
5652
5653
5654
5655
5656
5657
# File 'app/models/order.rb', line 5643

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)


2952
2953
2954
# File 'app/models/order.rb', line 2952

def formatted_po_number
  po_number(include_po_prefix: include_po_prefix?)
end

#from_storeStore

Returns:

See Also:



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

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

#fully_funded_by_advance_replacement?Boolean

Returns:

  • (Boolean)


2502
2503
2504
# File 'app/models/order.rb', line 2502

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?Boolean

Returns:

  • (Boolean)


2506
2507
2508
# File 'app/models/order.rb', line 2506

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

#funded_by_cod?Boolean

Returns:

  • (Boolean)


2498
2499
2500
# File 'app/models/order.rb', line 2498

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

#funds_available_and_ready_for_warehouse?Boolean (protected)

Returns:

  • (Boolean)


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

def funds_available_and_ready_for_warehouse?
  all_funds_available? && ready_for_warehouse?
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:



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

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)


5436
5437
5438
5439
# File 'app/models/order.rb', line 5436

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)


5410
5411
5412
5413
5414
5415
5416
5417
5418
5419
5420
5421
5422
5423
5424
5425
5426
5427
5428
# File 'app/models/order.rb', line 5410

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?Boolean

Returns:

  • (Boolean)


1375
1376
1377
# File 'app/models/order.rb', line 1375

def has_authorized_payment?
  payments.all_authorized.any?
end

#has_committed_serial_number_reservations?Boolean

Returns:

  • (Boolean)


2708
2709
2710
# File 'app/models/order.rb', line 2708

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

#has_custom_packing_slip?Boolean

Returns:

  • (Boolean)


2333
2334
2335
# File 'app/models/order.rb', line 2333

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)


4407
4408
4409
# File 'app/models/order.rb', line 4407

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

#has_incomplete_reservations?Boolean

Returns:

  • (Boolean)


2700
2701
2702
# File 'app/models/order.rb', line 2700

def has_incomplete_reservations?
  has_unreserved_line_items? || has_committed_serial_number_reservations?
end

#has_selected_heated_items?Boolean

Returns:

  • (Boolean)


3021
3022
3023
# File 'app/models/order.rb', line 3021

def has_selected_heated_items?
  smartinstall_data.present?
end

#has_shipping_method?Boolean

Returns:

  • (Boolean)


2343
2344
2345
# File 'app/models/order.rb', line 2343

def has_shipping_method?
  chosen_shipping_method.present?
end

#has_unreserved_line_items?Boolean

Returns:

  • (Boolean)


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

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

#has_web_rooms_needing_installation_plans?Boolean

Returns:

  • (Boolean)


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

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)


4535
4536
4537
4538
4539
4540
4541
4542
4543
4544
4545
4546
4547
4548
4549
4550
4551
# File 'app/models/order.rb', line 4535

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


2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
# File 'app/models/order.rb', line 2236

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?Boolean

Returns:

  • (Boolean)


2220
2221
2222
# File 'app/models/order.rb', line 2220

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.

Returns:

  • (String)


2401
2402
2403
2404
2405
2406
2407
# File 'app/models/order.rb', line 2401

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

#include_po_prefix?Object

Alias for Customer#include_po_prefix?

Returns:

  • (Object)

    Customer#include_po_prefix?

See Also:



570
571
# File 'app/models/order.rb', line 570

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

#includes_schedulable_service?Boolean

Returns:

  • (Boolean)


2594
2595
2596
# File 'app/models/order.rb', line 2594

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)


3372
3373
3374
3375
3376
# File 'app/models/order.rb', line 3372

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)


3682
3683
3684
# File 'app/models/order.rb', line 3682

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)


3675
3676
3677
# File 'app/models/order.rb', line 3675

def installation_country_iso3
  opportunity.presence&.installation_country_iso3
end

#installation_is_within_range?Boolean

Returns:

  • (Boolean)


3025
3026
3027
3028
3029
3030
3031
# File 'app/models/order.rb', line 3025

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)


3660
3661
3662
# File 'app/models/order.rb', line 3660

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)


3668
3669
3670
# File 'app/models/order.rb', line 3668

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)


2468
2469
2470
2471
2472
2473
2474
# File 'app/models/order.rb', line 2468

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

#invoiced_local_sales_repParty

Returns:

See Also:



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

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

#invoiced_primary_sales_repParty

Returns:

See Also:



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

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

#invoiced_secondary_sales_repParty

Returns:

See Also:



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

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

#invoicesActiveRecord::Relation<Invoice>

Returns:

  • (ActiveRecord::Relation<Invoice>)

See Also:



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

has_many :invoices

#is_amazon_com?Object

Alias for Customer#is_amazon_com?

Returns:

  • (Object)

    Customer#is_amazon_com?

See Also:



570
571
# File 'app/models/order.rb', line 570

delegate :include_po_prefix?, :is_home_depot_usa?, :is_home_depot_can?, :is_walmart_ca?, :is_amazon_com?, :is_costco_ca?, :is_part_of_lowes_ca?, :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:



570
571
# File 'app/models/order.rb', line 570

delegate :include_po_prefix?, :is_home_depot_usa?, :is_home_depot_can?, :is_walmart_ca?, :is_amazon_com?, :is_costco_ca?, :is_part_of_lowes_ca?, :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:



570
571
# File 'app/models/order.rb', line 570

delegate :include_po_prefix?, :is_home_depot_usa?, :is_home_depot_can?, :is_walmart_ca?, :is_amazon_com?, :is_costco_ca?, :is_part_of_lowes_ca?, :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:



570
571
# File 'app/models/order.rb', line 570

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

#is_crm?Boolean

Returns:

  • (Boolean)


3378
3379
3380
# File 'app/models/order.rb', line 3378

def is_crm?
  order_reception_type == 'CRM'
end

#is_edi_order?Boolean

Returns:

  • (Boolean)


2003
2004
2005
# File 'app/models/order.rb', line 2003

def is_edi_order?
  edi_transaction_id.present?
end

#is_fba?Boolean

Returns:

  • (Boolean)


2051
2052
2053
# File 'app/models/order.rb', line 2051

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

#is_from_myprojects?Boolean

Returns:

  • (Boolean)


2359
2360
2361
# File 'app/models/order.rb', line 2359

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:



570
571
# File 'app/models/order.rb', line 570

delegate :include_po_prefix?, :is_home_depot_usa?, :is_home_depot_can?, :is_walmart_ca?, :is_amazon_com?, :is_costco_ca?, :is_part_of_lowes_ca?, :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:



570
571
# File 'app/models/order.rb', line 570

delegate :include_po_prefix?, :is_home_depot_usa?, :is_home_depot_can?, :is_walmart_ca?, :is_amazon_com?, :is_costco_ca?, :is_part_of_lowes_ca?, :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:



570
571
# File 'app/models/order.rb', line 570

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

#is_marketing_order?Boolean

Returns:

  • (Boolean)


3092
3093
3094
# File 'app/models/order.rb', line 3092

def is_marketing_order?
  order_type == MARKETING_ORDER
end

#is_online?Boolean

Returns:

  • (Boolean)


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

def is_online?
  order_reception_type == 'Online'
end

#is_part_of_lowes_ca?Object

Alias for Customer#is_part_of_lowes_ca?

Returns:

  • (Object)

    Customer#is_part_of_lowes_ca?

See Also:



570
571
# File 'app/models/order.rb', line 570

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

#is_part_of_lowes_com?Object

Alias for Customer#is_part_of_lowes_com?

Returns:

  • (Object)

    Customer#is_part_of_lowes_com?

See Also:



570
571
# File 'app/models/order.rb', line 570

delegate :include_po_prefix?, :is_home_depot_usa?, :is_home_depot_can?, :is_walmart_ca?, :is_amazon_com?, :is_costco_ca?, :is_part_of_lowes_ca?, :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)


1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
# File 'app/models/order.rb', line 1925

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?Boolean

Returns:

  • (Boolean)


3096
3097
3098
# File 'app/models/order.rb', line 3096

def is_regular_order?
  [SALES_ORDER, MARKETING_ORDER, TECH_ORDER].include?(order_type)
end

#is_remote_service?Boolean

Returns:

  • (Boolean)


2623
2624
2625
# File 'app/models/order.rb', line 2623

def is_remote_service?
  belongs_to_smartservice_group? && line_items.any? { |a| a.sku.include?('REMOTE') }
end

#is_rma_replacement?Boolean

Returns:

  • (Boolean)


3080
3081
3082
# File 'app/models/order.rb', line 3080

def is_rma_replacement?
  rma && [SALES_ORDER, MARKETING_ORDER, TECH_ORDER].include?(order_type)
end

#is_rma_return?Boolean

Returns:

  • (Boolean)


3068
3069
3070
# File 'app/models/order.rb', line 3068

def is_rma_return?
  order_type == CREDIT_ORDER
end

#is_sales_order?Boolean

Returns:

  • (Boolean)


3084
3085
3086
# File 'app/models/order.rb', line 3084

def is_sales_order?
  order_type == SALES_ORDER
end

#is_smartfit_service?Boolean

Returns:

  • (Boolean)


2598
2599
2600
# File 'app/models/order.rb', line 2598

def is_smartfit_service?
  line_items.smartfit_items.any?
end

#is_smartfix_service?Boolean

Returns:

  • (Boolean)


2606
2607
2608
# File 'app/models/order.rb', line 2606

def is_smartfix_service?
  line_items.smartfix_items.any?
end

#is_smartguide_service?Boolean

Returns:

  • (Boolean)


2610
2611
2612
# File 'app/models/order.rb', line 2610

def is_smartguide_service?
  line_items.smartguide_items.any?
end

#is_smartinstall_service?Boolean

Returns:

  • (Boolean)


2602
2603
2604
# File 'app/models/order.rb', line 2602

def is_smartinstall_service?
  line_items.smartinstall_items.any?
end

#is_store_transfer?Boolean

Returns:

  • (Boolean)


3076
3077
3078
# File 'app/models/order.rb', line 3076

def is_store_transfer?
  order_type == STORE_TRANSFER
end

#is_subject_to_minimum_qty_rules?Boolean

Returns:

  • (Boolean)


3100
3101
3102
# File 'app/models/order.rb', line 3100

def is_subject_to_minimum_qty_rules?
  [SALES_ORDER].include?(order_type)
end

#is_tech_order?Boolean

Returns:

  • (Boolean)


3088
3089
3090
# File 'app/models/order.rb', line 3088

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:



570
571
# File 'app/models/order.rb', line 570

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

#is_warehouse_pickup?Boolean

Returns:

  • (Boolean)


2696
2697
2698
# File 'app/models/order.rb', line 2696

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:



570
571
# File 'app/models/order.rb', line 570

delegate :include_po_prefix?, :is_home_depot_usa?, :is_home_depot_can?, :is_walmart_ca?, :is_amazon_com?, :is_costco_ca?, :is_part_of_lowes_ca?, :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)


1900
1901
1902
1903
1904
1905
1906
# File 'app/models/order.rb', line 1900

def job_name
  if quote
    quote.opportunity.name
  elsif opportunity
    opportunity.name
  end
end

#linked_support_casesActiveRecord::Relation<LinkedSupportCase>

Returns:

  • (ActiveRecord::Relation<LinkedSupportCase>)

See Also:



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

has_many :linked_support_cases, through: :room_configurations, source: :support_cases

#local_sales_repEmployee?

Local (on-site) sales rep on the order. Same closed-vs-open
semantics as #primary_sales_rep.

Returns:



1668
1669
1670
1671
1672
# File 'app/models/order.rb', line 1668

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



4302
4303
4304
4305
4306
4307
4308
4309
4310
4311
# File 'app/models/order.rb', line 4302

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.



5458
5459
5460
# File 'app/models/order.rb', line 5458

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)


4456
4457
4458
# File 'app/models/order.rb', line 4456

def marketplace_early_label_eligible?
  walmart_sww_eligible? || amazon_buy_shipping_eligible?
end

#material_alertsActiveRecord::Relation<MaterialAlert>

Returns:

See Also:



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

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.



4315
4316
4317
4318
4319
4320
4321
4322
4323
4324
4325
4326
4327
4328
4329
4330
4331
4332
4333
4334
4335
4336
4337
4338
4339
4340
4341
4342
4343
4344
4345
4346
# File 'app/models/order.rb', line 4315

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::Relation<MessagingLog>

Returns:

See Also:



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

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


2329
2330
2331
# File 'app/models/order.rb', line 2329

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).



5263
5264
5265
5266
5267
5268
5269
5270
5271
5272
5273
5274
5275
5276
5277
5278
5279
# File 'app/models/order.rb', line 5263

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.



5325
5326
5327
5328
5329
5330
5331
5332
5333
5334
5335
5336
5337
5338
5339
5340
5341
5342
5343
5344
5345
5346
5347
# File 'app/models/order.rb', line 5325

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.



5285
5286
5287
5288
# File 'app/models/order.rb', line 5285

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)


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

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.



5602
5603
5604
5605
5606
# File 'app/models/order.rb', line 5602

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


5447
5448
5449
5450
5451
5452
5453
# File 'app/models/order.rb', line 5447

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?Boolean

Returns:

  • (Boolean)


1749
1750
1751
# File 'app/models/order.rb', line 1749

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)


2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
# File 'app/models/order.rb', line 2060

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)


2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
# File 'app/models/order.rb', line 2079

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



3187
3188
3189
3190
3191
3192
3193
3194
3195
3196
3197
3198
3199
3200
3201
3202
3203
3204
3205
3206
3207
3208
3209
3210
# File 'app/models/order.rb', line 3187

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


2384
2385
2386
2387
2388
2389
2390
2391
2392
# File 'app/models/order.rb', line 2384

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)


1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
# File 'app/models/order.rb', line 1991

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 subscriber re-queries
by id, so a delivery destroyed later in the same transaction
(purge_empty_quoting_deliveries) is a clean no-op (AppSignal #4958).



5303
5304
5305
5306
5307
5308
5309
5310
5311
5312
5313
5314
5315
5316
5317
5318
# File 'app/models/order.rb', line 5303

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

Returns:

  • (Boolean)


2863
2864
2865
2866
# File 'app/models/order.rb', line 2863

def ok_to_delete?( = nil)
  &.is_admin? ||
    (cancelled? && (line_items.empty? || reference_number.blank?))
end

#online_order?Boolean

Returns:

  • (Boolean)


2115
2116
2117
2118
# File 'app/models/order.rb', line 2115

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)


5501
5502
5503
# File 'app/models/order.rb', line 5501

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:



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

def opportunities
  opps = []
  opps << opportunity
  opps += room_configurations.map(&:opportunity)
  opps.uniq.compact
end

#opportunityOpportunity



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

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)


2097
2098
2099
# File 'app/models/order.rb', line 2097

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)


2106
2107
2108
2109
2110
2111
2112
2113
# File 'app/models/order.rb', line 2106

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?Boolean

Returns:

  • (Boolean)


1636
1637
1638
# File 'app/models/order.rb', line 1636

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


2746
2747
2748
2749
2750
2751
2752
2753
2754
# File 'app/models/order.rb', line 2746

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)


1585
1586
1587
# File 'app/models/order.rb', line 1585

def order_type_title
  ALL_ORDER_TYPES[order_type] || 'Unknown'
end

#partially_shipped?Boolean

Returns:

  • (Boolean)


5533
5534
5535
# File 'app/models/order.rb', line 5533

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)


3745
3746
3747
# File 'app/models/order.rb', line 3745

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:



3322
3323
3324
# File 'app/models/order.rb', line 3322

def party_for_order_confirmation
  contact || customer
end

#pay_on_spiff?Boolean

Returns:

  • (Boolean)


1518
1519
1520
# File 'app/models/order.rb', line 1518

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)


2414
2415
2416
2417
2418
# File 'app/models/order.rb', line 2414

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)


2177
2178
2179
# File 'app/models/order.rb', line 2177

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


1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
# File 'app/models/order.rb', line 1791

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::Relation<Payment>

Returns:

  • (ActiveRecord::Relation<Payment>)

See Also:



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

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:



2196
2197
2198
# File 'app/models/order.rb', line 2196

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:



1514
1515
1516
# File 'app/models/order.rb', line 1514

def payments_with_fraud_report
  payments.non_voided.select { |a| a.fraud_report.present? && !a.payment_approved? }
end

#paypal_invoices_paid?Boolean

Returns:

  • (Boolean)


1912
1913
1914
1915
1916
1917
# File 'app/models/order.rb', line 1912

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)


2936
2937
2938
2939
2940
2941
2942
2943
2944
2945
# File 'app/models/order.rb', line 2936

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



2995
2996
2997
# File 'app/models/order.rb', line 2995

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


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

def po_numbers
  payments.where.not(po_number: nil).distinct.pluck(:po_number)
end

#potential_fraud?Boolean

Returns:

  • (Boolean)


1492
1493
1494
# File 'app/models/order.rb', line 1492

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



1501
1502
1503
1504
1505
1506
1507
# File 'app/models/order.rb', line 1501

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)


2491
2492
2493
2494
2495
2496
# File 'app/models/order.rb', line 2491

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::Relation<PresetJob>

Returns:

See Also:



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

has_many :preset_jobs, inverse_of: :order

#prevent_recalculate_shipping?Boolean

Returns:

  • (Boolean)


3696
3697
3698
# File 'app/models/order.rb', line 3696

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:



2047
2048
2049
# File 'app/models/order.rb', line 2047

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)


2720
2721
2722
# File 'app/models/order.rb', line 2720

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:



1647
1648
1649
1650
1651
1652
# File 'app/models/order.rb', line 1647

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)


1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
# File 'app/models/order.rb', line 1216

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.



3049
3050
3051
3052
3053
3054
3055
3056
# File 'app/models/order.rb', line 3049

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)


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

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)


5467
5468
5469
# File 'app/models/order.rb', line 5467

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



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

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



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

belongs_to :purchase_order, optional: true

#quoteQuote

Returns:

See Also:



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

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:



1830
1831
1832
1833
1834
1835
# File 'app/models/order.rb', line 1830

def quotes
  qs = []
  qs << quote
  qs += room_configurations.map { |rc| rc.quotes.completed_quotes.last }
  qs.uniq.compact
end

#ready_for_pending_payment?Boolean

Returns:

  • (Boolean)


2586
2587
2588
# File 'app/models/order.rb', line 2586

def ready_for_pending_payment?
  ready_for_shipping?
end

#ready_for_service?Boolean

Returns:

  • (Boolean)


2627
2628
2629
# File 'app/models/order.rb', line 2627

def ready_for_service?
  belongs_to_smartservice_group?
end

#ready_for_shipping?Boolean

Returns:

  • (Boolean)


2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
# File 'app/models/order.rb', line 2631

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?Boolean

Returns:

  • (Boolean)


2582
2583
2584
# File 'app/models/order.rb', line 2582

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)


1185
1186
1187
# File 'app/models/order.rb', line 1185

def recipient_name
  shipping_address&.company_name || shipping_address&.person_name || customer&.full_name
end

Activity records corresponding to #related_activities_ids.

Returns:



2142
2143
2144
# File 'app/models/order.rb', line 2142

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


2135
2136
2137
# File 'app/models/order.rb', line 2135

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.



2165
2166
2167
# File 'app/models/order.rb', line 2165

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:

  • rc (RoomConfiguration)

Returns:

  • (Hash)


1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
# File 'app/models/order.rb', line 1845

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?Boolean

Returns:

  • (Boolean)


1288
1289
1290
1291
1292
1293
1294
1295
1296
# File 'app/models/order.rb', line 1288

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?Boolean

Returns:

  • (Boolean)


2224
2225
2226
# File 'app/models/order.rb', line 2224

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.



3037
3038
3039
3040
3041
3042
3043
3044
# File 'app/models/order.rb', line 3037

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?Boolean

Returns:

  • (Boolean)


1536
1537
1538
# File 'app/models/order.rb', line 1536

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



1191
1192
1193
1194
1195
1196
1197
# File 'app/models/order.rb', line 1191

def reviewer
  reviewer_party = contact.presence || customer
  {
    email: reviewer_party&.email,
    name: reviewer_party&.full_name
  }
end

#rmaRma

Returns:

See Also:



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

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.



3648
3649
3650
3651
3652
3653
# File 'app/models/order.rb', line 3648

def rma_cancel
  Order.transaction do
    deliveries.active.each(&:destroy)
    destroy
  end
end

#rma_itemsActiveRecord::Relation<RmaItem>

Returns:

  • (ActiveRecord::Relation<RmaItem>)

See Also:



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

has_many :rma_items, inverse_of: :replacement_order

#rmasActiveRecord::Relation<Rma>

Returns:

  • (ActiveRecord::Relation<Rma>)

See Also:



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

has_many :rmas, foreign_key: :original_order_id

#sales_support_repEmployee

Returns:

See Also:



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

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)



4350
4351
4352
4353
4354
4355
4356
4357
4358
4359
4360
4361
4362
4363
4364
4365
4366
4367
# File 'app/models/order.rb', line 4350

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



2892
2893
2894
2895
# File 'app/models/order.rb', line 2892

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)


5811
5812
5813
5814
5815
5816
5817
5818
5819
# File 'app/models/order.rb', line 5811

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:



1658
1659
1660
1661
1662
# File 'app/models/order.rb', line 1658

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)


4514
4515
4516
4517
4518
4519
4520
4521
4522
4523
4524
4525
4526
4527
# File 'app/models/order.rb', line 4514

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)


3641
3642
3643
# File 'app/models/order.rb', line 3641

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.



3254
3255
3256
# File 'app/models/order.rb', line 3254

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



4257
4258
4259
4260
4261
4262
4263
4264
4265
4266
4267
4268
4269
4270
4271
4272
4273
4274
4275
4276
4277
4278
4279
4280
4281
4282
4283
4284
4285
4286
4287
4288
4289
4290
4291
4292
4293
4294
# File 'app/models/order.rb', line 4257

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})


3289
3290
3291
3292
3293
3294
3295
3296
3297
3298
3299
3300
3301
3302
3303
3304
3305
3306
3307
3308
3309
3310
3311
3312
3313
# File 'app/models/order.rb', line 3289

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.



3278
3279
3280
# File 'app/models/order.rb', line 3278

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.



3266
3267
3268
# File 'app/models/order.rb', line 3266

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})


5562
5563
5564
# File 'app/models/order.rb', line 5562

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.).



3272
3273
3274
# File 'app/models/order.rb', line 3272

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).



3260
3261
3262
# File 'app/models/order.rb', line 3260

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})


5554
5555
5556
# File 'app/models/order.rb', line 5554

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})


5574
5575
5576
5577
5578
5579
5580
5581
5582
5583
5584
# File 'app/models/order.rb', line 5574

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?Boolean

Returns:

  • (Boolean)


2200
2201
2202
# File 'app/models/order.rb', line 2200

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.



5776
5777
5778
# File 'app/models/order.rb', line 5776

def set_currency
  self.currency ||= customer&.store&.currency
end

#set_custom_order_agreementObject

Checks for the presence of custom products in excess of $2k



5360
5361
5362
5363
5364
5365
5366
# File 'app/models/order.rb', line 5360

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)


3598
3599
3600
3601
3602
# File 'app/models/order.rb', line 3598

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.



3584
3585
3586
3587
3588
3589
3590
# File 'app/models/order.rb', line 3584

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.



1614
1615
1616
1617
1618
1619
1620
1621
# File 'app/models/order.rb', line 1614

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.



2915
2916
2917
# File 'app/models/order.rb', line 2915

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



3221
3222
3223
3224
3225
3226
3227
3228
3229
3230
3231
3232
3233
3234
3235
3236
3237
3238
3239
3240
3241
3242
3243
3244
3245
3246
3247
3248
3249
3250
# File 'app/models/order.rb', line 3221

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



3116
3117
3118
3119
3120
3121
3122
3123
3124
3125
3126
3127
3128
3129
3130
3131
3132
3133
3134
3135
3136
3137
3138
3139
3140
3141
3142
3143
# File 'app/models/order.rb', line 3116

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)


5520
5521
5522
# File 'app/models/order.rb', line 5520

def ship_weight
  @ship_weight ||= [0.1, line_items.includes(:item).non_shipping.parents_only.sum(&:total_shipping_weight).round(1)].max
end

#shipmentsActiveRecord::Relation<Shipment>

Returns:

See Also:



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

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

#shipping_account_numberShippingAccountNumber

DELIVERY REFACTOR HERE



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

belongs_to :shipping_account_number, optional: true

#shipping_addressAddress

Returns:

See Also:



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

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)


2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
# File 'app/models/order.rb', line 2565

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?Boolean

Returns:

  • (Boolean)


1948
1949
1950
# File 'app/models/order.rb', line 1948

def shipping_cutoff_advance_order?
  line_items.goods.parents_only.any? { |a| !a.in_stock? }
end

#shipping_cutoff_next_day?Boolean

Returns:

  • (Boolean)


1944
1945
1946
# File 'app/models/order.rb', line 1944

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?Boolean

Returns:

  • (Boolean)


1940
1941
1942
# File 'app/models/order.rb', line 1940

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


1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
# File 'app/models/order.rb', line 1969

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

Warnings about shipping dates that don't block order release
These are displayed to users but don't prevent state transitions
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.



2320
2321
2322
# File 'app/models/order.rb', line 2320

def shipping_date_warnings
  []
end

#should_commit_stock?Boolean

Returns:

  • (Boolean)


1126
1127
1128
# File 'app/models/order.rb', line 1126

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:



2619
2620
2621
# File 'app/models/order.rb', line 2619

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


1275
1276
1277
# File 'app/models/order.rb', line 1275

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:



1284
1285
1286
# File 'app/models/order.rb', line 1284

def sms_messages
  SmsMessage.for_numbers(sms_enabled_numbers)
end

#sold_to_billing_addressAddress

Returns:

See Also:



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

belongs_to :sold_to_billing_address, class_name: 'Address', foreign_key: :sold_to_billing_address, optional: true

#sourceSource

Returns:

See Also:



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

belongs_to :source, optional: true

#spiff_enrollmentSpiffEnrollment



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

belongs_to :spiff_enrollment, optional: true

#spiff_repContact

Returns:

See Also:



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

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)


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

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)


1682
1683
1684
# File 'app/models/order.rb', line 1682

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


1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
# File 'app/models/order.rb', line 1714

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)


2552
2553
2554
# File 'app/models/order.rb', line 2552

def stock_status
  LineItem.inventory_check(self)
end

#stop_for_pre_pack?Boolean

Returns:

  • (Boolean)


5375
5376
5377
5378
5379
5380
5381
5382
5383
5384
5385
# File 'app/models/order.rb', line 5375

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?Boolean

Returns:

  • (Boolean)


5368
5369
5370
5371
5372
5373
# File 'app/models/order.rb', line 5368

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:



1122
1123
1124
# File 'app/models/order.rb', line 1122

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


5013
5014
5015
5016
5017
5018
5019
5020
5021
5022
# File 'app/models/order.rb', line 5013

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.



4912
4913
4914
4915
4916
4917
4918
4919
4920
4921
4922
4923
4924
4925
4926
4927
4928
4929
4930
4931
4932
4933
4934
4935
4936
4937
4938
4939
4940
# File 'app/models/order.rb', line 4912

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:



4986
4987
4988
4989
4990
4991
4992
4993
4994
4995
4996
4997
4998
4999
5000
5001
5002
5003
5004
5005
# File 'app/models/order.rb', line 4986

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:



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

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



5029
5030
5031
5032
5033
5034
5035
5036
5037
5038
5039
5040
5041
5042
5043
5044
5045
5046
5047
5048
5049
5050
5051
5052
5053
5054
5055
5056
# File 'app/models/order.rb', line 5029

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



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
4977
4978
4979
# File 'app/models/order.rb', line 4943

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)


5510
5511
5512
# File 'app/models/order.rb', line 5510

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



1470
1471
1472
# File 'app/models/order.rb', line 1470

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



1475
1476
1477
# File 'app/models/order.rb', line 1475

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)


1450
1451
1452
# File 'app/models/order.rb', line 1450

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)


1459
1460
1461
1462
# File 'app/models/order.rb', line 1459

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::Relation<SupportCase>

Returns:

See Also:



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

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)


2009
2010
2011
2012
2013
2014
# File 'app/models/order.rb', line 2009

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).



3703
3704
3705
# File 'app/models/order.rb', line 3703

def sync_opportunity
  opportunity&.sync_state
end

#technical_support_repTechnicalSupportRep

Returns:

  • (TechnicalSupportRep)

See Also:



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

has_one :technical_support_rep, through: :opportunity

#terms_available?Boolean (protected)

Returns:

  • (Boolean)


5802
5803
5804
# File 'app/models/order.rb', line 5802

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)


3615
3616
3617
# File 'app/models/order.rb', line 3615

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:



3150
3151
3152
# File 'app/models/order.rb', line 3150

def to_liquid
  Liquid::OrderDrop.new self
end

#to_sString

Returns #cart_identifier.

Returns:



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

def to_s
  cart_identifier
end

#to_storeStore

Returns:

See Also:



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

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)


2903
2904
2905
2906
2907
2908
2909
2910
# File 'app/models/order.rb', line 2903

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)


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

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)


2426
2427
2428
2429
2430
2431
2432
2433
2434
# File 'app/models/order.rb', line 2426

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?Boolean

Returns:

  • (Boolean)


5492
5493
5494
# File 'app/models/order.rb', line 5492

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)


2352
2353
2354
2355
2356
2357
# File 'app/models/order.rb', line 2352

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.



2522
2523
2524
2525
2526
# File 'app/models/order.rb', line 2522

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

#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)


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

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.



5769
5770
5771
# File 'app/models/order.rb', line 5769

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



5485
5486
5487
5488
5489
5490
# File 'app/models/order.rb', line 5485

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)


1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
# File 'app/models/order.rb', line 1878

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::Relation<Upload>

Returns:

  • (ActiveRecord::Relation<Upload>)

See Also:



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

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.

Returns:



3713
3714
3715
3716
3717
3718
3719
3720
3721
3722
3723
3724
3725
3726
3727
3728
3729
3730
3731
3732
3733
3734
3735
3736
3737
3738
# File 'app/models/order.rb', line 3713

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

Returns:

See Also:



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

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.



3752
3753
3754
# File 'app/models/order.rb', line 3752

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



4417
4418
4419
4420
4421
4422
4423
4424
4425
4426
4427
4428
4429
4430
4431
4432
4433
4434
4435
4436
4437
4438
4439
4440
4441
4442
4443
4444
4445
4446
4447
4448
4449
4450
4451
# File 'app/models/order.rb', line 4417

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



4081
4082
4083
4084
4085
4086
4087
4088
4089
4090
4091
4092
4093
4094
4095
4096
4097
4098
4099
4100
4101
4102
4103
4104
4105
4106
4107
4108
4109
4110
4111
4112
4113
4114
4115
4116
4117
4118
4119
4120
4121
4122
4123
4124
4125
4126
4127
4128
4129
4130
4131
4132
4133
4134
4135
4136
4137
4138
4139
4140
4141
4142
4143
4144
4145
4146
4147
4148
4149
4150
4151
4152
4153
4154
4155
4156
4157
4158
4159
4160
4161
# File 'app/models/order.rb', line 4081

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)



4563
4564
4565
4566
4567
4568
4569
# File 'app/models/order.rb', line 4563

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



5752
5753
5754
5755
5756
5757
5758
5759
5760
5761
5762
# File 'app/models/order.rb', line 5752

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::Relation<Voucher>

Returns:

  • (ActiveRecord::Relation<Voucher>)

See Also:



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

has_many :vouchers

#walmart_sww_eligible?Boolean

Check if order is eligible for Walmart Ship with Walmart early label purchase

Returns:

  • (Boolean)


4463
4464
4465
4466
4467
4468
4469
4470
4471
4472
4473
4474
# File 'app/models/order.rb', line 4463

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