Class: Order
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- Order
- Includes:
- Memery, Models::Auditable, Models::CouponDateOverridable, Models::InventoryCommittable, Models::Itemizable, Models::LegacyRateRequest, Models::Lineage, Models::LiquidMethods, Models::MultiRoom, Models::Notable, Models::Pickable, Models::Profitable, Models::RmaTransmittable, Models::ShipQuotable, Models::SourceAttributable, Models::TaxableResource, Models::ValidationErrorReporting, AuthorizationSplittable, PgSearch::Model
- Defined in:
- app/models/order.rb
Overview
== Schema Information
Table name: orders
Database name: primary
id :integer not null, primary key
abandoned_cart_reminder :enum default("reminder_not_sent"), not null
actual_shipping_cost :decimal(10, 2)
allow_advance_credit_without_cc :boolean
amazon_checkout_session :string
attention_name_override :string(255)
bill_shipping_to_customer :boolean
billing_emails :string is an Array
bo_notification_sent_at :datetime
bulk_buy_request :boolean
cancellation_reason :string
cod_collection_type :string(255)
completion_date :date
credit_request_state :string
currency :string(255)
custom_order_agreement_status :enum default("custom_order_agreement_not_required"), not null
customer_reference :string(255)
disable_auto_coupon :boolean default(FALSE), not null
discount_adjustment_needed :boolean
do_not_reserve_stock :boolean default(FALSE), not null
early_label_metadata :jsonb
edi_auto_cancel_options :jsonb
edi_channel_order_support :string
edi_delayed_delivery_acknowledged_at :datetime
edi_is_pick_slip_required :boolean default(FALSE), not null
edi_orchestrator_partner :string
edi_order_date :date
edi_original_order_message :text
edi_original_ship_code :string
edi_po_number :string
edi_shipping_option_name :string
exclude_from_payment_check :boolean default(FALSE), not null
exported :boolean default(FALSE), not null
first_cart_view_date :datetime
first_review_request_processed_date :datetime
first_view_date :datetime
future_release_date :date
google_campaign_attribution :jsonb
google_conversion_meta :jsonb
hold_bo_release :boolean default(FALSE), not null
incorrectly_packaged_ups_canada_order :boolean
incorrectly_packaged_ups_canada_order_fixed :boolean
is_manual_hold :boolean default(FALSE), not null
jde_b_ab :integer
jde_s_ab :integer
label_instructions :string(255)
line_offset :decimal(10, 2)
line_total :decimal(10, 2)
ltl_freight :boolean
ltl_freight_guaranteed :boolean
manual_release_only :boolean
max_discount_override :decimal(4, 2)
min_profit_markup :integer default(0)
non_commissionable :boolean default(FALSE), not null
openai_ads_conversion_meta :jsonb not null
order_reception_type :string(255) default("CRM"), not null
order_type :string(255) default("SO")
override_coupon_date :date
override_coupon_date_without_limits :boolean default(FALSE), not null
override_line_lock :boolean default(FALSE), not null
packaged_items_md5_hash :string(255)
pickup :boolean
pinterest_conversion_meta :jsonb
precreate_rma :boolean default(FALSE), not null
price_match :decimal(10, 2)
pricing_program_description :string(255)
pricing_program_discount :integer
profit_total :decimal(10, 2)
purchase_label_early :boolean default(FALSE), not null
recalculate_discounts :boolean default(FALSE), not null
recalculate_shipping :boolean default(TRUE), not null
reference_number :string(255)
requested_deliver_by :date
requested_ship_before :date
requested_ship_on_or_after :date
retrieving_shipping_costs :boolean
return_label :boolean default(FALSE), not null
revenue_consolidated_at_time_of_checkout :decimal(10, 2)
review_request_processed :boolean default(FALSE), not null
review_snoozed_until :datetime
reviewed :boolean default(FALSE), not null
rma_reference :string(255)
sales_support_commission_date :date
saturday_delivery :boolean default(FALSE), not null
second_review_request_processed :boolean default(FALSE), not null
second_review_request_processed_date :datetime
shipment_instructions :text
shipment_reference_number :string
shipped_date :date
shipping_cost :decimal(10, 2)
shipping_cost_at_time_of_checkout :decimal(10, 2)
shipping_coupon :decimal(10, 2)
shipping_issue_alerted :boolean
shipping_method :string(255)
shipping_phone :string(255)
ships_economy :boolean default(FALSE), not null
signature_confirmation :boolean default(FALSE), not null
single_origin :boolean default(FALSE), not null
smartset_redemption_code :string(255)
sold_to_billing_address :integer
spiff_state :string(255)
state :string(255)
suggested_packaging_text :text
tax_date :date
tax_exempt :boolean default(FALSE), not null
tax_offset :decimal(10, 2)
tax_total :decimal(10, 2)
taxable_total :decimal(12, 2)
total :decimal(10, 2)
tracking_email :string(255) default([]), is an Array
transmission_email :string(255) default([]), is an Array
transmission_fax :string(255) default([]), is an Array
txid :uuid
uploads_count :integer default(0)
who_will_install :string
created_at :datetime
updated_at :datetime
buying_group_id :integer
contact_id :integer
creator_id :integer
customer_id :integer
default_credit_card_vault_id :integer
deleter_id :integer
edi_transaction_id :string
from_store_id :integer
local_sales_rep_id :integer
opportunity_id :integer
parent_id :integer
primary_sales_rep_id :integer
purchase_order_id :integer
quote_id :integer
resource_tax_rate_id :integer
rma_id :integer
sales_support_rep_id :integer
secondary_sales_rep_id :integer
shipping_account_number_id :integer
shipping_address_id :integer
source_id :integer
spiff_enrollment_id :integer
spiff_rep_id :integer
tax_exemption_id :integer
to_store_id :integer
tracking_event_id :uuid
updater_id :integer
visit_id :bigint
Indexes
idx_customer_id_order_type_created_at (customer_id,order_type,created_at)
idx_customer_id_order_type_state (customer_id,order_type,state)
idx_customer_id_review_snoozed_until (customer_id,review_snoozed_until)
idx_customer_id_state_shipped_date (customer_id,state,shipped_date)
idx_opportunity_id_state (opportunity_id,state)
idx_order_type_shipping_address_id (order_type,shipping_address_id)
idx_shipping_address_id_state (shipping_address_id,state)
index_orders_on_contact_id (contact_id) USING hash
index_orders_on_customer_id (customer_id) USING hash
index_orders_on_customer_reference (customer_reference) USING gin
index_orders_on_edi_po_number (edi_po_number) USING hash
index_orders_on_google_campaign_attribution (google_campaign_attribution) USING gin
index_orders_on_google_conversion_meta (google_conversion_meta) USING gin
index_orders_on_google_conversion_meta_result (((google_conversion_meta ->> 'result'::text)))
index_orders_on_opportunity_id (opportunity_id) USING hash
index_orders_on_packaged_items_md5_hash (packaged_items_md5_hash) USING hash
index_orders_on_parent_id (parent_id)
index_orders_on_purchase_order_id (purchase_order_id) USING hash
index_orders_on_quote_id (quote_id) USING hash
index_orders_on_reference_number (reference_number) UNIQUE
index_orders_on_rma_id (rma_id) USING hash
index_orders_on_sales_support_rep_id (sales_support_rep_id)
index_orders_on_shipped_date (shipped_date) USING brin
index_orders_on_shipping_address_id (shipping_address_id) USING hash
index_orders_on_smartset_redemption_code (smartset_redemption_code) WHERE (smartset_redemption_code IS NOT NULL) USING hash
index_orders_on_source_id (source_id) USING hash
index_orders_on_spiff_enrollment_id (spiff_enrollment_id) WHERE (spiff_enrollment_id IS NOT NULL) USING hash
index_orders_on_spiff_rep_id (spiff_rep_id) WHERE (spiff_rep_id IS NOT NULL) USING hash
index_orders_on_state (state) USING hash
index_orders_on_tax_exemption_id (tax_exemption_id) WHERE (tax_exemption_id IS NOT NULL) USING hash
index_orders_on_txid (txid) USING hash
index_orders_on_updated_at (updated_at) USING brin
index_orders_on_visit_id (visit_id) WHERE (visit_id IS NOT NULL) USING hash
orders_unique_edi_transaction_id_by_customer (customer_id,edi_transaction_id) UNIQUE
Foreign Keys
fk_rails_... (contact_id => parties.id) ON DELETE => nullify
fk_rails_... (parent_id => orders.id)
fk_rails_... (purchase_order_id => purchase_orders.id)
fk_rails_... (rma_id => rmas.id) ON DELETE => cascade
fk_rails_... (shipping_address_id => addresses.id)
fk_rails_... (source_id => sources.id)
fk_rails_... (visit_id => visits.id) ON DELETE => nullify
orders_tax_exemption_id_fk (tax_exemption_id => tax_exemptions.id)
Defined Under Namespace
Modules: AuthorizationSplittable Classes: AuthorizationSplitWorker, AuthorizationSplitter, BackOrderClientNotification, ContactLookup, CreateVcProcurementOrdersFromCsv, DefaultTrackingEmailExtractor, FollowUpScheduler, FraudDetector, Mover, SendAbandonedCartEmails, Splitter
Constant Summary collapse
- SALES_ORDER =
Sales order.
'SO'- MARKETING_ORDER =
Marketing order.
'MO'- TECH_ORDER =
Tech order.
'TO'- CREDIT_ORDER =
Credit order.
'CO'- STORE_TRANSFER =
Store transfer.
'ST'- SHIPPING_STATES =
Recognised shipping states.
%i[awaiting_deliveries processing_deliveries].freeze
- SOLD_STATES =
Recognised sold states.
%i[awaiting_deliveries processing_deliveries partially_invoiced invoiced].freeze
- OPEN_FOR_CHANGE_STATES =
Recognised open for change states.
%i[cart in_shipping_estimate pending pending_payment pending_release_authorization in_cr_hold needs_serial_number_reservation cancelled fraudulent awaiting_completed_installation_plans].freeze
- OPEN_FOR_TAX_CHANGE_STATES =
Recognised open for tax change states.
%i[cart pending pending_payment pending_release_authorization in_cr_hold needs_serial_number_reservation crm_back_order awaiting_completed_installation_plans].freeze
- CAN_CR_HOLD_STATES =
Recognised can cr hold states.
%i[pending pending_payment awaiting_deliveries processing_deliveries profit_review crm_back_order].freeze
- RELEASABLE_STATES =
Recognised releasable states.
%i[cart in_cr_hold request_carrier crm_back_order pending_release_authorization profit_review pending_payment awaiting_deliveries processing_deliveries needs_serial_number_reservation awaiting_completed_installation_plans].freeze
- CANCELABLE_STATES =
Recognised cancelable states.
%i[cart pending awaiting_return profit_review crm_back_order pending_release_authorization pending_payment in_cr_hold].freeze
- ROOM_PICKABLE_STATES =
Recognised room pickable states.
%i[crm_back_order fraudulent cancelled pending pending_release_authorization pending_payment in_cr_hold].freeze
- CLOSED_STATES =
Recognised closed states.
%i[shipped invoiced cancelled fraudulent].freeze
- LOCKED_STATES =
Recognised locked states.
%i[cancelled awaiting_deliveries processing_deliveries shipped partially_invoiced invoiced fraudulent].freeze
- CAN_REQUEST_PRE_PACK_STATES =
Recognised can request pre pack states.
%i[pending pending_payment profit_review in_cr_hold needs_serial_number_reservation].freeze
- RESTRICTED_ORDER_TYPES =
Recognised restricted order types.
{ CREDIT_ORDER => 'Credit Order', STORE_TRANSFER => 'Store Transfer' }.freeze
- UNRESTRICTED_ORDER_TYPES =
Recognised unrestricted order types.
{ SALES_ORDER => 'Sales Order', MARKETING_ORDER => 'Marketing Order', TECH_ORDER => 'Tech Order' }.freeze
- ALL_ORDER_TYPES =
Recognised all order types.
RESTRICTED_ORDER_TYPES.merge(UNRESTRICTED_ORDER_TYPES)
- REFERENCE_NUMBER_PATTERN =
Regex pattern matching reference number.
Regexp.new("^(#{ALL_ORDER_TYPES.keys.join('|')})(\\d+)$", Regexp::IGNORECASE)
- ALL_STATES =
Recognised all states.
Order.state_machines[:state].states.map(&:name).freeze
- NON_CART_STATES =
Recognised non cart states.
ALL_STATES - [:cart]
- SAME_DAY_CUTOFF_HOUR =
Customer-facing same-day-shipping cutoff, in warehouse-local hours
(noon CT for the US warehouse, noon ET for the CA warehouse). The
actual carrier pickup cutoffs in Store::CARRIER_PICKUP_CUT_OFF_TIMES
run later; noon is the conservative customer promise. 12- EARLY_LABEL_RAPID_VOID_THRESHOLD_MINUTES =
Minimum time (in minutes) that must pass after early label purchase before auto-void is allowed
This prevents the race condition where label is voided before PDF is fully processed 3- AMAZON_OVER_FULFILL_PATTERN =
Regex pattern matching amazon over fulfill.
/Attempting to over-fulfill item\(s\)[\s\S]*?Existing shipmentIds for order:\[([^\]]+)\]/i
Constants included from Models::InventoryCommittable
Models::InventoryCommittable::STATES_WITH_NO_EXPIRATION
Constants included from Models::Auditable
Models::Auditable::ALWAYS_IGNORED
Constants included from Schedulable
Schedulable::SIMPLE_FORM_OPTIONS
Instance Attribute Summary collapse
-
#customer_id ⇒ Object
readonly
Customer can only have one cart per Contact.
-
#do_not_detect_shipping ⇒ Object
Returns the value of attribute do_not_detect_shipping.
-
#do_not_set_totals ⇒ Object
Returns the value of attribute do_not_set_totals.
-
#early_label_purchase_result ⇒ Object
Returns the value of attribute early_label_purchase_result.
- #from_store_id ⇒ Object readonly
-
#full_shipping_address_validation ⇒ Object
Returns the value of attribute full_shipping_address_validation.
-
#is_www ⇒ Object
Returns the value of attribute is_www.
-
#is_www_ship_by_zip ⇒ Object
Returns the value of attribute is_www_ship_by_zip.
- #max_discount_override ⇒ Object readonly
- #order_type ⇒ Object readonly
- #reference_number ⇒ Object readonly
-
#shipping_phone ⇒ Object
readonly
skip this validation for EDI orders, let it ride.
- #to_store_id ⇒ Object readonly
- #tracking_email ⇒ Object readonly
-
#update_shipping_address_with_contact ⇒ Object
Returns the value of attribute update_shipping_address_with_contact.
Attributes included from Models::Profitable
Attributes included from Models::Itemizable
#force_total_reset, #total_reset
Belongs to collapse
- #buying_group ⇒ BuyingGroup
- #contact ⇒ Contact
- #customer ⇒ Customer
- #default_credit_card_vault ⇒ CreditCardVault
- #from_store ⇒ Store
- #invoiced_local_sales_rep ⇒ Party
- #invoiced_primary_sales_rep ⇒ Party
- #invoiced_secondary_sales_rep ⇒ Party
- #opportunity ⇒ Opportunity
- #purchase_order ⇒ PurchaseOrder
- #quote ⇒ Quote
- #rma ⇒ Rma
- #sales_support_rep ⇒ Employee
-
#shipping_account_number ⇒ ShippingAccountNumber
DELIVERY REFACTOR HERE.
- #shipping_address ⇒ Address
- #sold_to_billing_address ⇒ Address
- #source ⇒ Source
- #spiff_enrollment ⇒ SpiffEnrollment
- #spiff_rep ⇒ Contact
- #to_store ⇒ Store
- #visit ⇒ Visit
Methods included from Models::Auditable
Methods included from Models::TaxableResource
Methods included from Models::Itemizable
Has one collapse
Has many collapse
- #activities ⇒ ActiveRecord::Relation<Activity>
- #communications ⇒ ActiveRecord::Relation<Communication>
- #credit_memos ⇒ ActiveRecord::Relation<CreditMemo>
- #deliveries ⇒ ActiveRecord::Relation<Delivery>
- #delivery_activities ⇒ ActiveRecord::Relation<Activity>
- #direct_shipments ⇒ ActiveRecord::Relation<Shipment>
- #drop_ship_purchase_orders ⇒ ActiveRecord::Relation<DropShipPurchaseOrder>
- #edi_communication_logs ⇒ ActiveRecord::Relation<EdiCommunicationLog>
- #edi_documents ⇒ ActiveRecord::Relation<EdiDocument>
- #invoices ⇒ ActiveRecord::Relation<Invoice>
- #linked_support_cases ⇒ ActiveRecord::Relation<LinkedSupportCase>
- #material_alerts ⇒ ActiveRecord::Relation<MaterialAlert>
- #messaging_logs ⇒ ActiveRecord::Relation<MessagingLog>
- #payments ⇒ ActiveRecord::Relation<Payment>
- #preset_jobs ⇒ ActiveRecord::Relation<PresetJob>
- #rma_items ⇒ ActiveRecord::Relation<RmaItem>
- #rmas ⇒ ActiveRecord::Relation<Rma>
- #shipments ⇒ ActiveRecord::Relation<Shipment>
- #uploads ⇒ ActiveRecord::Relation<Upload>
- #vouchers ⇒ ActiveRecord::Relation<Voucher>
Methods included from Models::Pickable
Methods included from Models::Itemizable
Has and belongs to many collapse
Methods included from Models::MultiRoom
Delegated Instance Attributes collapse
-
#billing_address ⇒ Object
Alias for Customer#billing_address.
-
#billing_entity ⇒ Object
Alias for Customer#billing_entity.
-
#catalog ⇒ Object
Alias for Customer#catalog.
-
#company ⇒ Object
Alias for Customer#company.
-
#include_po_prefix? ⇒ Object
Alias for Customer#include_po_prefix?.
-
#is_amazon_com? ⇒ Object
Alias for Customer#is_amazon_com?.
-
#is_amazon_seller_central? ⇒ Object
Alias for Customer#is_amazon_seller_central?.
-
#is_canadian_tire? ⇒ Object
Alias for Customer#is_canadian_tire?.
-
#is_costco_ca? ⇒ Object
Alias for Customer#is_costco_ca?.
-
#is_home_depot_can? ⇒ Object
Alias for Customer#is_home_depot_can?.
-
#is_home_depot_usa? ⇒ Object
Alias for Customer#is_home_depot_usa?.
-
#is_houzz? ⇒ Object
Alias for Customer#is_houzz?.
-
#is_part_of_lowes_ca? ⇒ Object
Alias for Customer#is_part_of_lowes_ca?.
-
#is_part_of_lowes_com? ⇒ Object
Alias for Customer#is_part_of_lowes_com?.
-
#is_walmart_ca? ⇒ Object
Alias for Customer#is_walmart_ca?.
-
#is_wasn4_or_wat0f? ⇒ Object
Alias for Customer#is_wasn4_or_wat0f?.
-
#store_id ⇒ Object
Alias for Store#id.
Class Method Summary collapse
-
.active ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are active.
-
.active_spiffs ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are active spiffs.
-
.all_awaiting_deliveries ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are all awaiting deliveries.
-
.all_order_types_for_select ⇒ Array<Array(String, String)>
Every order-type code the system understands, for filter dropdowns in admin search.
-
.assigned_to_rep ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are assigned to rep.
-
.awaiting_completed_installation_plans ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are awaiting completed installation plans.
-
.awaiting_payment_spiff ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are awaiting payment spiff.
-
.back_order ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are back order.
-
.by_company_id ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are by company id.
-
.by_primary_rep_id ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are by primary rep id.
-
.by_report_grouping ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are by report grouping.
-
.by_report_grouping_all_when_nil ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are by report grouping all when nil.
-
.by_sales_rep_id ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are by sales rep id.
-
.by_store ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are by store.
-
.by_store_id ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are by store id.
-
.cancelled ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are cancelled.
-
.carts ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are carts.
-
.co_only ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are co only.
-
.contains_coupon_ids ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are contains coupon ids.
-
.contains_item_ids ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are contains item ids.
-
.contains_service_items ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are contains service items.
-
.correctly_packaged ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are correctly packaged.
-
.custom_order_agreement_statuses_for_select ⇒ Array<Array(String, String)>
[label, value]pairs of every Custom-Order-Agreement status for the CRM dropdown. -
.customer_reference_search ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are customer reference search.
-
.draft_spiff ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are draft spiff.
-
.edi_orders ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are edi orders.
-
.future_release ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are future release.
-
.google_conversion_acknowledged ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are google conversion acknowledged.
-
.google_conversion_attempted ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are google conversion attempted.
-
.has_manual_preset_form ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are has manual preset form.
-
.held ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are held.
-
.held_sales_orders ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are held sales orders.
-
.in_progress ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are in progress.
-
.in_state ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are in state.
-
.incorrectly_packaged_ups_canada_order ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are incorrectly packaged ups canada order.
-
.invoiced ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are invoiced.
-
.like_lookup ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are like lookup.
-
.limit_to_fba ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are limit to fba.
-
.locked ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are locked.
-
.lookup ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are lookup.
-
.mo_only ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are mo only.
-
.most_recent_first ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are most recent first.
-
.non_carts ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are non carts.
-
.non_credit ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are non credit.
-
.not_cancelled ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are not cancelled.
-
.not_in_pre_pack ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are not in pre pack.
-
.not_open_for_change ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are not open for change.
-
.not_partially_invoiced ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are not partially invoiced.
-
.not_processing_deliveries ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are not processing deliveries.
-
.not_sold ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are not sold.
-
.open_for_change ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are open for change.
-
.open_for_tax_update ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are open for tax update.
-
.order_count(company_id = nil, where_conditions = nil, where_not_conditions = nil) ⇒ Integer
Counts orders matching the given filters.
-
.order_type_from_opportunity(opportunity) ⇒ String
Derive an order_type code from an Opportunity's type by appending
'O'(soS+O='SO'sales order,T+O='TO'tech order). -
.order_type_from_quote(quote) ⇒ String
Map a Quote type code to the matching order_type code: a tech quote (
TQ) becomes a tech order (TO), a marketing quote (MQ) becomes a marketing order (MO), etc. -
.paid_spiff ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are paid spiff.
-
.pending ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are pending.
-
.pending_payment ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are pending payment.
-
.pending_payment_and_unpaid_invoices ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are pending payment and unpaid invoices.
-
.po_number_barcode(po_number:, file_path: nil) ⇒ String?
Class-method form of #po_number_barcode: render an arbitrary PO number as a Code-128 barcode PNG.
-
.positive_value ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are positive value.
-
.profit_review ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are profit review.
-
.quick_stats_shipping ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are quick stats shipping.
-
.quick_stats_sold ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are quick stats sold.
-
.reception_type_for_select ⇒ Hash{String => String}
Memoised
{label => value}map for the order-reception-type filter ("Online" vs "CRM"). -
.returnable_types ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are returnable types.
-
.room_not_pickable ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are room not pickable.
-
.sales_orders ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are sales orders.
-
.selectable_order_types ⇒ Array<Array(String, String)>
Order-type codes a user is allowed to choose from in the new- order form.
-
.so_only ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are so only.
-
.sold ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are sold.
-
.st_only ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are st only.
-
.states_for_select ⇒ Array<Array(String, String)>
[human_name, machine_value]pairs of every order state, sorted alphabetically by display name. -
.with_amazon_payments ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are with amazon payments.
-
.with_associations ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are with associations.
-
.with_line_items ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are with line items.
-
.with_payments ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are with payments.
-
.with_review ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are with review.
-
.without_review ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are without review.
Instance Method Summary collapse
- #accounting_hold_order? ⇒ Boolean
-
#add_customer_to_campaign ⇒ Object
After-save hook: when an order's source changes to a source that's tied to a marketing campaign, enroll the customer in that campaign so subsequent campaign emails reach them.
-
#add_item(sku, qty) ⇒ Array<Hash>?
Convenience for #add_multiple_items with a single sku/qty pair.
-
#add_multiple_items(sku_array = []) ⇒ Array<Hash>
Add a batch of SKUs to the order in one save.
-
#adjusted_actual_shipping_cost ⇒ Float
Like #calculate_actual_shipping_cost but applies per-delivery accounting adjustments (recoveries, fuel surcharge corrections).
- #all_deliveries_cancelable?(current_user = nil) ⇒ Boolean
- #all_deliveries_invoiced? ⇒ Boolean
- #all_funds_available?(ignore_cod = false) ⇒ Boolean
- #all_funds_not_available_and_shippable? ⇒ Boolean protected
-
#all_items_in_stock ⇒ Boolean
True when every line item has enough on-hand inventory at its delivery's warehouse to fulfil immediately.
-
#all_participant_ids ⇒ Array<Integer>
Every party that should see this order in their CRM activity feed: all opportunity participants plus the customer and contact.
- #all_payments_are_valid? ⇒ Boolean
- #all_rooms_not_orderable? ⇒ Boolean
-
#all_support_cases ⇒ Array<Integer>
Convenience for
support_case_ids. -
#all_uploads ⇒ ActiveRecord::Relation<Upload>
Every Upload attached anywhere in the order tree — order itself, its deliveries, and the shipments under those deliveries.
- #allows_edi_split? ⇒ Boolean
- #already_has_smartinstall_request? ⇒ Boolean
-
#amazon_buy_shipping_eligible? ⇒ Boolean
Check if order is eligible for Amazon Buy Shipping early label purchase.
-
#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.
- #any_rooms_not_orderable? ⇒ Boolean
-
#applies_for_smartinstall ⇒ Boolean
Eligibility check for the SmartInstall service add-on (paid in-home installation).
-
#apply_tier2_pricing? ⇒ Boolean
Whether or not to apply the tier2 pricing (customer discount) by default.
-
#attention_name ⇒ String?
"ATTN: …" line written on shipping labels.
-
#attention_name=(value) ⇒ Object
Setter that stores the override unless the caller is sending the literal sentinel string
"inherited_attention_name"(used by the CRM form to mean "go back to the inherited value"), in which case we leave the override untouched. -
#auto_reserve_serial_numbers ⇒ Object
Walks every line item that requires serial-number reservation (heating mats, cables) and asks the line to grab the next available serial-number block from inventory.
- #awaiting_future_deliveries? ⇒ Boolean
-
#balance(ignore_cod = false, excluding_payment: nil) ⇒ BigDecimal
Outstanding amount on the order: total of all deliveries minus whatever payments have been authorised against each delivery (capped per delivery so over-authorisation on one delivery doesn't cancel out a balance owed on another).
- #belongs_to_smartservice_group? ⇒ Boolean
-
#build_activity ⇒ Activity
Builds (but does not save) a new Activity record attached to this order with the order's primary party already populated.
-
#build_early_label_carrier_info(label_result, delivery = nil) ⇒ Hash
Build carrier info hash for early label EDI confirm (Walmart only).
-
#build_early_label_tracking_url(carrier, tracking_number) ⇒ String?
Build tracking URL for early label.
-
#calculate_actual_shipping_cost ⇒ Float
Sum of actual carrier-billed shipping cost on every invoiced delivery (i.e. what we paid the carrier, not what the customer was charged).
- #can_auto_reserve_serial_numbers? ⇒ Boolean
- #can_be_cancelled? ⇒ Boolean
- #can_be_returned? ⇒ Boolean
- #can_cr_hold?(current_user = nil) ⇒ Boolean
- #can_edit_future_release_date? ⇒ Boolean
-
#cancel_deliveries_pre_pack ⇒ Object
Cancels the in-progress packaging-estimate work on every
pre_packdelivery. -
#cancel_or_destroy ⇒ Object
Carts get destroyed (no audit trail to keep).
- #cancelable? ⇒ Boolean
-
#cannot_delete_reason(account = nil) ⇒ String?
Human-readable explanation of why an order cannot currently be deleted, or
nilwhen #ok_to_delete? would let it through. -
#cart_identifier ⇒ String
Stable identifier suitable for URLs and abandoned-cart emails: the reference number once the order has one, otherwise a
SC<id>("Shopping Cart") fallback so brand-new carts still have a slug. - #cart_or_in_shipping_estimate? ⇒ Boolean
-
#check_payments_status ⇒ void
Reconcile the order's payments with the upstream gateways (Stripe, PayPal).
-
#check_sales_rep ⇒ Object
protected
Validation callback that prevents an order from listing the same rep as both primary and secondary, and from setting a secondary rep without a primary.
- #ci_invoice_credit_order? ⇒ Boolean
-
#clear_shipped_date ⇒ Object
Resets the
shipped_datecolumn when an order needs to be reverted to a pre-shipped state (e.g. cancellation after a partial ship). - #closed_state? ⇒ Boolean
-
#company_review_url ⇒ Object
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).
-
#company_review_url_for_confirmation(email: nil) ⇒ Object
Reviews.io dynamic link for the order confirmation / thank-you page.
- #complete? ⇒ Boolean
-
#completed_regular_deliveries ⇒ Array<Delivery>
Active deliveries that have actually shipped (parcel or freight) vs ones that completed in some other way (warehouse pickup, service-only).
- #completely_shipped? ⇒ Boolean
-
#consolidate_revenue ⇒ Object
Snapshot the order's total and shipping cost at the moment the customer completed checkout.
-
#contact_combo ⇒ 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.
-
#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.
-
#contact_combo_for_select ⇒ Array<Array(String, String)>
Active contacts under this order's customer formatted for a Rails
selecthelper, with each value encoded as"Contact|<id>"so the CRM "contact" field can distinguish between picking an existing contact and typing a new name. -
#copy_customer_reps ⇒ Object
In-memory mirror of #copy_invoice_reps that pulls reps from the current customer's reps_collaboration setup.
-
#copy_invoice_reps ⇒ Object
Pulls primary / secondary / local sales-rep ids off the order's invoice and writes them onto the order itself.
-
#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.
-
#copy_shipping_reference_number_to_deliveries ⇒ Object
After-save callback: propagate the order's
shipment_reference_number(used by Amazon FBA shipments and by marketplace seller programs as the carrier BOL) onto every delivery. -
#country ⇒ Country?
The Country that owns this order's billing entity (resolved through
customer → catalog → store → country). -
#create_credit_memo ⇒ void
Spawn a credit-memo invoice off this order (e.g. when the order is being voided after invoicing) and run it through TaxJar to back out any tax already reported.
-
#create_smartfix_ticket ⇒ Object
Open a SmartFix service ticket (paid repair visit) and schedule the service-confirmation activity.
-
#create_smartguide_ticket ⇒ Object
Open a SmartGuide service ticket (consultation).
-
#create_smartinstall_ticket ⇒ Object
Open a SmartInstall service ticket against this order via the shared #new_ss_ticket helper and schedule the kickoff pre-installation phone activity.
-
#crm_link ⇒ String
CRM-facing path to this order's show page (relative URL, suitable for use in CRM emails, Slack notifications, and audit logs).
-
#currency_symbol ⇒ String
Currency symbol (
$,€,£,C$, …) for the order's currency, resolved through themoneygem's locale-aware Currency table. -
#custom_shipping_labels ⇒ Object
Pulls all custom ship labels from the order, deliveries and shipments.
- #customer_qualifies_for_free_online_shipping? ⇒ Boolean
-
#deep_dup ⇒ Order
Deep-clones the order (line items, discounts, EDI documents) but resets the identity / lifecycle columns so the duplicate can be saved as a fresh order.
-
#default_billing_emails ⇒ Array<String>
Default email recipients for billing notifications: every email contact point flagged as a billing notification channel for the customer (or their billing entity), plus the tracking email, contact emails, and any per-order overrides on
billing_emails. - #deferred_payments_captured_and_ready_for_shipping? ⇒ Boolean
- #deletable? ⇒ Boolean
- #doesnt_already_has_smartinstall? ⇒ Boolean
-
#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.
-
#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.
-
#early_label_flash_message ⇒ Hash
Generate flash messages based on early label purchase result Called by controllers after order transitions to awaiting_deliveries.
-
#early_label_purchase_enabled_for_partner? ⇒ Boolean
Checks whether the partner's orchestrator has early label purchase enabled.
-
#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.
-
#early_label_shipments_match?(delivery) ⇒ Hash
Check if current shipments match the early label shipments Used to detect if warehouse staff changed the packing.
-
#early_label_upload ⇒ Upload?
Get the early label upload (PDF) if it exists.
-
#echecks_requiring_authorization ⇒ Array<Payment>
eCheck payments on this order that the fraud-review pipeline has flagged as needing accounting review before the order can leave CR hold.
-
#edi_cancellation_reason_description ⇒ String
Customer-facing description of the EDI cancellation reason on the order.
-
#edi_cancellation_reasons ⇒ Array<Array(String, String)>
Allowed cancellation-reason codes for the order's EDI partner, formatted as
[[label, code], …]for a Rails select. -
#edi_force_price_match ⇒ Array<Array<String>>
Force every parent line item with an EDI unit cost to match it on both the MSRP price and the discounted price.
-
#edi_orchestrator ⇒ Edi::BaseOrchestrator?
The Edi::BaseOrchestrator subclass responsible for this order's EDI partner, if it has one.
- #edi_price_matcheable? ⇒ Boolean
- #editing_locked? ⇒ Boolean
-
#effective_date_for_coupon ⇒ Date
Date used by the coupon engine to evaluate "is this coupon currently valid?" against
start_date/end_datewindows. -
#email_for_order_confirmation ⇒ String?
First non-blank email for the order's confirmation flow, checking (in order): the contact/customer's stored email, then the customer's email, then the contact's account email, then the customer's account email.
-
#email_options_for_tracking_email ⇒ Array<String>
Choices for the "tracking emails" multi-select on the order form: every email the customer has on file, plus whatever's currently set on
tracking_email(which may include free-typed addresses not yet on the customer record). - #empty? ⇒ Boolean
-
#encrypted_id ⇒ String
Reversibly encrypted form of the order's database id, used in public-facing URLs (payment link, cart-recovery emails) so the id doesn't appear directly in logs or share links.
-
#errors_with_deliveries_errors ⇒ Array<String>
Combined errors from the order itself and from each of its deliveries, suitable for surfacing to the CRM "save failed" banner so users see every reason at once.
-
#estimate_next_available_date_from_out_of_stock_items ⇒ Date?
Latest "next available" date across every fully out-of-stock line item (or nil if all items have stock).
-
#find_gbraid ⇒ String?
Same fallback chain as #find_gclid for the Android-app web-to-app
gbraidtoken used by Google Ads. -
#find_gclid ⇒ String?
Best-effort Google Click ID for this order.
-
#find_oppref ⇒ String?
OpenAI Ads (ChatGPT) click identifier, captured by the Tracker into
Visit#marketing_meta['oppref'](JSONB-only — no scalar column). -
#find_wbraid ⇒ String?
Same fallback chain as #find_gclid for the iOS-app web-to-app
wbraidtoken used by Google Ads. -
#fire_early_label_edi_confirm(delivery, label_result) ⇒ Object
Fire EDI ship confirm with early label tracking info.
-
#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.
-
#fire_early_label_edi_confirm_walmart(delivery, label_result) ⇒ Object
Walmart early label ship confirm — original Walmart-specific flow.
-
#first_po_number ⇒ String?
First non-blank PO number across the order's payments.
-
#first_tracking_email ⇒ String?
Single-string variant of #tracking_email (which is an array column).
-
#fix_future_release_date ⇒ Hash
Reconciles
requested_ship_on_or_after,future_release_date, andrequested_ship_beforeso they don't contradict each other: bumpsfuture_release_dateforward to honour the ship-on-or-after date, and clearsrequested_ship_beforewhen the user explicitly sets a future-release date past it. -
#formatted_po_number ⇒ String
po_numberwith the customer's preferred prefix applied (e.g."PO# 12345"vs bare"12345"), driven bycustomer.include_po_prefix?. - #fully_funded_by_advance_replacement? ⇒ Boolean
- #funded_by_advance_replacement? ⇒ Boolean
- #funded_by_cod? ⇒ Boolean
- #funds_available_and_ready_for_warehouse? ⇒ Boolean protected
-
#generate_spiff_training_activity ⇒ Activity
Schedules a 7-day-out follow-up training activity for the primary sales rep so they walk a SPIFF-eligible customer through their first WarmlyYours order.
-
#get_expected_ship_date_time ⇒ Time
Estimated delivery time = scheduled ship time + carrier-committed transit days from the selected shipping option.
-
#get_scheduled_ship_date_time ⇒ Time
When the order is expected to be picked up by the carrier.
- #has_authorized_payment? ⇒ Boolean
- #has_committed_serial_number_reservations? ⇒ Boolean
- #has_custom_packing_slip? ⇒ Boolean
-
#has_early_purchased_label? ⇒ Boolean
Check if order has an active (non-voided) early-purchased label.
- #has_incomplete_reservations? ⇒ Boolean
- #has_selected_heated_items? ⇒ Boolean
- #has_shipping_method? ⇒ Boolean
- #has_unreserved_line_items? ⇒ Boolean
- #has_web_rooms_needing_installation_plans? ⇒ Boolean
-
#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.
-
#hold_order_reasons ⇒ Array<String>
Human-readable list of reasons this order is currently being held by the CR/fraud/EDI pipeline — every line item the CRM hold-page surfaces to the user.
- #hold_orders? ⇒ Boolean
-
#human_state_name ⇒ String
Human-friendly version of the state machine state.
- #includes_schedulable_service? ⇒ Boolean
-
#inherited_attention_name ⇒ String?
Default attention-line value derived from the shipping address's person name, but only when the customer is a person (not a company); otherwise the company name should be used as the line 1 name and ATTN can stay blank.
-
#installation_country_iso ⇒ String?
ISO 2-letter country code of the installation site (US, CA, …).
-
#installation_country_iso3 ⇒ String?
ISO 3-letter country code of the installation site (USA, CAN, …).
- #installation_is_within_range? ⇒ Boolean
-
#installation_postal_code ⇒ String?
Postal code where the order will be installed (from the linked opportunity's installation address, not the shipping address).
-
#installation_state_code ⇒ String?
State/province code of the installation site (see #installation_postal_code).
-
#invoice_balance ⇒ BigDecimal
Sum of Invoice#balance across every invoice on the order — the outstanding A/R amount.
- #is_crm? ⇒ Boolean
- #is_edi_order? ⇒ Boolean
- #is_fba? ⇒ Boolean
- #is_from_myprojects? ⇒ Boolean
- #is_marketing_order? ⇒ Boolean
- #is_online? ⇒ Boolean
-
#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.
- #is_regular_order? ⇒ Boolean
- #is_remote_service? ⇒ Boolean
- #is_rma_replacement? ⇒ Boolean
- #is_rma_return? ⇒ Boolean
- #is_sales_order? ⇒ Boolean
- #is_smartfit_service? ⇒ Boolean
- #is_smartfix_service? ⇒ Boolean
- #is_smartguide_service? ⇒ Boolean
- #is_smartinstall_service? ⇒ Boolean
- #is_store_transfer? ⇒ Boolean
- #is_subject_to_minimum_qty_rules? ⇒ Boolean
- #is_tech_order? ⇒ Boolean
- #is_warehouse_pickup? ⇒ Boolean
-
#job_name ⇒ String?
Best-known job name for this order: the originating quote's opportunity name, falling back to the order's own opportunity name, or
nilif the order isn't linked to either. -
#local_sales_rep ⇒ Employee?
Local (on-site) sales rep on the order.
-
#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.
-
#mark_serial_coupons_as_used ⇒ Object
Burn any single-use coupon serial numbers that were applied to this order so they can't be redeemed again.
-
#marketplace_early_label_eligible? ⇒ Boolean
Check if order is eligible for marketplace early label purchase (Walmart SWW or Amazon Buy Shipping).
-
#merge_shipper_api_log(shipper) ⇒ Object
Merge raw HTTP API call logs from the shipper into the order's API log.
-
#minimum_order_quantity_violations ⇒ Array<Alert>
MOQ (minimum-order-quantity) alerts triggered by the current line items — e.g.
-
#move_deliveries_from_quoting! ⇒ Object
Promote any deliveries currently in
quoting(rate-shopping) state intoready_to_ship, copying the order-level shipment / label / future-release fields onto each delivery first. -
#move_deliveries_to_quoting! ⇒ Object
Force every delivery on the order back into the
quotingstate so the rate-shop can be re-run from scratch (e.g. after a shipping address change). -
#move_service_case_from_pending_service_payment ⇒ Object
When a SmartService order's payment finally clears, flip the associated SmartService support case out of
pending_service_paymentand into thecase_openstate so the service team can pick it up. -
#name ⇒ String
Human label like
"Order #SO123456 (PO# 0042)"(or withPO#sfor multi-PO orders). -
#need_to_recalculate_shipping ⇒ Object
Flag the order so the next save re-runs the carrier rate-shop and re-detects shipping options.
-
#needs_attention_issues ⇒ Array<String>
Human-readable list of mismatches between the order's state and its deliveries' states (e.g. order is
awaiting_deliveriesbut some deliveries are stillquoting). - #needs_spiff_training? ⇒ Boolean
-
#new_customer_online_order ⇒ Boolean
True when this is a brand-new online customer's first order — a key segmentation flag for new-customer email flows and acquisition reporting.
-
#new_customer_order ⇒ Boolean
True when this is the customer's only non-management-held order, i.e.
-
#new_ss_ticket(service, activity_type) ⇒ Object
Shared backend for #create_smartinstall_ticket, #create_smartfix_ticket, and #create_smartguide_ticket: builds the SmartService support case, attaches participants and rooms, and schedules the kickoff activity.
-
#next_six_months_with_end_dates ⇒ Array<Array(String, String)>
Six-month label/value pairs for end-of-month dates starting this month —
[["October 2026", "2026-10-31"], …]. -
#next_warehouse_ship_date(now: Time.current) ⇒ Date?
Earliest date the warehouse will hand the order to a carrier, honoring the same-day cutoff and the company holiday calendar.
-
#notify_pre_pack_cancellation ⇒ void
When an order is cancelled while it still has pre-pack deliveries (waiting on warehouse packing data), email each affected warehouse so they don't waste pack time on a dead order.
- #ok_to_delete?(account = nil) ⇒ Boolean
- #online_order? ⇒ Boolean
-
#open_activities_counter ⇒ Integer
Number of open follow-up activities tied to this order, for the CRM's per-order activity badge.
-
#opportunities ⇒ Array<Opportunity>
Every opportunity tied to the order — directly via #opportunity and indirectly via each RoomConfiguration's opportunity.
-
#opportunity_name ⇒ String?
Name of the linked Opportunity, or
nilif the order isn't tied to one. -
#opportunity_name=(val) ⇒ Object
Setter that finds or creates an Opportunity on the customer with the given name and links the order to it.
- #order_closed? ⇒ Boolean
-
#order_emails ⇒ Array<String>
Every email address relevant to this order: the customer + all their contacts, the transmission email, the tracking email, the bound contact's emails, and any per-order billing overrides.
-
#order_type_title ⇒ String
Human label for
order_type(e.g.'SO'→'Sales Order'). - #partially_shipped? ⇒ Boolean
-
#participants_options_for_select ⇒ Array<Array(String, Integer)>?
[name, party_id]pairs of every opportunity participant on this order, suitable for a Railsselecthelper. -
#party_for_order_confirmation ⇒ Party
The party the order-confirmation email goes to: the contact when one is bound, else the customer.
- #pay_on_spiff? ⇒ Boolean
-
#payment_method ⇒ String?
Category code (
Payment::CREDIT_CARD,Payment::PO, …) of the first authorised payment on the order — a quick "how was this paid?" hint. -
#payment_method_requires_authorization? ⇒ Boolean
True when any payment on the order is awaiting human authorization (e.g. unreviewed eCheck or fraud-flagged card).
-
#payment_options(source) ⇒ Array<String>
Allowed payment-method codes for this order, ordered by the canonical sort for the given UI surface.
-
#payments_requiring_authorization ⇒ Array<Payment>
Authorised payments that haven't yet been management-approved AND whose authorisation-review pipeline has flagged them as requiring sign-off (any reason — fraud risk, eCheck, large amount, etc.).
-
#payments_with_fraud_report ⇒ Array<Payment>
Non-voided payments that carry a fraud report and haven't been explicitly approved by management.
- #paypal_invoices_paid? ⇒ Boolean
-
#po_number(include_po_prefix: false, limit: nil) ⇒ String
Comma-joined PO number(s) across the order's authorised / captured payments.
-
#po_number_barcode(file_path: nil) ⇒ String
Instance-level wrapper around Order.po_number_barcode that uses this order's PO number.
-
#po_numbers ⇒ Array<String>
Distinct PO numbers across all payments on the order.
- #potential_fraud? ⇒ Boolean
-
#potential_fraud_reasons ⇒ Array<String>
Aggregate list of fraud-report reasons across every unapproved payment on the order.
-
#precreated_rma_credit_available ⇒ BigDecimal
Remaining RMA credit pre-authorised against this order, computed as
line_total_plus_tax − already-applied RMA credit payments. - #prevent_recalculate_shipping? ⇒ Boolean
-
#primary_party ⇒ Party
The party who's the "face" of this order — the contact when one is bound (e.g. dealer with multiple contacts), otherwise the customer.
-
#primary_rep_name ⇒ String
Display string for the primary sales rep, falling back to the literal
"Unassigned"so list views never show a blank cell. -
#primary_sales_rep ⇒ Employee?
Primary sales rep on the order.
-
#product_review_url ⇒ String?
Reviews.io product-review landing URL pre-filled with this order's customer/email/skus.
-
#prune_cart_rooms ⇒ Object
Drop room configurations from the cart whose line items have all been removed.
-
#public_path ⇒ String?
Public-portal path to the order ("My Account" view) when the customer has a portal account, otherwise
nil. -
#public_payment_link ⇒ String
Public, tokenised URL a customer can hit to pay an order without signing in.
-
#purchase_early_label_if_requested ⇒ Hash?
Purchase shipping label early if requested and order is eligible Called from after_transition to awaiting_deliveries.
-
#quotes ⇒ Array<Quote>
Every quote tied to the order — directly via #quote and indirectly via the most recent completed quote on each linked room configuration.
- #ready_for_pending_payment? ⇒ Boolean
- #ready_for_service? ⇒ Boolean
- #ready_for_shipping? ⇒ Boolean
- #ready_for_warehouse? ⇒ Boolean
-
#recipient_name ⇒ String?
Best-known recipient name for emails and shipping labels: company name on the shipping address, then the person name on the address, finally the customer's full name.
-
#related_activities ⇒ ActiveRecord::Relation<Activity>
Activity records corresponding to #related_activities_ids.
-
#related_activities_ids ⇒ Array<Integer>
Combined ids of every Activity hung directly off this order plus those hung off its deliveries — the ids the CRM "all activity for this order" feed should display.
-
#release_order_or_hold ⇒ Object
Release the order to the warehouse unless something requires accounting/management review first (#accounting_hold_order?), in which case the order stays on hold for manual release.
-
#remove_room_configuration(rc) ⇒ Hash
Detach a RoomConfiguration from the order, switching the order's bound opportunity/quote to whichever sibling room is left (or
nilif removing the last one). - #require_cc_for_advance_credit? ⇒ Boolean
- #requires_intervention? ⇒ Boolean
-
#reset_cart ⇒ Object
Wipe a cart back to empty: destroy all line items, quoting deliveries, and discounts, clear the shipping address, and prune any rooms that no longer have line items.
- #restricted_order_type? ⇒ Boolean
-
#reviewer ⇒ Object
Returns the reviewer info for Reviews.io invitation Prefers contact over customer if present.
-
#rma_cancel ⇒ Object
Hard-cancel an RMA replacement order: destroy all active deliveries first (so their inventory commits release) then destroy the order itself, all in one transaction.
-
#save_early_label_api_log ⇒ Object
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).
-
#schedule_follow_up_activity ⇒ Boolean
Run the FollowUpScheduler service against this order.
-
#search_text ⇒ String?
protected
Indexed search text for full-text matching: order reference, PO numbers, and RMA reference.
-
#secondary_sales_rep ⇒ Employee?
Secondary sales rep on the order.
-
#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.
-
#selection_name ⇒ String
Select-2 / Tom Select dropdown label — like #to_label but uses
"not shipped"when the order hasn't shipped yet. -
#send_back_order_notification ⇒ Object
Email the back-order team that this order moved into back-order state and needs follow-up with the customer.
-
#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.
-
#send_online_order_confirmation ⇒ Hash{Symbol => Symbol, String}
Build and send the
ONLINE_ORDER_CONFIRMemail via CommunicationBuilder, then pin the recipient address to the order'stracking_emailarray so subsequent ship/tracking emails go to the same place. -
#send_payment_automatically_authorized_notification ⇒ Object
Notify accounting that the auto-authoriser approved a payment without human review — useful for spot-checking the rules engine.
-
#send_profit_review_notification ⇒ Object
Email the management profit-review queue when this order's margin falls below the configured threshold and needs sign-off.
-
#send_ready_for_pickup_email ⇒ Hash{Symbol => Symbol, String}
Send the warehouse-pickup-ready email (
ORDER_PICKUPtemplate) used for "your order is ready at the will-call counter" notices. -
#send_release_authorization_notification ⇒ Object
Email management when an order needs explicit release-from-CR-hold authorisation (e.g. fraud-flagged payment, large order, etc.).
-
#send_request_carrier_assignment_notification ⇒ Object
Notify dispatch when an order is waiting on a manual carrier assignment (rate-shop returned no eligible options).
-
#send_tracking_email ⇒ Hash{Symbol => Symbol, String}
Send the standard
ORDER_TRACKINGemail with the carrier's tracking link. -
#send_tracking_template_email(template_code) ⇒ Hash{Symbol => Symbol, String}
Generic helper that drives both #send_tracking_email and #send_ready_for_pickup_email: builds the communication via CommunicationBuilder for the named system template and sends it to every address in
tracking_email. - #service_only_order? ⇒ Boolean
-
#set_currency ⇒ Object
protected
Before-validation callback: pin the order's currency to the customer's store currency when not already set, so currency never silently defaults to USD on non-US orders.
-
#set_custom_order_agreement ⇒ Object
Checks for the presence of custom products in excess of $2k.
-
#set_default_tracking_email(force: false) ⇒ Object
Populate
tracking_emailwith addresses extracted by DefaultTrackingEmailExtractor (customer + contact + opportunity participants). -
#set_min_profit_markup ⇒ Object
Before-validation callback: pin the order's minimum-profit markup percentage.
-
#set_opportunity_source ⇒ Object
After-save hook: when the order's source changes (and isn't the generic "unknown source"), propagate that source up to its opportunity.
-
#set_shipped_date ⇒ Object
Stamp the order's
shipped_datewith today, but only when blank — once shipped, the date is canonical and shouldn't drift on a re-save. -
#ship_from_attributes(delivery = nil) ⇒ Hash
Build the "ship from" address/phone/email/attention block for a carrier API call.
-
#ship_to_attributes ⇒ Hash
Build the "ship to" hash for carrier API calls: address, phone, email, and ATTN/name lines, with sensible cascading fallbacks (
attention_name→ address person name → literal"Customer", phone falls through customer's phones to the rep's, etc.) so the carrier never receives a blank field. -
#ship_weight ⇒ Float
Aggregate physical ship weight across all non-shipping line items, floored at 0.1 lb so carrier rate APIs that reject zero weights don't blow up.
-
#shipping_address_valid_and_not_po_box_if_present(rate_shopping = false) ⇒ Boolean
Validates the shipping address: must be present, must pass its own AR validations, and must not be a PO box for the chosen carrier (FedEx/UPS reject them; USPS accepts them).
- #shipping_cutoff_advance_order? ⇒ Boolean
- #shipping_cutoff_next_day? ⇒ Boolean
- #shipping_cutoff_same_day? ⇒ Boolean
-
#shipping_cutoff_status(now: Time.current) ⇒ Symbol
Whether the warehouse can still ship this order today.
-
#shipping_date_warnings ⇒ Object
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.
- #should_commit_stock? ⇒ Boolean
-
#smartservice_ticket ⇒ SupportCase?
The SmartService ticket currently waiting on payment for this order, or
nilif there isn't one. -
#sms_enabled_numbers ⇒ Array<String>
SMS-capable phone numbers for every order participant (customer, contact, opportunity participants), formatted in the E.164-style our SMS provider expects.
-
#sms_messages ⇒ ActiveRecord::Relation<SmsMessage>
SMS conversation thread tied to this order — every inbound / outbound message exchanged with any participant phone returned by #sms_enabled_numbers.
-
#spiff_reward ⇒ String
Eligible SPIFF reward amount formatted as
"%.2f". -
#state_description(describe_state = nil) ⇒ String?
User-facing description for a state name, optionally for a state other than the current one.
-
#state_list ⇒ Array<Symbol>
The ordered set of states this order can be in given its type and current data.
-
#stock_status ⇒ Symbol
Combined inventory status across all line items:
:ok,:partial, or:none. - #stop_for_pre_pack? ⇒ Boolean
- #stop_for_profit_review? ⇒ Boolean
-
#store ⇒ Store?
The Store this order is fulfilled from.
-
#store_additional_early_label_pdfs(additional_labels) ⇒ Object
Persist the secondary label PDFs returned by a multi-package marketplace label purchase.
-
#store_amazon_label_pdf_atomic(label_result, tracking_number, marketplace) ⇒ Object
Amazon: label data is returned inline — store it and verify persistence.
-
#store_early_label_pdf(label_data, tracking_number) ⇒ Upload?
Store early label PDF on the order.
-
#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.
-
#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.
-
#suggested_shipments ⇒ ActiveRecord::Relation?
Shipments still in the
suggestedstate — i.e. -
#support_case_id ⇒ Integer?
Single support-case binding for the CRM order-setup form's Tom Select picker, mirroring Rma#support_case_id.
- #support_case_id=(id) ⇒ Object
-
#support_case_ref ⇒ String
Comma-separated list of attached support case numbers.
-
#support_case_ref=(support_case_ref) ⇒ Object
Setter that resolves a comma-separated list of case numbers into the matching support-case ids and writes them to
support_case_ids. -
#suppress_edi_duplicate_warning? ⇒ Boolean
Suppresses duplicate order warnings for off-book delivery arrangements.
-
#sync_opportunity ⇒ Object
After-save hook: tell the linked opportunity to re-evaluate its state machine (the opportunity may need to advance from "active" to "sold" once the order is invoiced).
- #terms_available? ⇒ Boolean protected
-
#to_label ⇒ String
Compact label string used by autocomplete dropdowns and SMS threads —
[REF#] Customer (shipped-date). -
#to_liquid ⇒ Liquid::OrderDrop
Wrap the order in a Liquid::OrderDrop so it can be safely rendered inside customer-facing email templates without exposing internal model methods.
- #to_s ⇒ String
-
#total_cod ⇒ BigDecimal, Float
Cash-On-Delivery amount the carrier should collect from the consignee.
-
#total_money ⇒ String
Order total formatted as a
"%.2f"string for display where the template needs a fixed two-decimal value (CSV exports, EDI payloads). -
#total_payments_authorized ⇒ BigDecimal
Sum of authorised payment amounts across deliveries, capping each delivery at its own total so over-authorisation on one delivery doesn't shift coverage to another.
- #track_profit? ⇒ Boolean
-
#tracking_email_address ⇒ String
Inbound-email address that uniquely identifies this order — when a tracking-email reply lands at this address, ActionMailbox decrypts the id and re-attaches the message to the order.
-
#uncommit_undelivered_line_items ⇒ Object
Release inventory commits for line items that aren't attached to a delivery (typically left over after a delivery is destroyed mid-flow).
-
#unfulfilled_dropship_items ⇒ Boolean
True when any dropship line still lacks a fully-receipted supplier purchase-order item — i.e.
-
#update_customer_status ⇒ Object
protected
After-save callback: re-evaluate the customer's lifecycle state (lead → prospect → customer → returning customer) now that this order may have moved them up or down.
-
#update_linked_po_if_st ⇒ Boolean
For store-transfer orders, sync the linked PurchaseOrder record to the order's first delivery so accounting sees the same dates, quantities, and costs on both sides of the transfer.
-
#update_sales_support_rep(new_sales_support_rep_id, new_commission_date = nil) ⇒ Boolean
Reassign the sales-support rep on the order (and optionally update the support-commission date).
-
#versions_for_audit_trail(_params = {}) ⇒ ActiveRecord::Relation<RecordVersion>
Aggregate RecordVersion (PaperTrail) rows for the order's full audit trail — the order itself plus every line item, delivery, and discount that points back to it.
-
#void_credit_rma_item ⇒ Object
Void every RMA credit-item linked to this order's line items.
-
#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.
-
#void_early_label_if_exists ⇒ Hash?
Void early-purchased label when order is pulled back from awaiting_deliveries Called from before_transition to cancelled/fraudulent/in_cr_hold.
-
#void_early_label_upload ⇒ Object
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).
-
#void_payments(report_fraud = false) ⇒ Object
Void every authorised payment on the order (and refuse to act on captured ones — those need manual intervention).
-
#walmart_sww_eligible? ⇒ Boolean
Check if order is eligible for Walmart Ship with Walmart early label purchase.
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
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
Methods inherited from ApplicationRecord
ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation
Methods included from Schedulable
Methods included from Models::AfterCommittable
Methods included from Models::EventPublishable
Instance Attribute Details
#customer_id ⇒ Object (readonly)
Customer can only have one cart per Contact
Validations (if => #cart? ):
- Uniqueness ({ scope: %i[state contact_id], message: 'Only one cart is allowed per user' })
340 |
# File 'app/models/order.rb', line 340 validates :customer_id, uniqueness: { scope: %i[state contact_id], message: 'Only one cart is allowed per user' }, if: :cart? |
#do_not_detect_shipping ⇒ Object
Returns the value of attribute do_not_detect_shipping.
359 360 361 |
# File 'app/models/order.rb', line 359 def do_not_detect_shipping @do_not_detect_shipping end |
#do_not_set_totals ⇒ Object
Returns the value of attribute do_not_set_totals.
359 360 361 |
# File 'app/models/order.rb', line 359 def do_not_set_totals @do_not_set_totals end |
#early_label_purchase_result ⇒ Object
Returns the value of attribute early_label_purchase_result.
359 360 361 |
# File 'app/models/order.rb', line 359 def early_label_purchase_result @early_label_purchase_result end |
#from_store_id ⇒ Object (readonly)
344 |
# File 'app/models/order.rb', line 344 validates :from_store_id, :to_store_id, presence: { on: :create, if: proc { |o| o.order_type == STORE_TRANSFER } } |
#full_shipping_address_validation ⇒ Object
Returns the value of attribute full_shipping_address_validation.
359 360 361 |
# File 'app/models/order.rb', line 359 def full_shipping_address_validation @full_shipping_address_validation end |
#is_www ⇒ Object
Returns the value of attribute is_www.
359 360 361 |
# File 'app/models/order.rb', line 359 def is_www @is_www end |
#is_www_ship_by_zip ⇒ Object
Returns the value of attribute is_www_ship_by_zip.
359 360 361 |
# File 'app/models/order.rb', line 359 def is_www_ship_by_zip @is_www_ship_by_zip end |
#max_discount_override ⇒ Object (readonly)
347 |
# File 'app/models/order.rb', line 347 validates :max_discount_override, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 100, allow_nil: true } |
#order_type ⇒ Object (readonly)
341 |
# File 'app/models/order.rb', line 341 validates :order_type, presence: true |
#reference_number ⇒ Object (readonly)
343 |
# File 'app/models/order.rb', line 343 validates :reference_number, presence: { unless: :cart_or_in_shipping_estimate? } |
#shipping_phone ⇒ Object (readonly)
skip this validation for EDI orders, let it ride
Validations (unless => #is_edi_order? ):
- Phone_format
345 |
# File 'app/models/order.rb', line 345 validates :shipping_phone, phone_format: true, unless: :is_edi_order? |
#to_store_id ⇒ Object (readonly)
344 |
# File 'app/models/order.rb', line 344 validates :from_store_id, :to_store_id, presence: { on: :create, if: proc { |o| o.order_type == STORE_TRANSFER } } |
#tracking_email ⇒ Object (readonly)
346 |
# File 'app/models/order.rb', line 346 validates :tracking_email, email_format: true |
#update_shipping_address_with_contact ⇒ Object
Returns the value of attribute update_shipping_address_with_contact.
359 360 361 |
# File 'app/models/order.rb', line 359 def update_shipping_address_with_contact @update_shipping_address_with_contact end |
Class Method Details
.active ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are active. Active Record Scope
504 |
# File 'app/models/order.rb', line 504 scope :active, -> { where.not(state: %w[pending cancelled fraudulent cart in_shipping_estimate]) } |
.active_spiffs ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are active spiffs. Active Record Scope
538 |
# File 'app/models/order.rb', line 538 scope :active_spiffs, -> { where(spiff_state: %w[awaiting_payment paid]) } |
.all_awaiting_deliveries ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are all awaiting deliveries. Active Record Scope
455 |
# File 'app/models/order.rb', line 455 scope :all_awaiting_deliveries, -> { where(state: SHIPPING_STATES) } |
.all_order_types_for_select ⇒ Array<Array(String, String)>
Every order-type code the system understands, for filter
dropdowns in admin search.
1561 1562 1563 |
# File 'app/models/order.rb', line 1561 def self.all_order_types_for_select ALL_ORDER_TYPES.map { |code, desc| [desc, code] } end |
.assigned_to_rep ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are assigned to rep. Active Record Scope
483 484 485 486 487 488 489 490 491 492 493 494 495 |
# File 'app/models/order.rb', line 483 scope :assigned_to_rep, ->(party_or_ids) { # Guard against nil/blank input — without this, Rails turns the OR # branches into `IS NULL` predicates, which would match every order # missing any rep column. ids = Array.wrap(party_or_ids).compact next none if ids.empty? where.any_of( { primary_sales_rep_id: ids }, { secondary_sales_rep_id: ids }, { local_sales_rep_id: ids } ) } |
.awaiting_completed_installation_plans ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are awaiting completed installation plans. Active Record Scope
536 |
# File 'app/models/order.rb', line 536 scope :awaiting_completed_installation_plans, -> { where(state: :awaiting_completed_installation_plans) } |
.awaiting_payment_spiff ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are awaiting payment spiff. Active Record Scope
535 |
# File 'app/models/order.rb', line 535 scope :awaiting_payment_spiff, -> { where(spiff_state: 'awaiting_payment') } |
.back_order ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are back order. Active Record Scope
525 |
# File 'app/models/order.rb', line 525 scope :back_order, -> { where(state: :crm_back_order) } |
.by_company_id ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are by company id. Active Record Scope
460 |
# File 'app/models/order.rb', line 460 scope :by_company_id, ->(company_id) { joins(customer: { catalog: :store }).where(stores: { company_id: }) } |
.by_primary_rep_id ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are by primary rep id. Active Record Scope
461 |
# File 'app/models/order.rb', line 461 scope :by_primary_rep_id, ->(rep_id) { joins(:customer).where(parties: { primary_sales_rep_id: rep_id }) } |
.by_report_grouping ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are by report grouping. Active Record Scope
551 |
# File 'app/models/order.rb', line 551 scope :by_report_grouping, ->(report_grouping) { joins(:customer).where(parties: { report_grouping: report_grouping }) } |
.by_report_grouping_all_when_nil ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are by report grouping all when nil. Active Record Scope
550 |
# File 'app/models/order.rb', line 550 scope :by_report_grouping_all_when_nil, ->(report_grouping) { report_grouping.present? ? joins(:customer).where(parties: { report_grouping: report_grouping }) : joins(:customer) } |
.by_sales_rep_id ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are by sales rep id. Active Record Scope
553 554 555 556 557 558 559 560 561 |
# File 'app/models/order.rb', line 553 scope :by_sales_rep_id, lambda { |sales_rep_id| next none if sales_rep_id.blank? joins(:customer).where.any_of( { parties: { primary_sales_rep_id: sales_rep_id } }, { parties: { secondary_sales_rep_id: sales_rep_id } }, { parties: { local_sales_rep_id: sales_rep_id } } ) } |
.by_store ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are by store. Active Record Scope
500 |
# File 'app/models/order.rb', line 500 scope :by_store, ->(store) { where(currency: store.currency) } |
.by_store_id ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are by store id. Active Record Scope
459 |
# File 'app/models/order.rb', line 459 scope :by_store_id, ->(store_id) { joins(customer: :catalog).where(catalogs: { store_id: }) } |
.cancelled ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are cancelled. Active Record Scope
503 |
# File 'app/models/order.rb', line 503 scope :cancelled, -> { where(state: 'cancelled') } |
.carts ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are carts. Active Record Scope
510 |
# File 'app/models/order.rb', line 510 scope :carts, -> { where(state: :cart) } |
.co_only ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are co only. Active Record Scope
528 |
# File 'app/models/order.rb', line 528 scope :co_only, -> { where(order_type: CREDIT_ORDER) } |
.contains_coupon_ids ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are contains coupon ids. Active Record Scope
544 |
# File 'app/models/order.rb', line 544 scope :contains_coupon_ids, ->(coupon_ids) { where("EXISTS (select discounts.id from discounts where discounts.coupon_id IN (?) and discounts.itemizable_type = 'Order' and discounts.itemizable_id = orders.id)", coupon_ids) } |
.contains_item_ids ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are contains item ids. Active Record Scope
541 542 543 |
# File 'app/models/order.rb', line 541 scope :contains_item_ids, ->(item_ids) { where("EXISTS (select li.id from line_items li inner join catalog_items ci on ci.id= li.catalog_item_id inner join store_items si on si.id = ci.store_item_id where li.resource_id = orders.id and li.resource_type = 'Order' and si.item_id IN (?))", item_ids) } |
.contains_service_items ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are contains service items. Active Record Scope
545 |
# File 'app/models/order.rb', line 545 scope :contains_service_items, -> { contains_item_ids(Item.services.ids) } |
.correctly_packaged ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are correctly packaged. Active Record Scope
513 |
# File 'app/models/order.rb', line 513 scope :correctly_packaged, -> { where('orders.incorrectly_package_ups_canada_order IS NOT TRUE OR (orders.incorrectly_package_ups_canada_order IS TRUE and orders.incorrectly_packaged_ups_canada_order_fixed IS TRUE)') } |
.custom_order_agreement_statuses_for_select ⇒ Array<Array(String, String)>
[label, value] pairs of every Custom-Order-Agreement status
for the CRM dropdown.
1544 1545 1546 |
# File 'app/models/order.rb', line 1544 def self.custom_order_agreement_statuses_for_select Order.custom_order_agreement_statuses.keys.map { |e| [e.humanize, e] } end |
.customer_reference_search ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are customer reference search. Active Record Scope
566 |
# File 'app/models/order.rb', line 566 scope :customer_reference_search, ->(q) { where(Order[:customer_reference].matches("%#{q}%")).order([Arel.sql('orders.customer_reference <-> ?'), q]) } |
.draft_spiff ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are draft spiff. Active Record Scope
534 |
# File 'app/models/order.rb', line 534 scope :draft_spiff, -> { where.not(spiff_enrollment_id: nil).where(spiff_state: 'draft') } |
.edi_orders ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are edi orders. Active Record Scope
565 |
# File 'app/models/order.rb', line 565 scope :edi_orders, -> { where.not(orders: { edi_transaction_id: nil }) } |
.future_release ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are future release. Active Record Scope
552 |
# File 'app/models/order.rb', line 552 scope :future_release, -> { where.not(future_release_date: nil).where.not(order_type: 'CO').where.not(state: Order::CLOSED_STATES) } |
.google_conversion_acknowledged ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are google conversion acknowledged. Active Record Scope
475 476 477 478 |
# File 'app/models/order.rb', line 475 scope :google_conversion_acknowledged, -> { jsonb_where(column_name: :google_conversion_meta, json_keys: %w[result], operator: :eq, value: 'reported') .or(jsonb_where(column_name: :google_conversion_meta, json_keys: %w[result status], operator: :eq, value: 'reported')) } |
.google_conversion_attempted ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are google conversion attempted. Active Record Scope
469 470 471 |
# File 'app/models/order.rb', line 469 scope :google_conversion_attempted, -> { jsonb_where_exists(column_name: :google_conversion_meta, key: :attempted_at) } |
.has_manual_preset_form ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are has manual preset form. Active Record Scope
546 |
# File 'app/models/order.rb', line 546 scope :has_manual_preset_form, -> { where("EXISTS(select 1 from uploads where uploads.resource_type = 'Order' and uploads.resource_id = orders.id and uploads.category = 'manual_smart_preset_form')") } |
.held ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are held. Active Record Scope
524 |
# File 'app/models/order.rb', line 524 scope :held, -> { where(state: :in_cr_hold) } |
.held_sales_orders ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are held sales orders. Active Record Scope
540 |
# File 'app/models/order.rb', line 540 scope :held_sales_orders, -> { sales_orders.where(state: %w[pending pending_payment pending_release_authorization in_cr_hold]) } |
.in_progress ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are in progress. Active Record Scope
514 |
# File 'app/models/order.rb', line 514 scope :in_progress, -> { so_only.where.not(state: %w[cart invoiced cancelled fraudulent]) } |
.in_state ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are in state. Active Record Scope
458 |
# File 'app/models/order.rb', line 458 scope :in_state, ->(state) { where(state:) } |
.incorrectly_packaged_ups_canada_order ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are incorrectly packaged ups canada order. Active Record Scope
512 |
# File 'app/models/order.rb', line 512 scope :incorrectly_packaged_ups_canada_order, -> { where('orders.incorrectly_package_ups_canada_order IS TRUE and orders.incorrectly_packaged_ups_canada_order_fixed IS NOT TRUE') } |
.invoiced ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are invoiced. Active Record Scope
507 |
# File 'app/models/order.rb', line 507 scope :invoiced, -> { where(state: 'invoiced') } |
.like_lookup ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are like lookup. Active Record Scope
548 |
# File 'app/models/order.rb', line 548 scope :like_lookup, ->(q) { where('orders.reference_number like :term', { term: "%#{q}%" }) } |
.limit_to_fba ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are limit to fba. Active Record Scope
549 |
# File 'app/models/order.rb', line 549 scope :limit_to_fba, -> { joins(customer: :billing_address).where.any_of({ parties: { id: CustomerConstants::AMAZON_COM_ID } }, { addresses: { party_id: CustomerConstants::AMAZON_COM_ID } }) } |
.locked ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are locked. Active Record Scope
562 |
# File 'app/models/order.rb', line 562 scope :locked, -> { where(state: LOCKED_STATES) } |
.lookup ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are lookup. Active Record Scope
547 |
# File 'app/models/order.rb', line 547 scope :lookup, ->(q) { where(orders: { reference_number: q }) } |
.mo_only ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are mo only. Active Record Scope
529 |
# File 'app/models/order.rb', line 529 scope :mo_only, -> { where(order_type: MARKETING_ORDER) } |
.most_recent_first ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are most recent first. Active Record Scope
523 |
# File 'app/models/order.rb', line 523 scope :most_recent_first, -> { order('orders.created_at DESC') } |
.non_carts ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are non carts. Active Record Scope
511 |
# File 'app/models/order.rb', line 511 scope :non_carts, -> { where(state: NON_CART_STATES) } |
.non_credit ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are non credit. Active Record Scope
509 |
# File 'app/models/order.rb', line 509 scope :non_credit, -> { where.not(order_type: CREDIT_ORDER) } |
.not_cancelled ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are not cancelled. Active Record Scope
505 |
# File 'app/models/order.rb', line 505 scope :not_cancelled, -> { where.not(state: 'cancelled') } |
.not_in_pre_pack ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are not in pre pack. Active Record Scope
515 |
# File 'app/models/order.rb', line 515 scope :not_in_pre_pack, -> { so_only.where.not(state: %w[pre_pack]) } |
.not_open_for_change ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are not open for change. Active Record Scope
498 |
# File 'app/models/order.rb', line 498 scope :not_open_for_change, -> { where.not(state: OPEN_FOR_CHANGE_STATES) } |
.not_partially_invoiced ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are not partially invoiced. Active Record Scope
508 |
# File 'app/models/order.rb', line 508 scope :not_partially_invoiced, -> { where.not(state: %w[invoiced partially_invoiced]) } |
.not_processing_deliveries ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are not processing deliveries. Active Record Scope
506 |
# File 'app/models/order.rb', line 506 scope :not_processing_deliveries, -> { where.not(state: 'processing_deliveries') } |
.not_sold ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are not sold. Active Record Scope
457 |
# File 'app/models/order.rb', line 457 scope :not_sold, -> { where.not(state: SOLD_STATES) } |
.open_for_change ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are open for change. Active Record Scope
496 |
# File 'app/models/order.rb', line 496 scope :open_for_change, -> { where(state: OPEN_FOR_CHANGE_STATES) } |
.open_for_tax_update ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are open for tax update. Active Record Scope
497 |
# File 'app/models/order.rb', line 497 scope :open_for_tax_update, -> { where(state: OPEN_FOR_TAX_CHANGE_STATES) } |
.order_count(company_id = nil, where_conditions = nil, where_not_conditions = nil) ⇒ Integer
Counts orders matching the given filters. The customer→catalog
→store join is mandatory because all callers want the
company-aware count, never the raw global count.
1573 1574 1575 1576 1577 1578 1579 |
# File 'app/models/order.rb', line 1573 def self.order_count(company_id = nil, where_conditions = nil, where_not_conditions = nil) o = Order.joins(customer: { catalog: :store }).order('orders.id') o = o.by_company_id(company_id) unless company_id.nil? o = o.where(where_conditions) unless where_conditions.nil? o = o.where.not(where_not_conditions) unless where_not_conditions.nil? o.count end |
.order_type_from_opportunity(opportunity) ⇒ String
Derive an order_type code from an Opportunity's type by
appending 'O' (so S + O = 'SO' sales order, T + O =
'TO' tech order).
5838 5839 5840 |
# File 'app/models/order.rb', line 5838 def self.order_type_from_opportunity(opportunity) "#{opportunity.opportunity_type}O" end |
.order_type_from_quote(quote) ⇒ String
Map a Quote type code to the matching order_type code: a
tech quote (TQ) becomes a tech order (TO), a marketing
quote (MQ) becomes a marketing order (MO), etc. Defaults to
'SO' for the standard sales-quote → sales-order path.
5828 5829 5830 |
# File 'app/models/order.rb', line 5828 def self.order_type_from_quote(quote) { TQ: 'TO', MQ: 'MO', SQ: 'SO' }[quote.quote_type.to_sym] || 'SO' end |
.paid_spiff ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are paid spiff. Active Record Scope
537 |
# File 'app/models/order.rb', line 537 scope :paid_spiff, -> { where(spiff_state: 'paid') } |
.pending ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are pending. Active Record Scope
516 |
# File 'app/models/order.rb', line 516 scope :pending, -> { where(state: 'pending') } |
.pending_payment ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are pending payment. Active Record Scope
518 |
# File 'app/models/order.rb', line 518 scope :pending_payment, -> { where(state: 'pending_payment') } |
.pending_payment_and_unpaid_invoices ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are pending payment and unpaid invoices. Active Record Scope
519 520 521 522 |
# File 'app/models/order.rb', line 519 scope :pending_payment_and_unpaid_invoices, lambda { unpaid_invoice_order_ids = Invoice.where(state: 'unpaid').select(:order_id) where(id: unpaid_invoice_order_ids).or(pending_payment) } |
.po_number_barcode(po_number:, file_path: nil) ⇒ String?
Class-method form of #po_number_barcode: render an arbitrary
PO number as a Code-128 barcode PNG. Strips non-ASCII chars
before encoding to side-step a known barby issue with smart
quotes and other paste-in unicode (toretore/barby#61).
2965 2966 2967 2968 2969 2970 2971 2972 2973 2974 2975 2976 2977 2978 2979 2980 2981 2982 2983 2984 2985 2986 |
# File 'app/models/order.rb', line 2965 def self.(po_number:, file_path: nil) return if po_number.blank? # we just want ASCII here, no spaces or weird cut and paste characters, see https://github.com/toretore/barby/issues/61 po_number = po_number.to_s.scan(/\S/).join.encode(Encoding::ASCII_8BIT, invalid: :replace, undef: :replace, replace: '') require 'barby' require 'barby/barcode/code_128' require 'barby/outputter/png_outputter' = Barby::Code128B.new(po_number) png = .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_value ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are positive value. Active Record Scope
531 |
# File 'app/models/order.rb', line 531 scope :positive_value, -> { where(Order[:line_total].gt(0)) } |
.profit_review ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are profit review. Active Record Scope
517 |
# File 'app/models/order.rb', line 517 scope :profit_review, -> { where(state: 'profit_review') } |
.quick_stats_shipping ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are quick stats shipping. Active Record Scope
533 |
# File 'app/models/order.rb', line 533 scope :quick_stats_shipping, -> { so_only.positive_value.in_state(SHIPPING_STATES).select('sum(line_total) as sum_line_total, currency').group(:currency) } |
.quick_stats_sold ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are quick stats sold. Active Record Scope
532 |
# File 'app/models/order.rb', line 532 scope :quick_stats_sold, -> { so_only.positive_value.in_state(SOLD_STATES).select('sum(line_total) as sum_line_total, currency').group(:currency) } |
.reception_type_for_select ⇒ Hash{String => String}
Memoised {label => value} map for the order-reception-type
filter ("Online" vs "CRM"). Each invocation returns the same
frozen-shape hash so the form caches well.
3391 3392 3393 3394 3395 3396 3397 3398 |
# File 'app/models/order.rb', line 3391 def self.reception_type_for_select unless @reception_types @reception_types = {} @reception_types['Online'] = 'Online' @reception_types['CRM'] = 'CRM' end @reception_types end |
.returnable_types ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are returnable types. Active Record Scope
530 |
# File 'app/models/order.rb', line 530 scope :returnable_types, -> { where(order_type: [SALES_ORDER, MARKETING_ORDER, TECH_ORDER]) } |
.room_not_pickable ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are room not pickable. Active Record Scope
499 |
# File 'app/models/order.rb', line 499 scope :room_not_pickable, -> { where.not(state: ROOM_PICKABLE_STATES) } |
.sales_orders ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are sales orders. Active Record Scope
539 |
# File 'app/models/order.rb', line 539 scope :sales_orders, -> { non_carts.so_only.active } |
.selectable_order_types ⇒ Array<Array(String, String)>
Order-type codes a user is allowed to choose from in the new-
order form. Hides RESTRICTED_ORDER_TYPES (CRM-only types like
credit orders that are created via dedicated flows).
1553 1554 1555 |
# File 'app/models/order.rb', line 1553 def self.selectable_order_types UNRESTRICTED_ORDER_TYPES.map { |code, desc| [desc, code] } end |
.so_only ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are so only. Active Record Scope
527 |
# File 'app/models/order.rb', line 527 scope :so_only, -> { where(order_type: SALES_ORDER) } |
.sold ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are sold. Active Record Scope
456 |
# File 'app/models/order.rb', line 456 scope :sold, -> { where(state: SOLD_STATES) } |
.st_only ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are st only. Active Record Scope
526 |
# File 'app/models/order.rb', line 526 scope :st_only, -> { where(order_type: STORE_TRANSFER) } |
.states_for_select ⇒ Array<Array(String, String)>
[human_name, machine_value] pairs of every order state, sorted
alphabetically by display name. Used to populate the state
filter on CRM list views.
1594 1595 1596 |
# File 'app/models/order.rb', line 1594 def self.states_for_select state_machine.states.sort_by(&:human_name).map { |s| [s.human_name, s.value] } end |
.with_amazon_payments ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are with amazon payments. Active Record Scope
568 |
# File 'app/models/order.rb', line 568 scope :with_amazon_payments, -> { joins(:payments).where(payments: { category: Payment::AMAZON_PAY }) } |
.with_associations ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are with associations. Active Record Scope
501 |
# File 'app/models/order.rb', line 501 scope :with_associations, -> { includes(:shipments, :shipping_account_number, :shipping_address, :creator, { customer: [:buying_group, { catalog: :store }] }) } |
.with_line_items ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are with line items. Active Record Scope
502 |
# File 'app/models/order.rb', line 502 scope :with_line_items, -> { includes(line_items: { catalog_item: { store_item: :item } }) } |
.with_payments ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are with payments. Active Record Scope
567 |
# File 'app/models/order.rb', line 567 scope :with_payments, -> { joins(:payments) } |
.with_review ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are with review. Active Record Scope
563 |
# File 'app/models/order.rb', line 563 scope :with_review, -> { where(reviewed: true) } |
.without_review ⇒ ActiveRecord::Relation<Order>
A relation of Orders that are without review. Active Record Scope
564 |
# File 'app/models/order.rb', line 564 scope :without_review, -> { where(reviewed: false) } |
Instance Method Details
#accounting_hold_order? ⇒ Boolean
2169 2170 2171 |
# File 'app/models/order.rb', line 2169 def accounting_hold_order? customer.on_hold || || potential_fraud? end |
#activities ⇒ ActiveRecord::Relation<Activity>
296 |
# File 'app/models/order.rb', line 296 has_many :activities, as: :resource, dependent: :nullify |
#add_customer_to_campaign ⇒ Object
After-save hook: when an order's source changes to a source that's
tied to a marketing campaign, enroll the customer in that campaign
so subsequent campaign emails reach them. No-op when the source
isn't campaign-linked or hasn't actually changed.
1602 1603 1604 1605 1606 1607 1608 |
# File 'app/models/order.rb', line 1602 def add_customer_to_campaign return unless saved_change_to_source_id? return unless source return unless source.linked_to_campaign? source.add_customer_to_campaign(customer) end |
#add_item(sku, qty) ⇒ Array<Hash>?
Convenience for #add_multiple_items with a single sku/qty pair.
No-op when the order is in a locked state (post-pending).
2814 2815 2816 2817 2818 2819 |
# File 'app/models/order.rb', line 2814 def add_item(sku, qty) return if editing_locked? sku_array = [{ sku:, qty: }] add_multiple_items(sku_array) end |
#add_multiple_items(sku_array = []) ⇒ Array<Hash>
Add a batch of SKUs to the order in one save. Each entry is a
Hash with :sku and optional :qty (defaulting to 1):
[{sku: 'UDG4-4999', qty: 1}, {sku: 'SS-01', qty: 2}]. Bails
without raising when the order is locked or the cart already
has 100+ lines (bot suspicion). Triggers tier-2 / auto-coupon
recalculation on save and recovers from
index_discounts_unique_per_itemizable collisions by
reloading discounts and retrying.
2772 2773 2774 2775 2776 2777 2778 2779 2780 2781 2782 2783 2784 2785 2786 2787 2788 2789 2790 2791 2792 2793 2794 2795 2796 2797 2798 2799 2800 2801 2802 2803 2804 2805 2806 |
# File 'app/models/order.rb', line 2772 def add_multiple_items(sku_array = []) return if editing_locked? return if line_items.size > 100 # Then we suspect it's a bot adding things to the cart added_items = [] sku_array.each do |hsh| sku = hsh[:sku] qty = hsh[:qty] || 1 room_configuration_id = hsh[:room_configuration_id] ci = catalog.catalog_items.public_catalog_items.by_skus(sku).first raise Order::ItemNotFound, "#{sku} not found" if ci.blank? # Persist each line_item inline. Without this, add_line_item's # `line_items.reload` at the top of the next iteration discards the # in-memory build from this one — only the final SKU in the batch # would survive the post-loop save!. li = add_line_item(catalog_item_id: ci.id, quantity: qty, room_configuration_id:) added_items << { id: li.id, sku: li.sku, name: li.name, category: li.reported_category_name, quantity: li.quantity } end if added_items.present? self.recalculate_shipping = true # doesn't hurt to set it self.recalculate_discounts = true # ensure tier2 and auto-apply discounts are calculated self.force_total_reset = true begin save! rescue ActiveRecord::RecordNotUnique => e raise unless e..include?('index_discounts_unique_per_itemizable') discounts.reload save! end end added_items end |
#adjusted_actual_shipping_cost ⇒ Float
Like #calculate_actual_shipping_cost but applies per-delivery
accounting adjustments (recoveries, fuel surcharge corrections).
5621 5622 5623 |
# File 'app/models/order.rb', line 5621 def adjusted_actual_shipping_cost deliveries.invoiced.to_a.sum { |d| d.adjusted_actual_shipping_cost.to_f } end |
#all_deliveries_cancelable?(current_user = nil) ⇒ Boolean
1130 1131 1132 |
# File 'app/models/order.rb', line 1130 def all_deliveries_cancelable?(current_user = nil) deliveries.active.empty? || deliveries.active.all? { |d| d.cancelable?(current_user) } end |
#all_deliveries_invoiced? ⇒ Boolean
2514 2515 2516 |
# File 'app/models/order.rb', line 2514 def all_deliveries_invoiced? deliveries.active.present? && deliveries.active.all?(&:invoiced?) end |
#all_funds_available?(ignore_cod = false) ⇒ Boolean
2510 2511 2512 |
# File 'app/models/order.rb', line 2510 def all_funds_available?(ignore_cod = false) balance(ignore_cod) <= 0 end |
#all_funds_not_available_and_shippable? ⇒ Boolean (protected)
5794 5795 5796 |
# File 'app/models/order.rb', line 5794 def all_funds_not_available_and_shippable? !all_funds_available?(true) && shipping_address && chosen_shipping_method.present? end |
#all_items_in_stock ⇒ Boolean
True when every line item has enough on-hand inventory at its
delivery's warehouse to fulfil immediately. Computed via
LineItem.inventory_check, which considers committed inventory,
transit, and minimum-on-hand thresholds.
2544 2545 2546 |
# File 'app/models/order.rb', line 2544 def all_items_in_stock stock_status == :ok end |
#all_participant_ids ⇒ Array<Integer>
Every party that should see this order in their CRM activity feed:
all opportunity participants plus the customer and contact.
1262 1263 1264 1265 1266 1267 1268 |
# File 'app/models/order.rb', line 1262 def all_participant_ids party_ids = [] party_ids += opportunity.all_participants.ids if opportunity party_ids << customer_id party_ids << contact_id party_ids.compact.uniq end |
#all_payments_are_valid? ⇒ Boolean
1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 |
# File 'app/models/order.rb', line 1134 def all_payments_are_valid? return true unless payments.bread_payments.any? if payments.bread_payments.where(state: 'authorized').sum(:amount) >= total true else errors.add :base, 'Financed order cannot be modified to have a higher total than the existing payment. Please create a new order for the extra items or extra cost.' false end end |
#all_rooms_not_orderable? ⇒ Boolean
2126 2127 2128 |
# File 'app/models/order.rb', line 2126 def all_rooms_not_orderable? room_configurations.any? && room_configurations.all? { |rc| !rc.orderable? } end |
#all_support_cases ⇒ Array<Integer>
Convenience for support_case_ids. Kept so the CRM views can use
a single accessor whether the support cases came from a HABTM or
via a custom join.
1701 1702 1703 1704 1705 |
# File 'app/models/order.rb', line 1701 def all_support_cases support_case_ids linked_support_cases.ids SupportCase.where(id: support_case_ids).order('support_cases.case_number desc') end |
#all_uploads ⇒ ActiveRecord::Relation<Upload>
Every Upload attached anywhere in the order tree — order
itself, its deliveries, and the shipments under those deliveries.
Pre-loads the polymorphic resource so the CRM's documents tab
can render attribution without N+1.
5665 5666 5667 5668 5669 5670 5671 5672 5673 5674 5675 |
# File 'app/models/order.rb', line 5665 def all_uploads ret_uploads = if delivery_ids.present? shipment_ids = Shipment.where(delivery_id: delivery_ids).ids conditions = ["(resource_type = 'Order' and resource_id = :order_id)", "(resource_type = 'Delivery' and resource_id IN (:delivery_ids))"] conditions << "(resource_type = 'Shipment' and resource_id IN (:shipment_ids))" if shipment_ids.any? Upload.where(conditions.join(' OR '), order_id: id, delivery_ids:, shipment_ids:) else uploads end ret_uploads.includes(:resource).order(Upload[:created_at].desc) end |
#allows_edi_split? ⇒ Boolean
5254 5255 5256 |
# File 'app/models/order.rb', line 5254 def allows_edi_split? edi_channel_order_support.present? && edi_channel_order_support.in?(%w[SPLIT_ORDERS SPLIT_ORDER_LINES]) end |
#already_has_smartinstall_request? ⇒ Boolean
3017 3018 3019 |
# File 'app/models/order.rb', line 3017 def already_has_smartinstall_request? activities.where(activity_type_id: ActivityTypeConstants::LEAD_SSI).any? end |
#amazon_buy_shipping_eligible? ⇒ Boolean
Check if order is eligible for Amazon Buy Shipping early label purchase
4479 4480 4481 4482 4483 4484 4485 4486 4487 4488 4489 4490 |
# File 'app/models/order.rb', line 4479 def amazon_buy_shipping_eligible? return false unless is_edi_order? return false unless edi_orchestrator_partner&.start_with?('amazon_seller') begin orchestrator = Edi::Amazon::Orchestrator.new(edi_orchestrator_partner.to_sym) orchestrator&.buy_shipping_enabled? rescue StandardError => e Rails.logger.warn("[EarlyLabel] Error checking Amazon Buy Shipping eligibility: #{e.}") 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.
2339 2340 2341 |
# File 'app/models/order.rb', line 2339 def amzbs_packing_slip_included? deliveries.any? { |d| d.shipping_option&.carrier == 'AmazonSeller' } end |
#any_rooms_not_orderable? ⇒ Boolean
2120 2121 2122 2123 2124 |
# File 'app/models/order.rb', line 2120 def any_rooms_not_orderable? # (self.order_reception_type == "Online" or online_order?) and self.room_configurations.any?{|rc| (rc.room_layout_attached? and !rc.complete?)} # here we are now allowing direct order of online rooms via HW so relax the test for any orders with uncompleted rooms that have layouts room_configurations.any? { |rc| !rc.orderable? } end |
#applies_for_smartinstall ⇒ Boolean
Eligibility check for the SmartInstall service add-on (paid
in-home installation). Currently hard-coded to false pending the
service refactor mentioned inline; the legacy logic checks that
the customer is a homeowner inside service range, has selected
heated items, and doesn't already carry SmartInstall.
3006 3007 3008 3009 3010 3011 |
# File 'app/models/order.rb', line 3006 def applies_for_smartinstall return false # added by Roman Aug 19th until service refactor return true if installation_is_within_range? && customer.is_homeowner? && has_selected_heated_items? && doesnt_already_has_smartinstall? false end |
#apply_tier2_pricing? ⇒ Boolean
Whether or not to apply the tier2 pricing (customer discount) by default
3105 3106 3107 |
# File 'app/models/order.rb', line 3105 def apply_tier2_pricing? is_sales_order? end |
#attention_name ⇒ String?
"ATTN: …" line written on shipping labels. Returns the explicit
attention_name_override if the user set one, otherwise falls
back to #inherited_attention_name (the shipping address's
person name when the customer is an individual).
3350 3351 3352 |
# File 'app/models/order.rb', line 3350 def attention_name attention_name_override || inherited_attention_name end |
#attention_name=(value) ⇒ Object
Setter that stores the override unless the caller is sending the
literal sentinel string "inherited_attention_name" (used by the
CRM form to mean "go back to the inherited value"), in which case
we leave the override untouched.
3360 3361 3362 3363 3364 |
# File 'app/models/order.rb', line 3360 def attention_name=(value) return if inherited_attention_name && value =~ /inherited_attention_name/i self.attention_name_override = value end |
#auto_reserve_serial_numbers ⇒ Object
Walks every line item that requires serial-number reservation
(heating mats, cables) and asks the line to grab the next available
serial-number block from inventory. No-op for credit orders, which
don't ship physical product.
1530 1531 1532 1533 1534 |
# File 'app/models/order.rb', line 1530 def auto_reserve_serial_numbers return unless can_auto_reserve_serial_numbers? line_items.select(&:require_reservation?).each(&:auto_reserve_serial_numbers) end |
#awaiting_future_deliveries? ⇒ Boolean
5290 5291 5292 |
# File 'app/models/order.rb', line 5290 def awaiting_future_deliveries? awaiting_deliveries? && deliveries.for_future_release.present? end |
#balance(ignore_cod = false, excluding_payment: nil) ⇒ BigDecimal
Outstanding amount on the order: total of all deliveries minus
whatever payments have been authorised against each delivery
(capped per delivery so over-authorisation on one delivery doesn't
cancel out a balance owed on another). Always 0 for store
transfers (no money changes hands) and for COD-funded orders
unless ignore_cod: true is passed.
2450 2451 2452 2453 2454 2455 2456 2457 2458 2459 2460 2461 |
# File 'app/models/order.rb', line 2450 def balance(ignore_cod = false, excluding_payment: nil) return BigDecimal('0.0') if is_store_transfer? || (!ignore_cod && funded_by_cod?) total = deliveries.sum(:total) || BigDecimal('0.0') bal = total deliveries.each do |dq| dq_total = dq.total || BigDecimal('0.0') dq_auth = dq.(currency, excluding_payment: excluding_payment) || BigDecimal('0.0') bal -= [dq_auth, dq_total].min end bal < 0 ? BigDecimal('0.0') : bal end |
#belongs_to_smartservice_group? ⇒ Boolean
2590 2591 2592 |
# File 'app/models/order.rb', line 2590 def belongs_to_smartservice_group? is_smartfit_service? or is_smartinstall_service? or is_smartguide_service? or is_smartfix_service? end |
#billing_address ⇒ Object
Alias for Customer#billing_address
318 |
# File 'app/models/order.rb', line 318 delegate :catalog, :billing_address, :billing_entity, :company, to: :customer |
#billing_entity ⇒ Object
Alias for Customer#billing_entity
318 |
# File 'app/models/order.rb', line 318 delegate :catalog, :billing_address, :billing_entity, :company, to: :customer |
#build_activity ⇒ Activity
Builds (but does not save) a new Activity record attached to
this order with the order's primary party already populated.
Used by the CRM activity-form helpers to seed a new activity
without round-tripping through nested form params.
1692 1693 1694 |
# File 'app/models/order.rb', line 1692 def build_activity activities.build resource: self, party: primary_party end |
#build_early_label_carrier_info(label_result, delivery = nil) ⇒ Hash
Build carrier info hash for early label EDI confirm (Walmart only)
Delegates to ShipCodeMapper for carrier name + methodCode derivation,
matching the behavior of the normal (non-early) ship confirm flow
in ConfirmMessageProcessor#build_order_line_status -> ShipCodeMapper#carrier_info.
5215 5216 5217 5218 5219 5220 5221 5222 5223 5224 5225 5226 5227 5228 5229 5230 5231 5232 |
# File 'app/models/order.rb', line 5215 def build_early_label_carrier_info(label_result, delivery = nil) actual_carrier = label_result[:carrier] || 'OTHER' orchestrator = Edi::Walmart::Orchestrator.new(edi_orchestrator_partner.to_sym) mapper = orchestrator.ship_code_mapper carrier_code = mapper.carrier_code(actual_carrier) carrier_name = if carrier_code { carrier: carrier_code } else { otherCarrier: actual_carrier.to_s } end description = delivery&.selected_shipping_cost&.description method = mapper.extract_method_from_delivery(description, carrier_code || actual_carrier) { carrierName: carrier_name, methodCode: method } end |
#build_early_label_tracking_url(carrier, tracking_number) ⇒ String?
Build tracking URL for early label
5239 5240 5241 5242 5243 5244 5245 5246 5247 5248 5249 5250 5251 5252 |
# File 'app/models/order.rb', line 5239 def build_early_label_tracking_url(carrier, tracking_number) return nil if tracking_number.blank? case carrier&.downcase when /fedex/ "https://www.fedex.com/fedextrack/?trknbr=#{tracking_number}" when /ups/ "https://www.ups.com/track?tracknum=#{tracking_number}" when /usps/ "https://tools.usps.com/go/TrackConfirmAction?tLabels=#{tracking_number}" when /ontrac/ "https://www.ontrac.com/trackingdetail.asp?tracking=#{tracking_number}" end end |
#buying_group ⇒ BuyingGroup
278 |
# File 'app/models/order.rb', line 278 belongs_to :buying_group, optional: true |
#calculate_actual_shipping_cost ⇒ Float
Sum of actual carrier-billed shipping cost on every invoiced
delivery (i.e. what we paid the carrier, not what the customer
was charged). Used by accounting reports to track shipping margin.
5613 5614 5615 |
# File 'app/models/order.rb', line 5613 def calculate_actual_shipping_cost deliveries.invoiced.to_a.sum { |d| d.actual_shipping_cost.to_f } end |
#can_auto_reserve_serial_numbers? ⇒ Boolean
1522 1523 1524 |
# File 'app/models/order.rb', line 1522 def can_auto_reserve_serial_numbers? order_type != 'CO' end |
#can_be_cancelled? ⇒ Boolean
3756 3757 3758 3759 3760 3761 |
# File 'app/models/order.rb', line 3756 def can_be_cancelled? return false if editing_locked? return false if is_edi_order? && cancellation_reason.blank? cancelable? end |
#can_be_returned? ⇒ Boolean
1919 1920 1921 |
# File 'app/models/order.rb', line 1919 def can_be_returned? is_regular_order? end |
#can_cr_hold?(current_user = nil) ⇒ Boolean
2154 2155 2156 |
# File 'app/models/order.rb', line 2154 def can_cr_hold?(current_user = nil) CAN_CR_HOLD_STATES.include?(state.to_sym) && all_deliveries_cancelable?(current_user) end |
#can_edit_future_release_date? ⇒ Boolean
1176 1177 1178 |
# File 'app/models/order.rb', line 1176 def can_edit_future_release_date? awaiting_deliveries? && deliveries.for_future_release.any? end |
#cancel_deliveries_pre_pack ⇒ Object
Cancels the in-progress packaging-estimate work on every
pre_pack delivery. Called when the order is being held or
cancelled so the warehouse stops trying to compute box dimensions
for an order that won't ship.
5353 5354 5355 5356 5357 |
# File 'app/models/order.rb', line 5353 def cancel_deliveries_pre_pack deliveries.reload.each do |delivery| delivery.cancel_estimated_packaging if delivery.pre_pack? end end |
#cancel_or_destroy ⇒ Object
Carts get destroyed (no audit trail to keep). Real orders go
through the state-machine cancel event so cancellations are
logged and reversed properly.
3607 3608 3609 |
# File 'app/models/order.rb', line 3607 def cancel_or_destroy cart? ? destroy : cancel end |
#cancelable? ⇒ Boolean
2158 2159 2160 |
# File 'app/models/order.rb', line 2158 def cancelable? CANCELABLE_STATES.include?(state.to_sym) && deliveries.non_quoting.empty? end |
#cannot_delete_reason(account = nil) ⇒ String?
Human-readable explanation of why an order cannot currently be
deleted, or nil when #ok_to_delete? would let it through.
Used by the CRM delete-button confirmation dialog.
2875 2876 2877 2878 2879 2880 2881 2882 2883 2884 2885 |
# File 'app/models/order.rb', line 2875 def cannot_delete_reason(account = nil) if editing_locked? 'Order cannot be deleted because it is in pending processing or beyond or referenced by authorizations.' elsif payments.any?(&:captured?) 'Order cannot be deleted because it is referenced by authorizations.' elsif line_items.any?(&:has_linked_unvoided_rma_item?) 'Order cannot be deleted until all linked RMA items have been voided.' elsif pending? || account&.is_admin? 'Orders that are non-draft can only be deleted by an admin' end end |
#cart_identifier ⇒ String
Stable identifier suitable for URLs and abandoned-cart emails:
the reference number once the order has one, otherwise a SC<id>
("Shopping Cart") fallback so brand-new carts still have a slug.
3768 3769 3770 |
# File 'app/models/order.rb', line 3768 def cart_identifier reference_number || "SC#{id}" end |
#cart_or_in_shipping_estimate? ⇒ Boolean
3772 3773 3774 |
# File 'app/models/order.rb', line 3772 def cart_or_in_shipping_estimate? cart? || in_shipping_estimate? end |
#catalog ⇒ Object
Alias for Customer#catalog
318 |
# File 'app/models/order.rb', line 318 delegate :catalog, :billing_address, :billing_entity, :company, to: :customer |
#check_payments_status ⇒ void
This method returns an undefined value.
Reconcile the order's payments with the upstream gateways
(Stripe, PayPal). Captures funds we know were taken outside
Heatwave, kicks expired authorizations back to pending_payment,
and re-syncs prepayments to deliveries. No-op for orders whose
only payments are non-gateway types (PO, store credit, check,
echeck, etc.) because those don't have anything to reconcile.
1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 |
# File 'app/models/order.rb', line 1387 def check_payments_status # Sometimes we manually capture the funds in paypal or stripe and HW doesn't know about it. Or the authorization is expired, etc. # This method checks that and puts the payment in the right status return if payments.blank? return if exclude_from_payment_check # Skip payment verification for orders that only have non-gateway payment types # These payment types don't require reauthorization or gateway status checks: # - PO/VPO: Terms-based, pre-approved credit # - Advance Replacement & RMA Credit: Internal credits from RMA process # - Store Credit: Internal customer credit balance # - CHECK/CASH/WIRE: Manual payments processed offline # - ECHECK: Cannot be captured through gateway, requires manual processing # - BREAD/PLAID: No check methods implemented yet non_gateway_payment_types = [ Payment::PO, Payment::VPO, Payment::ADV_REPL, Payment::RMA_CREDIT, Payment::STORE_CREDIT, Payment::CHECK, Payment::CASH, Payment::WIRE, Payment::ECHECK, Payment::BREAD, Payment::PLAID ] return if payments.all? { |p| p.category.in?(non_gateway_payment_types) } # First credit cards payments.credit_cards.each(&:check_cc_payment_status) # Next Paypal payments payments.paypal_payments.each(&:check_paypal_payment_status) # Next paypal invoice payments.paypal_invoices.each(&:check_paypal_invoice_payment_status) # re-sync the prepayments to deliveries deliveries.each(&:relink_payments) # Reload to ensure we have fresh data after payment status checks # Payment status checks may have changed payment states, and we need # fresh delivery totals and payment associations reload deliveries.reload # after re-authorizing, check there are enough funds to cover the order total if all_funds_available? # nothing to do as all funds are available. This means the total of the order is either authorized or captured already. But not only captured. elsif can_pending_payment? && deliveries.none?(&:pre_pack?) && deliveries.none?(&:pending_manifest_completion?) OrdersMailer.order_with_insuficient_payment(self).deliver_later unless pending_payment? pending_payment! end end |
#check_sales_rep ⇒ Object (protected)
Validation callback that prevents an order from listing the same
rep as both primary and secondary, and from setting a secondary
rep without a primary. Skipped for store transfers (which have
no customer-side rep concept).
5784 5785 5786 5787 5788 5789 5790 5791 5792 |
# File 'app/models/order.rb', line 5784 def check_sales_rep # Skip sales rep validation for orders without customers (e.g., Store Transfers use from_store/to_store instead) return if customer.nil? errors.add('Sales rep', 'can only be primary or secondary sales rep for a given customer at a time') if (primary_sales_rep == secondary_sales_rep) && primary_sales_rep return unless secondary_sales_rep && !primary_sales_rep errors.add('Order', 'must first have a primary sales rep to have have a secondary sales rep') end |
#ci_invoice_credit_order? ⇒ Boolean
3072 3073 3074 |
# File 'app/models/order.rb', line 3072 def ci_invoice_credit_order? order_type == CREDIT_ORDER && rma&.original_invoice&.invoice_type == Invoice::CI end |
#clear_shipped_date ⇒ Object
Resets the shipped_date column when an order needs to be
reverted to a pre-shipped state (e.g. cancellation after a partial
ship). No-op when nothing was set.
2922 2923 2924 |
# File 'app/models/order.rb', line 2922 def clear_shipped_date update_attribute(:shipped_date, nil) if shipped_date.present? end |
#closed_state? ⇒ Boolean
2150 2151 2152 |
# File 'app/models/order.rb', line 2150 def closed_state? CLOSED_STATES.include?(state.to_sym) end |
#communications ⇒ ActiveRecord::Relation<Communication>
305 |
# File 'app/models/order.rb', line 305 has_many :communications, as: :resource, dependent: :nullify |
#company ⇒ Object
Alias for Customer#company
318 |
# File 'app/models/order.rb', line 318 delegate :catalog, :billing_address, :billing_entity, :company, to: :customer |
#company_review_url ⇒ Object
Reviews.io dynamic link for company review using the order reference as order_id
and the customer CN number as customer_identifier (for CRM context).
1201 1202 1203 |
# File 'app/models/order.rb', line 1201 def company_review_url Api::ReviewsIo::DynamicLinkBuilder.for_order(self) end |
#company_review_url_for_confirmation(email: nil) ⇒ Object
Reviews.io dynamic link for the order confirmation / thank-you page.
Uses the customer email instead of the CN identifier.
1207 1208 1209 |
# File 'app/models/order.rb', line 1207 def company_review_url_for_confirmation(email: nil) Api::ReviewsIo::DynamicLinkBuilder.for_order_confirmation(self, email: email) end |
#complete? ⇒ Boolean
2375 2376 2377 |
# File 'app/models/order.rb', line 2375 def complete? invoiced? end |
#completed_regular_deliveries ⇒ Array<Delivery>
Active deliveries that have actually shipped (parcel or freight)
vs ones that completed in some other way (warehouse pickup,
service-only). Used to decide partial-vs-full ship status.
5529 5530 5531 |
# File 'app/models/order.rb', line 5529 def completed_regular_deliveries deliveries.active.select(&:completed_regular_delivery?) end |
#completely_shipped? ⇒ Boolean
5537 5538 5539 |
# File 'app/models/order.rb', line 5537 def completely_shipped? deliveries.active.all?(&:completed_regular_delivery?) end |
#consolidate_revenue ⇒ Object
Snapshot the order's total and shipping cost at the moment the
customer completed checkout. The Google Ads conversion ping later
sends these values, and any discrepancy with the live total
(which may be re-tier'd by promotions) is investigated.
5590 5591 5592 5593 5594 5595 5596 5597 |
# File 'app/models/order.rb', line 5590 def consolidate_revenue # Method to save the order total that the customer paid at the time of checkout and compare this value to the # value we send as a conversion to google ads. return unless online_order? update_column(:revenue_consolidated_at_time_of_checkout, total) update_column(:shipping_cost_at_time_of_checkout, shipping_cost || 0.0) end |
#contact ⇒ Contact
274 |
# File 'app/models/order.rb', line 274 belongs_to :contact, inverse_of: :orders, optional: true |
#contact_combo ⇒ 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
1147 1148 1149 1150 1151 |
# File 'app/models/order.rb', line 1147 def contact_combo return unless contact_id "Contact|#{contact_id}" end |
#contact_combo=(val) ⇒ Object
Used by dynamic contact lookup on order creation allowing interaction with the tom-select input on account_managers.html.erb
Contact can either be existing (single integer value for contact_id) or new contact if val is in format Customer|customer_id|full_name
1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 |
# File 'app/models/order.rb', line 1155 def contact_combo=(val) if val.blank? self.contact = nil return end contact_name, self.contact_id = val.split('|') return unless contact_id.blank? && name.present? self.contact = Contact.create(name: contact_name.squish.titleize, customer_id:) end |
#contact_combo_for_select ⇒ Array<Array(String, String)>
Active contacts under this order's customer formatted for a Rails
select helper, with each value encoded as "Contact|<id>" so the
CRM "contact" field can distinguish between picking an existing
contact and typing a new name.
1172 1173 1174 |
# File 'app/models/order.rb', line 1172 def contact_combo_for_select customer.contacts.where(inactive: false).map { |cnt| [cnt.to_s, "Contact|#{cnt.id}"] } end |
#copy_customer_reps ⇒ Object
In-memory mirror of #copy_invoice_reps that pulls reps from the
current customer's reps_collaboration setup. Called during order
construction so a fresh order inherits whoever owns the customer.
1252 1253 1254 1255 1256 |
# File 'app/models/order.rb', line 1252 def copy_customer_reps self.primary_sales_rep_id = primary_sales_rep.try(:id) self.secondary_sales_rep_id = secondary_sales_rep.try(:id) self.local_sales_rep_id = local_sales_rep.try(:id) end |
#copy_invoice_reps ⇒ Object
Pulls primary / secondary / local sales-rep ids off the order's
invoice and writes them onto the order itself. Used when an order
is invoiced after rep reassignment so the order's commission split
matches the invoice that actually shipped.
1239 1240 1241 1242 1243 1244 1245 1246 1247 |
# File 'app/models/order.rb', line 1239 def copy_invoice_reps return unless (invoice = invoices.first) update( primary_sales_rep_id: invoice.primary_sales_rep_id, secondary_sales_rep_id: invoice.secondary_sales_rep_id, local_sales_rep_id: invoice.local_sales_rep_id ) end |
#copy_items_from(order) ⇒ Boolean
Copy non-shipping line items from another order (typically a guest cart)
into this one, then destroy the source if it is itself a cart.
Atomicity matters: this is the guest -> account cart transfer path that
runs silently on login (Authenticable#handle_cart_transfer). The previous
implementation called add_multiple_items and then unconditionally
order.destroy if order.cart? — so when add_multiple_items raised
Order::ItemNotFound, returned early on editing_locked? / 100-item guard,
or hit a save failure, the source cart was destroyed while the items
never made it into the target. The customer's items vanished at checkout
with no error to the user and (often) no exception report.
Now: bail out early on no-op conditions, wrap copy + destroy in a single
transaction, and on any failure leave both carts untouched and report the
error so we can see it in AppSignal.
1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 |
# File 'app/models/order.rb', line 1317 def copy_items_from(order) return false if order.nil? # Preserve room_configuration_id so the cart UI keeps grouping merged items # under their original room after a guest→account cart transfer. sku_array = order.line_items.non_shipping.parents_only.map do |li| { sku: li.sku, qty: li.quantity, room_configuration_id: li.room_configuration_id } end if sku_array.empty? order.destroy! if order.cart? return true end if editing_locked? ErrorReporting.warning( 'Order#copy_items_from aborted: target cart is editing_locked', source: :web, source_cart_id: order.id, target_cart_id: id, target_state: state ) return false end if line_items.size + sku_array.size > 100 ErrorReporting.warning( 'Order#copy_items_from aborted: would exceed 100 line items', source: :web, source_cart_id: order.id, target_cart_id: id, existing: line_items.size, incoming: sku_array.size ) return false end copied = false ActiveRecord::Base.transaction do copied = add_multiple_items(sku_array).present? unless copied ErrorReporting.warning( 'Order#copy_items_from aborted: add_multiple_items returned no items (race with editing_locked? or 100-item guard)', source: :web, source_cart_id: order.id, target_cart_id: id, target_state: state, target_line_item_count: line_items.size ) raise ActiveRecord::Rollback end order.destroy! if order.cart? end copied rescue Order::ItemNotFound, ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved, ActiveRecord::RecordNotDestroyed => e ErrorReporting.error( e, source: :web, source_cart_id: order&.id, target_cart_id: id, skus: sku_array.pluck(:sku) ) false end |
#copy_shipping_reference_number_to_deliveries ⇒ Object
After-save callback: propagate the order's
shipment_reference_number (used by Amazon FBA shipments and by
marketplace seller programs as the carrier BOL) onto every
delivery. Skipped unless the column actually changed AND has a
value.
1628 1629 1630 1631 1632 1633 1634 |
# File 'app/models/order.rb', line 1628 def copy_shipping_reference_number_to_deliveries # The shipment reference (or FBA ID) can be used as carrier_bol number when present # Only run when shipment_reference_number actually changed and is present return unless saved_change_to_shipment_reference_number? && shipment_reference_number.present? deliveries.update_all(carrier_bol: shipment_reference_number) end |
#country ⇒ Country?
The Country that owns this order's billing entity (resolved
through customer → catalog → store → country). Used by tax,
shipping, and locale-aware code paths. Returns nil if any link
in the chain is missing rather than raising.
2369 2370 2371 2372 2373 |
# File 'app/models/order.rb', line 2369 def country customer.catalog.store.country rescue StandardError nil end |
#create_credit_memo ⇒ void
This method returns an undefined value.
Spawn a credit-memo invoice off this order (e.g. when the order is
being voided after invoicing) and run it through TaxJar to back
out any tax already reported.
3063 3064 3065 3066 |
# File 'app/models/order.rb', line 3063 def create_credit_memo CreditMemo.new_credit_memo_from_order(self) credit_memo.evaluate_taxjar_submission end |
#create_smartfix_ticket ⇒ Object
Open a SmartFix service ticket (paid repair visit) and schedule
the service-confirmation activity.
3163 3164 3165 |
# File 'app/models/order.rb', line 3163 def create_smartfix_ticket new_ss_ticket('SmartFix', ActivityTypeConstants::SSFIX_SERVICE_CONFIRM) end |
#create_smartguide_ticket ⇒ Object
Open a SmartGuide service ticket (consultation). Picks the
on-site vs remote variant based on whether the order contains the
SGS_ONSITE_FIXRATE SKU.
3170 3171 3172 3173 3174 3175 3176 |
# File 'app/models/order.rb', line 3170 def create_smartguide_ticket if line_items.joins(:item).merge(Item.where(sku: 'SGS_ONSITE_FIXRATE')).any? new_ss_ticket('SmartGuide', ActivityTypeConstants::SGS_ONSITE_PREPLAN_MEET) else new_ss_ticket('SmartGuide', ActivityTypeConstants::SGS_REMOTE_PREPLAN_MEET) end end |
#create_smartinstall_ticket ⇒ Object
Open a SmartInstall service ticket against this order via the
shared #new_ss_ticket helper and schedule the kickoff
pre-installation phone activity.
3157 3158 3159 |
# File 'app/models/order.rb', line 3157 def create_smartinstall_ticket new_ss_ticket('SmartInstall', ActivityTypeConstants::SSI_PREPLAN_MEET) end |
#credit_memo ⇒ CreditMemo
292 |
# File 'app/models/order.rb', line 292 has_one :credit_memo, foreign_key: :credit_order_id |
#credit_memos ⇒ ActiveRecord::Relation<CreditMemo>
300 |
# File 'app/models/order.rb', line 300 has_many :credit_memos, foreign_key: :original_order_id |
#crm_link ⇒ String
CRM-facing path to this order's show page (relative URL, suitable
for use in CRM emails, Slack notifications, and audit logs).
2849 2850 2851 |
# File 'app/models/order.rb', line 2849 def crm_link UrlHelper.instance.order_path(self) end |
#currency_symbol ⇒ String
Currency symbol ($, €, £, C$, …) for the order's currency,
resolved through the money gem's locale-aware Currency table.
2216 2217 2218 |
# File 'app/models/order.rb', line 2216 def currency_symbol Money::Currency.new(currency).symbol end |
#custom_shipping_labels ⇒ Object
Pulls all custom ship labels from the order, deliveries and shipments
3620 3621 3622 3623 3624 3625 3626 3627 3628 3629 3630 |
# File 'app/models/order.rb', line 3620 def custom_shipping_labels label_array = [] label_array += uploads.custom_ship_labels deliveries.each do |d| label_array += d.uploads.custom_ship_labels d.shipments.each do |shipment| label_array += shipment.uploads.custom_ship_labels end end label_array.compact.uniq end |
#customer ⇒ Customer
270 |
# File 'app/models/order.rb', line 270 belongs_to :customer, inverse_of: :orders, optional: true |
#customer_qualifies_for_free_online_shipping? ⇒ Boolean
1936 1937 1938 |
# File 'app/models/order.rb', line 1936 def customer_qualifies_for_free_online_shipping? discounts.free_online_shipping.present? end |
#deep_dup ⇒ Order
Deep-clones the order (line items, discounts, EDI documents) but
resets the identity / lifecycle columns so the duplicate can be
saved as a fresh order. Records the source order's id on the copy
via #parent_id, marks the copy pending, and suppresses
shipping auto-detect so the caller can reconfigure shipping
explicitly. Used by "Duplicate Order" CRM action and by order
splits.
587 588 589 590 591 592 593 594 595 596 597 598 |
# File 'app/models/order.rb', line 587 def deep_dup deep_clone( include: %i[discounts edi_documents], except: %i[txid reference_number created_at creator_id edi_transaction_id edi_po_number] ) do |original, copy| if copy.is_a?(Order) copy.state = 'pending' copy.do_not_detect_shipping = true copy.parent_id = original.id end end end |
#default_billing_emails ⇒ Array<String>
Default email recipients for billing notifications: every email
contact point flagged as a billing notification channel for the
customer (or their billing entity), plus the tracking email,
contact emails, and any per-order overrides on billing_emails.
De-duplicates and sorts so the form renders deterministically.
2731 2732 2733 2734 2735 2736 2737 2738 |
# File 'app/models/order.rb', line 2731 def default_billing_emails emails = [] billing_customer = customer.billing_entity emails += NotificationChannel.joins(:contact_point).merge(ContactPoint.emails).where(customer_id: [customer_id, billing_customer&.id].compact.uniq).pluck(ContactPoint[:detail]) emails << billing_customer.contact_points.transmittable.emails.pick(:detail) if billing_customer.profile && (billing_customer.profile.homeowner? || billing_customer.profile.direct_pro?) emails.compact.uniq.sort end |
#default_credit_card_vault ⇒ CreditCardVault
282 |
# File 'app/models/order.rb', line 282 belongs_to :default_credit_card_vault, class_name: 'CreditCardVault', optional: true |
#deferred_payments_captured_and_ready_for_shipping? ⇒ Boolean
1908 1909 1910 |
# File 'app/models/order.rb', line 1908 def deferred_payments_captured_and_ready_for_shipping? paypal_invoices_paid? && ready_for_shipping? end |
#deletable? ⇒ Boolean
2208 2209 2210 |
# File 'app/models/order.rb', line 2208 def deletable? %w[pending pending_payment crm_back_order in_cr_hold].include? state end |
#deliveries ⇒ ActiveRecord::Relation<Delivery>
312 |
# File 'app/models/order.rb', line 312 has_many :deliveries, -> { order(:origin_address_id) }, dependent: :destroy, autosave: true |
#delivery_activities ⇒ ActiveRecord::Relation<Activity>
297 |
# File 'app/models/order.rb', line 297 has_many :delivery_activities, class_name: 'Activity', through: :deliveries, source: :activities |
#direct_shipments ⇒ ActiveRecord::Relation<Shipment>
301 |
# File 'app/models/order.rb', line 301 has_many :direct_shipments, class_name: 'Shipment' |
#doesnt_already_has_smartinstall? ⇒ Boolean
3013 3014 3015 |
# File 'app/models/order.rb', line 3013 def doesnt_already_has_smartinstall? line_items.smartinstall_items.empty? end |
#download_and_store_early_label_pdf_atomic(shipper, label_result, is_amazon: false) ⇒ Upload?
SAFEGUARD A: Atomic PDF download and storage
Downloads and stores the label PDF with retries, ensuring it's persisted before returning.
This prevents the race condition where void happens before PDF is saved.
Amazon: label data is returned inline from create_label — store directly.
Walmart: requires a separate download_label API call with retries.
4895 4896 4897 4898 4899 4900 4901 4902 4903 4904 4905 4906 4907 4908 |
# File 'app/models/order.rb', line 4895 def download_and_store_early_label_pdf_atomic(shipper, label_result, is_amazon: false) tracking_number = label_result[:tracking_number] carrier = label_result[:carrier] marketplace = is_amazon ? 'Amazon' : 'Walmart' if Rails.env.development? Rails.logger.info('[EarlyLabel] Development mode - creating mock label PDF') return store_mock_early_label_pdf(tracking_number, carrier) end return store_amazon_label_pdf_atomic(label_result, tracking_number, marketplace) if is_amazon store_walmart_label_pdf_atomic(shipper, label_result, tracking_number, carrier, marketplace) end |
#drop_ship_purchase_orders ⇒ ActiveRecord::Relation<DropShipPurchaseOrder>
314 |
# File 'app/models/order.rb', line 314 has_many :drop_ship_purchase_orders, -> { order(:id) }, through: :deliveries |
#early_label_failure_notification(error_message) ⇒ Object
Send EDI admin notification when early label purchase fails
This ensures failures are not silent and the team is notified
4373 4374 4375 4376 4377 4378 4379 4380 4381 4382 4383 4384 4385 4386 4387 4388 4389 4390 4391 4392 4393 4394 4395 4396 4397 4398 4399 4400 4401 4402 |
# File 'app/models/order.rb', line 4373 def early_label_failure_notification() 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}" = <<~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: #{} ACTION REQUIRED: The early label was NOT purchased automatically. The warehouse will need to: 1. Purchase the label manually at ship-label time, OR 2. Purchase via #{portal} and upload as manual_ship_label This order will proceed through normal warehouse flow without early tracking. MSG EdiMailer.notify_edi_admin_of_warning(subject, ).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.}") end |
#early_label_flash_message ⇒ Hash
Generate flash messages based on early label purchase result
Called by controllers after order transitions to awaiting_deliveries
4575 4576 4577 4578 4579 4580 4581 4582 4583 4584 4585 4586 4587 4588 4589 4590 4591 4592 4593 4594 4595 4596 4597 4598 4599 4600 |
# File 'app/models/order.rb', line 4575 def return nil if early_label_purchase_result.blank? result = early_label_purchase_result marketplace = edi_orchestrator_partner&.start_with?('amazon_seller') ? 'Amazon' : 'Walmart' if result[:success] if result[:already_purchased] { type: :info, message: "Early shipping label already purchased - Tracking: #{result[:tracking_number]} (#{result[:carrier]})" } else { type: :success, message: "Early shipping label purchased successfully! Tracking: #{result[:tracking_number]} (#{result[:carrier]}) - Tracking has been sent to #{marketplace}." } end else { type: :error, message: "Failed to purchase early shipping label: #{result[:error]}. The order has been released but no tracking was sent to #{marketplace}." } end end |
#early_label_purchase_enabled_for_partner? ⇒ Boolean
Checks whether the partner's orchestrator has early label purchase enabled.
Walmart uses early_label_purchase_enabled?, Amazon uses buy_shipping_enabled?.
4494 4495 4496 4497 4498 4499 4500 4501 4502 4503 4504 4505 |
# File 'app/models/order.rb', line 4494 def early_label_purchase_enabled_for_partner? if edi_orchestrator_partner&.start_with?('walmart_seller') Edi::Walmart::Orchestrator.new(edi_orchestrator_partner.to_sym).early_label_purchase_enabled? elsif edi_orchestrator_partner&.start_with?('amazon_seller') Edi::Amazon::Orchestrator.new(edi_orchestrator_partner.to_sym).buy_shipping_enabled? else false end rescue StandardError => e Rails.logger.warn("[EarlyLabel] Error checking early label purchase enabled: #{e.}") 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
4245 4246 4247 4248 4249 4250 |
# File 'app/models/order.rb', line 4245 def early_label_purchased_recently? return false if early_label_purchased_at.blank? minutes_since_purchase = (Time.current - early_label_purchased_at) / 60 minutes_since_purchase < EARLY_LABEL_RAPID_VOID_THRESHOLD_MINUTES end |
#early_label_shipments_match?(delivery) ⇒ Hash
Check if current shipments match the early label shipments
Used to detect if warehouse staff changed the packing
4834 4835 4836 4837 4838 4839 4840 4841 4842 4843 4844 4845 4846 4847 4848 4849 4850 4851 4852 4853 4854 4855 4856 4857 4858 4859 4860 4861 4862 4863 4864 4865 4866 4867 4868 4869 4870 4871 4872 4873 4874 4875 4876 4877 4878 4879 4880 4881 4882 |
# File 'app/models/order.rb', line 4834 def early_label_shipments_match?(delivery) return { match: true } unless has_early_purchased_label? current_shipments = delivery.shipments.non_voided.to_a early_count = early_label_shipments_count || 0 early_data = early_label_shipments_data || [] # Check shipment count if current_shipments.size != early_count return { match: false, reason: "Shipment count changed: early label was for #{early_count} shipment(s), now #{current_shipments.size}" } end # Check dimensions/weights for significant changes current_shipments.each_with_index do |shipment, idx| early_shipment = early_data[idx] next unless early_shipment # Check for significant dimension changes (more than 20% or 2 inches) %i[length width height].each do |dim| early_val = early_shipment[dim.to_s]&.to_f || 0 current_val = shipment.send(dim)&.to_f || 0 diff = (early_val - current_val).abs if diff > 2 && diff > (early_val * 0.2) return { match: false, reason: "Shipment #{idx + 1} #{dim} changed significantly: was #{early_val.round(1)}in, now #{current_val.round(1)}in" } end end # Check for significant weight changes (more than 20% or 1 lb) early_weight = early_shipment['weight']&.to_f || 0 current_weight = shipment.weight&.to_f || 0 weight_diff = (early_weight - current_weight).abs if weight_diff > 1 && weight_diff > (early_weight * 0.2) return { match: false, reason: "Shipment #{idx + 1} weight changed significantly: was #{early_weight.round(1)}lbs, now #{current_weight.round(1)}lbs" } end end { match: true } end |
#early_label_upload ⇒ Upload?
Get the early label upload (PDF) if it exists
4556 4557 4558 |
# File 'app/models/order.rb', line 4556 def early_label_upload uploads.in_category('early_ship_label').first end |
#echecks_requiring_authorization ⇒ Array<Payment>
eCheck payments on this order that the fraud-review pipeline has
flagged as needing accounting review before the order can leave
CR hold. Driven by payment.authorization_review[:required].
2186 2187 2188 |
# File 'app/models/order.rb', line 2186 def payments..where(category: Payment::ECHECK).select { |pp| pp.[:required] == true } end |
#edi_cancellation_reason_description ⇒ String
Customer-facing description of the EDI cancellation reason on the
order. Canadian Tire wants the raw 3-char code on the wire;
everywhere else we render "<code>: human-readable reason" so
warehouse staff can read it at a glance.
3406 3407 3408 3409 3410 3411 3412 3413 3414 |
# File 'app/models/order.rb', line 3406 def edi_cancellation_reason_description if is_canadian_tire? # just return 3 character reason code for Canadian Tire cancellation_reason else # here we only want to return a description for when we use a code, fallback to humanize "#{cancellation_reason}: #{(edi_cancellation_reasons.find { |arr| arr.last == cancellation_reason }&.first || cancellation_reason).to_s.humanize.downcase}" end end |
#edi_cancellation_reasons ⇒ Array<Array(String, String)>
Allowed cancellation-reason codes for the order's EDI partner,
formatted as [[label, code], …] for a Rails select. Each EDI
partner has its own enumeration; falls through to a generic set
for partners that don't pin codes.
3422 3423 3424 3425 3426 3427 3428 3429 3430 3431 3432 3433 3434 3435 3436 3437 3438 3439 3440 3441 3442 3443 3444 3445 3446 3447 3448 3449 3450 3451 3452 3453 3454 3455 3456 3457 3458 3459 3460 3461 3462 3463 3464 3465 3466 3467 3468 3469 3470 3471 3472 3473 3474 3475 3476 3477 3478 3479 3480 3481 3482 3483 3484 3485 3486 3487 3488 3489 3490 3491 3492 3493 3494 3495 3496 3497 3498 3499 3500 3501 3502 3503 3504 3505 3506 3507 3508 3509 3510 3511 3512 3513 3514 3515 3516 3517 3518 3519 3520 3521 3522 3523 3524 3525 3526 3527 3528 3529 3530 3531 3532 3533 3534 3535 3536 3537 3538 3539 3540 3541 3542 3543 3544 3545 3546 3547 3548 3549 3550 3551 3552 3553 3554 3555 3556 3557 3558 3559 3560 3561 3562 3563 3564 3565 3566 3567 3568 3569 3570 3571 3572 3573 3574 3575 3576 3577 |
# File 'app/models/order.rb', line 3422 def edi_cancellation_reasons if is_amazon_com? [ ['Shipping 100 percent of ordered product', '00'], ['Canceled due to missing/invalid SKU', '02'], ['Canceled out of stock', '03'], ['Buyer request', '99'], ['Canceled due to duplicate Amazon Ship ID', '04'], ['Canceled due to missing/invalid Bill To Location Code', '05'], ['Canceled due to missing/invalid Ship From Location Code', '06'], ['Canceled due to missing/invalid Customer Ship to Name', '07'], ['Canceled due to missing/invalid Customer Ship to Address Line 1 ', '08'], ['Canceled due to missing/invalid Customer Ship to City', '09'], ['Canceled due to missing/invalid Customer Ship to State', '10'], ['Canceled due to missing/invalid Customer Ship to Postal Code ', '11'], ['Canceled due to missing/invalid Customer Ship to Country Code ', '12'], ['Canceled due to missing/invalid Shipping Carrier/Shipping Method ', '13'], ['Canceled due to missing/invalid Unit Price', '20'], ['Canceled due to missing/invalid Ship to Address Line 2', '21'], ['Canceled due to missing/invalid Ship to Address Line 3', '22'], ['Canceled due to Tax Nexus Issue', '50'], ['Canceled due to Restricted SKU/Qty', '51'], ['Canceled due to USPS >$400', '53'], ['Canceled due to Missing AmazonShipID', '54'], ['Canceled due to Missing AmazonOrderID', '55'], ['Canceled due to Missing LineItemId', '56'], ['Canceled due to discontinued item', '71'] ] elsif is_home_depot_usa? [ ['Bad SKU', 'bad_sku'], ["Cancelled at Merchant's Request", 'merchant_request'], ['Invalid Item Cost', 'invalid_item_cost'], ['Invalid method of shipment', 'invalid_ship_method'], ['Merchant detected fraud', 'merchant_detected_fraud'], ['Order Info Missing', 'info_missing'], ['Out of Stock', 'out_of_stock'], ['Product Has Been Discontinued', 'discontinued'], ['Supplier detected fraud', 'supplier_detected_fraud'] ] elsif is_home_depot_can? [ ['Backorder Cancellation', 'backorder_cancel'], ['Bad Address', 'bad_address'], ['Bad SKU', 'bad_sku'], ["Cancelled at Merchant's Request", 'merchant_request'], ['Cannot fulfill the order in time', 'fulfill_time_expired'], ['Cannot Ship as Ordered', 'cannot_meet_all_reqs'], ['Cannot ship to Country', 'cant_shipto_country'], ['Cannot ship to PO Box', 'cannot_shipto_POBOX'], ['Customer Refused Delivery', 'customer_refused'], ['Duplicate Order', 'duplicate_order'], ['Order Entry Error', 'order_entry_error'], ['Order Info Missing', 'info_missing'], ['Other', 'other'], ['Product Has Been Discontinued', 'discontinued'] ] elsif is_costco_ca? [ ['Bad Address', 'bad_address'], ['Bad SKU', 'bad_sku'], ["Cancelled at Merchant's Request", 'merchant_request'], ['Carrier does not service delivery location', 'carrier_does_not_service_area'], ['Customer Changed Mind', 'customer_request'], ['Duplicate Order', 'duplicate_order'], ['Minimum Order Not Met', 'min_order_not_met'], ['Order Entry Error', 'order_entry_error'], ['Order Info Missing', 'info_missing'], ['Out of Stock', 'out_of_stock'], ['Product Has Been Discontinued', 'discontinued'], ['To close order and allow reissue', 'close_and_reissue'], ['Unable to contact recipient', 'unable_to_contact_recipient'] ] elsif is_part_of_lowes_ca? [ ['Bad Address', 'bad_address'], ['Bad SKU', 'bad_sku'], ["Cancelled at Merchant's Request", 'merchant_request'], ['Cannot fulfill the order in time', 'fulfill_time_expired'], ['Cannot Ship as Ordered', 'cannot_meet_all_reqs'], ['Customer Changed Mind', 'customer_request'], ['Invalid Item Cost', 'invalid_item_cost'], ['Order Info Missing', 'info_missing'], ['Out of Stock', 'out_of_stock'], ['Product Has Been Discontinued', 'discontinued'] ] elsif is_part_of_lowes_com? [ ['Bad SKU', 'bad_sku'], ["Cancelled at Merchant's Request", 'merchant_request'], ['Minimum Order Not Met', 'min_order_not_met'], ['Other', 'other'], ['Invalid Item Cost', 'invalid_item_cost'], ['Out of Stock', 'out_of_stock'], ['Product Has Been Discontinued', 'discontinued'] ] elsif is_walmart_ca? [ ['Backorder Cancellation', 'backorder_cancel'], ['Bad SKU', 'bad_sku'], ["Cancelled at Merchant's Request", 'merchant_request'], ['Customer Changed Mind', 'customer_request'], ['Out of Stock', 'out_of_stock'], ['Product Has Been Discontinued', 'discontinued'] ] elsif edi_orchestrator_partner == 'walmart_seller_us' [ ['Backorder Cancellation', 'backorder_cancel'], ['Bad SKU', 'bad_sku'], ["Cancelled at Merchant's Request", 'merchant_request'], ['Customer Changed Mind', 'customer_request'], ['Out of Stock', 'out_of_stock'], ['Product Has Been Discontinued', 'discontinued'] ] elsif is_canadian_tire? [ ['Out of Stock', 'W01'], ['Not Enough Stock', 'W13'], ['Discontinued Item', 'A83'], ['Incorrect Address', 'A03'], ['Invalid Ship Instructions', '051'], ["Can't Ship on Time", 'D50'], ['Cancelled at Retailer Request', 'ABN'], ['Other', 'A13'], ['Bad Sku', 'A80'], ['Cannot ship to country', 'A05'], ['Cannot ship to PO box', 'A06'], ['Cannot ship USPS', 'A82'], ['Carrier does not service delivery location', 'D01'], ['Duplicate order', 'A07'], ['Invalid UOM', 'SOW'], ['Item Recall', 'IV1'], ['Minimum order not met', 'MIN'], ['Order entry error', 'W05'], ['Order info missing', 'B14'], ['Preorder cancellation', 'POA'], ['Fraud', '030'], ['To close order and allow reissue', 'RUN'], ['Unable to contact recipient', 'A58'] ] else %w[ bad_address bad_sku merchant_request carrier_wont_svc_loc customer_request customer_refused duplicate_order info_missing out_of_stock discontinued close_and_reissue ] end end |
#edi_communication_logs ⇒ ActiveRecord::Relation<EdiCommunicationLog>
311 |
# File 'app/models/order.rb', line 311 has_many :edi_communication_logs, through: :edi_documents |
#edi_documents ⇒ ActiveRecord::Relation<EdiDocument>
310 |
# File 'app/models/order.rb', line 310 has_many :edi_documents, dependent: :destroy |
#edi_force_price_match ⇒ Array<Array<String>>
Force every parent line item with an EDI unit cost to match it on
both the MSRP price and the discounted price. Used to align an
EDI-imported order back to the partner's quoted price after the
CRM has re-sourced or repriced items.
2033 2034 2035 2036 2037 2038 2039 |
# File 'app/models/order.rb', line 2033 def edi_force_price_match errs = [] line_items.parents_only.where.not(edi_unit_cost: nil).find_each do |li| errs << li.errors. unless li.update(price: li.edi_unit_cost) && li.update(discounted_price: li.edi_unit_cost) end errs end |
#edi_orchestrator ⇒ Edi::BaseOrchestrator?
The Edi::BaseOrchestrator subclass responsible for this order's
EDI partner, if it has one. Returns nil for non-EDI orders.
5740 5741 5742 5743 |
# File 'app/models/order.rb', line 5740 def edi_orchestrator # simple order to orchestrator mapping, nil otherwise Edi::BaseOrchestrator.orchestrator_for_customer_id(customer_id) if edi_transaction_id end |
#edi_price_matcheable? ⇒ Boolean
2016 2017 2018 2019 2020 2021 2022 2023 2024 |
# File 'app/models/order.rb', line 2016 def edi_price_matcheable? return false unless is_edi_order? target_lines = line_items.goods.parents_only return false if target_lines.blank? return false unless target_lines.all?(&:edi_unit_cost) target_lines.any? { |li| li.price != li.edi_unit_cost || li.discounted_price != li.edi_unit_cost } end |
#editing_locked? ⇒ Boolean
2146 2147 2148 |
# File 'app/models/order.rb', line 2146 def editing_locked? (LOCKED_STATES.include?(state.to_sym) || purchase_order&.should_lock_linked_order?).to_b end |
#effective_date_for_coupon ⇒ Date
Date used by the coupon engine to evaluate "is this coupon
currently valid?" against start_date/end_date windows. Carts
use today; existing orders use the order's override_coupon_date
(if any) or the created_at so the coupon evaluation is stable
after the order is placed even if the coupon's window later moves.
1486 1487 1488 1489 1490 |
# File 'app/models/order.rb', line 1486 def effective_date_for_coupon return Date.current if cart? override_coupon_date.presence || created_at.try(:to_date) || Date.current end |
#email_for_order_confirmation ⇒ String?
First non-blank email for the order's confirmation flow,
checking (in order): the contact/customer's stored email, then
the customer's email, then the contact's account email, then the
customer's account email. Returns nil if every lookup fails.
3332 3333 3334 3335 3336 3337 3338 3339 3340 3341 3342 |
# File 'app/models/order.rb', line 3332 def email_for_order_confirmation party_for_order_confirmation.email || customer.email || (begin party_for_order_confirmation.account.email rescue StandardError nil end) || (begin customer.account.email rescue StandardError nil end) end |
#email_options_for_tracking_email ⇒ Array<String>
Choices for the "tracking emails" multi-select on the order form:
every email the customer has on file, plus whatever's currently
set on tracking_email (which may include free-typed addresses
not yet on the customer record).
3692 3693 3694 |
# File 'app/models/order.rb', line 3692 def [customer.all_emails, tracking_email].flatten.compact.uniq end |
#empty? ⇒ Boolean
2204 2205 2206 |
# File 'app/models/order.rb', line 2204 def empty? line_items.non_shipping.empty? end |
#encrypted_id ⇒ String
Reversibly encrypted form of the order's database id, used in
public-facing URLs (payment link, cart-recovery emails) so the id
doesn't appear directly in logs or share links.
5476 5477 5478 |
# File 'app/models/order.rb', line 5476 def encrypted_id Encryption.encrypt_string(id.to_s) end |
#errors_with_deliveries_errors ⇒ Array<String>
Combined errors from the order itself and from each of its
deliveries, suitable for surfacing to the CRM "save failed"
banner so users see every reason at once.
5630 5631 5632 |
# File 'app/models/order.rb', line 5630 def errors_with_deliveries_errors (errors. + deliveries.map { |d| d.errors. }).flatten end |
#estimate_next_available_date_from_out_of_stock_items ⇒ Date?
Latest "next available" date across every fully out-of-stock
line item (or nil if all items have stock). Used to estimate when
a back-ordered order can ship. Bounded recursion (max_depth: 10)
protects against pathological substitution chains in the catalog.
5393 5394 5395 5396 5397 5398 5399 5400 5401 |
# File 'app/models/order.rb', line 5393 def estimate_next_available_date_from_out_of_stock_items # this returns nil if no items are out of stock, or latest available store item's next available date line_items.goods.select do |li| li.stock_status == :none end.filter_map do |li| # Use depth-limited version to prevent infinite recursion li.catalog_item.store_item.next_available_with_depth_limit(max_depth: 10)&.next_available_date end.max end |
#find_gbraid ⇒ String?
Same fallback chain as #find_gclid for the Android-app
web-to-app gbraid token used by Google Ads.
5708 5709 5710 5711 5712 5713 5714 |
# File 'app/models/order.rb', line 5708 def find_gbraid visit&. || quote&.visit&. || opportunity&.visit&. || customer&.visit&. || customer&.visits&.last_marketing_value(:gbraid) end |
#find_gclid ⇒ String?
Best-effort Google Click ID for this order. Walks the order's
visit, then the quote's visit, then the opportunity's visit,
then the customer-level captures so a conversion ping always has
an attribution token if any was ever recorded.
5683 5684 5685 5686 5687 5688 5689 5690 |
# File 'app/models/order.rb', line 5683 def find_gclid visit&. || quote&.visit&. || opportunity&.visit&. || customer&.gclid || customer&.visit&. || customer&.visits&.last_marketing_value(:gclid) end |
#find_oppref ⇒ String?
OpenAI Ads (ChatGPT) click identifier, captured by the Tracker into
Visit#marketing_meta['oppref'] (JSONB-only — no scalar column). Used
by the CAPI reporter to populate the event's top-level oppref field,
which OpenAI requires us to forward ourselves on server-side events.
Fallback chain mirrors #find_gclid but queries the JSONB blob instead
of a scalar column; the last-resort SQL lookup uses GIN-indexed
marketing_meta @> '{"oppref": ...}' containment.
5726 5727 5728 5729 5730 5731 5732 5733 5734 |
# File 'app/models/order.rb', line 5726 def find_oppref visit_oppref = ->(v) { (v&. || {})['oppref'].presence } visit_oppref.call(visit) || visit_oppref.call(quote&.visit) || visit_oppref.call(opportunity&.visit) || visit_oppref.call(customer&.visit) || customer&.visits&.last_marketing_value(:oppref) end |
#find_wbraid ⇒ String?
Same fallback chain as #find_gclid for the iOS-app web-to-app
wbraid token used by Google Ads.
5696 5697 5698 5699 5700 5701 5702 |
# File 'app/models/order.rb', line 5696 def find_wbraid visit&. || quote&.visit&. || opportunity&.visit&. || customer&.visit&. || customer&.visits&.last_marketing_value(:wbraid) end |
#fire_early_label_edi_confirm(delivery, label_result) ⇒ Object
Fire EDI ship confirm with early label tracking info.
This sends tracking to the marketplace immediately, without waiting for warehouse processing.
Branches for Amazon (packageDetail format) vs Walmart (orderShipment format).
5064 5065 5066 5067 5068 5069 5070 5071 5072 5073 5074 5075 5076 5077 5078 5079 5080 5081 5082 5083 |
# File 'app/models/order.rb', line 5064 def fire_early_label_edi_confirm(delivery, label_result) return unless is_edi_order? Rails.logger.info("[EarlyLabel] Firing EDI ship confirm for order #{reference_number}") begin if edi_orchestrator_partner&.start_with?('amazon_seller') # Buy Shipping V2 purchaseShipment automatically notifies Amazon about the # shipment — a separate confirmShipment via the Orders API is redundant and # fails with "PackageToUpdateNotFound" because Buy Shipping manages packages # in a different subsystem than the Orders API expects. Rails.logger.info("[EarlyLabel] Skipping EDI ship confirm for Amazon Buy Shipping order #{reference_number} — Buy Shipping V2 handles notification automatically") else fire_early_label_edi_confirm_walmart(delivery, label_result) end rescue StandardError => e Rails.logger.error("[EarlyLabel] Failed to fire EDI ship confirm for order #{reference_number}: #{e.}") Rails.logger.error(e.backtrace.first(5).join("\n")) end end |
#fire_early_label_edi_confirm_amazon(delivery, label_result) ⇒ Object
Amazon early label ship confirm — mirrors Edi::Amazon::ConfirmMessageProcessor#acknowledge_order
but builds the message from order data since no shipment record exists yet.
5087 5088 5089 5090 5091 5092 5093 5094 5095 5096 5097 5098 5099 5100 5101 5102 5103 5104 5105 5106 5107 5108 5109 5110 5111 5112 5113 5114 5115 5116 5117 5118 5119 5120 5121 5122 5123 5124 5125 5126 5127 5128 5129 5130 5131 5132 5133 5134 5135 5136 5137 5138 |
# File 'app/models/order.rb', line 5087 def fire_early_label_edi_confirm_amazon(delivery, label_result) orchestrator = Edi::Amazon::Orchestrator.new(edi_orchestrator_partner.to_sym) label_data = label_result.respond_to?(:to_h) ? label_result.to_h : label_result tracking_number = label_data[:tracking_number] effective_carrier = label_data[:carrier] carrier_code = Edi::Amazon::ConfirmMessageProcessor::CARRIER_TO_CARRIER_CODE_MAP_HASH[effective_carrier.to_sym] || 'Other' = raise 'edi_original_order_message is blank — cannot build ship confirm' if .blank? order_hash = JSON.parse().with_indifferent_access order_items = (order_hash[:OrderItems] || []).map do |item| { orderItemId: item[:OrderItemId], quantity: (item[:QuantityOrdered] || 1).to_i } end = { 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: .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..process(ecl) end |
#fire_early_label_edi_confirm_walmart(delivery, label_result) ⇒ Object
Walmart early label ship confirm — original Walmart-specific flow
5141 5142 5143 5144 5145 5146 5147 5148 5149 5150 5151 5152 5153 5154 5155 5156 5157 5158 5159 5160 5161 5162 5163 5164 5165 5166 5167 5168 5169 5170 5171 5172 5173 5174 5175 5176 5177 5178 5179 5180 5181 5182 5183 5184 5185 5186 5187 5188 5189 5190 5191 5192 5193 5194 5195 5196 5197 5198 5199 5200 5201 5202 5203 5204 |
# File 'app/models/order.rb', line 5141 def fire_early_label_edi_confirm_walmart(delivery, label_result) orchestrator = Edi::Walmart::Orchestrator.new(edi_orchestrator_partner.to_sym) = raise 'edi_original_order_message is blank — cannot build Walmart ship confirm' if .blank? order_hash = JSON.parse().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 = { orderShipment: { orderLines: { orderLine: shipped_order_lines } } } ecl = EdiCommunicationLog.create!( partner: orchestrator.partner, category: 'order_confirm', data: .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_number ⇒ String?
First non-blank PO number across the order's payments. Used in
places that need just one PO for display where #po_number would
otherwise return a comma-separated list.
2481 2482 2483 |
# File 'app/models/order.rb', line 2481 def first_po_number payments.find(&:po_number)&.po_number end |
#first_tracking_email ⇒ String?
Single-string variant of #tracking_email (which is an array
column). Used in carrier API payloads that accept exactly one
notification email per shipment.
5546 5547 5548 |
# File 'app/models/order.rb', line 5546 def first_tracking_email tracking_email&.first end |
#fix_future_release_date ⇒ Hash
Reconciles requested_ship_on_or_after, future_release_date,
and requested_ship_before so they don't contradict each other:
bumps future_release_date forward to honour the
ship-on-or-after date, and clears requested_ship_before when
the user explicitly sets a future-release date past it.
Returns a { message: "…" } hash describing any change so the
CRM can flash the user.
5643 5644 5645 5646 5647 5648 5649 5650 5651 5652 5653 5654 5655 5656 5657 |
# File 'app/models/order.rb', line 5643 def fix_future_release_date res = {} if requested_ship_on_or_after && (requested_ship_on_or_after > Date.current) && (future_release_date.nil? || (future_release_date < requested_ship_on_or_after)) # check if we are shipping too soon and don't have a future date set or have one set too soon res[:message] = "Order ship on or after date was set past today (#{requested_ship_on_or_after}) and order future release date is set to #{future_release_date || 'none'}, so order future release date was updated to match." update(future_release_date: requested_ship_on_or_after) end # If future_release_date is after requested_ship_before, the user explicitly set a future hold. # Clear requested_ship_before since it conflicts with the intentional future release. # (Previously this would override the user's future_release_date, causing unexpected immediate release) if requested_ship_before && future_release_date && (future_release_date > requested_ship_before) res[:message] = "Order future release date (#{future_release_date}) is after the ship before date (#{requested_ship_before}). Clearing ship before date to respect the future release hold." update(requested_ship_before: nil) end res end |
#formatted_po_number ⇒ String
po_number with the customer's preferred prefix applied (e.g.
"PO# 12345" vs bare "12345"), driven by
customer.include_po_prefix?. Used in CRM list views and emails.
2952 2953 2954 |
# File 'app/models/order.rb', line 2952 def formatted_po_number po_number(include_po_prefix: include_po_prefix?) end |
#from_store ⇒ Store
286 |
# File 'app/models/order.rb', line 286 belongs_to :from_store, class_name: 'Store', optional: true |
#fully_funded_by_advance_replacement? ⇒ Boolean
2502 2503 2504 |
# File 'app/models/order.rb', line 2502 def fully_funded_by_advance_replacement? deliveries.any? && deliveries.all? { |dq| dq.payments.any? && dq.payments.all? { |pp| pp.category == Payment::ADV_REPL } } end |
#funded_by_advance_replacement? ⇒ Boolean
2506 2507 2508 |
# File 'app/models/order.rb', line 2506 def funded_by_advance_replacement? deliveries.any? { |_dq| payments.any? { |pp| pp.category == Payment::ADV_REPL } } end |
#funded_by_cod? ⇒ Boolean
2498 2499 2500 |
# File 'app/models/order.rb', line 2498 def funded_by_cod? deliveries.all?(&:funded_by_cod?) && deliveries.any? end |
#funds_available_and_ready_for_warehouse? ⇒ Boolean (protected)
5798 5799 5800 |
# File 'app/models/order.rb', line 5798 def funds_available_and_ready_for_warehouse? all_funds_available? && ready_for_warehouse? end |
#generate_spiff_training_activity ⇒ Activity
Schedules a 7-day-out follow-up training activity for the primary
sales rep so they walk a SPIFF-eligible customer through their
first WarmlyYours order. Called automatically when
#needs_spiff_training? returns true.
1759 1760 1761 |
# File 'app/models/order.rb', line 1759 def generate_spiff_training_activity Activity.create(activity_type_id: ActivityTypeConstants::SPIFFACTTRAIN, target_datetime: 7.days.from_now, assigned_resource: primary_sales_rep, party: customer, resource: self) end |
#get_expected_ship_date_time ⇒ Time
Estimated delivery time = scheduled ship time +
carrier-committed transit days from the selected shipping
option. Falls back to a 4-working-day commitment when no rate
has been picked yet.
5436 5437 5438 5439 |
# File 'app/models/order.rb', line 5436 def get_expected_ship_date_time days_committment = (deliveries.first&.selected_shipping_cost&.days_commitment || 4.0).ceil # put in some fallback of 4 days days_committment.working.days.since(get_scheduled_ship_date_time) end |
#get_scheduled_ship_date_time ⇒ Time
When the order is expected to be picked up by the carrier. Honors
future_release_date when set, falls back to today's 15:55 cutoff
in the store's local timezone, and rolls forward to the next
working day at 10:00 if we're already past cutoff or on a
weekend. Back-orders use the latest stock-available date.
5410 5411 5412 5413 5414 5415 5416 5417 5418 5419 5420 5421 5422 5423 5424 5425 5426 5427 5428 |
# File 'app/models/order.rb', line 5410 def get_scheduled_ship_date_time order_ship_date_time = future_release_date if future_release_date.present? && (future_release_date > Time.current) if crm_back_order? order_ship_date_time = estimate_next_available_date_from_out_of_stock_items || 10.working.days.since(Time.current) # is 10 days a good fallback? elsif order_ship_date_time.nil? Time.use_zone(store.time_zone_string) do # WY Canada is Eastern Time, Wy US is Central today_cut_off_time = Time.zone.parse('15:55:00') order_ship_date = (future_release_date || today_cut_off_time).strftime('%Y-%m-%d') # use future release date if present or today_cut_off_time order_ship_time = today_cut_off_time.strftime('%R:%S') if Time.current > today_cut_off_time || today_cut_off_time.on_weekend? early_pickup_time = Time.zone.parse('10:00:00') order_ship_date = 1.working.day.since(early_pickup_time).strftime('%Y-%m-%d') order_ship_time = early_pickup_time.strftime('%R:%S') end order_ship_date_time = Time.zone.parse("#{order_ship_date} #{order_ship_time}") end end order_ship_date_time.to_time end |
#has_authorized_payment? ⇒ Boolean
1375 1376 1377 |
# File 'app/models/order.rb', line 1375 def payments..any? end |
#has_committed_serial_number_reservations? ⇒ Boolean
2708 2709 2710 |
# File 'app/models/order.rb', line 2708 def has_committed_serial_number_reservations? line_items.any? { |li| li.require_reservation? && !li.all_reserved_serial_numbers_available? } end |
#has_custom_packing_slip? ⇒ Boolean
2333 2334 2335 |
# File 'app/models/order.rb', line 2333 def has_custom_packing_slip? uploads.in_category('custom_packing_slip_pdf').present? || amzbs_packing_slip_included? end |
#has_early_purchased_label? ⇒ Boolean
Check if order has an active (non-voided) early-purchased label
4407 4408 4409 |
# File 'app/models/order.rb', line 4407 def has_early_purchased_label? early_label_tracking_number.present? && early_label_voided_at.nil? end |
#has_incomplete_reservations? ⇒ Boolean
2700 2701 2702 |
# File 'app/models/order.rb', line 2700 def has_incomplete_reservations? has_unreserved_line_items? || has_committed_serial_number_reservations? end |
#has_selected_heated_items? ⇒ Boolean
3021 3022 3023 |
# File 'app/models/order.rb', line 3021 def has_selected_heated_items? smartinstall_data.present? end |
#has_shipping_method? ⇒ Boolean
2343 2344 2345 |
# File 'app/models/order.rb', line 2343 def has_shipping_method? chosen_shipping_method.present? end |
#has_unreserved_line_items? ⇒ Boolean
2704 2705 2706 |
# File 'app/models/order.rb', line 2704 def has_unreserved_line_items? line_items.any? { |li| li.require_reservation? && !li.fully_reserved? } end |
#has_web_rooms_needing_installation_plans? ⇒ Boolean
2712 2713 2714 |
# File 'app/models/order.rb', line 2712 def has_web_rooms_needing_installation_plans? room_configurations.any? { |rc| rc.web_room? && rc.room_layout_attached? && !rc.complete? && !rc.installation_plans_attached? } end |
#hold_for_early_label_mismatch!(_delivery, reason) ⇒ Object
Puts the order on CR hold with a descriptive note when early label
purchase cannot proceed because the selected shipping rate doesn't
match available marketplace rates.
4535 4536 4537 4538 4539 4540 4541 4542 4543 4544 4545 4546 4547 4548 4549 4550 4551 |
# File 'app/models/order.rb', line 4535 def hold_for_early_label_mismatch!(_delivery, reason) note_text = <<~NOTE.strip ⚠ Early label purchase BLOCKED: #{reason} Please select a valid marketplace shipping rate (e.g. an AMZBS rate for Amazon orders) on the Shipping tab, then release the order. NOTE if can_cr_hold? cr_hold quick_note(note_text) if respond_to?(:quick_note) else quick_note(note_text) if respond_to?(:quick_note) Rails.logger.warn("[EarlyLabel] Cannot CR-hold order #{reference_number} (state=#{state}) — note added instead") end early_label_failure_notification(reason) end |
#hold_order_reasons ⇒ Array<String>
Human-readable list of reasons this order is currently being held
by the CR/fraud/EDI pipeline — every line item the CRM hold-page
surfaces to the user. An empty array means the order is releasable.
Combines validation across customer state, billing/shipping data,
EDI partner constraints (price match, shipping option, packing
slip), and freight readiness.
2236 2237 2238 2239 2240 2241 2242 2243 2244 2245 2246 2247 2248 2249 2250 2251 2252 2253 2254 2255 2256 2257 2258 2259 2260 2261 2262 2263 2264 2265 2266 2267 2268 2269 2270 2271 2272 2273 2274 2275 2276 2277 2278 2279 2280 2281 2282 2283 2284 2285 2286 2287 2288 2289 2290 2291 2292 2293 2294 2295 2296 2297 2298 2299 2300 2301 2302 2303 2304 2305 2306 2307 2308 2309 2310 2311 2312 2313 2314 |
# File 'app/models/order.rb', line 2236 def hold_order_reasons res = [] res << 'Order was manually held and so can only be manually released.' if is_manual_hold? res << 'Customer is in a state requiring orders to be held (lead_qualify, guest, bankrupt or closed)' if customer.lead_qualify? || customer.guest? || customer.bankrupt? || customer.closed? res << 'Logged in customer placing order has qc orders flag set' if is_online? && customer.qc_orders? && !CurrentScope.employee_logged_in? res << 'Customer has no billing address' if billing_address.nil? res << 'Order has no shipping method' unless has_shipping_method? if all_rooms_not_orderable? # we should let empty rooms through as long as other rooms in the order are orderable, case in point a tiny corner/closet room that we could not fit heating elements into res << 'Rooms/Heated Spaces are not orderable, empty line items' end res << 'Order ships via freight but shipping address is not freight ready, please set shipping address freight fields and recalculate shipping' if ships_freight_but_address_not_freight_ready? res << 'Deliveries are awaiting packaging estimates' if deliveries.any?(&:pre_pack?) if is_edi_order? res << "EDI order price match discrepancy, expects line total to be #{price_match}" if price_match.present? && price_match != line_total res << 'EDI order missing line number, all non shipping lines must have an edi line number' if line_items.parents_only.non_shipping.where(edi_line_number: nil).present? # Skip shipping option validation when order is being cancelled - shipping method doesn't matter for cancellations if cancellation_reason.blank? if edi_original_ship_code.to_s.upcase.exclude?('UNSP') && edi_shipping_option_name.present? && deliveries.any? { |d| !d.shipping_option_matches?(edi_shipping_option_name) } # unless carrier is unspecified ie UNSP, force match on mapped edi_shipping_option_name res << "EDI order must use original shipping option: #{edi_shipping_option_name}" end # For Walmart SWW orders, allow any WalmartSeller shipping option even if edi_shipping_option_name wasn't set # This handles orders created before the 'sww' shipping option name was added walmart_sww_ok = edi_orchestrator_partner&.start_with?('walmart_seller') && deliveries.all? { |d| d.shipping_option&.carrier == 'WalmartSeller' } # For Amazon Buy Shipping orders, allow any AmazonSeller shipping option amazon_amzbs_ok = edi_orchestrator_partner&.start_with?('amazon_seller') && deliveries.all? { |d| d.shipping_option&.carrier == 'AmazonSeller' } marketplace_ok = walmart_sww_ok || amazon_amzbs_ok res << "EDI order must use original shipping option matched from: #{edi_original_ship_code}" if edi_original_ship_code.present? && edi_shipping_option_name.blank? && !marketplace_ok end if deliveries.any? { |d| (!d.signature_confirmation && signature_confirmation?) || (d.signature_confirmation && !signature_confirmation?) } res << "EDI order deliveries must use EDI order signature_confirmation option: #{signature_confirmation}" end if edi_is_pick_slip_required? && !has_custom_packing_slip? link_snippet = '' encoded_po = ERB::Util.url_encode(edi_po_number.to_s) packing_slip_url = if customer&.is_houzz? "https://www.houzz.com//printBuyerOrder//orderId=#{encoded_po}" elsif customer&.is_amazon_seller_central? "https://sellercentral.amazon.com/orders/packing-slip?orderId=#{encoded_po}" elsif customer&.is_amazon_vendor_central? "https://vendorcentral.amazon.com/hz/vendor/members/df/orders?id=#{encoded_po}" end if packing_slip_url escaped_url = ERB::Util.html_escape(packing_slip_url) link_snippet = ", get it manually here: <a href=\"#{escaped_url}\" target=\"_blank\" rel=\"noopener noreferrer\"><i class=\"fa-sharp fa-solid fa-arrow-up-right-from-square\"></i> #{escaped_url}</a>" end res << "This EDI order requires a custom packing slip, but it failed to get attached#{link_snippet}" end # Home depot canada rule + item ERT240-1.5x35, remove trap when order has been found. BYPASS in notes for false positive if customer.id == 10_358 && line_items.any? { |li| li.item_id == 1910 } && !activities.notes_only.where(Activity[:notes].matches('%BYPASS%')).exists? res << 'EDI order for HDC contains item: ERT240-1.5x35, contact india to ensure not a duplicate of PO 0088974632' end if .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: #{[:bad_vendor_skus].join(', ')}" if [:bad_vendor_skus]&.any? bad_merchant_skus_msg = "bad merchant SKUs: #{[:bad_merchant_skus].join(', ')}" if [: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. unless ready_for_shipping? res.uniq end |
#hold_orders? ⇒ Boolean
2220 2221 2222 |
# File 'app/models/order.rb', line 2220 def hold_orders? hold_order_reasons.present? end |
#human_state_name ⇒ String
Human-friendly version of the state machine state. Mostly defers
to AASM/state_machines super, but rewrites the bland
"awaiting deliveries" into "awaiting service delivery" for orders
whose only outstanding deliveries are services (warranty visits,
SmartInstall) so the CRM list distinguishes them at a glance.
2401 2402 2403 2404 2405 2406 2407 |
# File 'app/models/order.rb', line 2401 def human_state_name if deliveries.any?(&:service_ready_to_fulfill?) && awaiting_deliveries? 'awaiting service delivery' else super end end |
#include_po_prefix? ⇒ Object
Alias for Customer#include_po_prefix?
570 571 |
# File 'app/models/order.rb', line 570 delegate :include_po_prefix?, :is_home_depot_usa?, :is_home_depot_can?, :is_walmart_ca?, :is_amazon_com?, :is_costco_ca?, :is_part_of_lowes_ca?, :is_part_of_lowes_com?, :is_houzz?, :is_wasn4_or_wat0f?, :is_canadian_tire?, :is_amazon_seller_central?, to: :customer |
#includes_schedulable_service? ⇒ Boolean
2594 2595 2596 |
# File 'app/models/order.rb', line 2594 def includes_schedulable_service? belongs_to_smartservice_group? end |
#inherited_attention_name ⇒ String?
Default attention-line value derived from the shipping address's
person name, but only when the customer is a person (not a
company); otherwise the company name should be used as the line 1
name and ATTN can stay blank.
3372 3373 3374 3375 3376 |
# File 'app/models/order.rb', line 3372 def inherited_attention_name n = nil n = shipping_address.person_name if shipping_address && customer&.is_person? n end |
#installation_country_iso ⇒ String?
ISO 2-letter country code of the installation site (US, CA, …).
3682 3683 3684 |
# File 'app/models/order.rb', line 3682 def installation_country_iso opportunity.presence&.installation_country_iso end |
#installation_country_iso3 ⇒ String?
ISO 3-letter country code of the installation site (USA, CAN, …).
3675 3676 3677 |
# File 'app/models/order.rb', line 3675 def installation_country_iso3 opportunity.presence&.installation_country_iso3 end |
#installation_is_within_range? ⇒ Boolean
3025 3026 3027 3028 3029 3030 3031 |
# File 'app/models/order.rb', line 3025 def installation_is_within_range? zip_code = shipping_address&.zip distance = SmartServicesController.helpers.calculate_distance_from_lz(zip_code) return true if distance.present? && (distance <= 100) # within 100 miles from office false end |
#installation_postal_code ⇒ String?
Postal code where the order will be installed (from the linked
opportunity's installation address, not the shipping address).
Used by service-area lookups for SmartInstall eligibility.
3660 3661 3662 |
# File 'app/models/order.rb', line 3660 def installation_postal_code opportunity.presence&.installation_postal_code end |
#installation_state_code ⇒ String?
State/province code of the installation site (see
#installation_postal_code).
3668 3669 3670 |
# File 'app/models/order.rb', line 3668 def installation_state_code opportunity.presence&.installation_state_code end |
#invoice_balance ⇒ BigDecimal
Sum of Invoice#balance across every invoice on the order — the
outstanding A/R amount. Differs from #balance in that this
looks at invoices once they exist; balance looks at deliveries.
2468 2469 2470 2471 2472 2473 2474 |
# File 'app/models/order.rb', line 2468 def invoice_balance bal = BigDecimal('0.0') invoices.to_a.each do |i| bal += i.balance end bal end |
#invoiced_local_sales_rep ⇒ Party
285 |
# File 'app/models/order.rb', line 285 belongs_to :invoiced_local_sales_rep, class_name: 'Party', foreign_key: :local_sales_rep_id, optional: true |
#invoiced_primary_sales_rep ⇒ Party
283 |
# File 'app/models/order.rb', line 283 belongs_to :invoiced_primary_sales_rep, class_name: 'Party', foreign_key: :primary_sales_rep_id, optional: true |
#invoiced_secondary_sales_rep ⇒ Party
284 |
# File 'app/models/order.rb', line 284 belongs_to :invoiced_secondary_sales_rep, class_name: 'Party', foreign_key: :secondary_sales_rep_id, optional: true |
#invoices ⇒ ActiveRecord::Relation<Invoice>
302 |
# File 'app/models/order.rb', line 302 has_many :invoices |
#is_amazon_com? ⇒ Object
Alias for Customer#is_amazon_com?
570 571 |
# File 'app/models/order.rb', line 570 delegate :include_po_prefix?, :is_home_depot_usa?, :is_home_depot_can?, :is_walmart_ca?, :is_amazon_com?, :is_costco_ca?, :is_part_of_lowes_ca?, :is_part_of_lowes_com?, :is_houzz?, :is_wasn4_or_wat0f?, :is_canadian_tire?, :is_amazon_seller_central?, to: :customer |
#is_amazon_seller_central? ⇒ Object
Alias for Customer#is_amazon_seller_central?
570 571 |
# File 'app/models/order.rb', line 570 delegate :include_po_prefix?, :is_home_depot_usa?, :is_home_depot_can?, :is_walmart_ca?, :is_amazon_com?, :is_costco_ca?, :is_part_of_lowes_ca?, :is_part_of_lowes_com?, :is_houzz?, :is_wasn4_or_wat0f?, :is_canadian_tire?, :is_amazon_seller_central?, to: :customer |
#is_canadian_tire? ⇒ Object
Alias for Customer#is_canadian_tire?
570 571 |
# File 'app/models/order.rb', line 570 delegate :include_po_prefix?, :is_home_depot_usa?, :is_home_depot_can?, :is_walmart_ca?, :is_amazon_com?, :is_costco_ca?, :is_part_of_lowes_ca?, :is_part_of_lowes_com?, :is_houzz?, :is_wasn4_or_wat0f?, :is_canadian_tire?, :is_amazon_seller_central?, to: :customer |
#is_costco_ca? ⇒ Object
Alias for Customer#is_costco_ca?
570 571 |
# File 'app/models/order.rb', line 570 delegate :include_po_prefix?, :is_home_depot_usa?, :is_home_depot_can?, :is_walmart_ca?, :is_amazon_com?, :is_costco_ca?, :is_part_of_lowes_ca?, :is_part_of_lowes_com?, :is_houzz?, :is_wasn4_or_wat0f?, :is_canadian_tire?, :is_amazon_seller_central?, to: :customer |
#is_crm? ⇒ Boolean
3378 3379 3380 |
# File 'app/models/order.rb', line 3378 def is_crm? order_reception_type == 'CRM' end |
#is_edi_order? ⇒ Boolean
2003 2004 2005 |
# File 'app/models/order.rb', line 2003 def is_edi_order? edi_transaction_id.present? end |
#is_fba? ⇒ Boolean
2051 2052 2053 |
# File 'app/models/order.rb', line 2051 def is_fba? is_store_transfer? && customer&.is_amazon_com? end |
#is_from_myprojects? ⇒ Boolean
2359 2360 2361 |
# File 'app/models/order.rb', line 2359 def is_from_myprojects? !customer_reference.to_s.strip.empty? end |
#is_home_depot_can? ⇒ Object
Alias for Customer#is_home_depot_can?
570 571 |
# File 'app/models/order.rb', line 570 delegate :include_po_prefix?, :is_home_depot_usa?, :is_home_depot_can?, :is_walmart_ca?, :is_amazon_com?, :is_costco_ca?, :is_part_of_lowes_ca?, :is_part_of_lowes_com?, :is_houzz?, :is_wasn4_or_wat0f?, :is_canadian_tire?, :is_amazon_seller_central?, to: :customer |
#is_home_depot_usa? ⇒ Object
Alias for Customer#is_home_depot_usa?
570 571 |
# File 'app/models/order.rb', line 570 delegate :include_po_prefix?, :is_home_depot_usa?, :is_home_depot_can?, :is_walmart_ca?, :is_amazon_com?, :is_costco_ca?, :is_part_of_lowes_ca?, :is_part_of_lowes_com?, :is_houzz?, :is_wasn4_or_wat0f?, :is_canadian_tire?, :is_amazon_seller_central?, to: :customer |
#is_houzz? ⇒ Object
Alias for Customer#is_houzz?
570 571 |
# File 'app/models/order.rb', line 570 delegate :include_po_prefix?, :is_home_depot_usa?, :is_home_depot_can?, :is_walmart_ca?, :is_amazon_com?, :is_costco_ca?, :is_part_of_lowes_ca?, :is_part_of_lowes_com?, :is_houzz?, :is_wasn4_or_wat0f?, :is_canadian_tire?, :is_amazon_seller_central?, to: :customer |
#is_marketing_order? ⇒ Boolean
3092 3093 3094 |
# File 'app/models/order.rb', line 3092 def is_marketing_order? order_type == MARKETING_ORDER end |
#is_online? ⇒ Boolean
3382 3383 3384 |
# File 'app/models/order.rb', line 3382 def is_online? order_reception_type == 'Online' end |
#is_part_of_lowes_ca? ⇒ Object
Alias for Customer#is_part_of_lowes_ca?
570 571 |
# File 'app/models/order.rb', line 570 delegate :include_po_prefix?, :is_home_depot_usa?, :is_home_depot_can?, :is_walmart_ca?, :is_amazon_com?, :is_costco_ca?, :is_part_of_lowes_ca?, :is_part_of_lowes_com?, :is_houzz?, :is_wasn4_or_wat0f?, :is_canadian_tire?, :is_amazon_seller_central?, to: :customer |
#is_part_of_lowes_com? ⇒ Object
Alias for Customer#is_part_of_lowes_com?
570 571 |
# File 'app/models/order.rb', line 570 delegate :include_po_prefix?, :is_home_depot_usa?, :is_home_depot_can?, :is_walmart_ca?, :is_amazon_com?, :is_costco_ca?, :is_part_of_lowes_ca?, :is_part_of_lowes_com?, :is_houzz?, :is_wasn4_or_wat0f?, :is_canadian_tire?, :is_amazon_seller_central?, to: :customer |
#is_price_editable? ⇒ Boolean
Tells whether or not the order can have its line item discounted and msrp price editable by the
price editable concerns, this method is called by the ability check first.
1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 |
# File 'app/models/order.rb', line 1925 def is_price_editable? case order_type when Order::STORE_TRANSFER !editing_locked? when Order::CREDIT_ORDER !(ready_for_printing? || printed?) else false end end |
#is_regular_order? ⇒ Boolean
3096 3097 3098 |
# File 'app/models/order.rb', line 3096 def is_regular_order? [SALES_ORDER, MARKETING_ORDER, TECH_ORDER].include?(order_type) end |
#is_remote_service? ⇒ Boolean
2623 2624 2625 |
# File 'app/models/order.rb', line 2623 def is_remote_service? belongs_to_smartservice_group? && line_items.any? { |a| a.sku.include?('REMOTE') } end |
#is_rma_replacement? ⇒ Boolean
3080 3081 3082 |
# File 'app/models/order.rb', line 3080 def is_rma_replacement? rma && [SALES_ORDER, MARKETING_ORDER, TECH_ORDER].include?(order_type) end |
#is_rma_return? ⇒ Boolean
3068 3069 3070 |
# File 'app/models/order.rb', line 3068 def is_rma_return? order_type == CREDIT_ORDER end |
#is_sales_order? ⇒ Boolean
3084 3085 3086 |
# File 'app/models/order.rb', line 3084 def is_sales_order? order_type == SALES_ORDER end |
#is_smartfit_service? ⇒ Boolean
2598 2599 2600 |
# File 'app/models/order.rb', line 2598 def is_smartfit_service? line_items.smartfit_items.any? end |
#is_smartfix_service? ⇒ Boolean
2606 2607 2608 |
# File 'app/models/order.rb', line 2606 def is_smartfix_service? line_items.smartfix_items.any? end |
#is_smartguide_service? ⇒ Boolean
2610 2611 2612 |
# File 'app/models/order.rb', line 2610 def is_smartguide_service? line_items.smartguide_items.any? end |
#is_smartinstall_service? ⇒ Boolean
2602 2603 2604 |
# File 'app/models/order.rb', line 2602 def is_smartinstall_service? line_items.smartinstall_items.any? end |
#is_store_transfer? ⇒ Boolean
3076 3077 3078 |
# File 'app/models/order.rb', line 3076 def is_store_transfer? order_type == STORE_TRANSFER end |
#is_subject_to_minimum_qty_rules? ⇒ Boolean
3100 3101 3102 |
# File 'app/models/order.rb', line 3100 def is_subject_to_minimum_qty_rules? [SALES_ORDER].include?(order_type) end |
#is_tech_order? ⇒ Boolean
3088 3089 3090 |
# File 'app/models/order.rb', line 3088 def is_tech_order? order_type == TECH_ORDER end |
#is_walmart_ca? ⇒ Object
Alias for Customer#is_walmart_ca?
570 571 |
# File 'app/models/order.rb', line 570 delegate :include_po_prefix?, :is_home_depot_usa?, :is_home_depot_can?, :is_walmart_ca?, :is_amazon_com?, :is_costco_ca?, :is_part_of_lowes_ca?, :is_part_of_lowes_com?, :is_houzz?, :is_wasn4_or_wat0f?, :is_canadian_tire?, :is_amazon_seller_central?, to: :customer |
#is_warehouse_pickup? ⇒ Boolean
2696 2697 2698 |
# File 'app/models/order.rb', line 2696 def is_warehouse_pickup? shipping_address&.is_warehouse end |
#is_wasn4_or_wat0f? ⇒ Object
Alias for Customer#is_wasn4_or_wat0f?
570 571 |
# File 'app/models/order.rb', line 570 delegate :include_po_prefix?, :is_home_depot_usa?, :is_home_depot_can?, :is_walmart_ca?, :is_amazon_com?, :is_costco_ca?, :is_part_of_lowes_ca?, :is_part_of_lowes_com?, :is_houzz?, :is_wasn4_or_wat0f?, :is_canadian_tire?, :is_amazon_seller_central?, to: :customer |
#job_name ⇒ String?
Best-known job name for this order: the originating quote's
opportunity name, falling back to the order's own opportunity
name, or nil if the order isn't linked to either. Used by
warehouse pack-list headings and job-folder titles.
1900 1901 1902 1903 1904 1905 1906 |
# File 'app/models/order.rb', line 1900 def job_name if quote quote.opportunity.name elsif opportunity opportunity.name end end |
#linked_support_cases ⇒ ActiveRecord::Relation<LinkedSupportCase>
306 |
# File 'app/models/order.rb', line 306 has_many :linked_support_cases, through: :room_configurations, source: :support_cases |
#local_sales_rep ⇒ Employee?
Local (on-site) sales rep on the order. Same closed-vs-open
semantics as #primary_sales_rep.
1668 1669 1670 1671 1672 |
# File 'app/models/order.rb', line 1668 def local_sales_rep return invoiced_local_sales_rep if order_closed? invoiced_local_sales_rep || customer&.local_sales_rep end |
#log_early_label_event(event_type, message, details = {}) ⇒ Object
Log an event to the early label API log
Used for debugging and visibility into what happened during early label purchase
4302 4303 4304 4305 4306 4307 4308 4309 4310 4311 |
# File 'app/models/order.rb', line 4302 def log_early_label_event(event_type, , details = {}) @early_label_api_log ||= [] @early_label_api_log << { timestamp: Time.current.iso8601, event: event_type, message: , details: details } Rails.logger.info("[EarlyLabel] #{event_type}: #{} #{details.presence&.to_json}") end |
#mark_serial_coupons_as_used ⇒ Object
Burn any single-use coupon serial numbers that were applied to
this order so they can't be redeemed again. Called when the
order transitions out of pending-payment.
5458 5459 5460 |
# File 'app/models/order.rb', line 5458 def mark_serial_coupons_as_used discounts.includes(:coupon_serial_number).joins(:coupon_serial_number).find_each { |d| d.coupon_serial_number.update_attribute!(:used, true) } end |
#marketplace_early_label_eligible? ⇒ Boolean
Check if order is eligible for marketplace early label purchase (Walmart SWW or Amazon Buy Shipping)
4456 4457 4458 |
# File 'app/models/order.rb', line 4456 def marketplace_early_label_eligible? walmart_sww_eligible? || amazon_buy_shipping_eligible? end |
#material_alerts ⇒ ActiveRecord::Relation<MaterialAlert>
309 |
# File 'app/models/order.rb', line 309 has_many :material_alerts, dependent: :destroy |
#merge_shipper_api_log(shipper) ⇒ Object
Merge raw HTTP API call logs from the shipper into the order's API log.
Works for both Walmart SWW and Amazon Buy Shipping carriers.
4315 4316 4317 4318 4319 4320 4321 4322 4323 4324 4325 4326 4327 4328 4329 4330 4331 4332 4333 4334 4335 4336 4337 4338 4339 4340 4341 4342 4343 4344 4345 4346 |
# File 'app/models/order.rb', line 4315 def merge_shipper_api_log(shipper) return unless shipper.respond_to?(:api_log) && shipper.api_log.present? prefix = case shipper when Shipping::AmazonSeller then 'amzbs_http' when Shipping::WalmartSeller then 'sww_http' else 'marketplace_http' end @early_label_api_log ||= [] @early_label_api_log_offset ||= 0 new_entries = shipper.api_log[@early_label_api_log_offset..] @early_label_api_log_offset = shipper.api_log.length (new_entries || []).each do |entry| @early_label_api_log << { timestamp: entry[:timestamp], event: "#{prefix}_#{entry[:operation]}", message: "HTTP #{entry[:method]} #{entry[:url]}", details: { operation: entry[:operation], method: entry[:method], url: entry[:url], request_payload: entry[:request_payload], response_status: entry[:response_status], response_body: entry[:response_body], success: entry[:success], error: entry[:error] } } end end |
#messaging_logs ⇒ ActiveRecord::Relation<MessagingLog>
308 |
# File 'app/models/order.rb', line 308 has_many :messaging_logs, dependent: :destroy, as: :resource |
#minimum_order_quantity_violations ⇒ Array<Alert>
MOQ (minimum-order-quantity) alerts triggered by the current
line items — e.g. "this control requires a minimum of 5 units".
Surfaced in #hold_order_reasons.
2329 2330 2331 |
# File 'app/models/order.rb', line 2329 def minimum_order_quantity_violations Item::Materials::Checks::Moq.new.process(container: self).alerts end |
#move_deliveries_from_quoting! ⇒ Object
Promote any deliveries currently in quoting (rate-shopping)
state into ready_to_ship, copying the order-level shipment /
label / future-release fields onto each delivery first. Skips
pre-pack deliveries (which still need warehouse packing data
before they can be released).
5263 5264 5265 5266 5267 5268 5269 5270 5271 5272 5273 5274 5275 5276 5277 5278 5279 |
# File 'app/models/order.rb', line 5263 def move_deliveries_from_quoting! deliveries.quoting.each do |delivery| next if delivery.pre_pack? delivery.shipment_instructions = shipment_instructions delivery.label_instructions = label_instructions delivery.future_release_date = future_release_date delivery.manual_release_only = manual_release_only delivery.do_not_reserve_stock = do_not_reserve_stock # Save before ready_to_ship! because ready_to_ship! calls reload inside an advisory lock, # which would discard unsaved changes (including future_release_date). # Without this save, orders with future release dates incorrectly go to at_warehouse # instead of future_release state. delivery.save! delivery.ready_to_ship! end end |
#move_deliveries_to_quoting! ⇒ Object
Force every delivery on the order back into the quoting state
so the rate-shop can be re-run from scratch (e.g. after a
shipping address change). Cancels deliveries first if they're
past the cancelable threshold. Wrapped in a transaction so a
mid-loop failure rolls everything back.
5325 5326 5327 5328 5329 5330 5331 5332 5333 5334 5335 5336 5337 5338 5339 5340 5341 5342 5343 5344 5345 5346 5347 |
# File 'app/models/order.rb', line 5325 def move_deliveries_to_quoting! deliveries.reload.each do |delivery| Rails.logger.info("[Order] Moving delivery #{delivery.id} from #{delivery.state} to quoting") begin if delivery.can_back_to_quoting? delivery.back_to_quoting! Rails.logger.info("[Order] Successfully moved delivery #{delivery.id} to quoting") else Rails.logger.warn("[Order] Cannot move delivery #{delivery.id} to quoting - transition not allowed from state: #{delivery.state}") # Try to force it by calling cancel first if delivery.cancelable? delivery.cancel delivery.back_to_quoting! if delivery.can_back_to_quoting? Rails.logger.info("[Order] Successfully moved delivery #{delivery.id} to quoting after cancel") end end rescue StandardError => e Rails.logger.error("[Order] Error moving delivery #{delivery.id} to quoting: #{e.}") 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_payment ⇒ Object
When a SmartService order's payment finally clears, flip the
associated SmartService support case out of
pending_service_payment and into the case_open state so the
service team can pick it up.
5285 5286 5287 5288 |
# File 'app/models/order.rb', line 5285 def move_service_case_from_pending_service_payment service_case = support_cases&.services&.pending_service_payment&.first service_case.presence&.case_open end |
#name ⇒ String
Human label like "Order #SO123456 (PO# 0042)" (or with PO#s
for multi-PO orders). Used in mailer subjects, CRM headers, and
support-case titles.
2826 2827 2828 2829 2830 2831 2832 2833 2834 |
# File 'app/models/order.rb', line 2826 def name po_numbers_text = '' po_numbers = po_number if po_numbers.present? po_numbers_text = " (PO# #{po_numbers})" unless po_numbers.index(',') po_numbers_text = " (PO#s #{po_numbers})" if po_numbers.index(',') end "Order ##{reference_number}#{po_numbers_text}" end |
#need_to_recalculate_shipping ⇒ Object
Flag the order so the next save re-runs the carrier rate-shop and
re-detects shipping options. Clears the suppression flag set by
earlier flows that wanted to bypass detection.
5602 5603 5604 5605 5606 |
# File 'app/models/order.rb', line 5602 def need_to_recalculate_shipping self.recalculate_shipping = true self.do_not_detect_shipping = false logger.debug "Deliverable, need_to_recalculate_shipping ID: #{id}, self.recalculate_shipping: #{recalculate_shipping}, self.do_not_detect_shipping: #{do_not_detect_shipping}" end |
#needs_attention_issues ⇒ Array<String>
Human-readable list of mismatches between the order's state and
its deliveries' states (e.g. order is awaiting_deliveries but
some deliveries are still quoting). Surfaces in the CRM
"Needs Attention" panel so dispatch can hand-fix them.
5447 5448 5449 5450 5451 5452 5453 |
# File 'app/models/order.rb', line 5447 def needs_attention_issues issues = [] issues << "order is #{state.humanize.downcase} but deliveries are still in quoting, need to hold the order and recalculate shipping" if SHIPPING_STATES.include?(state.to_sym) && deliveries.any?(&:quoting?) issues << "order is #{state.humanize.downcase} and deliveries in state Awaiting PO fulfillment, need to ensure PO fulfillment process proceeds" if deliveries.any?(&:awaiting_po_fulfillment?) issues << "order is #{state.humanize.downcase} and deliveries in state Processing PO fulfillment, need to ensure PO fulfillment process completed" if deliveries.any?(&:processing_po_fulfillment?) issues end |
#needs_spiff_training? ⇒ Boolean
1749 1750 1751 |
# File 'app/models/order.rb', line 1749 def needs_spiff_training? spiff_enrollment.present? && spiff_enrollment.spiff.is_active? && customer.orders.one? && customer.activities.where(activity_type_id: ActivityTypeConstants::SPIFFACTTRAIN).empty? end |
#new_customer_online_order ⇒ Boolean
True when this is a brand-new online customer's first order —
a key segmentation flag for new-customer email flows and
acquisition reporting.
2060 2061 2062 2063 2064 2065 2066 2067 2068 2069 2070 2071 |
# File 'app/models/order.rb', line 2060 def new_customer_online_order # puts "!!!new_customer_online_order, new_customer_order: #{new_customer_order}, self.customer.reception_type.downcase: #{self.customer.reception_type.downcase}, self.customer.account.present?: #{self.customer.account.present?}" # Check that this is a new customer/order and an online order Rails.logger.info "new_customer_online_order: id: #{id}, reference_number: #{reference_number}" if new_customer_order && online_order? Rails.logger.info "new_customer_online_order: TRUE id: #{id}, reference_number: #{reference_number}" true else Rails.logger.info "new_customer_online_order: FALSE id: #{id}, reference_number: #{reference_number}" false end end |
#new_customer_order ⇒ Boolean
True when this is the customer's only non-management-held order,
i.e. their first real purchase. Used by the welcome email flow
and by SmartInstall enrollment to skip "first-time customer"
bonuses for repeat customers.
2079 2080 2081 2082 2083 2084 2085 2086 2087 2088 2089 2090 |
# File 'app/models/order.rb', line 2079 def new_customer_order # puts "!!!new_customer_order, self.customer.orders.length: #{self.customer.orders.length}, !in_management_hold?: #{!in_management_hold?}" Rails.logger.info "new_customer_order: id: #{id}, reference_number: #{reference_number}" # Check that this is a new customer/order and not in manager hold (in the case of Canada situation above) if (customer.orders.length == 1) && !in_management_hold? Rails.logger.info "new_customer_order: TRUE id: #{id}, reference_number: #{reference_number}" true else Rails.logger.info "new_customer_order: FALSE id: #{id}, reference_number: #{reference_number}" false end end |
#new_ss_ticket(service, activity_type) ⇒ Object
Shared backend for #create_smartinstall_ticket,
#create_smartfix_ticket, and #create_smartguide_ticket:
builds the SmartService support case, attaches participants and
rooms, and schedules the kickoff activity. No-op if a service
ticket already exists on the order.
3187 3188 3189 3190 3191 3192 3193 3194 3195 3196 3197 3198 3199 3200 3201 3202 3203 3204 3205 3206 3207 3208 3209 3210 |
# File 'app/models/order.rb', line 3187 def new_ss_ticket(service, activity_type) return if support_cases.services.any? ticket = support_cases.new( case_type: 'SmartService', assigned_to_id: Employee::SCOTT, state: 'new', service_address_id: shipping_address.id, description: "#{service}: Order id #{id}. #{service} request for a project.", priority: 'High', service: ) ticket.build_participants_and_rooms(order_id: id) return unless ticket.save! # ticket.order_ids = [self.id] this is already being done in the build_participants_and_rooms method Activity.create(lock_target_datetime: false, activity_type_id: activity_type, target_datetime: 1.working.days.from_now, assigned_resource: Employee.find(Employee::SCOTT), # Assigned by default to JL party: customer, resource: ticket, notes: "Call Customer to confirm date and time of the #{service} service.") end |
#next_six_months_with_end_dates ⇒ Array<Array(String, String)>
Six-month label/value pairs for end-of-month dates starting
this month — [["October 2026", "2026-10-31"], …]. Powers the
ship-by month dropdown on EDI orders.
2384 2385 2386 2387 2388 2389 2390 2391 2392 |
# File 'app/models/order.rb', line 2384 def next_six_months_with_end_dates today = Time.zone.today (0..5).map do |i| date = today >> i month_name = date.strftime('%B %Y') end_of_month = date.end_of_month.strftime('%Y-%m-%d') [month_name, end_of_month] end end |
#next_warehouse_ship_date(now: Time.current) ⇒ Date?
Earliest date the warehouse will hand the order to a carrier,
honoring the same-day cutoff and the company holiday calendar.
nil when the warehouse company can't be resolved.
1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 |
# File 'app/models/order.rb', line 1991 def next_warehouse_ship_date(now: Time.current) company = store&.company return nil unless company local_today = now.in_time_zone(company.working_hours_timezone).to_date case shipping_cutoff_status(now: now) when :same_day then local_today when :after_cutoff then company.next_business_day(local_today + 1) when :non_working_day then company.next_business_day(local_today) end end |
#notify_pre_pack_cancellation ⇒ void
This method returns an undefined value.
When an order is cancelled while it still has pre-pack
deliveries (waiting on warehouse packing data), email each
affected warehouse so they don't waste pack time on a dead order.
Publishes one Events::DeliveryPrePackCancelled per affected delivery
from after_all_transactions_commit; the async subscriber re-queries
by id, so a delivery destroyed later in the same transaction
(purge_empty_quoting_deliveries) is a clean no-op (AppSignal #4958).
5303 5304 5305 5306 5307 5308 5309 5310 5311 5312 5313 5314 5315 5316 5317 5318 |
# File 'app/models/order.rb', line 5303 def notify_pre_pack_cancellation pre_pack_delivery_ids = deliveries.reload.where(state: "pre_pack").ids return unless pre_pack_delivery_ids.any? cancelled_by_id = PaperTrail.request.whodunnit ActiveRecord.after_all_transactions_commit do pre_pack_delivery_ids.each do |delivery_id| Rails.configuration.event_store.publish( Events::DeliveryPrePackCancelled.new(data: { delivery_id:, cancelled_by_id: }), stream_name: "Delivery-#{delivery_id}" ) rescue StandardError => e ErrorReporting.error(e) end end end |
#ok_to_delete?(account = nil) ⇒ Boolean
2863 2864 2865 2866 |
# File 'app/models/order.rb', line 2863 def ok_to_delete?(account = nil) account&.is_admin? || (cancelled? && (line_items.empty? || reference_number.blank?)) end |
#online_order? ⇒ Boolean
2115 2116 2117 2118 |
# File 'app/models/order.rb', line 2115 def online_order? (res = creator.blank? || !creator.is_employee?) && customer.account res end |
#open_activities_counter ⇒ Integer
Number of open follow-up activities tied to this order, for the
CRM's per-order activity badge. Only counts activities that
default-visible filters surface.
5501 5502 5503 |
# File 'app/models/order.rb', line 5501 def open_activities_counter activities.open_activities.visible_by_default.size end |
#opportunities ⇒ Array<Opportunity>
Every opportunity tied to the order — directly via
#opportunity and indirectly via each RoomConfiguration's
opportunity. Used by the CRM activity feed to roll the order up
under each linked job.
1818 1819 1820 1821 1822 1823 |
# File 'app/models/order.rb', line 1818 def opportunities opps = [] opps << opportunity opps += room_configurations.map(&:opportunity) opps.uniq.compact end |
#opportunity ⇒ Opportunity
273 |
# File 'app/models/order.rb', line 273 belongs_to :opportunity, inverse_of: :orders, optional: true |
#opportunity_name ⇒ String?
Name of the linked Opportunity, or nil if the order isn't
tied to one. Free-typed in the CRM order form; setting it
auto-finds-or-creates the opportunity.
2097 2098 2099 |
# File 'app/models/order.rb', line 2097 def opportunity_name opportunity.try(:name) end |
#opportunity_name=(val) ⇒ Object
Setter that finds or creates an Opportunity on the customer
with the given name and links the order to it. Passing nil or
blank detaches the opportunity entirely.
2106 2107 2108 2109 2110 2111 2112 2113 |
# File 'app/models/order.rb', line 2106 def opportunity_name=(val) if val.present? opp = customer.opportunities.find_or_create_by(name: val.strip) self.opportunity = opp else self.opportunity = nil end end |
#order_closed? ⇒ Boolean
1636 1637 1638 |
# File 'app/models/order.rb', line 1636 def order_closed? invoiced? || cancelled? || fraudulent? || printed? end |
#order_emails ⇒ Array<String>
Every email address relevant to this order: the customer + all
their contacts, the transmission email, the tracking email, the
bound contact's emails, and any per-order billing overrides.
De-duplicated and sorted.
2746 2747 2748 2749 2750 2751 2752 2753 2754 |
# File 'app/models/order.rb', line 2746 def order_emails emails = [] emails += customer.contacts_and_self_contact_points_by_category('email').pluck(:detail) emails += transmission_email emails += tracking_email emails += contact.contact_points.emails.pluck(:detail) if contact emails += billing_emails || [] emails.compact.uniq.sort end |
#order_type_title ⇒ String
Human label for order_type (e.g. 'SO' → 'Sales Order').
Falls back to 'Unknown' for codes not in ALL_ORDER_TYPES.
1585 1586 1587 |
# File 'app/models/order.rb', line 1585 def order_type_title ALL_ORDER_TYPES[order_type] || 'Unknown' end |
#partially_shipped? ⇒ Boolean
5533 5534 5535 |
# File 'app/models/order.rb', line 5533 def partially_shipped? deliveries.active.any?(&:completed_regular_delivery?) end |
#participants_options_for_select ⇒ Array<Array(String, Integer)>?
[name, party_id] pairs of every opportunity participant on
this order, suitable for a Rails select helper. nil when no
opportunity is linked.
3745 3746 3747 |
# File 'app/models/order.rb', line 3745 def opportunity&.opportunity_participants&.map { |scp| [scp.party.full_name, scp.party_id] } end |
#party_for_order_confirmation ⇒ Party
The party the order-confirmation email goes to: the contact when
one is bound, else the customer. Mirrors #primary_party but
carries different intent — the confirmation flow specifically
wants whoever placed the order, even if it's a contact under a
company account.
3322 3323 3324 |
# File 'app/models/order.rb', line 3322 def party_for_order_confirmation contact || customer end |
#pay_on_spiff? ⇒ Boolean
1518 1519 1520 |
# File 'app/models/order.rb', line 1518 def pay_on_spiff? spiff_enrollment.present? && (spiff_enrollment.spiff.eligible_reward(self) > 0) end |
#payment_method ⇒ String?
Category code (Payment::CREDIT_CARD, Payment::PO, …) of the
first authorised payment on the order — a quick "how was this
paid?" hint. Returns nil if there are no authorised payments.
2414 2415 2416 2417 2418 |
# File 'app/models/order.rb', line 2414 def payment_method payments..first.category rescue StandardError nil end |
#payment_method_requires_authorization? ⇒ Boolean
True when any payment on the order is awaiting human
authorization (e.g. unreviewed eCheck or fraud-flagged card).
2177 2178 2179 |
# File 'app/models/order.rb', line 2177 def .any? end |
#payment_options(source) ⇒ Array<String>
Allowed payment-method codes for this order, ordered by the
canonical sort for the given UI surface. The source selects
which surface is asking:
'cart'/'online_order_payment'— public checkout'online_order_invoices'— pay-an-invoice flow'crm'— internal CRM order entry
1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 |
# File 'app/models/order.rb', line 1791 def (source) = Payment.(billing_entity, self) .uniq! = [] if %w[cart online_order_payment].include?(source) = if includes_schedulable_service? [Payment::CREDIT_CARD] else [Payment::CREDIT_CARD, Payment::PAYPAL, Payment::PO, Payment::VPO, Payment::CHECK, Payment::WIRE] end elsif source == 'online_order_invoices' = [Payment::CREDIT_CARD] elsif source == 'crm' = [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 .each do |po| << po if .include?(po) end end |
#payments ⇒ ActiveRecord::Relation<Payment>
295 |
# File 'app/models/order.rb', line 295 has_many :payments, dependent: :nullify, inverse_of: :order |
#payments_requiring_authorization ⇒ Array<Payment>
Authorised payments that haven't yet been management-approved
AND whose authorisation-review pipeline has flagged them as
requiring sign-off (any reason — fraud risk, eCheck, large
amount, etc.). Used by #payment_method_requires_authorization?.
2196 2197 2198 |
# File 'app/models/order.rb', line 2196 def payments..select { |pp| !pp.payment_approved? && (pp.[:required] == true) } end |
#payments_with_fraud_report ⇒ Array<Payment>
Non-voided payments that carry a fraud report and haven't been
explicitly approved by management. Subset of #payments used by
#potential_fraud_reasons and the fraud-review UI.
1514 1515 1516 |
# File 'app/models/order.rb', line 1514 def payments_with_fraud_report payments.non_voided.select { |a| a.fraud_report.present? && !a.payment_approved? } end |
#paypal_invoices_paid? ⇒ Boolean
1912 1913 1914 1915 1916 1917 |
# File 'app/models/order.rb', line 1912 def paypal_invoices_paid? active_paypal_invoices = payments.paypal_invoices.where.not(state: %w[declined voided expired cancelled]) return true if active_paypal_invoices.empty? active_paypal_invoices.all?(&:captured?) end |
#po_number(include_po_prefix: false, limit: nil) ⇒ String
Comma-joined PO number(s) across the order's authorised /
captured payments. Store transfers use the linked
PurchaseOrder reference number instead. Pass
include_po_prefix: true to prepend "PO# ", or limit: N to
cap the number of payment rows scanned (avoids scanning
thousands of payments on a heavily-amended order).
2936 2937 2938 2939 2940 2941 2942 2943 2944 2945 |
# File 'app/models/order.rb', line 2936 def po_number(include_po_prefix: false, limit: nil) query = payments.where(state: %w[authorized captured]).where.not(po_number: [nil, '']) query = query.limit(limit) if limit po_numbers = query.distinct.pluck(:po_number) po_numbers = po_numbers.map(&:strip).uniq res = po_numbers.join(', ') res.insert(0, 'PO# ') if res.present? && include_po_prefix res = purchase_order.reference_number if is_store_transfer? res end |
#po_number_barcode(file_path: nil) ⇒ String
Instance-level wrapper around po_number_barcode that uses
this order's PO number. Used by warehouse pack-list PDFs to
render a scannable Code-128 barcode.
2995 2996 2997 |
# File 'app/models/order.rb', line 2995 def (file_path: nil) self.class.(po_number:, file_path:) end |
#po_numbers ⇒ Array<String>
Distinct PO numbers across all payments on the order.
2839 2840 2841 |
# File 'app/models/order.rb', line 2839 def po_numbers payments.where.not(po_number: nil).distinct.pluck(:po_number) end |
#potential_fraud? ⇒ Boolean
1492 1493 1494 |
# File 'app/models/order.rb', line 1492 def potential_fraud? payments..any? { |pp| pp.try(:fraud_report).try(:potential_fraud?) && !pp.payment_approved? } end |
#potential_fraud_reasons ⇒ Array<String>
Aggregate list of fraud-report reasons across every unapproved
payment on the order. Surfaced in the CR-hold UI so reviewers
see at a glance why the order tripped the fraud filter.
1501 1502 1503 1504 1505 1506 1507 |
# File 'app/models/order.rb', line 1501 def potential_fraud_reasons reasons = [] payments_with_fraud_report.each do |payment| reasons += payment.fraud_report.potential_fraud_reasons end reasons.uniq end |
#precreated_rma_credit_available ⇒ BigDecimal
Remaining RMA credit pre-authorised against this order, computed
as line_total_plus_tax − already-applied RMA credit payments.
Floored at 0 so the figure never goes negative when an RMA credit
exceeds the new order's total.
2491 2492 2493 2494 2495 2496 |
# File 'app/models/order.rb', line 2491 def precreated_rma_credit_available total_available = line_total_plus_tax spent = payments..where(category: Payment::RMA_CREDIT).sum(:amount) available = total_available - spent available < 0 ? BigDecimal('0.0') : available end |
#preset_jobs ⇒ ActiveRecord::Relation<PresetJob>
307 |
# File 'app/models/order.rb', line 307 has_many :preset_jobs, inverse_of: :order |
#prevent_recalculate_shipping? ⇒ Boolean
3696 3697 3698 |
# File 'app/models/order.rb', line 3696 def prevent_recalculate_shipping? is_credit_order? || SOLD_STATES.include?(state.to_sym) || retrieving_shipping_costs? end |
#primary_party ⇒ Party
The party who's the "face" of this order — the contact when one
is bound (e.g. dealer with multiple contacts), otherwise the
customer. Used everywhere the CRM needs a single party for
activity attribution and recipient resolution.
2047 2048 2049 |
# File 'app/models/order.rb', line 2047 def primary_party contact || customer end |
#primary_rep_name ⇒ String
Display string for the primary sales rep, falling back to the
literal "Unassigned" so list views never show a blank cell.
2720 2721 2722 |
# File 'app/models/order.rb', line 2720 def primary_rep_name primary_sales_rep.try(:full_name) || 'Unassigned' end |
#primary_sales_rep ⇒ Employee?
Primary sales rep on the order. For closed orders we return
whatever was stamped on the invoice at close time (even if
blank) so commissions match what was reported. For open orders
we fall back to the customer's current primary if the order
itself doesn't override.
1647 1648 1649 1650 1651 1652 |
# File 'app/models/order.rb', line 1647 def primary_sales_rep # if it's closed always return what's stored (even if blank) return invoiced_primary_sales_rep if order_closed? invoiced_primary_sales_rep || customer&.primary_sales_rep end |
#product_review_url ⇒ String?
Reviews.io product-review landing URL pre-filled with this
order's customer/email/skus. Returns nil when the order has no
customer email or no product SKUs (no review surface to render).
1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 |
# File 'app/models/order.rb', line 1216 def product_review_url return nil if customer&.email.blank? product_skus = line_items.non_shipping.joins(:item).pluck('items.sku').compact.uniq return nil unless product_skus.any? store_key = 'warmlyyours-com' params = { store: store_key, user: ERB::Util.url_encode(customer.full_name.presence || 'Customer'), order_id: ERB::Util.url_encode(reference_number), email: ERB::Util.url_encode(customer.email), products: ERB::Util.url_encode(product_skus.join(';')), type: 'product_review', rating: 5 } "https://www.reviews.io/store/landing_new_review?#{params.map { |k, v| "#{k}=#{v}" }.join('&')}" end |
#prune_cart_rooms ⇒ Object
Drop room configurations from the cart whose line items have
all been removed. Cart-only — orders keep their room
associations even when room contents move around.
3049 3050 3051 3052 3053 3054 3055 3056 |
# File 'app/models/order.rb', line 3049 def prune_cart_rooms # this method clears out associated rooms from the cart if none of the room line items remain # this is only for carts since we want to keep room association when linking rooms to quotes and orders in the crm before the rooms have been designed rcs = room_configurations rcs.each do |rc| remove_room_configuration(rc) if line_items.where(room_configuration_id: rc.id).empty? end end |
#public_path ⇒ String?
Public-portal path to the order ("My Account" view) when the
customer has a portal account, otherwise nil.
2857 2858 2859 2860 2861 |
# File 'app/models/order.rb', line 2857 def public_path return nil if customer&.account.blank? UrlHelper.instance.my_order_path(self) end |
#public_payment_link ⇒ String
Public, tokenised URL a customer can hit to pay an order without
signing in. The id is encrypted via Encryption.encrypt_string
so the URL doesn't leak sequential ids.
5467 5468 5469 |
# File 'app/models/order.rb', line 5467 def public_payment_link "https://#{WEB_HOSTNAME}/pay-order/#{encrypted_id}" end |
#purchase_early_label_if_requested ⇒ Hash?
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
3801 3802 3803 3804 3805 3806 3807 3808 3809 3810 3811 |
# File 'app/models/order.rb', line 3801 def purchase_early_label_if_requested return unless purchase_label_early? # Unpersisted records (tests) skip the lock — no DB row to lock against return purchase_early_label_locked unless persisted? Order.with_advisory_lock("early_label_purchase_#{id}", timeout_seconds: 0, disable_query_cache: true) do reload purchase_early_label_locked end || nil end |
#purchase_order ⇒ PurchaseOrder
280 |
# File 'app/models/order.rb', line 280 belongs_to :purchase_order, optional: true |
#quote ⇒ Quote
272 |
# File 'app/models/order.rb', line 272 belongs_to :quote, inverse_of: :orders, optional: true |
#quotes ⇒ Array<Quote>
Every quote tied to the order — directly via #quote and
indirectly via the most recent completed quote on each linked
room configuration.
1830 1831 1832 1833 1834 1835 |
# File 'app/models/order.rb', line 1830 def quotes qs = [] qs << quote qs += room_configurations.map { |rc| rc.quotes.completed_quotes.last } qs.uniq.compact end |
#ready_for_pending_payment? ⇒ Boolean
2586 2587 2588 |
# File 'app/models/order.rb', line 2586 def ready_for_pending_payment? ready_for_shipping? end |
#ready_for_service? ⇒ Boolean
2627 2628 2629 |
# File 'app/models/order.rb', line 2627 def ready_for_service? belongs_to_smartservice_group? end |
#ready_for_shipping? ⇒ Boolean
2631 2632 2633 2634 2635 2636 2637 2638 2639 2640 2641 2642 2643 2644 2645 2646 2647 2648 2649 2650 2651 2652 2653 2654 2655 2656 2657 2658 2659 2660 2661 2662 2663 2664 2665 2666 2667 2668 2669 2670 2671 2672 2673 2674 2675 2676 2677 2678 2679 2680 2681 2682 2683 2684 2685 2686 2687 2688 2689 2690 2691 2692 2693 2694 |
# File 'app/models/order.rb', line 2631 def ready_for_shipping? return false if shipping_address.nil? if has_unreserved_line_items? errors.add :base, "One or more line items require a serial number reservation. Line items: #{line_items.select { |li| li.require_reservation? && !li.fully_reserved? }.map(&:sku).join(', ')}" return false end if has_committed_serial_number_reservations? errors.add :base, 'Not all reserved serial numbers are available' return false end if is_sales_order? && price_match && price_match != line_total errors.add :base, "EDI order price match discrepancy, expects line total to be #{price_match}" return false end if is_sales_order? && customer&.customer_record&.always_custom_packing_list? && edi_is_pick_slip_required? && uploads.in_category('custom_packing_slip_pdf').blank? houzz_snippet = +'' if customer&.is_houzz? && edi_po_number.present? encoded_po = ERB::Util.url_encode(edi_po_number.to_s) escaped_url = ERB::Util.html_escape("https://www.houzz.com//printBuyerOrder//orderId=#{encoded_po}") houzz_snippet = ", get it here: <a href=\"#{escaped_url}\" target=\"_blank\" rel=\"noopener noreferrer\"><i class=\"fa-sharp fa-solid fa-arrow-up-right-from-square\"></i> #{escaped_url}</a>" end errors.add :base, "Customer requires a custom packing slip pdf for all orders#{houzz_snippet}".html_safe return false end # enforce that all Canadian Tire orders must ship to a store and use the store name format if is_sales_order? && customer&.billing_entity&.is_canadian_tire? && !((shipping_address.company_name || shipping_address.person_name) =~ CustomerConstants::CANADIAN_TIRE_STORE_NAME_REGEX).nil? != true errors.add :base, CustomerConstants::CANADIAN_TIRE_STORE_NAME_ERROR_MESSAGE return false end unless shipping_address_valid_and_not_po_box_if_present errors.add :base, 'Shipping address invalid' return false end if deliveries.any?(&:incurs_oversized_penalty?) errors.add :base, 'Shipping packages exceed oversized limits or include a pallet and would incur big penalties, please review packaging and consider shipping via LTL freight.' return false end # if shipping_address.is_warehouse # errors.add :base, 'Warehouse pickup, manager must release from management hold' # return false # end if shipping_address.is_warehouse || (order_type == Order::STORE_TRANSFER) || %w[USA CAN].exclude?(shipping_address.country_iso3) || shipping_address.verified_for_shipping || shipping_address.disable_address_correction? || shipping_address.override_all_address_validation? return true end shipping_address.require_carrier_validation = true carrier 'Purolator' if shipping_address.country_iso3 == 'CAN' shipping_address.validate_with_carrier = carrier shipping_address.do_not_accept_legacy_verified = true res = shipping_address.external_validation_with_carrier(true) unless res || is_www || errors.to_a.any? { |e| e.index('Customer must be contacted to obtain a valid shipping address') } # don't show this validation error for www errors.add :base, "Order carrier validation#{if carrier.present? " with #{carrier}" end} must pass for order to be released. Carrier validation can't be skipped by disabling address correction, but in exceptional circumstances can be skipped by a system administrator. In worst cases, customer may need to be contacted to obtain a valid shipping address. " end res end |
#ready_for_warehouse? ⇒ Boolean
2582 2583 2584 |
# File 'app/models/order.rb', line 2582 def ready_for_warehouse? all_items_in_stock && ready_for_shipping? end |
#recipient_name ⇒ String?
Best-known recipient name for emails and shipping labels:
company name on the shipping address, then the person name on
the address, finally the customer's full name.
1185 1186 1187 |
# File 'app/models/order.rb', line 1185 def recipient_name shipping_address&.company_name || shipping_address&.person_name || customer&.full_name end |
#related_activities ⇒ ActiveRecord::Relation<Activity>
Activity records corresponding to #related_activities_ids.
2142 2143 2144 |
# File 'app/models/order.rb', line 2142 def Activity.where(id: ) end |
#related_activities_ids ⇒ Array<Integer>
Combined ids of every Activity hung directly off this order
plus those hung off its deliveries — the ids the CRM "all
activity for this order" feed should display.
2135 2136 2137 |
# File 'app/models/order.rb', line 2135 def [activities.ids, delivery_activities.ids].flatten end |
#release_order_or_hold ⇒ Object
Release the order to the warehouse unless something requires
accounting/management review first (#accounting_hold_order?),
in which case the order stays on hold for manual release.
2165 2166 2167 |
# File 'app/models/order.rb', line 2165 def release_order_or_hold release_order unless accounting_hold_order? end |
#remove_room_configuration(rc) ⇒ Hash
Detach a RoomConfiguration from the order, switching the
order's bound opportunity/quote to whichever sibling room is
left (or nil if removing the last one). Recomputes discounts
and saves. Returns {status: Boolean, message: String} for the
CRM flash-message stack.
1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 |
# File 'app/models/order.rb', line 1845 def remove_room_configuration(rc) order_label = cart? ? 'cart' : 'order' if rc && room_configuration_ids.include?(rc.id) room_configurations.delete rc if opportunity_id == rc.opportunity_id update(opportunity: opportunities[1]) # this will be nil if no room_configurations, as desired. end if rc.quote_ids.include?(quote_id) update(quote: quotes[1]) # this will be nil if no room_configurations, as desired. end reset_discount(reset_item_pricing: false) res = save msg = if res "Room/Heated Space #{rc.name} has been removed from #{order_label}." else "Room/Heated Space #{rc.name} has been removed from #{order_label}." end else res = false msg = "Room/Heated Space #{rc.name} is not in the #{order_label}." end { status: res, message: msg } end |
#require_cc_for_advance_credit? ⇒ Boolean
1288 1289 1290 1291 1292 1293 1294 1295 1296 |
# File 'app/models/order.rb', line 1288 def require_cc_for_advance_credit? if allow_advance_credit_without_cc? false elsif rma && rma.rma_items.active.all?(&:will_not_be_returned?) false else customer.credit_card_vaults.valid.visible.empty? && rma.rma_items.any?(&:is_customer_fault?) end end |
#requires_intervention? ⇒ Boolean
2224 2225 2226 |
# File 'app/models/order.rb', line 2224 def requires_intervention? crm_back_order? || in_cr_hold? || pending_review? || || pending_payment? end |
#reset_cart ⇒ Object
Wipe a cart back to empty: destroy all line items, quoting
deliveries, and discounts, clear the shipping address, and prune
any rooms that no longer have line items. Used by the public
site's "empty cart" button.
3037 3038 3039 3040 3041 3042 3043 3044 |
# File 'app/models/order.rb', line 3037 def reset_cart line_items.destroy_all deliveries.quoting.destroy_all discounts.destroy_all self.shipping_address = nil save prune_cart_rooms end |
#restricted_order_type? ⇒ Boolean
1536 1537 1538 |
# File 'app/models/order.rb', line 1536 def restricted_order_type? RESTRICTED_ORDER_TYPES.key?(order_type) end |
#reviewer ⇒ Object
Returns the reviewer info for Reviews.io invitation
Prefers contact over customer if present
1191 1192 1193 1194 1195 1196 1197 |
# File 'app/models/order.rb', line 1191 def reviewer reviewer_party = contact.presence || customer { email: reviewer_party&.email, name: reviewer_party&.full_name } end |
#rma ⇒ Rma
275 |
# File 'app/models/order.rb', line 275 belongs_to :rma, inverse_of: :orders, optional: true |
#rma_cancel ⇒ Object
Hard-cancel an RMA replacement order: destroy all active
deliveries first (so their inventory commits release) then
destroy the order itself, all in one transaction.
3648 3649 3650 3651 3652 3653 |
# File 'app/models/order.rb', line 3648 def rma_cancel Order.transaction do deliveries.active.each(&:destroy) destroy end end |
#rma_items ⇒ ActiveRecord::Relation<RmaItem>
303 |
# File 'app/models/order.rb', line 303 has_many :rma_items, inverse_of: :replacement_order |
#rmas ⇒ ActiveRecord::Relation<Rma>
299 |
# File 'app/models/order.rb', line 299 has_many :rmas, foreign_key: :original_order_id |
#sales_support_rep ⇒ Employee
271 |
# File 'app/models/order.rb', line 271 belongs_to :sales_support_rep, class_name: 'Employee', optional: true |
#save_early_label_api_log ⇒ Object
Save the accumulated API log to the order's early_label_metadata
Called at the end of purchase_early_label_if_requested (success or failure)
4350 4351 4352 4353 4354 4355 4356 4357 4358 4359 4360 4361 4362 4363 4364 4365 4366 4367 |
# File 'app/models/order.rb', line 4350 def save_early_label_api_log return if @early_label_api_log.blank? # Append to existing log if present, or create new existing_log = early_label_api_log || [] new_log = existing_log + @early_label_api_log # Keep only the last 50 entries to prevent unbounded growth new_log = new_log.last(50) if new_log.length > 50 # Ensure early_label_metadata is a hash before merging = || {} update_column(:early_label_metadata, .merge('api_log' => new_log)) @early_label_api_log = nil rescue StandardError => e Rails.logger.error("[EarlyLabel] Failed to save API log: #{e.}") Rails.logger.error(e.backtrace.first(3).join("\n")) end |
#schedule_follow_up_activity ⇒ Boolean
Run the FollowUpScheduler service against this order.
The scheduler decides whether the customer is due for a
follow-up call/email and creates the activity if so.
2892 2893 2894 2895 |
# File 'app/models/order.rb', line 2892 def schedule_follow_up_activity result = Order::FollowUpScheduler.new.process(self) result.activity_scheduled? end |
#search_text ⇒ String? (protected)
Indexed search text for full-text matching: order reference,
PO numbers, and RMA reference. Carts return nil so they don't
pollute the search index.
5811 5812 5813 5814 5815 5816 5817 5818 5819 |
# File 'app/models/order.rb', line 5811 def search_text return nil if cart? st = [] st << reference_number st += payments.pluck(:po_number) st << rma_reference st.join(' ') end |
#secondary_sales_rep ⇒ Employee?
Secondary sales rep on the order. Same closed-vs-open semantics
as #primary_sales_rep.
1658 1659 1660 1661 1662 |
# File 'app/models/order.rb', line 1658 def secondary_sales_rep return invoiced_secondary_sales_rep if order_closed? invoiced_secondary_sales_rep || customer&.secondary_sales_rep end |
#selected_shipping_cost_supports_early_label?(selected_sc, delivery) ⇒ Boolean
Checks whether the delivery's selected shipping cost is compatible with
the marketplace early-label flow. Today this means AMZBS or SWW; long-term
it may also include Heatwave native ship-labels.
4514 4515 4516 4517 4518 4519 4520 4521 4522 4523 4524 4525 4526 4527 |
# File 'app/models/order.rb', line 4514 def selected_shipping_cost_supports_early_label?(selected_sc, delivery) return false unless delivery.present? && selected_sc.present? is_amazon = edi_orchestrator_partner&.start_with?('amazon_seller') is_walmart = edi_orchestrator_partner&.start_with?('walmart_seller') if is_amazon selected_sc.is_amzbs? elsif is_walmart selected_sc.is_sww? else false end end |
#selection_name ⇒ String
Select-2 / Tom Select dropdown label — like #to_label but
uses "not shipped" when the order hasn't shipped yet.
3641 3642 3643 |
# File 'app/models/order.rb', line 3641 def selection_name "#{reference_number} #{customer.full_name} (#{shipped_date.present? ? shipped_date.to_fs(:crm_default) : 'not shipped'})" end |
#send_back_order_notification ⇒ Object
Email the back-order team that this order moved into back-order
state and needs follow-up with the customer.
3254 3255 3256 |
# File 'app/models/order.rb', line 3254 def send_back_order_notification OrdersMailer.back_order_notification(self).deliver_later end |
#send_early_label_void_alert(reason:, details:) ⇒ Object
Send alert email when early label void fails or is blocked
SAFEGUARD C: Ensures operations team is notified of label issues
4257 4258 4259 4260 4261 4262 4263 4264 4265 4266 4267 4268 4269 4270 4271 4272 4273 4274 4275 4276 4277 4278 4279 4280 4281 4282 4283 4284 4285 4286 4287 4288 4289 4290 4291 4292 4293 4294 |
# File 'app/models/order.rb', line 4257 def send_early_label_void_alert(reason:, details:) marketplace = edi_orchestrator_partner&.start_with?('amazon_seller') ? 'Amazon' : 'Walmart' portal = marketplace == 'Amazon' ? 'Amazon Seller Central' : 'Walmart Seller Portal' Rails.logger.warn("[EarlyLabel] Sending void alert for order #{reference_number}: #{reason}") SystemMailer.generic_mailer( subject: "[URGENT] Early Label Void Issue - Order #{reference_number}", body: <<~BODY, An early label void issue occurred that requires attention. ORDER DETAILS: - Order: #{reference_number} - Tracking: #{early_label_tracking_number} - Carrier: #{early_label_carrier} - Label Purchased: #{early_label_purchased_at&.strftime('%Y-%m-%d %H:%M:%S %Z')} - EDI Partner: #{edi_orchestrator_partner} - PO Number: #{edi_po_number} - Marketplace: #{marketplace} ISSUE: - Reason: #{reason} - Details: #{details} ACTION REQUIRED: 1. Check the order in Heatwave: https://#{CRM_HOSTNAME}/en-US/orders/#{id} 2. Check the label status in #{portal} 3. If the label exists in #{marketplace} but not in Heatwave, manually void it in #{portal} 4. Update the order status as needed This is an automated alert from the Early Label Purchase system. BODY to: ORDERS_EMAIL, from: ADMINISTRATOR_EMAIL ).deliver_later rescue StandardError => e Rails.logger.error("[EarlyLabel] Failed to send void alert email: #{e.}") end |
#send_online_order_confirmation ⇒ Hash{Symbol => Symbol, String}
Build and send the ONLINE_ORDER_CONFIRM email via
CommunicationBuilder, then pin the recipient address to the
order's tracking_email array so subsequent ship/tracking
emails go to the same place. Returns a {status_code:, status_message:} hash for the controller's flash stack.
3289 3290 3291 3292 3293 3294 3295 3296 3297 3298 3299 3300 3301 3302 3303 3304 3305 3306 3307 3308 3309 3310 3311 3312 3313 |
# File 'app/models/order.rb', line 3289 def send_online_order_confirmation if email_for_order_confirmation sender = customer.primary_sales_rep co = CommunicationBuilder.new( resource: self, sender_party: sender, sender: sender || INFO_EMAIL, recipient_party: party_for_order_confirmation, emails: email_for_order_confirmation, recipient_name: party_for_order_confirmation.name, template_system_code: 'ONLINE_ORDER_CONFIRM', bcc: sender&.email, merge_options: { order: to_liquid } ).create if co.draft? { status_code: :error, status_message: co.errors_to_s } else # IMPORTANT!!! THIS MUST BE SET HERE update_column(:tracking_email, [email_for_order_confirmation]) { status_code: :ok, status_message: "Order confirmation e-mail sent to #{party_for_order_confirmation.name} #{email_for_order_confirmation}." } end else { status_code: :error, status_message: "Can't send: order/customer e-mail is blank!" } end end |
#send_payment_automatically_authorized_notification ⇒ Object
Notify accounting that the auto-authoriser approved a payment
without human review — useful for spot-checking the rules engine.
3278 3279 3280 |
# File 'app/models/order.rb', line 3278 def OrdersMailer.(self).deliver_later end |
#send_profit_review_notification ⇒ Object
Email the management profit-review queue when this order's
margin falls below the configured threshold and needs sign-off.
3266 3267 3268 |
# File 'app/models/order.rb', line 3266 def send_profit_review_notification OrdersMailer.order_profit_review_notification(self).deliver_later end |
#send_ready_for_pickup_email ⇒ Hash{Symbol => Symbol, String}
Send the warehouse-pickup-ready email (ORDER_PICKUP template)
used for "your order is ready at the will-call counter" notices.
5562 5563 5564 |
# File 'app/models/order.rb', line 5562 def send_ready_for_pickup_email send_tracking_template_email 'ORDER_PICKUP' end |
#send_release_authorization_notification ⇒ Object
Email management when an order needs explicit release-from-CR-hold
authorisation (e.g. fraud-flagged payment, large order, etc.).
3272 3273 3274 |
# File 'app/models/order.rb', line 3272 def OrdersMailer.(self).deliver_later end |
#send_request_carrier_assignment_notification ⇒ Object
Notify dispatch when an order is waiting on a manual carrier
assignment (rate-shop returned no eligible options).
3260 3261 3262 |
# File 'app/models/order.rb', line 3260 def send_request_carrier_assignment_notification DeliveryMailer.request_carrier_assignment_notification(self).deliver_later end |
#send_tracking_email ⇒ Hash{Symbol => Symbol, String}
Send the standard ORDER_TRACKING email with the carrier's
tracking link.
5554 5555 5556 |
# File 'app/models/order.rb', line 5554 def send_tracking_email send_tracking_template_email 'ORDER_TRACKING' end |
#send_tracking_template_email(template_code) ⇒ Hash{Symbol => Symbol, String}
Generic helper that drives both #send_tracking_email and
#send_ready_for_pickup_email: builds the communication via
CommunicationBuilder for the named system template and sends
it to every address in tracking_email. Returns a controller-
friendly {status_code:, status_message:} hash.
5574 5575 5576 5577 5578 5579 5580 5581 5582 5583 5584 |
# File 'app/models/order.rb', line 5574 def send_tracking_template_email(template_code) return { status_code: :error, status_message: "Can't send #{template_code}: no tracking e-mail present!" } if tracking_email.blank? sender = customer.primary_sales_rep co = CommunicationBuilder.new(resource: self, sender_party: sender, sender: (sender.nil? ? INFO_EMAIL : nil), recipient_party: customer, emails: tracking_email.join(','), template_system_code: template_code).create if co.draft? { status_code: :error, status_message: co.errors_to_s } else { status_code: :ok, status_message: "#{template_code} e-mail sent." } end end |
#service_only_order? ⇒ Boolean
2200 2201 2202 |
# File 'app/models/order.rb', line 2200 def service_only_order? line_items.non_shipping.any? && line_items.non_shipping.all?(&:is_service?) end |
#set_currency ⇒ Object (protected)
Before-validation callback: pin the order's currency to the
customer's store currency when not already set, so currency
never silently defaults to USD on non-US orders.
5776 5777 5778 |
# File 'app/models/order.rb', line 5776 def set_currency self.currency ||= customer&.store&.currency end |
#set_custom_order_agreement ⇒ Object
Checks for the presence of custom products in excess of $2k
5360 5361 5362 5363 5364 5365 5366 |
# File 'app/models/order.rb', line 5360 def set_custom_order_agreement return unless is_sales_order? return if custom_order_agreement_bypass? return unless meets_custom_products_threshold? update_column(:custom_order_agreement_status, Order.custom_order_agreement_statuses['custom_order_agreement_required']) end |
#set_default_tracking_email(force: false) ⇒ Object
Populate tracking_email with addresses extracted by
DefaultTrackingEmailExtractor (customer + contact +
opportunity participants). No-op when an address is already set
unless force: true is passed.
3598 3599 3600 3601 3602 |
# File 'app/models/order.rb', line 3598 def set_default_tracking_email(force: false) return unless force || tracking_email.empty? self.tracking_email = Order::DefaultTrackingEmailExtractor.new(self).emails end |
#set_min_profit_markup ⇒ Object
Before-validation callback: pin the order's minimum-profit
markup percentage. Sales orders default to the company-wide
default_sales_markup; everything else (RMA replacements,
marketing, tech orders) gets 0 so they don't trip the
profit-review hold.
3584 3585 3586 3587 3588 3589 3590 |
# File 'app/models/order.rb', line 3584 def set_min_profit_markup self.min_profit_markup = if is_sales_order? default_sales_markup else 0 end end |
#set_opportunity_source ⇒ Object
After-save hook: when the order's source changes (and isn't the
generic "unknown source"), propagate that source up to its
opportunity. Keeps opportunity-level acquisition reporting
consistent with whatever source the order ended up with.
1614 1615 1616 1617 1618 1619 1620 1621 |
# File 'app/models/order.rb', line 1614 def set_opportunity_source return unless source && saved_change_to_source_id? return if source&.unknown_source? return unless opportunity # If you made it this far, then you can update the opportunity source opportunity.update_column(:source_id, source.id) end |
#set_shipped_date ⇒ Object
Stamp the order's shipped_date with today, but only when blank
— once shipped, the date is canonical and shouldn't drift on a
re-save. Called by the after-ship state-machine transition.
2915 2916 2917 |
# File 'app/models/order.rb', line 2915 def set_shipped_date update_attribute(:shipped_date, Date.current) if shipped_date.blank? end |
#ship_from_attributes(delivery = nil) ⇒ Hash
Build the "ship from" address/phone/email/attention block for a
carrier API call. Defaults to the order's store warehouse, but
when delivery is supplied it uses the delivery's origin
address (which differs for dropship deliveries) and applies any
shipping-account-number override (used when the customer is
billing shipping to their own carrier account).
3221 3222 3223 3224 3225 3226 3227 3228 3229 3230 3231 3232 3233 3234 3235 3236 3237 3238 3239 3240 3241 3242 3243 3244 3245 3246 3247 3248 3249 3250 |
# File 'app/models/order.rb', line 3221 def ship_from_attributes(delivery = nil) res = {} # Here we use delivery, if present, to extract the origin address for the ship from, as well as the contact info. This is for drop shippers, but should also cover when shipper is WY US or Canada res[:address] = delivery&.origin_address || store.warehouse_address # sort of a kludge but for now use shipping configuration's sender phone for legacy matching res[:phone] = delivery&.origin_address&.party&.phone || SHIPPING_SHIPPER_CONFIGURATION[country.iso3.to_sym][:shipper_phone] res[:email] = delivery&.origin_address&.party&.email || SHIPPING_SHIPPER_CONFIGURATION[country.iso3.to_sym][:shipper_email] # post rb_any_ship_from, we actually want the ship_from to be the physical ship from and handle the shipper separately if bill_shipping_to_customer && delivery&.chosen_shipping_method&.shipping_account_number if delivery.chosen_shipping_method.shipping_account_number.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.shipping_account_number.address end if begin delivery.chosen_shipping_method.shipping_account_number.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.shipping_account_number.phone.detail end end # attention_name cannot be blank!!!! res[:attention_name] = res[:address].person_name res[:attention_name] = 'Shipping Department' if res[:attention_name].blank? res[:name] = (res[:address].company_name || res[:address].person_name) res[:phone] = res[:phone].scan(/[0-9]/).join if res[:phone] res end |
#ship_to_attributes ⇒ Hash
Build the "ship to" hash for carrier API calls: address, phone,
email, and ATTN/name lines, with sensible cascading fallbacks
(attention_name → address person name → literal "Customer",
phone falls through customer's phones to the rep's, etc.) so the
carrier never receives a blank field.
3116 3117 3118 3119 3120 3121 3122 3123 3124 3125 3126 3127 3128 3129 3130 3131 3132 3133 3134 3135 3136 3137 3138 3139 3140 3141 3142 3143 |
# File 'app/models/order.rb', line 3116 def ship_to_attributes res = {} res[:address] = shipping_address # attention_name cannot be blank!!!! res[:attention_name] = attention_name res[:attention_name] = res[:address].person_name if res[:attention_name].blank? res[:attention_name] = 'Customer' if res[:attention_name].blank? if res[:address].company_name res[:name] = res[:address].company_name res[:name] = 'Customer' if res[:name].blank? else res[:name] = attention_name res[:name] = res[:address].person_name if res[:name].blank? res[:name] = 'Customer' if res[:name].blank? end res[:phone] = shipping_phone.presence || customer.phone || customer.cell_phone || customer.primary_sales_rep&.direct_phone || customer.store.contact_number || SHIPPING_SHIPPER_CONFIGURATION.dig(country.iso3.to_sym, :shipper_phone) res[:email] = first_tracking_email&.strip.presence || customer.primary_sales_rep&.email || customer.store.contact_email || SHIPPING_SHIPPER_CONFIGURATION.dig(country.iso3.to_sym, :shipper_email) # need something here res end |
#ship_weight ⇒ Float
Aggregate physical ship weight across all non-shipping line
items, floored at 0.1 lb so carrier rate APIs that reject zero
weights don't blow up. Memoised so freight quote calls don't
re-walk the line items.
5520 5521 5522 |
# File 'app/models/order.rb', line 5520 def ship_weight @ship_weight ||= [0.1, line_items.includes(:item).non_shipping.parents_only.sum(&:total_shipping_weight).round(1)].max end |
#shipments ⇒ ActiveRecord::Relation<Shipment>
313 |
# File 'app/models/order.rb', line 313 has_many :shipments, -> { order(:id) }, through: :deliveries |
#shipping_account_number ⇒ ShippingAccountNumber
DELIVERY REFACTOR HERE
279 |
# File 'app/models/order.rb', line 279 belongs_to :shipping_account_number, optional: true |
#shipping_address ⇒ Address
288 |
# File 'app/models/order.rb', line 288 belongs_to :shipping_address, class_name: 'Address', validate: true, optional: true |
#shipping_address_valid_and_not_po_box_if_present(rate_shopping = false) ⇒ Boolean
Validates the shipping address: must be present, must pass its
own AR validations, and must not be a PO box for the chosen
carrier (FedEx/UPS reject them; USPS accepts them). Pass
rate_shopping: true to skip the carrier check during the
initial shop-rates pass — the carrier isn't picked yet so we
can't enforce its specific rules.
2565 2566 2567 2568 2569 2570 2571 2572 2573 2574 2575 2576 2577 2578 2579 2580 |
# File 'app/models/order.rb', line 2565 def shipping_address_valid_and_not_po_box_if_present(rate_shopping = false) res = false if shipping_address.nil? errors.add(:shipping_address, 'must be selected') elsif shipping_address.present? carrer_to_use = carrier carrer_to_use = nil if rate_shopping if shipping_address.valid? && shipping_address.not_a_po_box?(carrer_to_use) res = true else res = false shipping_address.errors..each { |msg| errors.add(:shipping_address, msg) } end end res end |
#shipping_cutoff_advance_order? ⇒ Boolean
1948 1949 1950 |
# File 'app/models/order.rb', line 1948 def shipping_cutoff_advance_order? line_items.goods.parents_only.any? { |a| !a.in_stock? } end |
#shipping_cutoff_next_day? ⇒ Boolean
1944 1945 1946 |
# File 'app/models/order.rb', line 1944 def shipping_cutoff_next_day? line_items.goods.parents_only.any? { |a| a.in_stock? && a.ships_via_freight? } # next day end |
#shipping_cutoff_same_day? ⇒ Boolean
1940 1941 1942 |
# File 'app/models/order.rb', line 1940 def shipping_cutoff_same_day? line_items.goods.parents_only.all? { |a| a.in_stock? && !a.ships_via_freight? } # same day end |
#shipping_cutoff_status(now: Time.current) ⇒ Symbol
Whether the warehouse can still ship this order today.
Branches on the warehouse company's working hours + holiday
calendar (USA/CAN), not the visitor's local clock.
1969 1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982 1983 |
# File 'app/models/order.rb', line 1969 def shipping_cutoff_status(now: Time.current) company = store&.company return :unknown unless company local_now = now.in_time_zone(company.working_hours_timezone) company.with_working_hours_config do if !WorkingHours.working_day?(local_now.to_date) :non_working_day elsif local_now.hour >= SAME_DAY_CUTOFF_HOUR :after_cutoff else :same_day end end end |
#shipping_date_warnings ⇒ Object
Warnings about shipping dates that don't block order release
These are displayed to users but don't prevent state transitions
Note: requested_ship_before is automatically advanced when the user revisits the shipping form,
so no warning is needed when it's in the past.
2320 2321 2322 |
# File 'app/models/order.rb', line 2320 def shipping_date_warnings [] end |
#should_commit_stock? ⇒ Boolean
1126 1127 1128 |
# File 'app/models/order.rb', line 1126 def should_commit_stock? (future_release_date.present? && !do_not_reserve_stock?) || all_funds_available? end |
#smartservice_ticket ⇒ SupportCase?
The SmartService ticket currently waiting on payment for this
order, or nil if there isn't one. Used by the post-payment
hook that flips the ticket open (#move_service_case_from_pending_service_payment).
2619 2620 2621 |
# File 'app/models/order.rb', line 2619 def smartservice_ticket support_cases&.services&.pending_service_payment&.first end |
#sms_enabled_numbers ⇒ Array<String>
SMS-capable phone numbers for every order participant
(customer, contact, opportunity participants), formatted in the
E.164-style our SMS provider expects.
1275 1276 1277 |
# File 'app/models/order.rb', line 1275 def sms_enabled_numbers ContactPoint.where(party_id: all_participant_ids).sms_numbers.order(:detail).map(&:formatted_for_sms).uniq end |
#sms_messages ⇒ ActiveRecord::Relation<SmsMessage>
SMS conversation thread tied to this order — every inbound /
outbound message exchanged with any participant phone returned
by #sms_enabled_numbers.
1284 1285 1286 |
# File 'app/models/order.rb', line 1284 def SmsMessage.for_numbers(sms_enabled_numbers) end |
#sold_to_billing_address ⇒ Address
289 |
# File 'app/models/order.rb', line 289 belongs_to :sold_to_billing_address, class_name: 'Address', foreign_key: :sold_to_billing_address, optional: true |
#spiff_enrollment ⇒ SpiffEnrollment
277 |
# File 'app/models/order.rb', line 277 belongs_to :spiff_enrollment, optional: true |
#spiff_rep ⇒ Contact
276 |
# File 'app/models/order.rb', line 276 belongs_to :spiff_rep, class_name: 'Contact', optional: true, inverse_of: :spiff_orders |
#spiff_reward ⇒ String
Eligible SPIFF reward amount formatted as "%.2f". Assumes the
order is enrolled in a SPIFF; raises if spiff_enrollment is
nil.
1777 1778 1779 |
# File 'app/models/order.rb', line 1777 def spiff_reward '%.2f' % spiff_enrollment.spiff.eligible_reward(self) end |
#state_description(describe_state = nil) ⇒ String?
User-facing description for a state name, optionally for a
state other than the current one. Pulled from
OrderConstants::STATE_DESCRIPTION so descriptions are managed
in one place.
1682 1683 1684 |
# File 'app/models/order.rb', line 1682 def state_description(describe_state = nil) OrderConstants::STATE_DESCRIPTION[describe_state || state] end |
#state_list ⇒ Array<Symbol>
The ordered set of states this order can be in given its type
and current data. Credit orders follow a fixed sequence; sales /
store-transfer / etc. orders compute the state list dynamically
based on which states they've actually visited (so the CRM
progress bar only shows applicable steps).
1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 |
# File 'app/models/order.rb', line 1714 def state_list if order_type == CREDIT_ORDER transition %i[cart pending pending_payment pending_release_authorization crm_back_order awaiting_deliveries in_cr_hold created awaiting_return] => :fraudulent, if: :can_be_cancelled? %i[created awaiting_return pending_review ready_for_printing printed cancelled] else states = [] states << :cart if cart? states << :pending states << :pending_payment states << :crm_back_order if crm_back_order? states << :pending_release_authorization if 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_status ⇒ Symbol
Combined inventory status across all line items: :ok,
:partial, or :none. Computed by LineItem.inventory_check.
2552 2553 2554 |
# File 'app/models/order.rb', line 2552 def stock_status LineItem.inventory_check(self) end |
#stop_for_pre_pack? ⇒ Boolean
5375 5376 5377 5378 5379 5380 5381 5382 5383 5384 5385 |
# File 'app/models/order.rb', line 5375 def stop_for_pre_pack? return false unless can_request_estimated_packaging? return false if customer.is_e_commerce_misc? return false if room_configurations.present? && !room_configurations.all?(&:transmittable_and_settled?) return false if deliveries.all? { |d| d.shipments.packed.any? && d.all_lines_allocated_to_shipments? } res = need_to_pre_pack_reasons return res if res&.any? false end |
#stop_for_profit_review? ⇒ Boolean
5368 5369 5370 5371 5372 5373 |
# File 'app/models/order.rb', line 5368 def stop_for_profit_review? # Etailer orders are immune return false if customer.is_e_commerce_misc? !profit_margins_met? end |
#store ⇒ Store?
The Store this order is fulfilled from. Store transfers carry
an explicit from_store; everything else inherits the
customer's store.
1122 1123 1124 |
# File 'app/models/order.rb', line 1122 def store from_store || customer&.store end |
#store_additional_early_label_pdfs(additional_labels) ⇒ Object
Persist the secondary label PDFs returned by a multi-package
marketplace label purchase. Each entry is {label_data:, tracking_number:}; missing or blank entries are skipped, and a
single-PDF failure doesn't abort the rest of the loop.
5013 5014 5015 5016 5017 5018 5019 5020 5021 5022 |
# File 'app/models/order.rb', line 5013 def store_additional_early_label_pdfs(additional_labels) additional_labels.each_with_index do |label, idx| next unless label[:label_data].present? && label[:tracking_number].present? store_early_label_pdf(label[:label_data], label[:tracking_number]) Rails.logger.info("[EarlyLabel] Stored additional package #{idx + 2} label PDF (tracking: #{label[:tracking_number]})") rescue StandardError => e Rails.logger.warn("[EarlyLabel] Failed to store additional package #{idx + 2} label PDF: #{e.}") end end |
#store_amazon_label_pdf_atomic(label_result, tracking_number, marketplace) ⇒ Object
Amazon: label data is returned inline — store it and verify persistence.
For multi-package orders, also stores additional package labels.
4912 4913 4914 4915 4916 4917 4918 4919 4920 4921 4922 4923 4924 4925 4926 4927 4928 4929 4930 4931 4932 4933 4934 4935 4936 4937 4938 4939 4940 |
# File 'app/models/order.rb', line 4912 def store_amazon_label_pdf_atomic(label_result, tracking_number, marketplace) label_data = label_result[:label_data] if label_data.blank? Rails.logger.warn('[EarlyLabel] Amazon label data not returned inline; label PDF will be attached at ship-label time') send_early_label_void_alert( reason: 'Amazon label data missing from purchase response', details: "Label was purchased but no inline label data was returned. " \ "The label exists in #{marketplace}'s system but PDF is not stored locally." ) return nil end upload = store_early_label_pdf(label_data, tracking_number) if upload&.persisted? Rails.logger.info("[EarlyLabel] Amazon label PDF stored as upload #{upload.id}") store_additional_early_label_pdfs(label_result[:additional_labels]) if label_result[:additional_labels].present? return upload end Rails.logger.error("[EarlyLabel] Amazon label PDF storage failed for order #{reference_number}") send_early_label_void_alert( reason: 'Amazon label PDF storage failed', details: "Label data was returned but could not be persisted. " \ "The label exists in #{marketplace}'s system but is not stored locally." ) nil end |
#store_early_label_pdf(label_data, tracking_number) ⇒ Upload?
Store early label PDF on the order
4986 4987 4988 4989 4990 4991 4992 4993 4994 4995 4996 4997 4998 4999 5000 5001 5002 5003 5004 5005 |
# File 'app/models/order.rb', line 4986 def store_early_label_pdf(label_data, tracking_number) filename = "early_ship_label_#{tracking_number}_#{Time.current.to_i}.pdf" upload = Upload.uploadify_from_data( file_name: filename, data: label_data, category: 'early_ship_label', resource: self ) if upload&.persisted? uploads << upload Rails.logger.info("[EarlyLabel] Stored early label PDF as upload #{upload.id}") end upload rescue StandardError => e Rails.logger.error("[EarlyLabel] Failed to store early label PDF: #{e.}") nil end |
#store_id ⇒ Object
Alias for Store#id
2843 |
# File 'app/models/order.rb', line 2843 delegate :id, to: :store, prefix: true |
#store_mock_early_label_pdf(tracking_number, _carrier) ⇒ Object
Store a mock label PDF for development/testing
Uses a pre-generated mock PDF since the sandbox doesn't return real label PDFs
5029 5030 5031 5032 5033 5034 5035 5036 5037 5038 5039 5040 5041 5042 5043 5044 5045 5046 5047 5048 5049 5050 5051 5052 5053 5054 5055 5056 |
# File 'app/models/order.rb', line 5029 def store_mock_early_label_pdf(tracking_number, _carrier) mock_pdf_path = Rails.root.join('test/sww_label_mock_pdf.pdf') unless File.exist?(mock_pdf_path) Rails.logger.error("[EarlyLabel] Mock PDF not found at #{mock_pdf_path}") return nil end mock_pdf_content = File.binread(mock_pdf_path) filename = "early_ship_label_#{tracking_number}_#{Time.current.to_i}.pdf" upload = Upload.uploadify_from_data( file_name: filename, data: mock_pdf_content, category: 'early_ship_label', resource: self ) if upload&.persisted? uploads << upload Rails.logger.info("[EarlyLabel] Stored mock early label PDF as upload #{upload.id}") end upload rescue StandardError => e Rails.logger.error("[EarlyLabel] Failed to store mock early label PDF: #{e.}") nil end |
#store_walmart_label_pdf_atomic(shipper, _label_result, tracking_number, carrier, marketplace) ⇒ Object
Walmart: download label via API with retries, then store and verify persistence
4943 4944 4945 4946 4947 4948 4949 4950 4951 4952 4953 4954 4955 4956 4957 4958 4959 4960 4961 4962 4963 4964 4965 4966 4967 4968 4969 4970 4971 4972 4973 4974 4975 4976 4977 4978 4979 |
# File 'app/models/order.rb', line 4943 def store_walmart_label_pdf_atomic(shipper, _label_result, tracking_number, carrier, marketplace) max_attempts = 3 delays = [1, 2, 3] max_attempts.times do |attempt| delay = delays[attempt] || 1 sleep(delay) Rails.logger.info("[EarlyLabel] Attempting PDF download (attempt #{attempt + 1}/#{max_attempts}) for tracking #{tracking_number}") download_result = shipper.download_label(carrier, tracking_number) merge_shipper_api_log(shipper) if download_result[:success] && download_result[:label_data].present? upload = store_early_label_pdf(download_result[:label_data], tracking_number) if upload&.persisted? Rails.logger.info("[EarlyLabel] PDF successfully stored (attempt #{attempt + 1}) as upload #{upload.id}") return upload else Rails.logger.warn("[EarlyLabel] PDF download succeeded but storage failed (attempt #{attempt + 1})") end else Rails.logger.warn("[EarlyLabel] PDF download failed (attempt #{attempt + 1}): #{download_result[:error]}") end end Rails.logger.error("[EarlyLabel] All PDF download attempts failed for order #{reference_number}, tracking #{tracking_number}") send_early_label_void_alert( reason: 'PDF download failed after all retries', details: "Label was created successfully but PDF could not be downloaded after #{max_attempts} attempts. " \ "The label exists in #{marketplace}'s system but is not stored locally." ) nil end |
#suggested_shipments ⇒ ActiveRecord::Relation?
Shipments still in the suggested state — i.e. proposed by
Shipping::DeterminePackaging but not yet confirmed by the
warehouse. Used by the early-label flow to know what to label.
5510 5511 5512 |
# File 'app/models/order.rb', line 5510 def suggested_shipments shipments.where(state: 'suggested') if respond_to? :shipments end |
#support_case_id ⇒ Integer?
Single support-case binding for the CRM order-setup form's Tom
Select picker, mirroring Rma#support_case_id. Backed by the HABTM
support_cases association so the form can reuse the shared
lookup_support_cases typeahead (which keys options by id).
1470 1471 1472 |
# File 'app/models/order.rb', line 1470 def support_case_id support_case_ids.first end |
#support_case_id=(id) ⇒ Object
1475 1476 1477 |
# File 'app/models/order.rb', line 1475 def support_case_id=(id) self.support_case_ids = Array(id.presence).map(&:to_i) end |
#support_case_ref ⇒ String
Comma-separated list of attached support case numbers. Used in
the CRM order form's free-typed support-case field so users can
paste in a list of case numbers without juggling ids.
1450 1451 1452 |
# File 'app/models/order.rb', line 1450 def support_case_ref support_cases.map(&:case_number).join(', ') end |
#support_case_ref=(support_case_ref) ⇒ Object
Setter that resolves a comma-separated list of case numbers
into the matching support-case ids and writes them to
support_case_ids. Tolerates spaces in the input.
1459 1460 1461 1462 |
# File 'app/models/order.rb', line 1459 def support_case_ref=(support_case_ref) refs = support_case_ref.delete(' ').split(',') self.support_case_ids = SupportCase.where(case_number: refs).ids end |
#support_cases ⇒ ActiveRecord::Relation<SupportCase>
316 |
# File 'app/models/order.rb', line 316 has_and_belongs_to_many :support_cases |
#suppress_edi_duplicate_warning? ⇒ Boolean
Suppresses duplicate order warnings for off-book delivery arrangements.
Returns true if warnings should be suppressed (acknowledged date is in the future and order not shipped).
2009 2010 2011 2012 2013 2014 |
# File 'app/models/order.rb', line 2009 def suppress_edi_duplicate_warning? return false if edi_delayed_delivery_acknowledged_at.blank? return false if shipped? # Auto-clear when shipped edi_delayed_delivery_acknowledged_at.to_date >= Date.current end |
#sync_opportunity ⇒ Object
After-save hook: tell the linked opportunity to re-evaluate its
state machine (the opportunity may need to advance from
"active" to "sold" once the order is invoiced).
3703 3704 3705 |
# File 'app/models/order.rb', line 3703 def sync_opportunity opportunity&.sync_state end |
#technical_support_rep ⇒ TechnicalSupportRep
293 |
# File 'app/models/order.rb', line 293 has_one :technical_support_rep, through: :opportunity |
#terms_available? ⇒ Boolean (protected)
5802 5803 5804 |
# File 'app/models/order.rb', line 5802 def terms_available? billing_entity.terms_credit_limit >= total end |
#to_label ⇒ String
Compact label string used by autocomplete dropdowns and SMS
threads — [REF#] Customer (shipped-date).
3615 3616 3617 |
# File 'app/models/order.rb', line 3615 def to_label "[#{reference_number}] #{customer.full_name} (#{shipped_date.to_fs(:crm_default)})" end |
#to_liquid ⇒ Liquid::OrderDrop
Wrap the order in a Liquid::OrderDrop so it can be safely
rendered inside customer-facing email templates without exposing
internal model methods.
3150 3151 3152 |
# File 'app/models/order.rb', line 3150 def to_liquid Liquid::OrderDrop.new self end |
#to_s ⇒ String
Returns #cart_identifier.
3633 3634 3635 |
# File 'app/models/order.rb', line 3633 def to_s cart_identifier end |
#to_store ⇒ Store
287 |
# File 'app/models/order.rb', line 287 belongs_to :to_store, class_name: 'Store', optional: true |
#total_cod ⇒ BigDecimal, Float
Cash-On-Delivery amount the carrier should collect from the
consignee. Returns 0 when the order isn't COD-funded; otherwise
subtracts any already-authorised pre-payments so we don't ask
the carrier to collect twice.
2903 2904 2905 2906 2907 2908 2909 2910 |
# File 'app/models/order.rb', line 2903 def total_cod # return 0 if the order is not funded by cod return 0.0 unless funded_by_cod? # else we need to deduct any pre_payments from the order total to work out what the cod amount should be # in case the balance was partially paid by another payment method total - end |
#total_money ⇒ String
Order total formatted as a "%.2f" string for display where the
template needs a fixed two-decimal value (CSV exports, EDI
payloads).
1768 1769 1770 |
# File 'app/models/order.rb', line 1768 def total_money '%.2f' % total end |
#total_payments_authorized ⇒ BigDecimal
Sum of authorised payment amounts across deliveries, capping
each delivery at its own total so over-authorisation on one
delivery doesn't shift coverage to another. The complement of
#balance.
2426 2427 2428 2429 2430 2431 2432 2433 2434 |
# File 'app/models/order.rb', line 2426 def amount = BigDecimal('0.0') deliveries.each do |dq| dq_total = dq.total || BigDecimal('0.0') dq_auth = dq. || BigDecimal('0.0') amount += [dq_auth, dq_total].min end amount < 0 ? BigDecimal('0.0') : amount end |
#track_profit? ⇒ Boolean
5492 5493 5494 |
# File 'app/models/order.rb', line 5492 def track_profit? profitable_line_items.present? && is_regular_order? end |
#tracking_email_address ⇒ String
Inbound-email address that uniquely identifies this order — when
a tracking-email reply lands at this address, ActionMailbox
decrypts the id and re-attaches the message to the order.
2352 2353 2354 2355 2356 2357 |
# File 'app/models/order.rb', line 2352 def tracking_email_address domain = Rails.application.config.x.email_domain prefix = 'ord' encrypted_id = Encryption.encrypt_string(id.to_s) "#{prefix}+id#{encrypted_id}@#{domain}" end |
#uncommit_undelivered_line_items ⇒ Object
Release inventory commits for line items that aren't attached to
a delivery (typically left over after a delivery is destroyed
mid-flow). Otherwise stock would stay reserved against a phantom
commit and warehouse pickers couldn't see it as available.
2522 2523 2524 2525 2526 |
# File 'app/models/order.rb', line 2522 def uncommit_undelivered_line_items orphaned = line_items.where(delivery_id: nil) .joins(:inventory_commits).distinct Item::InventoryCommitter.crm_uncommit(orphaned) if orphaned.any? end |
#unfulfilled_dropship_items ⇒ Boolean
True when any dropship line still lacks a fully-receipted
supplier purchase-order item — i.e. the supplier hasn't shipped
the order yet from their warehouse. Used to decide whether the
order can transition out of awaiting_po_fulfillment.
2534 2535 2536 |
# File 'app/models/order.rb', line 2534 def unfulfilled_dropship_items line_items.dropship.any? { |li| li.purchase_order_item.nil? || !li.purchase_order_item.fully_receipted? } end |
#update_customer_status ⇒ Object (protected)
After-save callback: re-evaluate the customer's lifecycle state
(lead → prospect → customer → returning customer) now that this
order may have moved them up or down.
5769 5770 5771 |
# File 'app/models/order.rb', line 5769 def update_customer_status customer&.set_status end |
#update_linked_po_if_st ⇒ Boolean
For store-transfer orders, sync the linked PurchaseOrder record
to the order's first delivery so accounting sees the same dates,
quantities, and costs on both sides of the transfer.
5485 5486 5487 5488 5489 5490 |
# File 'app/models/order.rb', line 5485 def update_linked_po_if_st return unless is_store_transfer? && deliveries.first po = PurchaseOrder.new_or_update_st_po_from_delivery(deliveries.first) po.present? && po.valid? end |
#update_sales_support_rep(new_sales_support_rep_id, new_commission_date = nil) ⇒ Boolean
Reassign the sales-support rep on the order (and optionally
update the support-commission date). For invoiced orders we
save with validate: false so a rep change after invoicing
doesn't accidentally fail validations that don't matter at
this point in the lifecycle.
1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 |
# File 'app/models/order.rb', line 1878 def update_sales_support_rep(new_sales_support_rep_id, new_commission_date = nil) # Allow updating sales_support_rep_id and commission date even for invoiced orders if invoiced? # For invoiced orders, update only the sales_support_rep_id and commission date without other validations self.sales_support_rep_id = new_sales_support_rep_id self.sales_support_commission_date = new_commission_date save(validate: false) else # For non-invoiced orders, use normal update process update( sales_support_rep_id: new_sales_support_rep_id, sales_support_commission_date: new_commission_date ) end end |
#uploads ⇒ ActiveRecord::Relation<Upload>
298 |
# File 'app/models/order.rb', line 298 has_many :uploads, -> { order(created_at: :desc) }, as: :resource, dependent: :destroy |
#versions_for_audit_trail(_params = {}) ⇒ ActiveRecord::Relation<RecordVersion>
Aggregate RecordVersion (PaperTrail) rows for the order's
full audit trail — the order itself plus every line item,
delivery, and discount that points back to it. Surfaced in the
CRM "Audit" tab.
3713 3714 3715 3716 3717 3718 3719 3720 3721 3722 3723 3724 3725 3726 3727 3728 3729 3730 3731 3732 3733 3734 3735 3736 3737 3738 |
# File 'app/models/order.rb', line 3713 def versions_for_audit_trail(_params = {}) query_sql = %q{ (item_type = 'Order' AND item_id = :id) OR ( item_type = 'LineItem' AND reference_data @> '{"resource_type": "Order"}' AND reference_data @> :resource_id_json ) OR ( item_type = 'Delivery' AND reference_data @> :order_id_json ) OR ( item_type = 'Discount' AND reference_data @> :itemizable_id_json AND reference_data @> '{"itemizable_type": "Order"}' ) } RecordVersion.where( query_sql, id:, resource_id_json: { resource_id: id }.to_json, order_id_json: { order_id: id }.to_json, itemizable_id_json: { itemizable_id: id }.to_json ) end |
#void_credit_rma_item ⇒ Object
Void every RMA credit-item linked to this order's line items.
Used when a credit order is being cancelled — the linked RMA
items should no longer be redeemable against future orders.
3752 3753 3754 |
# File 'app/models/order.rb', line 3752 def void_credit_rma_item line_items.filter_map(&:credit_rma_item).uniq.each(&:void!) end |
#void_early_label!(reason: 'Manual void requested', reset_flag: true) ⇒ Hash
Void the early label with a custom reason (public method for external callers)
Also resets purchase_label_early flag so the next ship-label goes through normal flow
4417 4418 4419 4420 4421 4422 4423 4424 4425 4426 4427 4428 4429 4430 4431 4432 4433 4434 4435 4436 4437 4438 4439 4440 4441 4442 4443 4444 4445 4446 4447 4448 4449 4450 4451 |
# File 'app/models/order.rb', line 4417 def void_early_label!(reason: 'Manual void requested', reset_flag: true) return { success: true, message: 'No early label to void' } unless has_early_purchased_label? Rails.logger.info("[EarlyLabel] Voiding early label for order #{reference_number}: tracking=#{early_label_tracking_number}, reason=#{reason}") begin client, marketplace_type = build_early_label_void_client void_api_result = if marketplace_type == :amazon client.cancel_shipment(early_label_sww_label_id) else client.discard_label(early_label_carrier, early_label_tracking_number) end if void_api_result.success Rails.logger.info("[EarlyLabel] Successfully voided early label for order #{reference_number}") void_additional_early_label_packages(client) if marketplace_type == :amazon update_attrs = { early_label_voided_at: Time.current, early_label_void_reason: reason } update_attrs[:purchase_label_early] = false if reset_flag update!(update_attrs) void_early_label_upload Rails.logger.info('[EarlyLabel] Reset purchase_label_early flag') if reset_flag { success: true } else Rails.logger.error("[EarlyLabel] Failed to void early label for order #{reference_number}: #{void_api_result.error}") update!(early_label_void_reason: "#{reason} (API failed: #{void_api_result.error})") { success: false, error: void_api_result.error } end rescue StandardError => e Rails.logger.error("[EarlyLabel] Error voiding early label for order #{reference_number}: #{e.}") update!(early_label_void_reason: "#{reason} (Exception: #{e.})") { success: false, error: e. } end end |
#void_early_label_if_exists ⇒ Hash?
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
4081 4082 4083 4084 4085 4086 4087 4088 4089 4090 4091 4092 4093 4094 4095 4096 4097 4098 4099 4100 4101 4102 4103 4104 4105 4106 4107 4108 4109 4110 4111 4112 4113 4114 4115 4116 4117 4118 4119 4120 4121 4122 4123 4124 4125 4126 4127 4128 4129 4130 4131 4132 4133 4134 4135 4136 4137 4138 4139 4140 4141 4142 4143 4144 4145 4146 4147 4148 4149 4150 4151 4152 4153 4154 4155 4156 4157 4158 4159 4160 4161 |
# File 'app/models/order.rb', line 4081 def void_early_label_if_exists unless has_early_purchased_label? cleanup_failed_early_label_state if purchase_label_early? && early_label_tracking_number.blank? return end # SAFEGUARD B: Check for rapid state transition if early_label_purchased_recently? minutes_since = ((Time.current - early_label_purchased_at) / 60).round(1) wait_minutes = (EARLY_LABEL_RAPID_VOID_THRESHOLD_MINUTES - minutes_since).ceil Rails.logger.warn("[EarlyLabel] Rapid void blocked for order #{reference_number}: label purchased #{minutes_since} min ago (threshold: #{EARLY_LABEL_RAPID_VOID_THRESHOLD_MINUTES} min)") errors.add(:base, "Early label was purchased #{minutes_since} minutes ago. Please wait #{wait_minutes} more minute(s) before holding/canceling, or void the label manually first.") throw :halt end Rails.logger.info("[EarlyLabel] Auto-voiding early label for order #{reference_number}: tracking=#{early_label_tracking_number}") # Initialize API log for void operation @early_label_api_log = [] log_early_label_event('void_start', "Starting void for tracking #{early_label_tracking_number}") begin client, marketplace_type = build_early_label_void_client void_api_result = if marketplace_type == :amazon client.cancel_shipment(early_label_sww_label_id) else client.discard_label(early_label_carrier, early_label_tracking_number) end log_early_label_event('api_response', 'void/discard response', { success: void_api_result.success, error: void_api_result.error }) if void_api_result.success Rails.logger.info("[EarlyLabel] Successfully voided early label for order #{reference_number}") log_early_label_event('void_success', "Label voided successfully") void_additional_early_label_packages(client) if marketplace_type == :amazon save_early_label_api_log update!( early_label_voided_at: Time.current, early_label_void_reason: 'Order transitioned out of awaiting_deliveries' ) void_early_label_upload { success: true } else Rails.logger.error("[EarlyLabel] Failed to void early label for order #{reference_number}: #{void_api_result.error}") log_early_label_event('void_failed', "Void API failed: #{void_api_result.error}") save_early_label_api_log # SAFEGUARD C: Send alert for void failure marketplace = edi_orchestrator_partner&.start_with?('amazon_seller') ? 'Amazon' : 'Walmart' send_early_label_void_alert( reason: "#{marketplace} API void failed", details: void_api_result.error ) update!(early_label_void_reason: "Void attempted but API failed: #{void_api_result.error}") { success: false, error: void_api_result.error } end rescue StandardError => e Rails.logger.error("[EarlyLabel] Error voiding early label for order #{reference_number}: #{e.}") log_early_label_event('void_exception', "Exception: #{e.}") save_early_label_api_log # SAFEGUARD C: Send alert for void exception (unless it's our own rapid-void exception) unless e..include?('Cannot auto-void early label') send_early_label_void_alert( reason: 'Void exception', details: e. ) end update!(early_label_void_reason: "Void failed with exception: #{e.}") { success: false, error: e. } end end |
#void_early_label_upload ⇒ Object
Re-categorize the early label PDF as voided so it no longer appears
as an active attachment but is kept for audit trail (mirrors the
ship_label_pdf -> voided_ship_label_pdf pattern in Shipment)
4563 4564 4565 4566 4567 4568 4569 |
# File 'app/models/order.rb', line 4563 def void_early_label_upload uploads.in_category('early_ship_label').update_all(category: 'voided_early_ship_label') Rails.logger.info("[EarlyLabel] Re-categorized early label PDF to voided_early_ship_label on order #{reference_number}") rescue StandardError => e Rails.logger.warn("[EarlyLabel] Failed to re-categorize early label PDF: #{e.}") # Non-fatal — metadata void is what matters end |
#void_payments(report_fraud = false) ⇒ Object
Void every authorised payment on the order (and refuse to act on
captured ones — those need manual intervention). Used when an
order is cancelled or marked fraudulent; pass report_fraud: true
to flag the void to the gateway as a chargeback risk.
5752 5753 5754 5755 5756 5757 5758 5759 5760 5761 5762 |
# File 'app/models/order.rb', line 5752 def void_payments(report_fraud = false) payments.each do |pp| if pp. 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 |
#vouchers ⇒ ActiveRecord::Relation<Voucher>
304 |
# File 'app/models/order.rb', line 304 has_many :vouchers |
#walmart_sww_eligible? ⇒ Boolean
Check if order is eligible for Walmart Ship with Walmart early label purchase
4463 4464 4465 4466 4467 4468 4469 4470 4471 4472 4473 4474 |
# File 'app/models/order.rb', line 4463 def walmart_sww_eligible? return false unless is_edi_order? return false unless edi_orchestrator_partner&.start_with?('walmart_seller') begin orchestrator = Edi::Walmart::Orchestrator.new(edi_orchestrator_partner.to_sym) orchestrator&.ship_with_walmart_enabled? rescue StandardError => e Rails.logger.warn("[EarlyLabel] Error checking SWW eligibility: #{e.}") false end end |