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 :integer 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
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
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

Classes: BackOrderClientNotification, ContactLookup, CreateVcProcurementOrdersFromCsv, DefaultTrackingEmailExtractor, FollowUpScheduler, FraudDetector, Mover, SendAbandonedCartEmails, Splitter

Constant Summary collapse

SALES_ORDER =
'SO'
MARKETING_ORDER =
'MO'
TECH_ORDER =
'TO'
CREDIT_ORDER =
'CO'
STORE_TRANSFER =
'ST'
SHIPPING_STATES =
%i[awaiting_deliveries processing_deliveries].freeze
SOLD_STATES =
%i[awaiting_deliveries processing_deliveries partially_invoiced invoiced].freeze
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 =
%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 =
%i[pending pending_payment awaiting_deliveries processing_deliveries profit_review crm_back_order].freeze
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 =
%i[cart pending awaiting_return profit_review crm_back_order pending_release_authorization pending_payment in_cr_hold].freeze
ROOM_PICKABLE_STATES =
%i[crm_back_order fraudulent cancelled pending pending_release_authorization pending_payment in_cr_hold].freeze
CLOSED_STATES =
%i[shipped invoiced cancelled fraudulent].freeze
LOCKED_STATES =
%i[cancelled awaiting_deliveries processing_deliveries shipped partially_invoiced invoiced fraudulent].freeze
CAN_REQUEST_PRE_PACK_STATES =
%i[pending pending_payment profit_review in_cr_hold needs_serial_number_reservation].freeze
RESTRICTED_ORDER_TYPES =
{ CREDIT_ORDER => 'Credit Order', STORE_TRANSFER => 'Store Transfer' }.freeze
UNRESTRICTED_ORDER_TYPES =
{ SALES_ORDER => 'Sales Order', MARKETING_ORDER => 'Marketing Order', TECH_ORDER => 'Tech Order' }.freeze
ALL_ORDER_TYPES =
RESTRICTED_ORDER_TYPES.merge(UNRESTRICTED_ORDER_TYPES)
REFERENCE_NUMBER_PATTERN =
Regexp.new("^(#{ALL_ORDER_TYPES.keys.join('|')})(\\d+)$", Regexp::IGNORECASE)
ALL_STATES =
Order.state_machines[:state].states.map(&:name).freeze
NON_CART_STATES =
ALL_STATES - [:cart]
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

Constants included from Models::InventoryCommittable

Models::InventoryCommittable::STATES_WITH_NO_EXPIRATION

Constants included from Models::Auditable

Models::Auditable::ALWAYS_IGNORED

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


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

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.



336
337
338
# File 'app/models/order.rb', line 336

def do_not_detect_shipping
  @do_not_detect_shipping
end

#do_not_set_totalsObject

Returns the value of attribute do_not_set_totals.



336
337
338
# File 'app/models/order.rb', line 336

def do_not_set_totals
  @do_not_set_totals
end

#early_label_purchase_resultObject

Returns the value of attribute early_label_purchase_result.



336
337
338
# File 'app/models/order.rb', line 336

def early_label_purchase_result
  @early_label_purchase_result
end

#from_store_idObject (readonly)



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

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.



336
337
338
# File 'app/models/order.rb', line 336

def full_shipping_address_validation
  @full_shipping_address_validation
end

#is_wwwObject

Returns the value of attribute is_www.



336
337
338
# File 'app/models/order.rb', line 336

def is_www
  @is_www
end

#is_www_ship_by_zipObject

Returns the value of attribute is_www_ship_by_zip.



336
337
338
# File 'app/models/order.rb', line 336

def is_www_ship_by_zip
  @is_www_ship_by_zip
end

#max_discount_overrideObject (readonly)



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

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

#order_typeObject (readonly)



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

validates :order_type, presence: true

#reference_numberObject (readonly)



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

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

#shipping_phoneObject (readonly)

skip this validation for EDI orders, let it ride

Validations (unless => proc { |o| o.is_edi_order? } ):

  • Phone_format


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

validates :shipping_phone, phone_format: true, unless: proc { |o| o.is_edi_order? }

#to_store_idObject (readonly)



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

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

#tracking_emailObject (readonly)



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

validates :tracking_email, email_format: true

#update_shipping_address_with_contactObject

Returns the value of attribute update_shipping_address_with_contact.



336
337
338
# File 'app/models/order.rb', line 336

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:



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

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:



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

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:



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

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

.all_order_types_for_selectObject



1344
1345
1346
# File 'app/models/order.rb', line 1344

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

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



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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

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



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

scope :by_sales_rep_id, ->(sales_rep_id) { joins(:customer).where('(parties.primary_sales_rep_id = :sales_rep_id) OR (parties.secondary_sales_rep_id = :sales_rep_id) OR (parties.local_sales_rep_id = :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:



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

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:



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

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:



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

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

.cartsActiveRecord::Relation<Order>

A relation of Orders that are carts. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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_selectObject



1336
1337
1338
# File 'app/models/order.rb', line 1336

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:



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

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:



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

scope :draft_spiff, -> { where("spiff_enrollment_id is not null and spiff_state = 'draft'") }

.edi_ordersActiveRecord::Relation<Order>

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

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



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

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:



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

scope :future_release, -> { where("(orders.future_release_date IS NOT NULL) AND orders.order_type <> 'CO' AND (orders.state NOT IN(#{Order::CLOSED_STATES.map { |s| "'#{s}'" }.join(',')}))") }

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



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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

.lockedActiveRecord::Relation<Order>

A relation of Orders that are locked. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



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

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:



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

scope :lookup, ->(q) { where('orders.reference_number = :term', { term: q }) }

.mo_onlyActiveRecord::Relation<Order>

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

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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

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



1348
1349
1350
1351
1352
1353
1354
# File 'app/models/order.rb', line 1348

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



4627
4628
4629
# File 'app/models/order.rb', line 4627

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

.order_type_from_quote(quote) ⇒ Object



4623
4624
4625
# File 'app/models/order.rb', line 4623

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:



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

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:



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

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:



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

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:



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

scope :pending_payment_and_unpaid_invoices, -> { Order.where(id: (joins(:invoices).where("invoices.state = 'unpaid'") + pending_payment).pluck(:id)) }

.po_number_barcode(po_number:, file_path: nil) ⇒ Object



2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
# File 'app/models/order.rb', line 2298

def self.po_number_barcode(po_number:, file_path: nil)
  return unless po_number.present?

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



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

scope :positive_value, -> { where('line_total > 0') }

.profit_reviewActiveRecord::Relation<Order>

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

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



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

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:



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

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:



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

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_selectObject



2607
2608
2609
2610
2611
2612
2613
2614
# File 'app/models/order.rb', line 2607

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:



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

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:



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

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:



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

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

.selectable_order_typesObject



1340
1341
1342
# File 'app/models/order.rb', line 1340

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:



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

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:



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

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:



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

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

.states_for_selectObject



1360
1361
1362
# File 'app/models/order.rb', line 1360

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:



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

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:



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

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:



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

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:



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

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:



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

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:



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

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

Instance Method Details

#accounting_hold_order?Boolean

Returns:

  • (Boolean)


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

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

#activitiesActiveRecord::Relation<Activity>

Returns:

See Also:



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

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

#add_customer_to_campaignObject



1364
1365
1366
1367
1368
1369
1370
# File 'app/models/order.rb', line 1364

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



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

def add_item(sku, qty)
  return if editing_locked?

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

#add_multiple_items(sku_array = []) ⇒ Object

sku array being ['UDG4-4999', qty: 1, 'SS-01', qty: 2]



2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
# File 'app/models/order.rb', line 2174

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
    hsh[:room_configuration_id]
    ci = catalog.catalog_items.public_catalog_items.by_skus(sku).first

    raise Order::ItemNotFound.new("#{sku} not found") unless ci.present?

    li = add_line_item(catalog_item_id: ci.id, quantity: qty, room_configuration_id: nil, do_not_autosave: true)
    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_costObject



4516
4517
4518
# File 'app/models/order.rb', line 4516

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)


1019
1020
1021
# File 'app/models/order.rb', line 1019

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)


1978
1979
1980
# File 'app/models/order.rb', line 1978

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

#all_funds_available?(ignore_cod = false) ⇒ Boolean

Returns:

  • (Boolean)


1974
1975
1976
# File 'app/models/order.rb', line 1974

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

#all_funds_not_available_and_shippable?Boolean (protected)

Returns:

  • (Boolean)


4601
4602
4603
# File 'app/models/order.rb', line 4601

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

#all_items_in_stockObject



1992
1993
1994
# File 'app/models/order.rb', line 1992

def all_items_in_stock
  stock_status == :ok
end

#all_participant_idsObject



1124
1125
1126
1127
1128
1129
1130
# File 'app/models/order.rb', line 1124

def all_participant_ids
  party_ids = []
  party_ids += opportunity.all_participants.pluck(:id) if opportunity
  party_ids << customer_id
  party_ids << contact_id
  party_ids.compact.uniq
end

#all_payments_are_valid?Boolean

Returns:

  • (Boolean)


1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
# File 'app/models/order.rb', line 1023

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)


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

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

#all_support_casesObject



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

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

#all_uploadsObject



4540
4541
4542
4543
4544
4545
4546
4547
4548
4549
4550
# File 'app/models/order.rb', line 4540

def all_uploads
  ret_uploads = if delivery_ids.present?
                  shipment_ids = Shipment.where(delivery_id: delivery_ids).pluck(:id)
                  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)


4281
4282
4283
# File 'app/models/order.rb', line 4281

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)


2336
2337
2338
# File 'app/models/order.rb', line 2336

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)


3508
3509
3510
3511
3512
3513
3514
3515
3516
3517
3518
3519
# File 'app/models/order.rb', line 3508

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)


1859
1860
1861
# File 'app/models/order.rb', line 1859

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

#any_rooms_not_orderable?Boolean

Returns:

  • (Boolean)


1687
1688
1689
1690
1691
# File 'app/models/order.rb', line 1687

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_smartinstallObject



2325
2326
2327
2328
2329
2330
# File 'app/models/order.rb', line 2325

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

  false
end

#apply_tier2_pricing?Boolean

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

Returns:

  • (Boolean)


2412
2413
2414
# File 'app/models/order.rb', line 2412

def apply_tier2_pricing?
  is_sales_order?
end

#attention_nameObject



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

def attention_name
  attention_name_override || inherited_attention_name
end

#attention_name=(value) ⇒ Object



2587
2588
2589
2590
2591
# File 'app/models/order.rb', line 2587

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

  self.attention_name_override = value
end

#auto_reserve_serial_numbersObject



1326
1327
1328
1329
1330
# File 'app/models/order.rb', line 1326

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)


4308
4309
4310
# File 'app/models/order.rb', line 4308

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

#balance(ignore_cod = false) ⇒ Object



1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
# File 'app/models/order.rb', line 1926

def balance(ignore_cod = false)
  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) || BigDecimal('0.0')
    bal -= if dq_auth >= dq_total
             dq_total
           else
             dq_auth
           end
  end
  bal < 0 ? BigDecimal('0.0') : bal
end

#belongs_to_smartservice_group?Boolean

Returns:

  • (Boolean)


2025
2026
2027
# File 'app/models/order.rb', line 2025

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:



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

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

#billing_entityObject

Alias for Customer#billing_entity

Returns:

  • (Object)

    Customer#billing_entity

See Also:



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

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

#build_activityObject



1416
1417
1418
# File 'app/models/order.rb', line 1416

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)



4240
4241
4242
4243
4244
4245
4246
4247
4248
4249
4250
4251
4252
4253
4254
4255
4256
4257
# File 'app/models/order.rb', line 4240

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)


4264
4265
4266
4267
4268
4269
4270
4271
4272
4273
4274
4275
4276
4277
# File 'app/models/order.rb', line 4264

def build_early_label_tracking_url(carrier, tracking_number)
  return nil unless tracking_number.present?

  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



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

belongs_to :buying_group, optional: true

#calculate_actual_shipping_costObject



4512
4513
4514
# File 'app/models/order.rb', line 4512

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)


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

def can_auto_reserve_serial_numbers?
  order_type != 'CO'
end

#can_be_cancelled?Boolean

Returns:

  • (Boolean)


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

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)


1571
1572
1573
# File 'app/models/order.rb', line 1571

def can_be_returned?
  is_regular_order?
end

#can_cr_hold?(current_user = nil) ⇒ Boolean

Returns:

  • (Boolean)


1713
1714
1715
# File 'app/models/order.rb', line 1713

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)


1059
1060
1061
# File 'app/models/order.rb', line 1059

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

#cancel_deliveries_pre_packObject



4349
4350
4351
4352
4353
# File 'app/models/order.rb', line 4349

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

#cancel_or_destroyObject



2797
2798
2799
# File 'app/models/order.rb', line 2797

def cancel_or_destroy
  cart? ? destroy : cancel
end

#cancelable?Boolean

Returns:

  • (Boolean)


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

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

#cannot_delete_reason(account = nil) ⇒ Object



2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
# File 'app/models/order.rb', line 2249

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? { |pp| pp.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_identifierObject



2897
2898
2899
# File 'app/models/order.rb', line 2897

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

#cart_or_in_shipping_estimate?Boolean

Returns:

  • (Boolean)


2901
2902
2903
# File 'app/models/order.rb', line 2901

def cart_or_in_shipping_estimate?
  cart? || in_shipping_estimate?
end

#catalogObject

Alias for Customer#catalog

Returns:

  • (Object)

    Customer#catalog

See Also:



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

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

#check_payments_statusObject



1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
# File 'app/models/order.rb', line 1227

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 unless payments.present?
  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.
  else
    if can_pending_payment? && !deliveries.any?(&:pre_pack?) && !deliveries.any?(&:pending_manifest_completion?)
      InternalMailer.order_with_insuficient_payment(self).deliver_later unless pending_payment?
      pending_payment!
    end
  end
end

#check_sales_repObject (protected)



4591
4592
4593
4594
4595
4596
4597
4598
4599
# File 'app/models/order.rb', line 4591

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)


2379
2380
2381
# File 'app/models/order.rb', line 2379

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

#clear_shipped_dateObject



2279
2280
2281
# File 'app/models/order.rb', line 2279

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

#closed_state?Boolean

Returns:

  • (Boolean)


1709
1710
1711
# File 'app/models/order.rb', line 1709

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

#communicationsActiveRecord::Relation<Communication>

Returns:

See Also:



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

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

#companyObject

Alias for Customer#company

Returns:

  • (Object)

    Customer#company

See Also:



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

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



1079
1080
1081
# File 'app/models/order.rb', line 1079

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.



1085
1086
1087
# File 'app/models/order.rb', line 1085

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

#complete?Boolean

Returns:

  • (Boolean)


1884
1885
1886
# File 'app/models/order.rb', line 1884

def complete?
  invoiced?
end

#completed_regular_deliveriesObject



4461
4462
4463
# File 'app/models/order.rb', line 4461

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

#completely_shipped?Boolean

Returns:

  • (Boolean)


4469
4470
4471
# File 'app/models/order.rb', line 4469

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

#consolidate_revenueObject



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

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:



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

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



1036
1037
1038
1039
1040
# File 'app/models/order.rb', line 1036

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



1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
# File 'app/models/order.rb', line 1044

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_selectObject



1055
1056
1057
# File 'app/models/order.rb', line 1055

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

#contact_pointContactPoint



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

belongs_to :contact_point, optional: true

#copy_customer_repsObject



1118
1119
1120
1121
1122
# File 'app/models/order.rb', line 1118

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



1108
1109
1110
1111
1112
1113
1114
1115
1116
# File 'app/models/order.rb', line 1108

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.



1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
# File 'app/models/order.rb', line 1169

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

  sku_array = order.line_items.non_shipping.parents_only.map { |li| { sku: li.sku, qty: li.quantity } }

  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



1381
1382
1383
1384
1385
1386
1387
# File 'app/models/order.rb', line 1381

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

#countryObject



1878
1879
1880
1881
1882
# File 'app/models/order.rb', line 1878

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

#create_credit_memoObject



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

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

#create_smartfix_ticketObject



2453
2454
2455
# File 'app/models/order.rb', line 2453

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

#create_smartguide_ticketObject



2457
2458
2459
2460
2461
2462
2463
# File 'app/models/order.rb', line 2457

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



2449
2450
2451
# File 'app/models/order.rb', line 2449

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

#credit_memoCreditMemo

Returns:

See Also:



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

has_one :credit_memo, foreign_key: :credit_order_id

#credit_memosActiveRecord::Relation<CreditMemo>

Returns:

See Also:



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

has_many :credit_memos, foreign_key: :original_order_id


2229
2230
2231
# File 'app/models/order.rb', line 2229

def crm_link
  UrlHelper.instance.order_path(self)
end

#currency_symbolObject



1753
1754
1755
# File 'app/models/order.rb', line 1753

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

#custom_shipping_labelsObject

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



2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
# File 'app/models/order.rb', line 2806

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:



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

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

#customer_qualifies_for_free_online_shipping?Boolean

Returns:

  • (Boolean)


1588
1589
1590
# File 'app/models/order.rb', line 1588

def customer_qualifies_for_free_online_shipping?
  discounts.free_online_shipping.present?
end

#deep_dupObject



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

def deep_dup
  deep_clone(
    include: [: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_emailsObject



2152
2153
2154
2155
2156
2157
2158
2159
# File 'app/models/order.rb', line 2152

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.first&.detail if billing_customer.profile && (billing_customer.profile.homeowner? || billing_customer.profile.direct_pro?)

  emails.compact.uniq.sort
end

#default_credit_card_vaultCreditCardVault



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

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

#deferred_payments_captured_and_ready_for_shipping?Boolean

Returns:

  • (Boolean)


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

def deferred_payments_captured_and_ready_for_shipping?
  paypal_invoices_paid? && ready_for_shipping?
end

#deletable?Boolean

Returns:

  • (Boolean)


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

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

#deliveriesActiveRecord::Relation<Delivery>

Returns:

See Also:



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

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

#delivery_activitiesActiveRecord::Relation<Activity>

Returns:

See Also:



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

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

#direct_shipmentsActiveRecord::Relation<Shipment>

Returns:

See Also:



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

has_many :direct_shipments, class_name: 'Shipment'

#doesnt_already_has_smartinstall?Boolean

Returns:

  • (Boolean)


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

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



3924
3925
3926
3927
3928
3929
3930
3931
3932
3933
3934
3935
3936
3937
3938
3939
# File 'app/models/order.rb', line 3924

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

  if is_amazon
    return store_amazon_label_pdf_atomic(label_result, tracking_number, marketplace)
  end

  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:



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

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



3402
3403
3404
3405
3406
3407
3408
3409
3410
3411
3412
3413
3414
3415
3416
3417
3418
3419
3420
3421
3422
3423
3424
3425
3426
3427
3428
3429
3430
3431
# File 'app/models/order.rb', line 3402

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

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



3604
3605
3606
3607
3608
3609
3610
3611
3612
3613
3614
3615
3616
3617
3618
3619
3620
3621
3622
3623
3624
3625
3626
3627
3628
3629
# File 'app/models/order.rb', line 3604

def early_label_flash_message
  return nil unless early_label_purchase_result.present?

  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)


3523
3524
3525
3526
3527
3528
3529
3530
3531
3532
3533
3534
# File 'app/models/order.rb', line 3523

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)


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

def early_label_purchased_recently?
  return false unless early_label_purchased_at.present?

  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 }



3863
3864
3865
3866
3867
3868
3869
3870
3871
3872
3873
3874
3875
3876
3877
3878
3879
3880
3881
3882
3883
3884
3885
3886
3887
3888
3889
3890
3891
3892
3893
3894
3895
3896
3897
3898
3899
3900
3901
3902
3903
3904
3905
3906
3907
3908
3909
3910
3911
# File 'app/models/order.rb', line 3863

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:



3585
3586
3587
# File 'app/models/order.rb', line 3585

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

#echecks_requiring_authorizationObject



1733
1734
1735
# File 'app/models/order.rb', line 1733

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

#edi_cancellation_reason_descriptionObject



2616
2617
2618
2619
2620
2621
2622
2623
2624
# File 'app/models/order.rb', line 2616

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.detect { |arr| arr.last == cancellation_reason }&.first || cancellation_reason).to_s.humanize.downcase}"
  end
end

#edi_cancellation_reasonsObject



2626
2627
2628
2629
2630
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
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
# File 'app/models/order.rb', line 2626

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:



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

has_many :edi_communication_logs, through: :edi_documents

#edi_documentsActiveRecord::Relation<EdiDocument>

Returns:

See Also:



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

has_many :edi_documents, dependent: :destroy

#edi_force_price_matchObject



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

def edi_force_price_match
  errs = []
  line_items.parents_only.where.not(edi_unit_cost: nil).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_orchestratorObject



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

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)


1617
1618
1619
1620
1621
1622
1623
1624
1625
# File 'app/models/order.rb', line 1617

def edi_price_matcheable?
  return false unless is_edi_order?

  target_lines = line_items.goods.parents_only
  return false unless target_lines.present?
  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)


1705
1706
1707
# File 'app/models/order.rb', line 1705

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

#effective_date_for_couponObject



1296
1297
1298
1299
1300
# File 'app/models/order.rb', line 1296

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_confirmationObject



2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
# File 'app/models/order.rb', line 2571

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_emailObject



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

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

#empty?Boolean

Returns:

  • (Boolean)


1745
1746
1747
# File 'app/models/order.rb', line 1745

def empty?
  line_items.non_shipping.empty?
end

#encrypted_idObject



4434
4435
4436
# File 'app/models/order.rb', line 4434

def encrypted_id
  Encryption.encrypt_string(id.to_s)
end

#errors_with_deliveries_errorsObject



4520
4521
4522
# File 'app/models/order.rb', line 4520

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_itemsObject



4383
4384
4385
4386
4387
4388
4389
4390
4391
# File 'app/models/order.rb', line 4383

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.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.compact.max_by { |d| d }
end

#find_gbraidObject



4560
4561
4562
# File 'app/models/order.rb', line 4560

def find_gbraid
  visit&.gbraid || quote&.visit&.gbraid || opportunity&.visit&.gbraid || customer.visit&.gbraid || customer.visits.where.not(gbraid: nil).last&.gbraid
end

#find_gclidObject



4552
4553
4554
# File 'app/models/order.rb', line 4552

def find_gclid
  visit&.gclid || quote&.visit&.gclid || opportunity&.visit&.gclid || customer.gclid || customer.visit&.gclid || customer.visits.where.not(gclid: nil).last&.gclid
end

#find_wbraidObject



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

def find_wbraid
  visit&.wbraid || quote&.visit&.wbraid || opportunity&.visit&.wbraid || customer.visit&.wbraid || customer.visits.where.not(wbraid: nil).last&.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



4089
4090
4091
4092
4093
4094
4095
4096
4097
4098
4099
4100
4101
4102
4103
4104
4105
4106
4107
4108
# File 'app/models/order.rb', line 4089

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.



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
4162
4163
# File 'app/models/order.rb', line 4112

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



4166
4167
4168
4169
4170
4171
4172
4173
4174
4175
4176
4177
4178
4179
4180
4181
4182
4183
4184
4185
4186
4187
4188
4189
4190
4191
4192
4193
4194
4195
4196
4197
4198
4199
4200
4201
4202
4203
4204
4205
4206
4207
4208
4209
4210
4211
4212
4213
4214
4215
4216
4217
4218
4219
4220
4221
4222
4223
4224
4225
4226
4227
4228
4229
# File 'app/models/order.rb', line 4166

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_numberObject



1951
1952
1953
# File 'app/models/order.rb', line 1951

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

#first_tracking_emailObject



4473
4474
4475
# File 'app/models/order.rb', line 4473

def first_tracking_email
  tracking_email&.first
end

#fix_future_release_dateObject



4524
4525
4526
4527
4528
4529
4530
4531
4532
4533
4534
4535
4536
4537
4538
# File 'app/models/order.rb', line 4524

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_numberObject



2294
2295
2296
# File 'app/models/order.rb', line 2294

def formatted_po_number
  po_number(include_po_prefix: include_po_prefix?)
end

#from_storeStore

Returns:

See Also:



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

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

#fully_funded_by_advance_replacement?Boolean

Returns:

  • (Boolean)


1966
1967
1968
# File 'app/models/order.rb', line 1966

def fully_funded_by_advance_replacement?
  (deliveries.count > 0) && deliveries.all? { |dq| (dq.payments.count > 0) && dq.payments.all? { |pp| pp.category == Payment::ADV_REPL } }
end

#funded_by_advance_replacement?Boolean

Returns:

  • (Boolean)


1970
1971
1972
# File 'app/models/order.rb', line 1970

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

#funded_by_cod?Boolean

Returns:

  • (Boolean)


1962
1963
1964
# File 'app/models/order.rb', line 1962

def funded_by_cod?
  deliveries.all?(&:funded_by_cod?) && (deliveries.count > 0)
end

#funds_available_and_ready_for_warehouse?Boolean (protected)

Returns:

  • (Boolean)


4605
4606
4607
# File 'app/models/order.rb', line 4605

def funds_available_and_ready_for_warehouse?
  all_funds_available? && ready_for_warehouse?
end

#generate_spiff_training_activityObject



1465
1466
1467
# File 'app/models/order.rb', line 1465

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_timeObject



4413
4414
4415
4416
# File 'app/models/order.rb', line 4413

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_timeObject



4393
4394
4395
4396
4397
4398
4399
4400
4401
4402
4403
4404
4405
4406
4407
4408
4409
4410
4411
# File 'app/models/order.rb', line 4393

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)


1223
1224
1225
# File 'app/models/order.rb', line 1223

def has_authorized_payment?
  payments.all_authorized.any?
end

#has_committed_serial_number_reservations?Boolean

Returns:

  • (Boolean)


2140
2141
2142
# File 'app/models/order.rb', line 2140

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)


1853
1854
1855
# File 'app/models/order.rb', line 1853

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)


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

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

#has_incomplete_reservations?Boolean

Returns:

  • (Boolean)


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

def has_incomplete_reservations?
  has_unreserved_line_items? || has_committed_serial_number_reservations?
end

#has_selected_heated_items?Boolean

Returns:

  • (Boolean)


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

def has_selected_heated_items?
  smartinstall_data.present?
end

#has_shipping_method?Boolean

Returns:

  • (Boolean)


1863
1864
1865
# File 'app/models/order.rb', line 1863

def has_shipping_method?
  chosen_shipping_method.present?
end

#has_unreserved_line_items?Boolean

Returns:

  • (Boolean)


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

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)


2144
2145
2146
# File 'app/models/order.rb', line 2144

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:



3564
3565
3566
3567
3568
3569
3570
3571
3572
3573
3574
3575
3576
3577
3578
3579
3580
# File 'app/models/order.rb', line 3564

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_reasonsObject



1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
# File 'app/models/order.rb', line 1765

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
    unless cancellation_reason.present?
      if !edi_original_ship_code.to_s.upcase.include?('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.present? && !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 = ''
      if customer&.is_houzz?
        link_snippet = ", get it manually here: <a href='https://www.houzz.com//printBuyerOrder//orderId=#{edi_po_number}' target=_new><i text='https://www.houzz.com//printBuyerOrder//orderId=#{edi_po_number}' class='fas fa-arrow-up-right-from-square'></i> https://www.houzz.com//printBuyerOrder//orderId=#{edi_po_number}</a>"
      end
      if customer&.is_amazon_seller_central?
        link_snippet = ", get it manually here: <a href='https://sellercentral.amazon.com/orders/packing-slip?orderId=#{edi_po_number}' target=_new><i text='https://sellercentral.amazon.com/orders/packing-slip?orderId=#{edi_po_number}' class='fas fa-arrow-up-right-from-square'></i> https://sellercentral.amazon.com/orders/packing-slip?orderId=#{edi_po_number}</a>"
      end
      if customer&.is_amazon_vendor_central?
        link_snippet = ", get it manually here: <a href='https://vendorcentral.amazon.com/hz/vendor/members/df/orders?id=#{edi_po_number}' target=_new><i text='https://vendorcentral.amazon.com/hz/vendor/members/df/orders?id=#{edi_po_number}' class='fas fa-arrow-up-right-from-square'></i> https://vendorcentral.amazon.com/hz/vendor/members/df/orders?id=#{edi_po_number}</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("notes ILIKE '%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)


1757
1758
1759
# File 'app/models/order.rb', line 1757

def hold_orders?
  hold_order_reasons.present?
end

#human_state_nameObject



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

def human_state_name
  if deliveries.any?(&:service_ready_to_fulfill?) and 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:



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

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)


2029
2030
2031
# File 'app/models/order.rb', line 2029

def includes_schedulable_service?
  belongs_to_smartservice_group?
end

#inherited_attention_nameObject



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

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

#installation_country_isoObject



2845
2846
2847
# File 'app/models/order.rb', line 2845

def installation_country_iso
  opportunity.presence&.installation_country_iso
end

#installation_country_iso3Object



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

def installation_country_iso3
  opportunity.presence&.installation_country_iso3
end

#installation_is_within_range?Boolean

Returns:

  • (Boolean)


2344
2345
2346
2347
2348
2349
2350
# File 'app/models/order.rb', line 2344

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

  false
end

#installation_postal_codeObject



2833
2834
2835
# File 'app/models/order.rb', line 2833

def installation_postal_code
  opportunity.presence&.installation_postal_code
end

#installation_state_codeObject



2837
2838
2839
# File 'app/models/order.rb', line 2837

def installation_state_code
  opportunity.presence&.installation_state_code
end

#invoice_balanceObject



1943
1944
1945
1946
1947
1948
1949
# File 'app/models/order.rb', line 1943

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:



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

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

#invoiced_primary_sales_repParty

Returns:

See Also:



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

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

#invoiced_secondary_sales_repParty

Returns:

See Also:



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

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:



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

has_many :invoices

#is_amazon_com?Object

Alias for Customer#is_amazon_com?

Returns:

  • (Object)

    Customer#is_amazon_com?

See Also:



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

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:



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

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:



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

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:



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

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)


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

def is_crm?
  order_reception_type == 'CRM'
end

#is_edi_order?Boolean

Returns:

  • (Boolean)


1604
1605
1606
# File 'app/models/order.rb', line 1604

def is_edi_order?
  edi_transaction_id.present?
end

#is_fba?Boolean

Returns:

  • (Boolean)


1639
1640
1641
# File 'app/models/order.rb', line 1639

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

#is_from_myprojects?Boolean

Returns:

  • (Boolean)


1874
1875
1876
# File 'app/models/order.rb', line 1874

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:



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

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:



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

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:



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

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)


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

def is_marketing_order?
  order_type == MARKETING_ORDER
end

#is_online?Boolean

Returns:

  • (Boolean)


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

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:



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

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:



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

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)


1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
# File 'app/models/order.rb', line 1577

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)


2403
2404
2405
# File 'app/models/order.rb', line 2403

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

#is_remote_service?Boolean

Returns:

  • (Boolean)


2053
2054
2055
# File 'app/models/order.rb', line 2053

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

#is_rma_replacement?Boolean

Returns:

  • (Boolean)


2387
2388
2389
# File 'app/models/order.rb', line 2387

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

#is_rma_return?Boolean

Returns:

  • (Boolean)


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

def is_rma_return?
  order_type == CREDIT_ORDER
end

#is_sales_order?Boolean

Returns:

  • (Boolean)


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

def is_sales_order?
  order_type == SALES_ORDER
end

#is_smartfit_service?Boolean

Returns:

  • (Boolean)


2033
2034
2035
# File 'app/models/order.rb', line 2033

def is_smartfit_service?
  line_items.smartfit_items.any?
end

#is_smartfix_service?Boolean

Returns:

  • (Boolean)


2041
2042
2043
# File 'app/models/order.rb', line 2041

def is_smartfix_service?
  line_items.smartfix_items.any?
end

#is_smartguide_service?Boolean

Returns:

  • (Boolean)


2045
2046
2047
# File 'app/models/order.rb', line 2045

def is_smartguide_service?
  line_items.smartguide_items.any?
end

#is_smartinstall_service?Boolean

Returns:

  • (Boolean)


2037
2038
2039
# File 'app/models/order.rb', line 2037

def is_smartinstall_service?
  line_items.smartinstall_items.any?
end

#is_store_transfer?Boolean

Returns:

  • (Boolean)


2383
2384
2385
# File 'app/models/order.rb', line 2383

def is_store_transfer?
  order_type == STORE_TRANSFER
end

#is_subject_to_minimum_qty_rules?Boolean

Returns:

  • (Boolean)


2407
2408
2409
# File 'app/models/order.rb', line 2407

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

#is_tech_order?Boolean

Returns:

  • (Boolean)


2395
2396
2397
# File 'app/models/order.rb', line 2395

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:



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

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)


2128
2129
2130
# File 'app/models/order.rb', line 2128

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:



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

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_nameObject



1552
1553
1554
1555
1556
1557
1558
# File 'app/models/order.rb', line 1552

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:



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

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

#local_sales_repObject



1406
1407
1408
1409
1410
# File 'app/models/order.rb', line 1406

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



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

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.present? ? details.to_json : ''}")
end

#mark_serial_coupons_as_usedObject



4426
4427
4428
# File 'app/models/order.rb', line 4426

def mark_serial_coupons_as_used
  discounts.includes(:coupon_serial_number).joins(:coupon_serial_number).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)


3485
3486
3487
# File 'app/models/order.rb', line 3485

def marketplace_early_label_eligible?
  walmart_sww_eligible? || amazon_buy_shipping_eligible?
end

#material_alertsActiveRecord::Relation<MaterialAlert>

Returns:

See Also:



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

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.



3344
3345
3346
3347
3348
3349
3350
3351
3352
3353
3354
3355
3356
3357
3358
3359
3360
3361
3362
3363
3364
3365
3366
3367
3368
3369
3370
3371
3372
3373
3374
3375
# File 'app/models/order.rb', line 3344

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:



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

has_many :messaging_logs, dependent: :destroy, as: :resource

#minimum_order_quantity_violationsObject



1849
1850
1851
# File 'app/models/order.rb', line 1849

def minimum_order_quantity_violations
  Item::Materials::Checks::Moq.new.process(container: self).alerts
end

#move_deliveries_from_quoting!Object



4285
4286
4287
4288
4289
4290
4291
4292
4293
4294
4295
4296
4297
4298
4299
4300
4301
# File 'app/models/order.rb', line 4285

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



4325
4326
4327
4328
4329
4330
4331
4332
4333
4334
4335
4336
4337
4338
4339
4340
4341
4342
4343
4344
4345
4346
4347
# File 'app/models/order.rb', line 4325

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



4303
4304
4305
4306
# File 'app/models/order.rb', line 4303

def move_service_case_from_pending_service_payment
  service_case = support_cases&.services&.pending_service_payment&.first
  service_case.presence&.case_open
end

#nameObject



2213
2214
2215
2216
2217
2218
2219
2220
2221
# File 'app/models/order.rb', line 2213

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



4506
4507
4508
4509
4510
# File 'app/models/order.rb', line 4506

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_issuesObject



4418
4419
4420
4421
4422
4423
4424
# File 'app/models/order.rb', line 4418

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)


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

def needs_spiff_training?
  spiff_enrollment.present? && spiff_enrollment.spiff.is_active? && (customer.orders.count == 1) && customer.activities.where(activity_type_id: ActivityTypeConstants::SPIFFACTTRAIN).empty?
end

#new_customer_online_orderObject



1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
# File 'app/models/order.rb', line 1643

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_orderObject



1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
# File 'app/models/order.rb', line 1656

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



2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
# File 'app/models/order.rb', line 2465

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_datesObject



1888
1889
1890
1891
1892
1893
1894
1895
1896
# File 'app/models/order.rb', line 1888

def next_six_months_with_end_dates
  today = Date.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

#notify_pre_pack_cancellationObject



4312
4313
4314
4315
4316
4317
4318
4319
4320
4321
4322
4323
# File 'app/models/order.rb', line 4312

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
  AfterCommitEverywhere.after_commit do
    cancelled_by = Party.find_by(id: cancelled_by_id)
    Delivery.where(id: pre_pack_delivery_ids).find_each do |delivery|
      delivery.send_delivery_pre_pack_cancelled_notification(cancelled_by: cancelled_by)
    end
  end
end

#ok_to_delete?(account = nil) ⇒ Boolean

Returns:

  • (Boolean)


2244
2245
2246
2247
# File 'app/models/order.rb', line 2244

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

#online_order?Boolean

Returns:

  • (Boolean)


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

def online_order?
  (res = creator.blank? || !creator.is_employee?) && customer.
  res
end

#open_activities_counterObject



4449
4450
4451
# File 'app/models/order.rb', line 4449

def open_activities_counter
  activities.open_activities.visible_by_default.size
end

#opportunitiesObject



1498
1499
1500
1501
1502
1503
# File 'app/models/order.rb', line 1498

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

#opportunityOpportunity



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

belongs_to :opportunity, inverse_of: :orders, optional: true

#opportunity_nameObject



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

def opportunity_name
  opportunity.try(:name)
end

#opportunity_name=(val) ⇒ Object



1673
1674
1675
1676
1677
1678
1679
1680
# File 'app/models/order.rb', line 1673

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)


1389
1390
1391
# File 'app/models/order.rb', line 1389

def order_closed?
  invoiced? || cancelled? || fraudulent? || printed?
end

#order_emailsObject



2161
2162
2163
2164
2165
2166
2167
2168
2169
# File 'app/models/order.rb', line 2161

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_titleObject



1356
1357
1358
# File 'app/models/order.rb', line 1356

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

#partially_shipped?Boolean

Returns:

  • (Boolean)


4465
4466
4467
# File 'app/models/order.rb', line 4465

def partially_shipped?
  deliveries.active.any?(&:completed_regular_delivery?)
end

#participants_options_for_selectObject



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

def participants_options_for_select
  opportunity&.opportunity_participants&.map { |scp| [scp.party.full_name, scp.party_id] }
end

#party_for_order_confirmationObject



2567
2568
2569
# File 'app/models/order.rb', line 2567

def party_for_order_confirmation
  contact || customer
end

#pay_on_spiff?Boolean

Returns:

  • (Boolean)


1318
1319
1320
# File 'app/models/order.rb', line 1318

def pay_on_spiff?
  spiff_enrollment.present? && (spiff_enrollment.spiff.eligible_reward(self) > 0)
end

#payment_methodObject



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

def payment_method
  payments.all_authorized.first.category
rescue StandardError
  nil
end

#payment_method_requires_authorization?Boolean

Returns:

  • (Boolean)


1729
1730
1731
# File 'app/models/order.rb', line 1729

def payment_method_requires_authorization?
  payments_requiring_authorization.any?
end

#payment_options(source) ⇒ Object



1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
# File 'app/models/order.rb', line 1477

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)
    if includes_schedulable_service?
      all_payment_options = [Payment::CREDIT_CARD]
    else
      all_payment_options = [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:



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

has_many :payments, dependent: :nullify, inverse_of: :order

#payments_requiring_authorizationObject



1737
1738
1739
# File 'app/models/order.rb', line 1737

def payments_requiring_authorization
  payments.all_authorized.select { |pp| !pp.payment_approved? && (pp.authorization_review[:required] == true) }
end

#payments_with_fraud_reportObject



1314
1315
1316
# File 'app/models/order.rb', line 1314

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

#paypal_invoices_paid?Boolean

Returns:

  • (Boolean)


1564
1565
1566
1567
1568
1569
# File 'app/models/order.rb', line 1564

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



2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
# File 'app/models/order.rb', line 2283

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



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

def po_number_barcode(file_path: nil)
  self.class.po_number_barcode(po_number:, file_path:)
end

#po_numbersObject



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

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

#potential_fraud?Boolean

Returns:

  • (Boolean)


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

def potential_fraud?
  payments.all_authorized.any? { |pp| pp.try(:fraud_report).try(:potential_fraud?) && !pp.payment_approved? }
end

#potential_fraud_reasonsObject



1306
1307
1308
1309
1310
1311
1312
# File 'app/models/order.rb', line 1306

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_availableObject



1955
1956
1957
1958
1959
1960
# File 'app/models/order.rb', line 1955

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:



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

has_many :preset_jobs, inverse_of: :order

#prevent_recalculate_shipping?Boolean

Returns:

  • (Boolean)


2853
2854
2855
# File 'app/models/order.rb', line 2853

def prevent_recalculate_shipping?
  is_credit_order? || SOLD_STATES.include?(state.to_sym) || retrieving_shipping_costs?
end

#primary_partyObject



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

def primary_party
  contact || customer
end

#primary_rep_nameObject



2148
2149
2150
# File 'app/models/order.rb', line 2148

def primary_rep_name
  primary_sales_rep.try(:full_name) || 'Unassigned'
end

#primary_sales_repObject



1393
1394
1395
1396
1397
1398
# File 'app/models/order.rb', line 1393

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_urlObject



1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
# File 'app/models/order.rb', line 1089

def product_review_url
  return nil unless customer&.email.present?

  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



2361
2362
2363
2364
2365
2366
2367
2368
# File 'app/models/order.rb', line 2361

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_pathObject



2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
# File 'app/models/order.rb', line 2233

def public_path
  # disabling authenticated links for now
  a = customer.
  return nil unless a.present?

  UrlHelper.instance.my_order_path(self)
  # if self.customer.account.auth_token_required?
  #   path = self.customer.account.append_token(path, persist=true)
  # end
end


4430
4431
4432
# File 'app/models/order.rb', line 4430

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 not applicable



2930
2931
2932
2933
2934
2935
2936
2937
2938
2939
2940
2941
2942
2943
2944
2945
2946
2947
2948
2949
2950
2951
2952
2953
2954
2955
2956
2957
2958
2959
2960
2961
2962
2963
2964
2965
2966
2967
2968
2969
2970
2971
2972
2973
2974
2975
2976
2977
2978
2979
2980
2981
2982
2983
2984
2985
2986
2987
2988
2989
2990
2991
2992
2993
2994
2995
2996
2997
2998
2999
3000
3001
3002
3003
3004
3005
3006
3007
3008
3009
3010
3011
3012
3013
3014
3015
3016
3017
3018
3019
3020
3021
3022
3023
3024
3025
3026
3027
3028
3029
3030
3031
3032
3033
3034
3035
3036
3037
3038
3039
3040
3041
3042
3043
3044
3045
3046
3047
3048
3049
3050
3051
3052
3053
3054
3055
3056
3057
3058
3059
3060
3061
3062
3063
3064
3065
3066
3067
3068
3069
3070
3071
3072
3073
3074
3075
3076
3077
3078
3079
3080
3081
3082
3083
3084
3085
3086
3087
3088
3089
3090
3091
3092
3093
3094
3095
3096
3097
3098
3099
3100
3101
3102
3103
3104
3105
3106
3107
3108
3109
3110
3111
3112
3113
3114
3115
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
3144
3145
3146
3147
3148
3149
3150
3151
3152
3153
3154
3155
3156
3157
3158
3159
3160
3161
3162
3163
3164
3165
3166
3167
3168
3169
3170
3171
3172
3173
3174
3175
3176
3177
3178
# File 'app/models/order.rb', line 2930

def purchase_early_label_if_requested
  # Initialize API log for this purchase attempt
  @early_label_api_log = []
  log_early_label_event('start', "Checking early label for order #{reference_number}")

  unless purchase_label_early?
    log_early_label_event('skip', 'purchase_label_early flag is false')
    Rails.logger.debug { "[EarlyLabel] Skipping - purchase_label_early is false for order #{reference_number}" }
    return
  end

  unless marketplace_early_label_eligible?
    error_msg = "Order not eligible for marketplace early label (partner: #{edi_orchestrator_partner})"
    log_early_label_event('skip', error_msg)
    Rails.logger.info("[EarlyLabel] Skipping - #{error_msg}")
    self.early_label_purchase_result = { success: false, error: 'Order is not eligible for marketplace early label purchase' }
    return early_label_purchase_result
  end

  unless early_label_purchase_enabled_for_partner?
    log_early_label_event('skip', 'early_label_purchase_disabled on orchestrator')
    Rails.logger.info("[EarlyLabel] Skipping - early label purchase disabled for partner #{edi_orchestrator_partner}")
    return
  end

  if has_early_purchased_label?
    log_early_label_event('skip', "Already has early label: #{early_label_tracking_number}")
    Rails.logger.info("[EarlyLabel] Skipping - order #{reference_number} already has early label: #{early_label_tracking_number}")
    self.early_label_purchase_result = { success: true, tracking_number: early_label_tracking_number, carrier: early_label_carrier, already_purchased: true }
    return early_label_purchase_result
  end

  delivery = deliveries.first
  selected_sc = delivery&.selected_shipping_cost

  unless selected_shipping_cost_supports_early_label?(selected_sc, delivery)
    carrier_desc = selected_sc&.shipping_option&.name || 'none'
    log_early_label_event('skip', "Selected shipping cost (#{carrier_desc}) does not support early label — unchecking flag and skipping")
    Rails.logger.info("[EarlyLabel] Skipping — selected shipping cost '#{carrier_desc}' is not a marketplace rate for order #{reference_number}, clearing purchase_label_early")
    update_column(:purchase_label_early, false)
    return
  end

  log_early_label_event('begin_purchase', "Starting early label purchase for order #{reference_number}")
  Rails.logger.info("[EarlyLabel] Purchasing early label for order #{reference_number}")

  unless delivery&.supported_shipping_carrier?
    error_msg = "Delivery #{delivery&.id} does not have supported shipping carrier (carrier: #{delivery&.carrier})"
    log_early_label_event('error', error_msg)
    early_label_failure_notification(error_msg)
    Rails.logger.warn("[EarlyLabel] Skipping - #{error_msg}")
    self.early_label_purchase_result = { success: false, error: "Delivery does not have a supported shipping carrier (#{delivery&.carrier})" }
    save_early_label_api_log
    return early_label_purchase_result
  end

  begin
    is_amazon = edi_orchestrator_partner&.start_with?('amazon_seller')

    # Check for existing shipments - if none exist, auto-create suggested shipments
    # This is necessary because Walmart EDI orders don't have shipments until warehouse processing
    existing_shipments = delivery.shipments.non_voided.to_a
    if existing_shipments.empty?
      log_early_label_event('auto_create_shipments', "No shipments exist for delivery #{delivery.id}, creating suggested shipments")
      Rails.logger.info("[EarlyLabel] No shipments exist for delivery #{delivery.id}, auto-creating suggested shipments")
      Shipping::CreateSuggestedShipment.new.process(delivery)
      delivery.reload
      existing_shipments = delivery.shipments.non_voided.to_a

      if existing_shipments.empty?
        error_msg = "Failed to create suggested shipments for delivery #{delivery.id} - no packaging data available"
        log_early_label_event('error', error_msg)
        early_label_failure_notification(error_msg)
        Rails.logger.error("[EarlyLabel] Cannot purchase early label - #{error_msg}")
        self.early_label_purchase_result = { success: false, error: 'Could NOT purchase ship labels early: failed to create suggested shipments (no packaging data available)' }
        save_early_label_api_log
        return early_label_purchase_result
      end
      log_early_label_event('shipments_created', "Auto-created #{existing_shipments.size} suggested shipment(s)")
      Rails.logger.info("[EarlyLabel] Auto-created #{existing_shipments.size} suggested shipment(s) for early label purchase")
    end
    log_early_label_event('shipments_ready', "Using #{existing_shipments.size} shipment(s) for label purchase")
    Rails.logger.info("[EarlyLabel] Using #{existing_shipments.size} shipment(s) for label purchase")

    if is_amazon && existing_shipments.many?
      log_early_label_event('skip_multi_package', "Amazon early label purchase does not yet support multiple shipments (#{existing_shipments.size}). Labels will be purchased at ship-label time.")
      Rails.logger.info("[EarlyLabel] Skipping early label for multi-package Amazon order #{reference_number} (#{existing_shipments.size} shipments) — warehouse will purchase labels at ship-label time")
      save_early_label_api_log
      return nil
    end

    shipper = build_early_label_shipper(delivery, existing_shipments)

    log_early_label_event('api_call', 'Calling find_rates API', { po_number: edi_po_number })
    rates_response = shipper.find_rates
    merge_shipper_api_log(shipper)
    log_early_label_event('api_response', 'find_rates response', {
      success: rates_response[:success],
      rates_count: rates_response[:rates]&.length || 0,
      message: rates_response[:message]
    })

    unless rates_response[:success] && rates_response[:rates].any?
      error_msg = "Failed to get shipping rates: #{rates_response[:message]}"
      log_early_label_event('error', error_msg)
      early_label_failure_notification(error_msg)
      Rails.logger.error("[EarlyLabel] Failed to get rates for order #{reference_number}: #{rates_response[:message]}")
      self.early_label_purchase_result = { success: false, error: error_msg }
      save_early_label_api_log
      return early_label_purchase_result
    end

    rate = match_early_label_rate(delivery, rates_response[:rates], is_amazon:)

    unless rate
      so_name = delivery.selected_shipping_cost&.shipping_option&.name || 'unknown'
      error_msg = "User-selected rate '#{so_name}' is no longer available in fresh marketplace rates — holding order for re-selection"
      log_early_label_event('hold', error_msg, {
        selected_shipping_cost_id: delivery.selected_shipping_cost_id,
        selected_option: so_name,
        available_rates_count: rates_response[:rates]&.length || 0
      })
      Rails.logger.warn("[EarlyLabel] #{error_msg} for order #{reference_number}")
      hold_for_early_label_mismatch!(delivery, error_msg)
      save_early_label_api_log
      self.early_label_purchase_result = { success: false, error: error_msg, held: true }
      return early_label_purchase_result
    end

    label_rate = if is_amazon
                   rate.merge(
                     request_token: rate[:amz_request_token],
                     rate_id: rate[:amz_rate_id],
                     carrier_id: rate[:amz_carrier_id],
                     carrier_name: rate[:amz_carrier_name],
                     service_name: rate[:amz_service_name]
                   )
                 else
                   box_items = build_early_label_box_items(delivery)
                   rate.merge(
                     carrier_id: rate[:sww_carrier_id],
                     service_type: rate[:sww_service_type],
                     box_items: box_items
                   )
                 end

    log_early_label_event('api_call', 'Calling create_label API', {
      po_number: edi_po_number,
      carrier: label_rate[:carrier_id] || label_rate[:amz_carrier_id],
      service: label_rate[:service_type] || label_rate[:amz_service_name]
    })
    label_result = shipper.create_label(label_rate)
    merge_shipper_api_log(shipper)
    log_early_label_event('api_response', 'create_label response', {
      success: label_result[:success],
      tracking_number: label_result[:tracking_number],
      carrier: label_result[:carrier],
      error: label_result[:error]
    })

    unless label_result[:success]
      error_msg = "Failed to create shipping label: #{label_result[:error]}"
      log_early_label_event('error', error_msg)
      early_label_failure_notification(error_msg)
      Rails.logger.error("[EarlyLabel] Failed to create label for order #{reference_number}: #{label_result[:error]}")
      self.early_label_purchase_result = { success: false, error: error_msg }
      save_early_label_api_log
      return early_label_purchase_result
    end

    # SAFEGUARD A: Atomic label purchase - PDF must be saved before marking complete
    # Download and store the label PDF BEFORE updating metadata
    # This prevents the race condition where void happens before PDF is downloaded
    marketplace = is_amazon ? 'Amazon' : 'Walmart'
    log_early_label_event('pdf_download', 'Downloading and storing label PDF', {
      tracking_number: label_result[:tracking_number],
      carrier: label_result[:carrier]
    })
    pdf_upload = download_and_store_early_label_pdf_atomic(shipper, label_result, is_amazon:)

    unless pdf_upload
      error_msg = "Label was created but PDF could not be downloaded/stored. The label exists in #{marketplace} but is not recoverable locally."
      log_early_label_event('error', error_msg, {
        tracking_number: label_result[:tracking_number],
        carrier: label_result[:carrier],
        partial_failure: true
      })
      early_label_failure_notification("#{error_msg} Tracking: #{label_result[:tracking_number]}")
      Rails.logger.error("[EarlyLabel] PDF storage failed for order #{reference_number} - marking purchase as failed")
      self.early_label_purchase_result = {
        success: false,
        error: "Label was created but PDF could not be stored. The label exists in #{marketplace} but is not recoverable locally. Please void and recreate.",
        tracking_number: label_result[:tracking_number],
        carrier: label_result[:carrier],
        partial_failure: true
      }
      save_early_label_api_log
      return early_label_purchase_result
    end

    log_early_label_event('pdf_stored', 'Label PDF successfully stored', { upload_id: pdf_upload.id })

    # ONLY update metadata after PDF is confirmed saved
    update!(
      early_label_tracking_number: label_result[:tracking_number],
      early_label_carrier: label_result[:carrier],
      early_label_sww_label_id: is_amazon ? label_result[:shipment_id] : label_result[:label_id],
      early_label_service_type: is_amazon ? label_result[:service_name] : label_result[:service_type],
      early_label_purchased_at: Time.current,
      early_label_delivery_id: delivery.id,
      early_label_voided_at: nil,
      early_label_void_reason: nil,
      early_label_shipments_count: existing_shipments.size,
      early_label_shipments_data: build_early_label_shipments_data(existing_shipments)
    )

    if label_result[:additional_labels].present?
      additional_packages = label_result[:additional_labels].map do |l|
        { shipment_id: l[:shipment_id], tracking_number: l[:tracking_number] }
      end
      meta =  || {}
      meta['additional_packages'] = additional_packages
      update_column(:early_label_metadata, meta)
    end

    log_early_label_event('success', 'Early label purchase completed', {
      tracking_number: label_result[:tracking_number],
      carrier: label_result[:carrier],
      service_type: label_result[:service_type]
    })
    Rails.logger.info("[EarlyLabel] Successfully purchased early label for order #{reference_number}: tracking=#{label_result[:tracking_number]}")

    log_early_label_event('edi_confirm', "Sending EDI ship confirm to #{is_amazon ? 'Amazon' : 'Walmart'}")
    fire_early_label_edi_confirm(delivery, label_result)

    save_early_label_api_log
    self.early_label_purchase_result = { success: true, tracking_number: label_result[:tracking_number], carrier: label_result[:carrier] }
    early_label_purchase_result
  rescue StandardError => e
    error_msg = "Exception during early label purchase: #{e.message}"
    log_early_label_event('exception', error_msg, { backtrace: e.backtrace.first(3) })
    early_label_failure_notification(error_msg)
    save_early_label_api_log
    Rails.logger.error("[EarlyLabel] Error purchasing early label for order #{reference_number}: #{e.message}")
    Rails.logger.error(e.backtrace.first(5).join("\n"))
    self.early_label_purchase_result = { success: false, error: e.message }
    early_label_purchase_result
  end
end

#purchase_orderPurchaseOrder



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

belongs_to :purchase_order, optional: true

#quoteQuote

Returns:

See Also:



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

belongs_to :quote, inverse_of: :orders, optional: true

#quotesObject



1505
1506
1507
1508
1509
1510
# File 'app/models/order.rb', line 1505

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)


2021
2022
2023
# File 'app/models/order.rb', line 2021

def ready_for_pending_payment?
  ready_for_shipping?
end

#ready_for_service?Boolean

Returns:

  • (Boolean)


2057
2058
2059
# File 'app/models/order.rb', line 2057

def ready_for_service?
  belongs_to_smartservice_group?
end

#ready_for_shipping?Boolean

Returns:

  • (Boolean)


2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
# File 'app/models/order.rb', line 2061

def ready_for_shipping?
  if shipping_address.nil?
    return false
  end # or self.chosen_shipping_method.blank?

  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? }.collect(&: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 = String.new
    if customer&.is_houzz? && edi_po_number.present?
      houzz_snippet = ", get it here: <a href='https://www.houzz.com//printBuyerOrder//orderId=#{edi_po_number}' target=_new><i text='https://www.houzz.com//printBuyerOrder//orderId=#{edi_po_number}' class='fas fa-arrow-up-right-from-square'></i> https://www.houzz.com//printBuyerOrder//orderId=#{edi_po_number}</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) != 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? { |d| d.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].include?(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
  if shipping_address.country_iso3 == 'CAN'
    'Purolator'
  end # Canadian addresses can be really skitchy so only use Purolator's city/postal code validation
  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)


2017
2018
2019
# File 'app/models/order.rb', line 2017

def ready_for_warehouse?
  all_items_in_stock && ready_for_shipping?
end

#recipient_nameObject



1063
1064
1065
# File 'app/models/order.rb', line 1063

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


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

def related_activities
  Activity.where(id: related_activities_ids)
end


1697
1698
1699
# File 'app/models/order.rb', line 1697

def related_activities_ids
  [activities.pluck(:id), delivery_activities.pluck(:id)].flatten
end

#release_order_or_holdObject



1721
1722
1723
# File 'app/models/order.rb', line 1721

def release_order_or_hold
  release_order unless accounting_hold_order?
end

#remove_room_configuration(rc) ⇒ Object



1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
# File 'app/models/order.rb', line 1512

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)


1140
1141
1142
1143
1144
1145
1146
1147
1148
# File 'app/models/order.rb', line 1140

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)


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

def requires_intervention?
  crm_back_order? || in_cr_hold? || pending_review? || pending_release_authorization? || pending_payment?
end

#reset_cartObject



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

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)


1332
1333
1334
# File 'app/models/order.rb', line 1332

def restricted_order_type?
  RESTRICTED_ORDER_TYPES.keys.include?(order_type)
end

#reviewerObject

Returns the reviewer info for Reviews.io invitation
Prefers contact over customer if present



1069
1070
1071
1072
1073
1074
1075
# File 'app/models/order.rb', line 1069

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

#rmaRma

Returns:

See Also:



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

belongs_to :rma, inverse_of: :orders, optional: true

#rma_cancelObject



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

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

#rma_itemsActiveRecord::Relation<RmaItem>

Returns:

  • (ActiveRecord::Relation<RmaItem>)

See Also:



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

has_many :rma_items, inverse_of: :replacement_order

#rmasActiveRecord::Relation<Rma>

Returns:

  • (ActiveRecord::Relation<Rma>)

See Also:



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

has_many :rmas, foreign_key: :original_order_id

#sales_support_repEmployee

Returns:

See Also:



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

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)



3379
3380
3381
3382
3383
3384
3385
3386
3387
3388
3389
3390
3391
3392
3393
3394
3395
3396
# File 'app/models/order.rb', line 3379

def save_early_label_api_log
  return unless @early_label_api_log.present?

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



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

def schedule_follow_up_activity
  result = Order::FollowUpScheduler.new.process(self)
  result.activity_scheduled?
end

#search_textObject (protected)



4613
4614
4615
4616
4617
4618
4619
4620
4621
# File 'app/models/order.rb', line 4613

def search_text
  return nil if cart?

  st = []
  st << reference_number
  st += payments.pluck(:po_number)
  st << rma_reference
  st.join(' ')
end

#secondary_sales_repObject



1400
1401
1402
1403
1404
# File 'app/models/order.rb', line 1400

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)


3543
3544
3545
3546
3547
3548
3549
3550
3551
3552
3553
3554
3555
3556
# File 'app/models/order.rb', line 3543

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_nameObject



2822
2823
2824
# File 'app/models/order.rb', line 2822

def selection_name
  "#{reference_number} #{customer.full_name} (#{shipped_date.present? ? shipped_date.to_fs(:crm_default) : 'not shipped'})"
end

#send_back_order_notificationObject



2521
2522
2523
# File 'app/models/order.rb', line 2521

def send_back_order_notification
  InternalMailer.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



3286
3287
3288
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
3314
3315
3316
3317
3318
3319
3320
3321
3322
3323
# File 'app/models/order.rb', line 3286

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

  InternalMailer.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_confirmationObject



2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
# File 'app/models/order.rb', line 2541

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



2537
2538
2539
# File 'app/models/order.rb', line 2537

def send_payment_automatically_authorized_notification
  InternalMailer.payment_automatically_authorized(self).deliver_later
end

#send_profit_review_notificationObject



2529
2530
2531
# File 'app/models/order.rb', line 2529

def send_profit_review_notification
  InternalMailer.order_profit_review_notification(self).deliver_later
end

#send_ready_for_pickup_emailObject



4481
4482
4483
# File 'app/models/order.rb', line 4481

def send_ready_for_pickup_email
  send_tracking_template_email 'ORDER_PICKUP'
end

#send_release_authorization_notificationObject



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

def send_release_authorization_notification
  InternalMailer.release_authorization_required(self).deliver_later
end

#send_request_carrier_assignment_notificationObject



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

def send_request_carrier_assignment_notification
  InternalMailer.request_carrier_assignment_notification(self).deliver_later
end

#send_tracking_emailObject



4477
4478
4479
# File 'app/models/order.rb', line 4477

def send_tracking_email
  send_tracking_template_email 'ORDER_TRACKING'
end

#send_tracking_template_email(template_code) ⇒ Object



4485
4486
4487
4488
4489
4490
4491
4492
4493
4494
4495
# File 'app/models/order.rb', line 4485

def send_tracking_template_email(template_code)
  return { status_code: :error, status_message: "Can't send #{template_code}: no tracking e-mail present!" } unless tracking_email.present?

  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)


1741
1742
1743
# File 'app/models/order.rb', line 1741

def service_only_order?
  line_items.non_shipping.any? && line_items.non_shipping.all?(&:is_service?)
end

#set_currencyObject (protected)



4587
4588
4589
# File 'app/models/order.rb', line 4587

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

#set_custom_order_agreementObject

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



4356
4357
4358
4359
4360
4361
4362
# File 'app/models/order.rb', line 4356

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



2791
2792
2793
2794
2795
# File 'app/models/order.rb', line 2791

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



2783
2784
2785
2786
2787
2788
2789
# File 'app/models/order.rb', line 2783

def set_min_profit_markup
  self.min_profit_markup = if is_sales_order?
                             default_sales_markup
                           else
                             0
                           end
end

#set_opportunity_sourceObject



1372
1373
1374
1375
1376
1377
1378
1379
# File 'app/models/order.rb', line 1372

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



2275
2276
2277
# File 'app/models/order.rb', line 2275

def set_shipped_date
  update_attribute(:shipped_date, Date.current) unless shipped_date.present?
end

#ship_from_attributes(delivery = nil) ⇒ Object



2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
# File 'app/models/order.rb', line 2490

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 && delivery.chosen_shipping_method && 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' unless res[:attention_name].present?
  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_attributesObject



2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
# File 'app/models/order.rb', line 2416

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 unless res[:attention_name].present?
  res[:attention_name] = 'Customer' unless res[:attention_name].present?
  if res[:address].company_name
    res[:name] = res[:address].company_name
    res[:name] = 'Customer' unless res[:name].present?
  else
    res[:name] = attention_name
    res[:name] = res[:address].person_name unless res[:name].present?
    res[:name] = 'Customer' unless res[:name].present?
  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_weightObject



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

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

#shipmentsActiveRecord::Relation<Shipment>

Returns:

See Also:



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

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

#shipping_account_numberShippingAccountNumber

DELIVERY REFACTOR HERE



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

belongs_to :shipping_account_number, optional: true

#shipping_addressAddress

Returns:

See Also:



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

belongs_to :shipping_address, class_name: 'Address', validate: true, optional: true

#shipping_address_valid_and_not_po_box_if_present(rate_shopping = false) ⇒ Object



2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
# File 'app/models/order.rb', line 2000

def shipping_address_valid_and_not_po_box_if_present(rate_shopping = false)
  res = false
  if shipping_address.nil?
    errors[: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
      errors[:shipping_address] << shipping_address.errors.full_messages
    end
  end
  res
end

#shipping_cutoff_advance_order?Boolean

Returns:

  • (Boolean)


1600
1601
1602
# File 'app/models/order.rb', line 1600

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

#shipping_cutoff_next_day?Boolean

Returns:

  • (Boolean)


1596
1597
1598
# File 'app/models/order.rb', line 1596

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)


1592
1593
1594
# File 'app/models/order.rb', line 1592

def shipping_cutoff_same_day?
  line_items.goods.parents_only.all? { |a| a.in_stock? && !a.ships_via_freight? } # same day
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.



1845
1846
1847
# File 'app/models/order.rb', line 1845

def shipping_date_warnings
  []
end

#should_commit_stock?Boolean

Returns:

  • (Boolean)


1015
1016
1017
# File 'app/models/order.rb', line 1015

def should_commit_stock?
  (future_release_date.present? && !do_not_reserve_stock?) || all_funds_available?
end

#smartservice_ticketObject



2049
2050
2051
# File 'app/models/order.rb', line 2049

def smartservice_ticket
  support_cases&.services&.pending_service_payment&.first
end

#sms_enabled_numbersObject



1132
1133
1134
# File 'app/models/order.rb', line 1132

def sms_enabled_numbers
  ContactPoint.where(party_id: all_participant_ids).sms_numbers.order(:detail).map(&:formatted_for_sms).uniq
end

#sms_messagesObject



1136
1137
1138
# File 'app/models/order.rb', line 1136

def sms_messages
  SmsMessage.for_numbers(sms_enabled_numbers)
end

#sold_to_billing_addressAddress

Returns:

See Also:



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

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

#sourceSource

Returns:

See Also:



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

belongs_to :source, optional: true

#spiff_enrollmentSpiffEnrollment



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

belongs_to :spiff_enrollment, optional: true

#spiff_repContact

Returns:

See Also:



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

belongs_to :spiff_rep, class_name: 'Contact', optional: true, inverse_of: :spiff_orders

#spiff_rewardObject



1473
1474
1475
# File 'app/models/order.rb', line 1473

def spiff_reward
  '%.2f' % spiff_enrollment.spiff.eligible_reward(self)
end

#state_description(describe_state = nil) ⇒ Object



1412
1413
1414
# File 'app/models/order.rb', line 1412

def state_description(describe_state = nil)
  OrderConstants::STATE_DESCRIPTION[describe_state || state]
end

#state_listObject



1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
# File 'app/models/order.rb', line 1426

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_statusObject



1996
1997
1998
# File 'app/models/order.rb', line 1996

def stock_status
  LineItem.inventory_check(self)
end

#stop_for_pre_pack?Boolean

Returns:

  • (Boolean)


4371
4372
4373
4374
4375
4376
4377
4378
4379
4380
4381
# File 'app/models/order.rb', line 4371

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)


4364
4365
4366
4367
4368
4369
# File 'app/models/order.rb', line 4364

def stop_for_profit_review?
  # Etailer orders are immune
  return false if customer.is_e_commerce_misc?

  !profit_margins_met?
end

#storeObject



1011
1012
1013
# File 'app/models/order.rb', line 1011

def store
  from_store || customer&.store
end

#store_additional_early_label_pdfs(additional_labels) ⇒ Object



4038
4039
4040
4041
4042
4043
4044
4045
4046
4047
# File 'app/models/order.rb', line 4038

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.



3943
3944
3945
3946
3947
3948
3949
3950
3951
3952
3953
3954
3955
3956
3957
3958
3959
3960
3961
3962
3963
3964
3965
3966
3967
3968
3969
3970
3971
# File 'app/models/order.rb', line 3943

def store_amazon_label_pdf_atomic(label_result, tracking_number, marketplace)
  label_data = label_result[:label_data]

  unless label_data.present?
    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:



4017
4018
4019
4020
4021
4022
4023
4024
4025
4026
4027
4028
4029
4030
4031
4032
4033
4034
4035
4036
# File 'app/models/order.rb', line 4017

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:



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

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)


4054
4055
4056
4057
4058
4059
4060
4061
4062
4063
4064
4065
4066
4067
4068
4069
4070
4071
4072
4073
4074
4075
4076
4077
4078
4079
4080
4081
# File 'app/models/order.rb', line 4054

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



3974
3975
3976
3977
3978
3979
3980
3981
3982
3983
3984
3985
3986
3987
3988
3989
3990
3991
3992
3993
3994
3995
3996
3997
3998
3999
4000
4001
4002
4003
4004
4005
4006
4007
4008
4009
4010
# File 'app/models/order.rb', line 3974

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_shipmentsObject



4453
4454
4455
# File 'app/models/order.rb', line 4453

def suggested_shipments
  shipments.where(state: 'suggested') if respond_to? :shipments
end

#support_case_refObject



1287
1288
1289
# File 'app/models/order.rb', line 1287

def support_case_ref
  support_cases.collect(&:case_number).join(', ')
end

#support_case_ref=(support_case_ref) ⇒ Object



1291
1292
1293
1294
# File 'app/models/order.rb', line 1291

def support_case_ref=(support_case_ref)
  refs = support_case_ref.delete(' ').split(',')
  self.support_case_ids = SupportCase.where(case_number: refs).pluck(:id)
end

#support_casesActiveRecord::Relation<SupportCase>

Returns:

See Also:



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

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)


1610
1611
1612
1613
1614
1615
# File 'app/models/order.rb', line 1610

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



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

def sync_opportunity
  opportunity&.sync_state
end

#technical_support_repTechnicalSupportRep

Returns:

  • (TechnicalSupportRep)

See Also:



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

has_one :technical_support_rep, through: :opportunity

#terms_available?Boolean (protected)

Returns:

  • (Boolean)


4609
4610
4611
# File 'app/models/order.rb', line 4609

def terms_available?
  billing_entity.terms_credit_limit >= total
end

#to_labelObject



2801
2802
2803
# File 'app/models/order.rb', line 2801

def to_label
  "[#{reference_number}] #{customer.full_name} (#{shipped_date.to_fs(:crm_default)})"
end

#to_liquidObject



2445
2446
2447
# File 'app/models/order.rb', line 2445

def to_liquid
  Liquid::OrderDrop.new self
end

#to_sObject



2818
2819
2820
# File 'app/models/order.rb', line 2818

def to_s
  cart_identifier
end

#to_storeStore

Returns:

See Also:



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

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

#total_codObject



2266
2267
2268
2269
2270
2271
2272
2273
# File 'app/models/order.rb', line 2266

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_moneyObject



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

def total_money
  '%.2f' % total
end

#total_payments_authorizedObject



1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
# File 'app/models/order.rb', line 1912

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 += if dq_auth >= dq_total
                dq_total
              else
                dq_auth
              end
  end
  amount < 0 ? BigDecimal('0.0') : amount
end

#track_profit?Boolean

Returns:

  • (Boolean)


4445
4446
4447
# File 'app/models/order.rb', line 4445

def track_profit?
  profitable_line_items.present? && is_regular_order?
end

#tracking_email_addressObject



1867
1868
1869
1870
1871
1872
# File 'app/models/order.rb', line 1867

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



1982
1983
1984
1985
1986
# File 'app/models/order.rb', line 1982

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_itemsObject



1988
1989
1990
# File 'app/models/order.rb', line 1988

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)



4583
4584
4585
# File 'app/models/order.rb', line 4583

def update_customer_status
  customer&.set_status
end

#update_linked_po_if_stObject



4438
4439
4440
4441
4442
4443
# File 'app/models/order.rb', line 4438

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



1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
# File 'app/models/order.rb', line 1536

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:



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

has_many :uploads, -> { order(created_at: :desc) }, as: :resource, dependent: :destroy

#versions_for_audit_trail(_params = {}) ⇒ Object



2861
2862
2863
2864
2865
2866
2867
2868
2869
2870
2871
2872
2873
2874
2875
2876
2877
2878
2879
2880
# File 'app/models/order.rb', line 2861

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": :id}'
                )
                OR (
                  item_type = 'Delivery'
                    AND reference_data @> '{"order_id": :id}'
                )
                OR (
                  item_type = 'Discount'
                    AND reference_data @> '{"itemizable_id": :id}'
                    AND reference_data @> '{"itemizable_type": "Order"}'
                )
              }
  RecordVersion.where(query_sql, id:)
end

#visitVisit

Returns:

See Also:



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

belongs_to :visit, optional: true

#void_credit_rma_itemObject



2886
2887
2888
# File 'app/models/order.rb', line 2886

def void_credit_rma_item
  line_items.map(&:credit_rma_item).compact.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



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

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



3191
3192
3193
3194
3195
3196
3197
3198
3199
3200
3201
3202
3203
3204
3205
3206
3207
3208
3209
3210
3211
3212
3213
3214
3215
3216
3217
3218
3219
3220
3221
3222
3223
3224
3225
3226
3227
3228
3229
3230
3231
3232
3233
3234
3235
3236
3237
3238
3239
3240
3241
3242
3243
3244
3245
3246
3247
3248
3249
3250
3251
3252
3253
3254
3255
3256
3257
3258
3259
3260
3261
3262
3263
3264
3265
3266
3267
3268
# File 'app/models/order.rb', line 3191

def void_early_label_if_exists
  return unless has_early_purchased_label?

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



3592
3593
3594
3595
3596
3597
3598
# File 'app/models/order.rb', line 3592

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



4569
4570
4571
4572
4573
4574
4575
4576
4577
4578
4579
# File 'app/models/order.rb', line 4569

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:



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

has_many :vouchers

#walmart_sww_eligible?Boolean

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

Returns:

  • (Boolean)


3492
3493
3494
3495
3496
3497
3498
3499
3500
3501
3502
3503
# File 'app/models/order.rb', line 3492

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